package com.tt.honeyDue.models import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json /** * Shared encoder/decoder for `.honeydue` payloads across Android and iOS. * * This keeps package JSON shape in one place while each platform owns * native share-sheet presentation details. */ object HoneyDueShareCodec { private val json = Json { prettyPrint = true ignoreUnknownKeys = true encodeDefaults = true } fun encodeContractorPackage(contractor: Contractor, exportedBy: String? = null): String { return encodeSharedContractor(contractor.toSharedContractor(exportedBy)) } fun encodeSharedContractor(sharedContractor: SharedContractor): String { return json.encodeToString(SharedContractor.serializer(), sharedContractor) } fun encodeSharedResidence(sharedResidence: SharedResidence): String { return json.encodeToString(SharedResidence.serializer(), sharedResidence) } fun decodeSharedContractorOrNull(jsonContent: String): SharedContractor? { return try { json.decodeFromString(SharedContractor.serializer(), jsonContent) } catch (_: Exception) { null } } fun decodeSharedResidenceOrNull(jsonContent: String): SharedResidence? { return try { json.decodeFromString(SharedResidence.serializer(), jsonContent) } catch (_: Exception) { null } } fun createContractorImportRequestOrNull( jsonContent: String, availableSpecialties: List ): ContractorCreateRequest? { val shared = decodeSharedContractorOrNull(jsonContent) ?: return null val specialtyIds = shared.resolveSpecialtyIds(availableSpecialties) return shared.toCreateRequest(specialtyIds) } fun extractResidenceShareCodeOrNull(jsonContent: String): String? { return decodeSharedResidenceOrNull(jsonContent)?.shareCode } /** * Build a filesystem-safe package filename with `.honeydue` extension. * * Strips only the characters that are actually unsafe on iOS / Android * filesystems (`/`, `\`, `:`, `*`, `?`, `"`, `<`, `>`, `|`, control * chars). Spaces and apostrophes are kept intact so the recipient sees * the original residence / contractor name in the iOS QuickLook title * bar — gitea#7 called out the previous behaviour rendering * "The_Tartt's" instead of "The Tartt's". Internal whitespace is * collapsed to single spaces and trimmed; falls back to "honeyDue" if * the input is blank after sanitising. */ fun safeShareFileName(displayName: String): String { val safeName = displayName // Keep whitespace through the filter so adjacent space+tab // sequences survive to the regex-collapse step below. Drop // only non-whitespace control chars (NUL etc.) plus the // explicit filesystem-unsafe set. .filter { it !in UNSAFE_FILENAME_CHARS && (it.isWhitespace() || !it.isISOControl()) } .replace(Regex("\\s+"), " ") .trim() .take(50) .ifBlank { "honeyDue" } return "$safeName.honeydue" } private val UNSAFE_FILENAME_CHARS = setOf('/', '\\', ':', '*', '?', '"', '<', '>', '|') }