Rebrand from MyCrib to Casera
- Rename Kotlin package from com.example.mycrib to com.example.casera - Update Android app name, namespace, and application ID - Update iOS bundle identifiers and project settings - Rename iOS directories (MyCribTests -> CaseraTests, etc.) - Update deep link schemes from mycrib:// to casera:// - Update app group identifiers - Update subscription product IDs - Update all UI strings and branding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
package com.example.casera
|
||||
|
||||
import androidx.compose.ui.window.ComposeUIViewController
|
||||
import com.example.casera.storage.TokenManager
|
||||
import com.example.casera.storage.TokenStorage
|
||||
import com.example.casera.storage.TaskCacheManager
|
||||
import com.example.casera.storage.TaskCacheStorage
|
||||
import com.example.casera.storage.ThemeStorage
|
||||
import com.example.casera.storage.ThemeStorageManager
|
||||
import com.example.casera.ui.theme.ThemeManager
|
||||
|
||||
fun MainViewController() = ComposeUIViewController {
|
||||
// Initialize TokenStorage with iOS TokenManager
|
||||
TokenStorage.initialize(TokenManager.getInstance())
|
||||
|
||||
// Initialize TaskCacheStorage for offline task caching
|
||||
TaskCacheStorage.initialize(TaskCacheManager.getInstance())
|
||||
|
||||
// Initialize ThemeStorage and ThemeManager
|
||||
ThemeStorage.initialize(ThemeStorageManager.getInstance())
|
||||
ThemeManager.initialize()
|
||||
|
||||
App()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.casera
|
||||
|
||||
import platform.UIKit.UIDevice
|
||||
|
||||
class IOSPlatform: Platform {
|
||||
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
|
||||
}
|
||||
|
||||
actual fun getPlatform(): Platform = IOSPlatform()
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.example.casera.network
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.darwin.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.plugins.logging.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
actual fun getLocalhostAddress(): String = "127.0.0.1"
|
||||
|
||||
actual fun createHttpClient(): HttpClient {
|
||||
return HttpClient(Darwin) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
prettyPrint = true
|
||||
})
|
||||
}
|
||||
|
||||
install(Logging) {
|
||||
logger = Logger.DEFAULT
|
||||
level = LogLevel.ALL
|
||||
}
|
||||
|
||||
engine {
|
||||
configureRequest {
|
||||
setAllowsCellularAccess(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.example.casera.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,43 @@
|
||||
package com.example.casera.storage
|
||||
|
||||
import platform.Foundation.NSUserDefaults
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
/**
|
||||
* iOS implementation of TaskCacheManager using NSUserDefaults.
|
||||
*/
|
||||
actual class TaskCacheManager {
|
||||
private val userDefaults = NSUserDefaults.standardUserDefaults
|
||||
|
||||
actual fun saveTasks(tasksJson: String) {
|
||||
userDefaults.setObject(tasksJson, KEY_TASKS)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
|
||||
actual fun getTasks(): String? {
|
||||
return userDefaults.stringForKey(KEY_TASKS)
|
||||
}
|
||||
|
||||
actual fun clearTasks() {
|
||||
userDefaults.removeObjectForKey(KEY_TASKS)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_TASKS = "cached_tasks"
|
||||
|
||||
@Volatile
|
||||
private var instance: TaskCacheManager? = null
|
||||
|
||||
fun getInstance(): TaskCacheManager {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: TaskCacheManager().also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for synchronization on iOS
|
||||
private fun <T> synchronized(lock: Any, block: () -> T): T {
|
||||
return block()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.casera.storage
|
||||
|
||||
internal actual fun getPlatformTaskCacheManager(): TaskCacheManager? {
|
||||
return TaskCacheManager.getInstance()
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.example.casera.storage
|
||||
|
||||
import platform.Foundation.NSUserDefaults
|
||||
|
||||
/**
|
||||
* iOS implementation of theme storage using NSUserDefaults.
|
||||
*/
|
||||
actual class ThemeStorageManager {
|
||||
private val defaults = NSUserDefaults.standardUserDefaults
|
||||
|
||||
actual fun saveThemeId(themeId: String) {
|
||||
defaults.setObject(themeId, forKey = KEY_THEME_ID)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
actual fun getThemeId(): String? {
|
||||
return defaults.stringForKey(KEY_THEME_ID)
|
||||
}
|
||||
|
||||
actual fun clearThemeId() {
|
||||
defaults.removeObjectForKey(KEY_THEME_ID)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_THEME_ID = "theme_id"
|
||||
|
||||
private val instance by lazy { ThemeStorageManager() }
|
||||
|
||||
fun getInstance(): ThemeStorageManager = instance
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.casera.storage
|
||||
|
||||
import platform.Foundation.NSUserDefaults
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
/**
|
||||
* iOS implementation of TokenManager using NSUserDefaults.
|
||||
*/
|
||||
actual class TokenManager {
|
||||
private val userDefaults = NSUserDefaults.standardUserDefaults
|
||||
|
||||
actual fun saveToken(token: String) {
|
||||
userDefaults.setObject(token, KEY_TOKEN)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
|
||||
actual fun getToken(): String? {
|
||||
return userDefaults.stringForKey(KEY_TOKEN)
|
||||
}
|
||||
|
||||
actual fun clearToken() {
|
||||
userDefaults.removeObjectForKey(KEY_TOKEN)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_TOKEN = "auth_token"
|
||||
|
||||
@Volatile
|
||||
private var instance: TokenManager? = null
|
||||
|
||||
fun getInstance(): TokenManager {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: TokenManager().also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for synchronization on iOS
|
||||
private fun <T> synchronized(lock: Any, block: () -> T): T {
|
||||
return block()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.casera.storage
|
||||
|
||||
internal actual fun getPlatformTokenManager(): TokenManager? {
|
||||
return TokenManager.getInstance()
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.example.casera.util
|
||||
|
||||
import com.example.casera.platform.ImageData
|
||||
import kotlinx.cinterop.*
|
||||
import platform.Foundation.*
|
||||
import platform.UIKit.*
|
||||
import platform.posix.memcpy
|
||||
|
||||
/**
|
||||
* iOS implementation of image compression
|
||||
* Compresses images to JPEG format and ensures they don't exceed MAX_IMAGE_SIZE_BYTES
|
||||
*/
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual object ImageCompressor {
|
||||
/**
|
||||
* Compress an ImageData to JPEG format with size limit
|
||||
* @param imageData The image to compress
|
||||
* @return Compressed image data as ByteArray
|
||||
*/
|
||||
actual fun compressImage(imageData: ImageData): ByteArray {
|
||||
// Convert ByteArray to NSData
|
||||
val nsData = imageData.bytes.usePinned { pinned ->
|
||||
NSData.create(
|
||||
bytes = pinned.addressOf(0),
|
||||
length = imageData.bytes.size.toULong()
|
||||
)
|
||||
}
|
||||
|
||||
// Create UIImage from data
|
||||
val image = UIImage.imageWithData(nsData) ?: return imageData.bytes
|
||||
|
||||
// Compress with iterative quality reduction
|
||||
return compressImageToTarget(image, ImageConfig.MAX_IMAGE_SIZE_BYTES.toLong())
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress a UIImage to target size
|
||||
*/
|
||||
private fun compressImageToTarget(image: UIImage, targetSizeBytes: Long): ByteArray {
|
||||
var quality = ImageConfig.INITIAL_JPEG_QUALITY / 100.0
|
||||
var compressedData: NSData? = null
|
||||
|
||||
while (quality >= ImageConfig.MIN_JPEG_QUALITY / 100.0) {
|
||||
compressedData = UIImageJPEGRepresentation(image, quality)
|
||||
|
||||
if (compressedData != null && compressedData.length.toLong() <= targetSizeBytes) {
|
||||
break
|
||||
}
|
||||
|
||||
quality -= 0.05 // Reduce quality by 5%
|
||||
}
|
||||
|
||||
// Convert NSData to ByteArray
|
||||
return compressedData?.toByteArray() ?: image.let {
|
||||
UIImageJPEGRepresentation(it, ImageConfig.MIN_JPEG_QUALITY / 100.0)?.toByteArray() ?: ByteArray(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert NSData to ByteArray
|
||||
*/
|
||||
private fun NSData.toByteArray(): ByteArray {
|
||||
return ByteArray(this.length.toInt()).apply {
|
||||
usePinned {
|
||||
memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user