Rebrand from Casera/MyCrib to honeyDue
Total rebrand across KMM project: - Kotlin package: com.example.casera -> com.tt.honeyDue (dirs + declarations) - Gradle: rootProject.name, namespace, applicationId - Android: manifest, strings.xml (all languages), widget resources - iOS: pbxproj bundle IDs, Info.plist, entitlements, xcconfig - iOS directories: Casera/ -> HoneyDue/, CaseraTests/ -> HoneyDueTests/, etc. - Swift source: all class/struct/enum renames - Deep links: casera:// -> honeydue://, .casera -> .honeydue - App icons replaced with honeyDue honeycomb icon - Domains: casera.treytartt.com -> honeyDue.treytartt.com - Bundle IDs: com.tt.casera -> com.tt.honeyDue - Database table names preserved Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package com.tt.honeyDue.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tt.honeyDue.models.Contractor
|
||||
|
||||
/**
|
||||
* iOS implementation is a no-op - import is handled in Swift layer via ContractorSharingManager.swift.
|
||||
* The iOS iOSApp.swift handles file imports directly.
|
||||
*/
|
||||
@Composable
|
||||
actual fun ContractorImportHandler(
|
||||
pendingContractorImportUri: Any?,
|
||||
onClearContractorImport: () -> Unit,
|
||||
onImportSuccess: (Contractor) -> Unit
|
||||
) {
|
||||
// No-op on iOS - import handled in Swift layer
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.tt.honeyDue.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.interop.LocalUIViewController
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.models.honeyDueShareCodec
|
||||
import com.tt.honeyDue.models.Contractor
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.usePinned
|
||||
import platform.Foundation.*
|
||||
import platform.UIKit.UIActivityViewController
|
||||
import platform.UIKit.UIViewController
|
||||
|
||||
@Composable
|
||||
actual fun rememberShareContractor(): (Contractor) -> Unit {
|
||||
val viewController = LocalUIViewController.current
|
||||
|
||||
return share@{ contractor: Contractor ->
|
||||
val currentUsername = DataManager.currentUser.value?.username ?: "Unknown"
|
||||
val jsonContent = honeyDueShareCodec.encodeContractorPackage(contractor, currentUsername)
|
||||
val fileUrl = writeShareFile(jsonContent, contractor.name) ?: return@share
|
||||
presentShareSheet(viewController, fileUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
private fun writeShareFile(jsonContent: String, displayName: String): NSURL? {
|
||||
val fileName = honeyDueShareCodec.safeShareFileName(displayName)
|
||||
val filePath = NSTemporaryDirectory().plus(fileName)
|
||||
|
||||
val bytes = jsonContent.encodeToByteArray()
|
||||
val data = bytes.usePinned { pinned ->
|
||||
NSData.create(bytes = pinned.addressOf(0), length = bytes.size.toULong())
|
||||
}
|
||||
val didCreate = NSFileManager.defaultManager.createFileAtPath(
|
||||
path = filePath,
|
||||
contents = data,
|
||||
attributes = null
|
||||
)
|
||||
if (!didCreate) return null
|
||||
|
||||
return NSURL.fileURLWithPath(filePath)
|
||||
}
|
||||
|
||||
private fun presentShareSheet(viewController: UIViewController, fileUrl: NSURL) {
|
||||
val activityViewController = UIActivityViewController(
|
||||
activityItems = listOf(fileUrl),
|
||||
applicationActivities = null
|
||||
)
|
||||
viewController.presentViewController(
|
||||
viewControllerToPresent = activityViewController,
|
||||
animated = true,
|
||||
completion = null
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.tt.honeyDue.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
/**
|
||||
* iOS implementation - no-op since iOS haptics are handled by SwiftUI.
|
||||
* This is only used when running the shared Compose code on iOS
|
||||
* (which isn't the primary iOS UI).
|
||||
*/
|
||||
class IOSHapticFeedbackPerformer : HapticFeedbackPerformer {
|
||||
override fun perform(type: HapticFeedbackType) {
|
||||
// iOS haptic feedback is handled natively in SwiftUI views
|
||||
// This is a no-op for the Compose layer on iOS
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberHapticFeedback(): HapticFeedbackPerformer {
|
||||
return remember { IOSHapticFeedbackPerformer() }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.tt.honeyDue.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import org.jetbrains.skia.Image
|
||||
|
||||
@Composable
|
||||
actual fun rememberImageBitmap(imageData: ImageData): ImageBitmap? {
|
||||
return remember(imageData) {
|
||||
try {
|
||||
val skiaImage = Image.makeFromEncoded(imageData.bytes)
|
||||
skiaImage.toComposeImageBitmap()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.tt.honeyDue.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.interop.LocalUIViewController
|
||||
import kotlinx.cinterop.*
|
||||
import platform.Foundation.*
|
||||
import platform.PhotosUI.*
|
||||
import platform.UIKit.*
|
||||
import platform.darwin.NSObject
|
||||
import platform.posix.memcpy
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
@Composable
|
||||
actual fun rememberImagePicker(
|
||||
onImagesPicked: (List<ImageData>) -> Unit
|
||||
): () -> Unit {
|
||||
val viewController = LocalUIViewController.current
|
||||
|
||||
val pickerDelegate = remember {
|
||||
object : NSObject(), PHPickerViewControllerDelegateProtocol {
|
||||
override fun picker(
|
||||
picker: PHPickerViewController,
|
||||
didFinishPicking: List<*>
|
||||
) {
|
||||
picker.dismissViewControllerAnimated(true, null)
|
||||
|
||||
val results = didFinishPicking as List<PHPickerResult>
|
||||
if (results.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val images = mutableListOf<ImageData>()
|
||||
var processedCount = 0
|
||||
|
||||
results.forEach { result ->
|
||||
val itemProvider = result.itemProvider
|
||||
|
||||
// Check if the item has an image using UTType
|
||||
if (itemProvider.hasItemConformingToTypeIdentifier("public.image")) {
|
||||
itemProvider.loadFileRepresentationForTypeIdentifier(
|
||||
typeIdentifier = "public.image",
|
||||
completionHandler = { url, error ->
|
||||
if (error == null && url != null) {
|
||||
// Read the image data from the file URL
|
||||
val imageData = NSData.dataWithContentsOfURL(url)
|
||||
if (imageData != null) {
|
||||
// Convert to UIImage and then to JPEG for consistent format
|
||||
val image = UIImage.imageWithData(imageData)
|
||||
if (image != null) {
|
||||
val jpegData = UIImageJPEGRepresentation(image, 0.8)
|
||||
if (jpegData != null) {
|
||||
val bytes = jpegData.toByteArray()
|
||||
val fileName = "image_${NSDate().timeIntervalSince1970.toLong()}_${processedCount}.jpg"
|
||||
synchronized(images) {
|
||||
images.add(ImageData(bytes, fileName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processedCount++
|
||||
|
||||
// When all images are processed, call the callback
|
||||
if (processedCount == results.size) {
|
||||
if (images.isNotEmpty()) {
|
||||
onImagesPicked(images.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
processedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
val config = PHPickerConfiguration().apply {
|
||||
setSelectionLimit(5)
|
||||
setFilter(PHPickerFilter.imagesFilter())
|
||||
}
|
||||
|
||||
val picker = PHPickerViewController(config).apply {
|
||||
setDelegate(pickerDelegate)
|
||||
}
|
||||
|
||||
viewController.presentViewController(picker, animated = true, completion = null)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
private fun NSData.toByteArray(): ByteArray {
|
||||
return ByteArray(length.toInt()).apply {
|
||||
usePinned {
|
||||
memcpy(it.addressOf(0), bytes, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
@Composable
|
||||
actual fun rememberCameraPicker(
|
||||
onImageCaptured: (ImageData) -> Unit
|
||||
): () -> Unit {
|
||||
val viewController = LocalUIViewController.current
|
||||
|
||||
val cameraDelegate = remember {
|
||||
object : NSObject(), UIImagePickerControllerDelegateProtocol, UINavigationControllerDelegateProtocol {
|
||||
override fun imagePickerController(
|
||||
picker: UIImagePickerController,
|
||||
didFinishPickingMediaWithInfo: Map<Any?, *>
|
||||
) {
|
||||
picker.dismissViewControllerAnimated(true, null)
|
||||
|
||||
val image = didFinishPickingMediaWithInfo[UIImagePickerControllerOriginalImage] as? UIImage
|
||||
if (image != null) {
|
||||
// Convert to JPEG
|
||||
val jpegData = UIImageJPEGRepresentation(image, 0.8)
|
||||
if (jpegData != null) {
|
||||
val bytes = jpegData.toByteArray()
|
||||
val fileName = "camera_${NSDate().timeIntervalSince1970.toLong()}.jpg"
|
||||
onImageCaptured(ImageData(bytes, fileName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun imagePickerControllerDidCancel(picker: UIImagePickerController) {
|
||||
picker.dismissViewControllerAnimated(true, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Check if camera is available
|
||||
if (UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypeCamera)) {
|
||||
val picker = UIImagePickerController().apply {
|
||||
setSourceType(UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypeCamera)
|
||||
setDelegate(cameraDelegate)
|
||||
setCameraCaptureMode(UIImagePickerControllerCameraCaptureMode.UIImagePickerControllerCameraCaptureModePhoto)
|
||||
}
|
||||
|
||||
viewController.presentViewController(picker, animated = true, completion = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T : Any> synchronized(lock: Any, block: () -> T): T {
|
||||
return block()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.tt.honeyDue.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.ui.subscription.UpgradeScreen
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* iOS: Purchase flow is handled in Swift via StoreKitManager.
|
||||
* Restore calls backend to refresh subscription status.
|
||||
*/
|
||||
@Composable
|
||||
actual fun PlatformUpgradeScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onSubscriptionChanged: () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
UpgradeScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onPurchase = { _ ->
|
||||
// iOS purchase flow is handled by StoreKitManager in Swift layer
|
||||
onNavigateBack()
|
||||
},
|
||||
onRestorePurchases = {
|
||||
scope.launch {
|
||||
APILayer.getSubscriptionStatus(forceRefresh = true)
|
||||
onSubscriptionChanged()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.tt.honeyDue.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tt.honeyDue.models.JoinResidenceResponse
|
||||
|
||||
/**
|
||||
* iOS implementation is a no-op - import is handled in Swift layer via ResidenceSharingManager.swift.
|
||||
* The iOS iOSApp.swift handles file imports directly.
|
||||
*/
|
||||
@Composable
|
||||
actual fun ResidenceImportHandler(
|
||||
pendingResidenceImportUri: Any?,
|
||||
onClearResidenceImport: () -> Unit,
|
||||
onImportSuccess: (JoinResidenceResponse) -> Unit
|
||||
) {
|
||||
// No-op on iOS - import handled in Swift layer
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.tt.honeyDue.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.interop.LocalUIViewController
|
||||
import com.tt.honeyDue.models.honeyDueShareCodec
|
||||
import com.tt.honeyDue.models.Residence
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.usePinned
|
||||
import platform.Foundation.*
|
||||
import platform.UIKit.UIActivityViewController
|
||||
import platform.UIKit.UIViewController
|
||||
|
||||
@Composable
|
||||
actual fun rememberShareResidence(): Pair<ResidenceSharingState, (Residence) -> Unit> {
|
||||
val viewController = LocalUIViewController.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var state by remember { mutableStateOf(ResidenceSharingState()) }
|
||||
|
||||
val shareFunction: (Residence) -> Unit = share@{ residence ->
|
||||
scope.launch {
|
||||
state = ResidenceSharingState(isLoading = true)
|
||||
|
||||
when (val result = APILayer.generateSharePackage(residence.id)) {
|
||||
is ApiResult.Success -> {
|
||||
val jsonContent = honeyDueShareCodec.encodeSharedResidence(result.data)
|
||||
val fileUrl = writeShareFile(jsonContent, residence.name)
|
||||
if (fileUrl == null) {
|
||||
state = ResidenceSharingState(isLoading = false, error = "Failed to create share package")
|
||||
return@launch
|
||||
}
|
||||
|
||||
state = ResidenceSharingState(isLoading = false)
|
||||
presentShareSheet(viewController, fileUrl)
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
state = ResidenceSharingState(
|
||||
isLoading = false,
|
||||
error = result.message.ifBlank { "Failed to generate share package" }
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
state = ResidenceSharingState(isLoading = false, error = "Failed to generate share package")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(state, shareFunction)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
private fun writeShareFile(jsonContent: String, displayName: String): NSURL? {
|
||||
val fileName = honeyDueShareCodec.safeShareFileName(displayName)
|
||||
val filePath = NSTemporaryDirectory().plus(fileName)
|
||||
|
||||
val bytes = jsonContent.encodeToByteArray()
|
||||
val data = bytes.usePinned { pinned ->
|
||||
NSData.create(bytes = pinned.addressOf(0), length = bytes.size.toULong())
|
||||
}
|
||||
val didCreate = NSFileManager.defaultManager.createFileAtPath(
|
||||
path = filePath,
|
||||
contents = data,
|
||||
attributes = null
|
||||
)
|
||||
if (!didCreate) return null
|
||||
|
||||
return NSURL.fileURLWithPath(filePath)
|
||||
}
|
||||
|
||||
private fun presentShareSheet(viewController: UIViewController, fileUrl: NSURL) {
|
||||
val activityViewController = UIActivityViewController(
|
||||
activityItems = listOf(fileUrl),
|
||||
applicationActivities = null
|
||||
)
|
||||
viewController.presentViewController(
|
||||
viewControllerToPresent = activityViewController,
|
||||
animated = true,
|
||||
completion = null
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user