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:
Trey t
2025-11-28 21:10:38 -06:00
parent 8dbc816a33
commit c6eef720ed
215 changed files with 767 additions and 767 deletions

View File

@@ -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()
}

View File

@@ -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()

View File

@@ -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)
}
}
}
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -0,0 +1,5 @@
package com.example.casera.storage
internal actual fun getPlatformTaskCacheManager(): TaskCacheManager? {
return TaskCacheManager.getInstance()
}

View File

@@ -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
}
}

View File

@@ -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()
}

View File

@@ -0,0 +1,5 @@
package com.example.casera.storage
internal actual fun getPlatformTokenManager(): TokenManager? {
return TokenManager.getInstance()
}

View File

@@ -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)
}
}
}
}