diff --git a/FloconAndroid/.idea/xcode.xml b/FloconAndroid/.idea/xcode.xml new file mode 100644 index 000000000..4eb324224 --- /dev/null +++ b/FloconAndroid/.idea/xcode.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/FloconAndroid/flocon-base/build.gradle.kts b/FloconAndroid/flocon-base/build.gradle.kts index 32a8e7893..d92d2d3e4 100644 --- a/FloconAndroid/flocon-base/build.gradle.kts +++ b/FloconAndroid/flocon-base/build.gradle.kts @@ -15,12 +15,10 @@ kotlin { jvm() - /* iosX64() iosArm64() iosSimulatorArm64() - */ - + sourceSets { val commonMain by getting { dependencies { @@ -38,7 +36,6 @@ kotlin { } } - /* val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting @@ -48,7 +45,6 @@ kotlin { iosArm64Main.dependsOn(this) iosSimulatorArm64Main.dependsOn(this) } - */ } } diff --git a/FloconAndroid/flocon-no-op/build.gradle.kts b/FloconAndroid/flocon-no-op/build.gradle.kts index 7dc7679d5..ee80a3e2d 100644 --- a/FloconAndroid/flocon-no-op/build.gradle.kts +++ b/FloconAndroid/flocon-no-op/build.gradle.kts @@ -15,12 +15,10 @@ kotlin { jvm() - /* iosX64() iosArm64() iosSimulatorArm64() - */ - + sourceSets { val commonMain by getting { dependencies { @@ -39,7 +37,6 @@ kotlin { } } - /* val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting @@ -49,7 +46,6 @@ kotlin { iosArm64Main.dependsOn(this) iosSimulatorArm64Main.dependsOn(this) } - */ } } diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index 25443132e..7ab35c0b7 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -16,12 +16,10 @@ kotlin { jvm() - /* iosX64() iosArm64() iosSimulatorArm64() - */ - + sourceSets { val commonMain by getting { dependencies { @@ -50,7 +48,6 @@ kotlin { } } - /* val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting @@ -61,9 +58,18 @@ kotlin { iosSimulatorArm64Main.dependsOn(this) dependencies { implementation(libs.ktor.client.core) + implementation(libs.ktor.client.darwin) + + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.serialization.kotlinx.json) + + implementation(libs.androidx.sqlite.bundled) + + // to store the device id + implementation("com.russhwolf:multiplatform-settings:1.3.0") } } - */ } } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt index ded188966..481ef24e0 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt @@ -6,12 +6,14 @@ import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.core.FloconPlugin import io.github.openflocon.flocon.model.FloconMessageFromServer import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update internal class FloconDeeplinksPluginImpl( private val sender: FloconMessageSender, ) : FloconPlugin, FloconDeeplinksPlugin { - private val deeplinks = java.util.concurrent.atomic.AtomicReference?>(null) + private val deeplinks = MutableStateFlow?>(null) override fun onMessageReceived( messageFromServer: FloconMessageFromServer, @@ -21,13 +23,15 @@ internal class FloconDeeplinksPluginImpl( override fun onConnectedToServer() { // on connected, send known dashboard - deeplinks.get()?.let { + deeplinks.value?.let { registerDeeplinks(it) } } override fun registerDeeplinks(deeplinks: List) { - this.deeplinks.set(deeplinks) + this.deeplinks.update { + deeplinks + } try { sender.send( diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/Flocon.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/Flocon.ios.kt index f986f2127..14a0608d9 100644 --- a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/Flocon.ios.kt +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/Flocon.ios.kt @@ -1,6 +1,8 @@ package io.github.openflocon.flocon object Flocon : FloconCore() { - + fun initialize() { + super.initializeFlocon(context = FloconContext()) + } } diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/FloconCore.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/FloconCore.ios.kt new file mode 100644 index 000000000..1bbf0ee58 --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/FloconCore.ios.kt @@ -0,0 +1,7 @@ +package io.github.openflocon.flocon + +actual class FloconContext + +internal actual fun displayClearTextError(context: FloconContext) { + // no op on ios +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/FloconFile.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/FloconFile.ios.kt new file mode 100644 index 000000000..3a937a4da --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/FloconFile.ios.kt @@ -0,0 +1,5 @@ +package io.github.openflocon.flocon + +internal actual class FloconFile { + // TODO +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/ServerHost.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/ServerHost.ios.kt new file mode 100644 index 000000000..ae08a2327 --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/ServerHost.ios.kt @@ -0,0 +1,5 @@ +package io.github.openflocon.flocon + +internal actual fun getServerHost(floconContext: FloconContext): String { + return "localhost" +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/core/AppInfos.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/core/AppInfos.ios.kt new file mode 100644 index 000000000..79db76d8e --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/core/AppInfos.ios.kt @@ -0,0 +1,41 @@ +package io.github.openflocon.flocon.core + +import com.russhwolf.settings.NSUserDefaultsSettings +import io.github.openflocon.flocon.FloconContext +import platform.Foundation.NSBundle +import platform.Foundation.NSUUID +import platform.Foundation.NSUserDefaults +import platform.UIKit.UIDevice + +private fun getDeviceId() : String { + val settings = NSUserDefaultsSettings( + NSUserDefaults.standardUserDefaults() + ) + val id = settings.getStringOrNull("deviceId") + return if(id != null) { + id + } else { + val newId = NSUUID.UUID().UUIDString() + settings.putString("deviceId", newId) + newId + } +} + +private fun deviceName(): String { + val device = UIDevice.currentDevice + return "${device.systemName()} ${device.model}" +} + +internal actual fun getAppInfos(floconContext: FloconContext): AppInfos { + val bundle = NSBundle.mainBundle + val appName = bundle.objectForInfoDictionaryKey("CFBundleName") as? String ?: "Unknown" + val appPackageName = bundle.bundleIdentifier ?: "Unknown" + + return AppInfos( + deviceId = getDeviceId(), + deviceName = deviceName(), + appName = appName, + appPackageName = appPackageName, + platform = "ios", + ) +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.ios.kt new file mode 100644 index 000000000..26eef7645 --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/database/FloconDatabasePlugin.ios.kt @@ -0,0 +1,125 @@ +package io.github.openflocon.flocon.plugins.database + +import androidx.sqlite.SQLiteConnection +import androidx.sqlite.driver.NativeSQLiteDriver +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.plugins.database.model.FloconDatabaseModel +import io.github.openflocon.flocon.plugins.database.model.fromdevice.DatabaseExecuteSqlResponse +import io.github.openflocon.flocon.plugins.database.model.fromdevice.DeviceDataBaseDataModel +import platform.Foundation.NSFileManager +import platform.posix.close + +internal actual fun buildFloconDatabaseDataSource(context: FloconContext): FloconDatabaseDataSource { + return FloconDatabaseDataSourceIos(context) +} + +internal class FloconDatabaseDataSourceIos( + private val context: FloconContext +) : FloconDatabaseDataSource { + + override fun executeSQL( + registeredDatabases: List, + databaseName: String, + query: String + ): DatabaseExecuteSqlResponse { + val fileManager = NSFileManager.defaultManager + if (!fileManager.fileExistsAtPath(databaseName)) { + return DatabaseExecuteSqlResponse.Error( + message = "Database file not found: $databaseName", + originalSql = query + ) + } + + val driver = NativeSQLiteDriver() + val connection = driver.open(fileName = databaseName) + + return try { + val firstWord = getFirstWord(query).uppercase() + when (firstWord) { + "SELECT", "PRAGMA", "EXPLAIN" -> executeSelect(connection, query) + "INSERT" -> executeInsert(connection, query) + "UPDATE", "DELETE" -> executeUpdateDelete(connection, query) + else -> executeRawQuery(connection, query) + } + } catch (t: Throwable) { + DatabaseExecuteSqlResponse.Error( + message = t.message ?: "Error executing SQL", + originalSql = query + ) + } finally { + connection.close() + } + } + + override fun getAllDataBases( + registeredDatabases: List + ): List { + val fileManager = NSFileManager.defaultManager + return registeredDatabases.mapNotNull { + if (fileManager.fileExistsAtPath(it.absolutePath)) { + DeviceDataBaseDataModel( + id = it.absolutePath, + name = it.displayName + ) + } else null + } + } +} + +// --- SQL execution helpers --- + +private fun executeSelect(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + val cursor = connection.prepare(query).use { statement -> + val columnCount = statement.getColumnCount() + val columns = (0 until columnCount).map { statement.getColumnName(it) } + val rows = mutableListOf>() + + while (statement.step()) { + val row = (0 until columnCount).map { idx -> + statement.getText(idx) + } + rows.add(row) + } + + statement.close() // maybe remove + DatabaseExecuteSqlResponse.Select(columns, rows) + } + return cursor +} + +private fun executeUpdateDelete(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + connection.prepare(query).use { statement -> + statement.close() + } + // sqlite-kt n'expose pas encore `changes()`, on renvoie 0 + return DatabaseExecuteSqlResponse.UpdateDelete(affectedCount = 0) +} + +private fun executeInsert(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + connection.prepare(query).use { statement -> + statement.close() + } + + // Récupération du dernier ID inséré + var id = -1L + connection.prepare("SELECT last_insert_rowid()").use { + id = if (it.step()) it.getLong(0) else -1L + it.close() // maybe remove + } + + return DatabaseExecuteSqlResponse.Insert(id) +} + +private fun executeRawQuery(connection: SQLiteConnection, query: String): DatabaseExecuteSqlResponse { + connection.prepare(query).use { statement -> + statement.close() // maybe remove + } + return DatabaseExecuteSqlResponse.RawSuccess +} + +// --- Utilities --- +private fun getFirstWord(s: String): String { + val trimmed = s.trim() + val firstSpace = trimmed.indexOf(' ') + return if (firstSpace >= 0) trimmed.substring(0, firstSpace) else trimmed +} diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.ios.kt new file mode 100644 index 000000000..8ee95eefc --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/FloconDevicePluginImpl.ios.kt @@ -0,0 +1,7 @@ +package io.github.openflocon.flocon.plugins.device + +import io.github.openflocon.flocon.FloconContext + +internal actual fun restartApp(context: FloconContext) { + // TODO +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.ios.kt new file mode 100644 index 000000000..af0dafd29 --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/device/GetAppIconUtils.ios.kt @@ -0,0 +1,7 @@ +package io.github.openflocon.flocon.plugins.device + +import io.github.openflocon.flocon.FloconContext + +actual fun getAppIconBase64(context: FloconContext): String? { + return null // TODO +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt new file mode 100644 index 000000000..a4e79769c --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/files/FloconFilesPlugin.ios.kt @@ -0,0 +1,35 @@ +package io.github.openflocon.flocon.plugins.files + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.FloconFile +import io.github.openflocon.flocon.plugins.files.model.fromdevice.FileDataModel + +internal actual fun fileDataSource(context: FloconContext): FileDataSource { + return FileDataSourceIOs() +} + +// TODO +internal class FileDataSourceIOs : FileDataSource { + override fun getFile( + path: String, + isConstantPath: Boolean + ): FloconFile? { + return null + } + + override fun getFolderContent( + path: String, + isConstantPath: Boolean + ): List { + return emptyList() + } + + override fun deleteFile(path: String) { + // TODO + } + + override fun deleteFolderContent(folder: FloconFile) { + // TODO + } + +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.ios.kt new file mode 100644 index 000000000..bdccd4100 --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.ios.kt @@ -0,0 +1,29 @@ +package io.github.openflocon.flocon.plugins.network + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig +import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse + +internal actual fun buildFloconNetworkDataSource(context: FloconContext): FloconNetworkDataSource { + return FloconNetworkDataSourceIOs() +} + +// TODO +internal class FloconNetworkDataSourceIOs : FloconNetworkDataSource { + override fun saveMocksToFile(mocks: List) { + // TODO + } + + override fun loadMocksFromFile(): List { + return emptyList() + } + + override fun saveBadNetworkConfig(config: BadQualityConfig?) { + // TODO + } + + override fun loadBadNetworkConfig(): BadQualityConfig? { + return null + } + +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt new file mode 100644 index 000000000..2d24594e8 --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt @@ -0,0 +1,30 @@ +package io.github.openflocon.flocon.plugins.sharedprefs + +import io.github.openflocon.flocon.FloconContext +import io.github.openflocon.flocon.plugins.sharedprefs.model.SharedPreferencesDescriptor +import io.github.openflocon.flocon.plugins.sharedprefs.model.fromdevice.SharedPreferenceRowDataModel +import io.github.openflocon.flocon.plugins.sharedprefs.model.todevice.ToDeviceEditSharedPreferenceValueMessage + +internal actual fun buildFloconSharedPreferencesDataSource(context: FloconContext): FloconSharedPreferencesDataSource { + return FloconSharedPreferencesDataSourceIOs() +} + +// TODO try to bind with ios storage +internal class FloconSharedPreferencesDataSourceIOs : FloconSharedPreferencesDataSource { + override fun getAllSharedPreferences(): List { + return emptyList() + } + + override fun getSharedPreferenceContent(sharedPreferencesName: String): List { + return emptyList() + } + + override fun setSharedPreferenceRowValue( + sharedPreferencesName: String, + preferenceName: String, + message: ToDeviceEditSharedPreferenceValueMessage + ) { + // TODO + } + +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.ios.kt new file mode 100644 index 000000000..96c94ada7 --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/websocket/FloconHttpClient.ios.kt @@ -0,0 +1,81 @@ +package io.github.openflocon.flocon.websocket + +import io.github.openflocon.flocon.FloconFile +import io.github.openflocon.flocon.model.FloconFileInfo +import io.ktor.client.HttpClient +import io.ktor.client.engine.darwin.Darwin +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +internal actual fun buildFloconHttpClient(): FloconHttpClient { + return FloconHttpClientIOs() +} + +internal class FloconHttpClientIOs() : FloconHttpClient { + + // client configurable selon la plateforme (Android, iOS, JVM, etc.) + private val client = HttpClient(Darwin.create()) { + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + prettyPrint = false + }) + } + install(Logging) { + level = LogLevel.INFO + } + } + + override suspend fun send( + file: FloconFile, + infos: FloconFileInfo, + address: String, + port: Int, + deviceId: String, + appPackageName: String, + appInstance: Long + ): Boolean { + /* no op for now on ios + val uploadUrl = "http://$address:$port/upload" + + try { + val response: HttpResponse = client.submitFormWithBinaryData( + url = uploadUrl, + formData = formData { + append("remotePath", infos.path) + append("requestId", infos.requestId) + append("deviceId", deviceId) + append("appPackageName", appPackageName) + append("appInstance", appInstance.toString()) + + // Ajout du fichier binaire + val fileBytes = file.file.readBytes() + append( + key = "file", + value = fileBytes, + headers = Headers.build { + append(HttpHeaders.ContentDisposition, "filename=\"${file.file.name}\"") + append(HttpHeaders.ContentType, "application/octet-stream") + } + ) + } + ) + + if (response.status.isSuccess()) { + println("✅ Upload réussi : ${response.bodyAsText()}") + return true + } else { + println("❌ Erreur serveur (${response.status}): ${response.bodyAsText()}") + return false + } + } catch (e: Exception) { + println("❌ Erreur lors de l’envoi : ${e.message}") + return false + } + */ + return false + } +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/websocket/FloconWebSocketClient.ios.kt b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/websocket/FloconWebSocketClient.ios.kt new file mode 100644 index 000000000..25e557a97 --- /dev/null +++ b/FloconAndroid/flocon/src/iosMain/kotlin/io/github/openflocon/flocon/websocket/FloconWebSocketClient.ios.kt @@ -0,0 +1,110 @@ +package io.github.openflocon.flocon.websocket + + +import io.github.openflocon.flocon.FloconLogger +import io.ktor.client.* +import io.ktor.client.engine.darwin.Darwin +import io.ktor.client.plugins.websocket.* +import io.ktor.http.HttpMethod +import io.ktor.websocket.* +import kotlinx.coroutines.* + +internal actual fun buildFloconWebSocketClient(): FloconWebSocketClient { + return FloconWebSocketClientIOs() +} + +internal class FloconWebSocketClientIOs() : FloconWebSocketClient { + + private val client = HttpClient(Darwin.create()) { + install(WebSockets) + } + + private var session: DefaultClientWebSocketSession? = null + private val queue = MessageQueue(capacity = 200) + + override suspend fun connect( + address: String, + port: Int, + onMessageReceived: (String) -> Unit, + onClosed: () -> Unit, + ) { + if (session != null) return + + try { + FloconLogger.log("🔌 Trying to connect ws://$address:$port ...") + + val wsSession = client.webSocketSession( + method = HttpMethod.Get, + host = address, + port = port, + path = "/" + ) + + session = wsSession + FloconLogger.log("✅ WebSocket connected at ws://$address:$port") + + // Lancer un job pour écouter les messages entrants + CoroutineScope(Dispatchers.Default).launch { + try { + for (frame in wsSession.incoming) { + when (frame) { + is Frame.Text -> { + val text = frame.readText() + FloconLogger.log("WEBSOCKET <----- received : $text") + onMessageReceived(text) + } + is Frame.Close -> { + FloconLogger.log("🔒 WebSocket closed (${frame.readReason()})") + disconnect() + onClosed() + break + } + else -> Unit + } + } + FloconLogger.log("❌ WebSocket closed without reason") + } catch (e: Exception) { + FloconLogger.logError("❌ WebSocket error : ${e.message}", e) + } finally { + disconnect() + onClosed() + } + } + + } catch (t: Throwable) { + FloconLogger.logError("❌ WebSocket connection failed : ${t.message}", t) + throw t + } + } + + override suspend fun sendMessage(message: String): Boolean { + val currentSession = session + if (currentSession == null) { + queue.add(message) + FloconLogger.log("🕒 WebSocket not connected, message saved in the queue : $message") + return false + } + + return try { + currentSession.send(Frame.Text(message)) + FloconLogger.log("WEBSOCKET ----> Sent : $message") + true + } catch (e: Exception) { + FloconLogger.logError("❌ Failed to send : ${e.message}", e) + false + } + } + + override suspend fun sendPendingMessages() { + var message = queue.poll() + while (message != null) { + sendMessage(message) + message = queue.poll() + } + } + + override suspend fun disconnect() { + session?.close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnecting")) + session = null + } +} diff --git a/FloconAndroid/gradle/libs.versions.toml b/FloconAndroid/gradle/libs.versions.toml index 42801d01f..bcd03c606 100644 --- a/FloconAndroid/gradle/libs.versions.toml +++ b/FloconAndroid/gradle/libs.versions.toml @@ -18,7 +18,7 @@ composeBom = "2025.06.01" appcompat = "1.7.1" material = "1.12.0" okhttpBom = "4.12.0" -room = "2.8.1" +room = "2.7.2" # for grpc gson = "2.11.0" grpc = "1.70.0" @@ -33,6 +33,7 @@ sqliteJdbc = "3.50.3.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } apollo-http-okhttprealization = { module = "com.apollographql.apollo:apollo-http-okhttprealization", version.ref = "apollo" } apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" } @@ -68,6 +69,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesBom" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } @@ -87,6 +89,7 @@ sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sql [plugins] android-application = { id = "com.android.application", version.ref = "agp" } +androidx-room = { id = "androidx.room", version.ref = "room" } compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/FloconAndroid/iosApp/Configuration/Config.xcconfig b/FloconAndroid/iosApp/Configuration/Config.xcconfig new file mode 100644 index 000000000..174571bab --- /dev/null +++ b/FloconAndroid/iosApp/Configuration/Config.xcconfig @@ -0,0 +1,7 @@ +TEAM_ID= + +PRODUCT_NAME=MyApplication +PRODUCT_BUNDLE_IDENTIFIER=com.florent37.myapplication.MyApplication$(TEAM_ID) + +CURRENT_PROJECT_VERSION=1 +MARKETING_VERSION=1.0 \ No newline at end of file diff --git a/FloconAndroid/iosApp/iosApp.xcodeproj/project.pbxproj b/FloconAndroid/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 000000000..d060c14b0 --- /dev/null +++ b/FloconAndroid/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,373 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + 945F32D77964E525ED8EF789 /* MyApplication.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyApplication.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 2222611597AAAB7FA468A2D4 /* Exceptions for "iosApp" folder in "iosApp" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = F72BA213590F4A64A9920B23 /* iosApp */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 9F68AB18AE61F38D531E138A /* iosApp */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2222611597AAAB7FA468A2D4 /* Exceptions for "iosApp" folder in "iosApp" target */, + ); + path = iosApp; + sourceTree = ""; + }; + 4CE8BEAB133D682E2FAB0FA0 /* Configuration */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Configuration; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 17300DE33A8E57440B0A39DA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 648A03B5F372A28A6911BD98 = { + isa = PBXGroup; + children = ( + 4CE8BEAB133D682E2FAB0FA0 /* Configuration */, + 9F68AB18AE61F38D531E138A /* iosApp */, + 2D6DEEEAD01D508D294BF2D1 /* Products */, + ); + sourceTree = ""; + }; + 2D6DEEEAD01D508D294BF2D1 /* Products */ = { + isa = PBXGroup; + children = ( + 945F32D77964E525ED8EF789 /* MyApplication.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F72BA213590F4A64A9920B23 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = BEF6CD0831484A55FEE5EE5B /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + 641A9F6B03025D7E56985AB4 /* Compile Kotlin Framework */, + 5ADB98277D7997E7E3EA4A03 /* Sources */, + 17300DE33A8E57440B0A39DA /* Frameworks */, + F2CA5FC4BCA7479C5529496F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 9F68AB18AE61F38D531E138A /* iosApp */, + ); + name = iosApp; + packageProductDependencies = ( + ); + productName = iosApp; + productReference = 945F32D77964E525ED8EF789 /* MyApplication.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F634829F4BFB9DAE2DCEC0CF /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + F72BA213590F4A64A9920B23 = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = 9F469B551C5AAB0B6BEBF1CC /* Build configuration list for PBXProject "iosApp" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 648A03B5F372A28A6911BD98; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 2D6DEEEAD01D508D294BF2D1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F72BA213590F4A64A9920B23 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F2CA5FC4BCA7479C5529496F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 641A9F6B03025D7E56985AB4 /* Compile Kotlin Framework */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Kotlin Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :sample-multiplatform:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5ADB98277D7997E7E3EA4A03 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 9A35A7AE2A76F92A3C969D7C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 4CE8BEAB133D682E2FAB0FA0 /* Configuration */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + E4F51F41DF48307B7966ED4C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 4CE8BEAB133D682E2FAB0FA0 /* Configuration */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A628CA29E04D790DEF8559BF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 89C9FAC4CD5DAEA923D097C1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 9F469B551C5AAB0B6BEBF1CC /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9A35A7AE2A76F92A3C969D7C /* Debug */, + E4F51F41DF48307B7966ED4C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BEF6CD0831484A55FEE5EE5B /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A628CA29E04D790DEF8559BF /* Debug */, + 89C9FAC4CD5DAEA923D097C1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F634829F4BFB9DAE2DCEC0CF /* Project object */; +} \ No newline at end of file diff --git a/FloconAndroid/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/FloconAndroid/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/FloconAndroid/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/FloconAndroid/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/florentchampigny.xcuserdatad/xcschemes/xcschememanagement.plist b/FloconAndroid/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/florentchampigny.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 000000000..ee3458dd7 --- /dev/null +++ b/FloconAndroid/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/florentchampigny.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/florentchampigny.xcuserdatad/xcschemes/iosApp.xcscheme b/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/florentchampigny.xcuserdatad/xcschemes/iosApp.xcscheme new file mode 100644 index 000000000..e18c140af --- /dev/null +++ b/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/florentchampigny.xcuserdatad/xcschemes/iosApp.xcscheme @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + diff --git a/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/florentchampigny.xcuserdatad/xcschemes/xcschememanagement.plist b/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/florentchampigny.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 000000000..fa59f97d5 --- /dev/null +++ b/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/florentchampigny.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + iosApp.xcscheme + + orderHint + 0 + + + + diff --git a/FloconAndroid/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/FloconAndroid/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/FloconAndroid/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FloconAndroid/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/FloconAndroid/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..4e8d485bf --- /dev/null +++ b/FloconAndroid/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,36 @@ +{ + "images" : [ + { + "filename" : "app-icon-1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FloconAndroid/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/FloconAndroid/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png new file mode 100644 index 000000000..53fc536fb Binary files /dev/null and b/FloconAndroid/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png differ diff --git a/FloconAndroid/iosApp/iosApp/Assets.xcassets/Contents.json b/FloconAndroid/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/FloconAndroid/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FloconAndroid/iosApp/iosApp/ContentView.swift b/FloconAndroid/iosApp/iosApp/ContentView.swift new file mode 100644 index 000000000..c765ff2a5 --- /dev/null +++ b/FloconAndroid/iosApp/iosApp/ContentView.swift @@ -0,0 +1,21 @@ +import UIKit +import SwiftUI +import ComposeApp + +struct ComposeView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + MainViewControllerKt.MainViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +struct ContentView: View { + var body: some View { + ComposeView() + .ignoresSafeArea() + } +} + + + diff --git a/FloconAndroid/iosApp/iosApp/Info.plist b/FloconAndroid/iosApp/iosApp/Info.plist new file mode 100644 index 000000000..11845e1da --- /dev/null +++ b/FloconAndroid/iosApp/iosApp/Info.plist @@ -0,0 +1,8 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/FloconAndroid/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/FloconAndroid/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/FloconAndroid/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FloconAndroid/iosApp/iosApp/iOSApp.swift b/FloconAndroid/iosApp/iosApp/iOSApp.swift new file mode 100644 index 000000000..d83dca611 --- /dev/null +++ b/FloconAndroid/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file diff --git a/FloconAndroid/ktor-interceptor/build.gradle.kts b/FloconAndroid/ktor-interceptor/build.gradle.kts index c8ff913ea..4434e9f19 100644 --- a/FloconAndroid/ktor-interceptor/build.gradle.kts +++ b/FloconAndroid/ktor-interceptor/build.gradle.kts @@ -15,12 +15,10 @@ kotlin { jvm() - /* iosX64() iosArm64() iosSimulatorArm64() - */ - + sourceSets { val commonMain by getting { dependencies { @@ -40,7 +38,6 @@ kotlin { } } - /* val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting @@ -50,7 +47,6 @@ kotlin { iosArm64Main.dependsOn(this) iosSimulatorArm64Main.dependsOn(this) } - */ } } diff --git a/FloconAndroid/sample-multiplatform/build.gradle.kts b/FloconAndroid/sample-multiplatform/build.gradle.kts index 3ad2f90cb..56992d2ba 100644 --- a/FloconAndroid/sample-multiplatform/build.gradle.kts +++ b/FloconAndroid/sample-multiplatform/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) + alias(libs.plugins.androidx.room) } kotlin { @@ -15,7 +16,7 @@ kotlin { } } } - + jvm("desktop") { compilations.all { kotlinOptions { @@ -24,12 +25,17 @@ kotlin { } } - /* - iosX64() - iosArm64() - iosSimulatorArm64() - */ - + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "ComposeApp" + isStatic = true + } + } + sourceSets { val commonMain by getting { dependencies { @@ -39,12 +45,13 @@ kotlin { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) - + // Ktor client core implementation(libs.ktor.client.core) // room implementation(libs.androidx.room.runtime) + implementation(libs.androidx.sqlite.bundled) // Compose Multiplatform implementation(compose.runtime) @@ -56,7 +63,7 @@ kotlin { implementation(libs.coil.network.ktor) } } - + val androidMain by getting { dependencies { implementation(libs.kotlinx.coroutines.android) @@ -68,7 +75,7 @@ kotlin { implementation(libs.ktor.client.okhttp) } } - + val desktopMain by getting { dependencies { // Ktor client for desktop/JVM @@ -84,7 +91,6 @@ kotlin { } } - /* val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting @@ -98,7 +104,6 @@ kotlin { implementation(libs.ktor.client.cio) } } - */ } } @@ -117,14 +122,14 @@ android { } signingConfigs { - named("debug") { + named("debug") { // just a dummy keystore to be able to test the release build keyAlias = "release" keyPassword = "release" storeFile = file("../app/release.jks") storePassword = "release" } - register("release") { + register("release") { keyAlias = "release" keyPassword = "release" storeFile = file("../app/release.jks") @@ -146,23 +151,36 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - + buildFeatures { compose = true } - + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") sourceSets["main"].res.srcDirs("src/androidMain/res") } dependencies { - ksp(libs.androidx.room.compiler) + listOf( + "kspAndroid", + "kspDesktop", + "kspIosSimulatorArm64", + "kspIosX64", + "kspIosArm64" + ).forEach { + add(it, libs.androidx.room.compiler) + } + //ksp(libs.androidx.room.compiler) +} + +room { + schemaDirectory("$projectDir/schemas") } compose.desktop { application { mainClass = "io.github.openflocon.flocon.myapplication.multi.MainKt" - + nativeDistributions { targetFormats( org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg, diff --git a/FloconAndroid/sample-multiplatform/src/androidMain/AndroidManifest.xml b/FloconAndroid/sample-multiplatform/src/androidMain/AndroidManifest.xml index 8fc30e8b4..7943fc036 100644 --- a/FloconAndroid/sample-multiplatform/src/androidMain/AndroidManifest.xml +++ b/FloconAndroid/sample-multiplatform/src/androidMain/AndroidManifest.xml @@ -5,6 +5,7 @@ { + override fun initialize(): DogDatabase +} \ No newline at end of file diff --git a/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/database/FoodDatabase.kt b/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/database/FoodDatabase.kt index 5704b6d7d..a3dca7eca 100644 --- a/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/database/FoodDatabase.kt +++ b/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/database/FoodDatabase.kt @@ -1,7 +1,9 @@ package io.github.openflocon.flocon.myapplication.multi.database +import androidx.room.ConstructedBy import androidx.room.Database import androidx.room.RoomDatabase +import androidx.room.RoomDatabaseConstructor import io.github.openflocon.flocon.myapplication.multi.database.dao.FoodDao import io.github.openflocon.flocon.myapplication.multi.database.model.FoodEntity @@ -10,7 +12,13 @@ import io.github.openflocon.flocon.myapplication.multi.database.model.FoodEntity version = 1, exportSchema = false, ) +@ConstructedBy(FoodDatabaseConstructor::class) abstract class FoodDatabase : RoomDatabase() { abstract fun foodDao(): FoodDao } +// room will generate the constructor +@Suppress("KotlinNoActualForExpect") +expect object FoodDatabaseConstructor : RoomDatabaseConstructor { + override fun initialize(): FoodDatabase +} \ No newline at end of file diff --git a/FloconAndroid/sample-multiplatform/src/iosMain/kotlin/com/florent37/myapplication/Databases.kt b/FloconAndroid/sample-multiplatform/src/iosMain/kotlin/com/florent37/myapplication/Databases.kt new file mode 100644 index 000000000..883a94170 --- /dev/null +++ b/FloconAndroid/sample-multiplatform/src/iosMain/kotlin/com/florent37/myapplication/Databases.kt @@ -0,0 +1,55 @@ +package com.florent37.myapplication + +import androidx.room.Room +import androidx.sqlite.driver.NativeSQLiteDriver +import io.github.openflocon.flocon.myapplication.multi.database.DogDatabase +import io.github.openflocon.flocon.myapplication.multi.database.FoodDatabase +import io.github.openflocon.flocon.plugins.database.floconRegisterDatabase +import kotlinx.cinterop.ExperimentalForeignApi +import platform.Foundation.NSDocumentDirectory +import platform.Foundation.NSFileManager +import platform.Foundation.NSUserDomainMask + +object Databases { + private var dogDatabase: DogDatabase? = null + + fun getDogDatabase(): DogDatabase { + val dbFile = "${documentDirectory()}/dog_database.db" + floconRegisterDatabase(absolutePath = dbFile, displayName = "Dog Database") + return dogDatabase ?: run { + val instance = Room.databaseBuilder( + name = dbFile, + ).fallbackToDestructiveMigration( + dropAllTables = true + ).setDriver(NativeSQLiteDriver()).build() + dogDatabase = instance + instance + } + } + + private var foodDatabase: FoodDatabase? = null + + fun getFoodDatabase(): FoodDatabase { + val dbFile = "${documentDirectory()}/food_database.db" + floconRegisterDatabase(absolutePath = dbFile, displayName = "Food Database") + return foodDatabase ?: run { + val instance = Room.databaseBuilder( + name = dbFile, + ).setDriver(NativeSQLiteDriver()).build() + foodDatabase = instance + instance + } + } +} + +@OptIn(ExperimentalForeignApi::class) +private fun documentDirectory(): String { + val documentDirectory = NSFileManager.defaultManager.URLForDirectory( + directory = NSDocumentDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = false, + error = null, + ) + return requireNotNull(documentDirectory?.path) +} \ No newline at end of file diff --git a/FloconAndroid/sample-multiplatform/src/iosMain/kotlin/com/florent37/myapplication/MainViewController.kt b/FloconAndroid/sample-multiplatform/src/iosMain/kotlin/com/florent37/myapplication/MainViewController.kt new file mode 100644 index 000000000..a29b13ec8 --- /dev/null +++ b/FloconAndroid/sample-multiplatform/src/iosMain/kotlin/com/florent37/myapplication/MainViewController.kt @@ -0,0 +1,32 @@ +package com.florent37.myapplication + +import androidx.compose.ui.window.ComposeUIViewController +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.ktor.FloconKtorPlugin +import io.github.openflocon.flocon.myapplication.multi.DummyHttpKtorCaller +import io.github.openflocon.flocon.myapplication.multi.database.initializeDatabases +import io.github.openflocon.flocon.myapplication.multi.ui.App +import io.ktor.client.HttpClient +import io.ktor.client.engine.darwin.Darwin + +fun MainViewController() = ComposeUIViewController( + configure = { + Flocon.initialize() + + initializeDatabases( + dogDatabase = Databases.getDogDatabase(), + foodDatabase = Databases.getFoodDatabase(), + ) + + val ktorClient = HttpClient(Darwin) { + install(FloconKtorPlugin) { + isImage = { + it.request.url.toString().contains("picsum.photos") + } + } + } + DummyHttpKtorCaller.initialize(ktorClient) + } +) { + App() +} \ No newline at end of file