diff --git a/.github/workflows/verifyPr.yaml b/.github/workflows/verifyPr.yaml index 81b10c8a..8851ef42 100644 --- a/.github/workflows/verifyPr.yaml +++ b/.github/workflows/verifyPr.yaml @@ -93,3 +93,27 @@ jobs: with: name: failed-screenshot-tests path: app-screenshot-tests/build/paparazzi/failures + + build-ios: + runs-on: "macos-latest" + concurrency: + group: ${{ github.workflow }}-ios-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + lfs: false + submodules: recursive + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + java-version: '21' + distribution: temurin + - uses: burrunan/gradle-cache-action@663fbad34e03c8f12b27f4999ac46e3d90f87eca + with: + debug: false + concurrent: true + read-only: true + - name: Compile shared iOS framework + run: "./gradlew :app-ios:linkDebugFrameworkIosSimulatorArm64" diff --git a/app-ios/.gitignore b/app-ios/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/app-ios/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app-ios/build.gradle.kts b/app-ios/build.gradle.kts new file mode 100644 index 00000000..2b231912 --- /dev/null +++ b/app-ios/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("org.jetbrains.kotlin.multiplatform") + id("org.jetbrains.kotlin.plugin.compose") + id("org.jetbrains.compose") +} + +kotlin { + listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target -> + target.binaries.framework { + baseName = "Shared" + } + } + + sourceSets { + iosMain.dependencies { + implementation(libs.compose.runtime) + implementation(libs.compose.foundation) + implementation(libs.compose.material3) + implementation(libs.compose.ui) + } + } +} diff --git a/app-ios/src/iosMain/kotlin/com/matejdro/micropebble/ios/MainViewController.kt b/app-ios/src/iosMain/kotlin/com/matejdro/micropebble/ios/MainViewController.kt new file mode 100644 index 00000000..973eddd0 --- /dev/null +++ b/app-ios/src/iosMain/kotlin/com/matejdro/micropebble/ios/MainViewController.kt @@ -0,0 +1,21 @@ +package com.matejdro.micropebble.ios + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.ComposeUIViewController +import platform.UIKit.UIViewController + +fun MainViewController(): UIViewController = ComposeUIViewController { + MaterialTheme { + Surface(modifier = Modifier.fillMaxSize()) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("microPebble — running on iOS") + } + } + } +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 4d2eddc2..e3defcad 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(libs.kotlin.plugin) implementation(libs.kotlin.plugin.compose) implementation(libs.kotlin.plugin.serialization) + implementation(libs.compose.gradlePlugin) implementation(libs.kotlinova.gradle) implementation(libs.metro.plugin) implementation(libs.moduleGraphAssert) diff --git a/buildSrc/src/main/kotlin/library-kmp-module.gradle.kts b/buildSrc/src/main/kotlin/library-kmp-module.gradle.kts new file mode 100644 index 00000000..12076b88 --- /dev/null +++ b/buildSrc/src/main/kotlin/library-kmp-module.gradle.kts @@ -0,0 +1,54 @@ +import dev.detekt.gradle.Detekt +import org.gradle.accessors.dm.LibrariesForLibs +import tasks.setupTooManyKotlinFilesTaskForCommon + +val libs = the() + +plugins { + id("org.jetbrains.kotlin.multiplatform") + id("com.android.kotlin.multiplatform.library") + id("org.jetbrains.kotlin.plugin.compose") + id("org.jetbrains.compose") + + id("checks") +} + +val isMac = System.getProperty("os.name").contains("mac", ignoreCase = true) + +kotlin { + jvmToolchain(21) + + androidLibrary { + // Modules with resources must override this. + namespace = "com.matejdro.micropebble.noresources" + compileSdk = 36 + minSdk = 30 + } + + if (isMac) { + iosX64() + iosArm64() + iosSimulatorArm64() + } + + compilerOptions { + optIn.add("kotlin.time.ExperimentalTime") + optIn.add("kotlin.uuid.ExperimentalUuidApi") + optIn.add("kotlin.ExperimentalUnsignedTypes") + freeCompilerArgs.add("-Xannotation-default-target=param-property") + } + + sourceSets { + commonMain.dependencies { + implementation(libs.compose.runtime) + } + } +} + +if (name.startsWith("common-")) { + setupTooManyKotlinFilesTaskForCommon() +} + +tasks.register("runDebugDetekt") { + dependsOn(tasks.withType()) +} diff --git a/buildSrc/src/main/kotlin/util/PluginAccessors.kt b/buildSrc/src/main/kotlin/util/PluginAccessors.kt index f27c80ef..fdd6d424 100644 --- a/buildSrc/src/main/kotlin/util/PluginAccessors.kt +++ b/buildSrc/src/main/kotlin/util/PluginAccessors.kt @@ -16,6 +16,9 @@ inline val PluginDependenciesSpec.androidAppModule: PluginDependencySpec inline val PluginDependenciesSpec.androidLibraryModule: PluginDependencySpec get() = id("library-android-module") +inline val PluginDependenciesSpec.kmpLibraryModule: PluginDependencySpec + get() = id("library-kmp-module") + inline val PluginDependenciesSpec.pureKotlinModule: PluginDependencySpec get() = id("pure-kotlin-module") diff --git a/common-navigation/build.gradle.kts b/common-navigation/build.gradle.kts index 2062b4e8..0e37260e 100644 --- a/common-navigation/build.gradle.kts +++ b/common-navigation/build.gradle.kts @@ -1,14 +1,21 @@ plugins { - androidLibraryModule - compose - parcelize - serialization + kmpLibraryModule + id("org.jetbrains.kotlin.plugin.serialization") } -dependencies { - api(libs.kotlinova.navigation) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.core) - implementation(libs.androidx.compose.material3.sizeClasses) - implementation(libs.kotlinova.compose) +kotlin { + androidLibrary { + namespace = "com.matejdro.micropebble.navigation" + } + + sourceSets { + androidMain.dependencies { + api(libs.kotlinova.navigation) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.core) + implementation(libs.kotlin.serialization.core) + implementation(libs.compose.animation) + implementation(libs.compose.ui) + } + } } diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/animation/PredictiveBackFadeAnimationSpec.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/animation/PredictiveBackFadeAnimationSpec.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/animation/PredictiveBackFadeAnimationSpec.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/animation/PredictiveBackFadeAnimationSpec.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/instructions/ReplaceTabContentWith.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/instructions/ReplaceTabContentWith.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/instructions/ReplaceTabContentWith.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/instructions/ReplaceTabContentWith.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreSourcesScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreSourcesScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreSourcesScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/AppstoreSourcesScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/BluetoothScanScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/BluetoothScanScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/BluetoothScanScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/BluetoothScanScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/CalendarListScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/CalendarListScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/CalendarListScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/CalendarListScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/DeveloperConnectionScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/DeveloperConnectionScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/DeveloperConnectionScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/DeveloperConnectionScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/FirmwareUpdateScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/FirmwareUpdateScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/FirmwareUpdateScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/FirmwareUpdateScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/HomeScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/HomeScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/HomeScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/HomeScreenKey.kt diff --git a/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt new file mode 100644 index 00000000..c333f2c4 --- /dev/null +++ b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt @@ -0,0 +1,11 @@ +package com.matejdro.micropebble.navigation.keys + +import com.matejdro.micropebble.navigation.keys.base.Tab +import com.matejdro.micropebble.navigation.keys.base.TabKey +import kotlinx.serialization.Serializable +import si.inova.kotlinova.navigation.screenkeys.ScreenKey + +@Serializable +data object NotificationAppListKey : ScreenKey(), TabKey { + override val tab get() = Tab.NOTIFICATIONS +} diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/OnboardingKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/OnboardingKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/OnboardingKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/OnboardingKey.kt diff --git a/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt new file mode 100644 index 00000000..ae047f5f --- /dev/null +++ b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt @@ -0,0 +1,11 @@ +package com.matejdro.micropebble.navigation.keys + +import com.matejdro.micropebble.navigation.keys.base.Tab +import com.matejdro.micropebble.navigation.keys.base.TabKey +import kotlinx.serialization.Serializable +import si.inova.kotlinova.navigation.screenkeys.ScreenKey + +@Serializable +data object WatchListKey : ScreenKey(), TabKey { + override val tab get() = Tab.WATCHES +} diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchSettingsScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchSettingsScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchSettingsScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchSettingsScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt similarity index 66% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt index f0c66507..03ad4144 100644 --- a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt +++ b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WatchappListKey.kt @@ -1,5 +1,7 @@ package com.matejdro.micropebble.navigation.keys +import com.matejdro.micropebble.navigation.keys.base.Tab +import com.matejdro.micropebble.navigation.keys.base.TabKey import com.matejdro.micropebble.navigation.keys.common.InputFile import kotlinx.serialization.Serializable import si.inova.kotlinova.navigation.screenkeys.ScreenKey @@ -7,7 +9,9 @@ import si.inova.kotlinova.navigation.screenkeys.ScreenKey @Serializable data class WatchappListKey( val pbwFile: InputFile? = null, -) : ScreenKey() { +) : ScreenKey(), TabKey { + override val tab get() = Tab.WATCH_APPS + override fun getScopeTag(): String { return this::class.java.name } diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WebservicesAuthScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WebservicesAuthScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WebservicesAuthScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/WebservicesAuthScreenKey.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/base/BaseScreenKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/BaseScreenKey.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/base/BaseScreenKey.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/BaseScreenKey.kt diff --git a/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt new file mode 100644 index 00000000..426871af --- /dev/null +++ b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt @@ -0,0 +1,6 @@ +package com.matejdro.micropebble.navigation.keys.base + +/** + * Key for a screen that shows a list of tabs + container. Container will be filled from the next entry on the backstack. + */ +interface TabContainerKey diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/common/InputFile.kt b/common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/common/InputFile.kt similarity index 100% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/common/InputFile.kt rename to common-navigation/src/androidMain/kotlin/com/matejdro/micropebble/navigation/keys/common/InputFile.kt diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt b/common-navigation/src/commonMain/kotlin/com/matejdro/micropebble/navigation/keys/base/Tab.kt similarity index 50% rename from common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt rename to common-navigation/src/commonMain/kotlin/com/matejdro/micropebble/navigation/keys/base/Tab.kt index 693be87c..19e83823 100644 --- a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/base/TabContainerKey.kt +++ b/common-navigation/src/commonMain/kotlin/com/matejdro/micropebble/navigation/keys/base/Tab.kt @@ -2,13 +2,20 @@ package com.matejdro.micropebble.navigation.keys.base import androidx.compose.runtime.Composable import androidx.compose.runtime.staticCompositionLocalOf -import si.inova.kotlinova.navigation.screenkeys.ScreenKey -/** - * Key for a screen that shows a list of tabs + container. Container will be filled from the next entry on the backstack. - */ -interface TabContainerKey +enum class Tab { + WATCHES, + WATCH_APPS, + NOTIFICATIONS, + TOOLS, +} + +/** Implemented by screen keys that act as a top-level tab destination. */ +interface TabKey { + val tab: Tab +} val LocalSelectedTabContent = staticCompositionLocalOf { error("SelectedTabContent not provided") } -data class SelectedTabContent(val content: @Composable () -> Unit, val key: ScreenKey, val contentKey: Any) +/** Content shown inside a tab container, plus which [Tab] is currently selected (null when none is). */ +data class SelectedTabContent(val content: @Composable () -> Unit, val tab: Tab?, val contentKey: Any) diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt b/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt deleted file mode 100644 index 9643d748..00000000 --- a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/NotificationAppListKey.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.matejdro.micropebble.navigation.keys - -import kotlinx.serialization.Serializable -import si.inova.kotlinova.navigation.screenkeys.ScreenKey - -@Serializable -data object NotificationAppListKey : ScreenKey() diff --git a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt b/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt deleted file mode 100644 index 12f699a7..00000000 --- a/common-navigation/src/main/kotlin/com/matejdro/micropebble/navigation/keys/WatchListKey.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.matejdro.micropebble.navigation.keys - -import kotlinx.serialization.Serializable -import si.inova.kotlinova.navigation.screenkeys.ScreenKey - -@Serializable -data object WatchListKey : ScreenKey() diff --git a/config/libs.toml b/config/libs.toml index d8d33910..7b905f27 100644 --- a/config/libs.toml +++ b/config/libs.toml @@ -21,6 +21,10 @@ androidx-test-runner = "1.7.0" androidx-work = "2.11.2" coil = "3.4.0" composeDnd = "0.4.0" +# @pin CMP material3 tracks its own version; 1.10.0-alpha05 matches libpebble3 (1.11.x alpha is broken) +composeMaterial3 = "1.10.0-alpha05" +# @pin needs to match libpebble3's Compose Multiplatform version +composeMultiplatform = "1.10.1" composePreference = "2.2.0" composeWebview = "0.33.6" dependencyAnalysis = "3.15.0" @@ -102,6 +106,15 @@ androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } composeDnd = { module = "com.mohamedrejeb.dnd:compose-dnd", version.ref = "composeDnd" } +# Compose Multiplatform artifacts (used by KMP modules; android variants relocate to androidx.compose) +compose-animation = { module = "org.jetbrains.compose.animation:animation", version.ref = "composeMultiplatform" } +compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" } +compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" } +# Compose Multiplatform Gradle plugin, used by the library-kmp-module convention plugin in buildSrc +compose-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "composeMultiplatform" } +compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMaterial3" } +compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } +compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } composePreference = { module = "me.zhanghai.compose.preference:preference", version.ref = "composePreference" } composeWebview = { module = "io.github.kevinnzou:compose-webview", version.ref = "composeWebview" } dependencyAnalysis = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" } diff --git a/home/ui/build.gradle.kts b/home/ui/build.gradle.kts index 92d83750..117f30f4 100644 --- a/home/ui/build.gradle.kts +++ b/home/ui/build.gradle.kts @@ -1,33 +1,74 @@ plugins { - androidLibraryModule - compose - di - navigation - parcelize - serialization - showkase + kmpLibraryModule + id("dev.zacsweers.metro") + id("com.google.devtools.ksp") + id("org.jetbrains.kotlin.plugin.serialization") } -android { - namespace = "com.matejdro.micropebble.home.ui" - androidResources.enable = true +kotlin { + androidLibrary { + namespace = "com.matejdro.micropebble.home.ui" + androidResources.enable = true + } + + sourceSets { + commonMain.dependencies { + implementation(projects.commonNavigation) + implementation(libs.compose.foundation) + implementation(libs.compose.material3) + implementation(libs.compose.animation) + implementation(libs.compose.ui) + implementation(libs.compose.components.resources) + } + + androidMain { + languageSettings.optIn("com.google.accompanist.permissions.ExperimentalPermissionsApi") + + dependencies { + api(projects.notification.api) + api(projects.common) + api(projects.logging.api) + api(projects.voice.api) + api(libs.androidx.core) + api(libs.kotlinova.core) + api(libs.kotlinova.navigation) + + implementation(projects.sharedResources) + implementation(projects.commonCompose) + implementation(libs.accompanist.permissions) + implementation(libs.composePreference) + implementation(libs.kotlin.coroutines) + implementation(libs.kotlin.serialization.core) + implementation(libs.dispatch) + implementation(libs.androidx.compose.material3.sizeClasses) + implementation(libs.libpebble3) + implementation(libs.showkase) + + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.lifecycle.compose) + implementation(libs.kotlinova.compose) + } + } + } +} + +val stableClassesFile = project.layout.settingsDirectory.file("config/global_compose_stable_classes.txt") +composeCompiler { + stabilityConfigurationFiles.add(stableClassesFile) +} + +compose.resources { + packageOfResClass = "com.matejdro.micropebble.home.resources" +} + +ksp { + arg("skipPrivatePreviews", "true") } dependencies { - api(projects.notification.api) - api(projects.common) - api(projects.logging.api) - api(projects.voice.api) - api(libs.androidx.core) - api(libs.kotlinova.core) - api(libs.kotlinova.navigation) - - implementation(projects.sharedResources) - implementation(projects.commonCompose) - implementation(libs.accompanist.permissions) - implementation(libs.composePreference) - implementation(libs.kotlin.coroutines) - implementation(libs.dispatch) - implementation(libs.androidx.compose.material3.sizeClasses) - implementation(libs.libpebble3) + add("kspAndroid", libs.showkase.processor) + add("kspAndroid", libs.kotlinova.navigation.compiler) } diff --git a/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/HomeScreen.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/HomeScreen.kt new file mode 100644 index 00000000..d29cc5d3 --- /dev/null +++ b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/HomeScreen.kt @@ -0,0 +1,95 @@ +package com.matejdro.micropebble.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import com.airbnb.android.showkase.annotation.ShowkaseComposable +import com.matejdro.micropebble.navigation.instructions.ReplaceTabContentWith +import com.matejdro.micropebble.navigation.keys.HomeScreenKey +import com.matejdro.micropebble.navigation.keys.NotificationAppListKey +import com.matejdro.micropebble.navigation.keys.WatchListKey +import com.matejdro.micropebble.navigation.keys.WatchappListKey +import com.matejdro.micropebble.navigation.keys.base.LocalSelectedTabContent +import com.matejdro.micropebble.navigation.keys.base.SelectedTabContent +import com.matejdro.micropebble.navigation.keys.base.Tab +import com.matejdro.micropebble.tools.ToolsScreenKey +import com.matejdro.micropebble.ui.debugging.FullScreenPreviews +import com.matejdro.micropebble.ui.debugging.PreviewTheme +import si.inova.kotlinova.navigation.navigator.Navigator +import si.inova.kotlinova.navigation.screenkeys.ScreenKey +import si.inova.kotlinova.navigation.screens.InjectNavigationScreen +import si.inova.kotlinova.navigation.screens.Screen + +@InjectNavigationScreen +class HomeScreen( + private val navigator: Navigator, +) : Screen() { + @Composable + override fun Content(key: HomeScreenKey) { + Surface { + HomeScreenContent( + selectedContent = LocalSelectedTabContent.current, + switchTab = { navigator.navigate(ReplaceTabContentWith(it.toScreenKey())) }, + ) + } + } +} + +private fun Tab.toScreenKey(): ScreenKey = when (this) { + Tab.WATCHES -> WatchListKey + Tab.WATCH_APPS -> WatchappListKey() + Tab.NOTIFICATIONS -> NotificationAppListKey + Tab.TOOLS -> ToolsScreenKey +} + +@FullScreenPreviews +@Composable +@ShowkaseComposable(group = "Test") +internal fun HomePhonePreview() { + PreviewTheme { + HomeScreenContent( + selectedContent = SelectedTabContent( + { + Box( + Modifier + .fillMaxSize() + .background(Color.Red) + ) + }, + Tab.WATCH_APPS, + "" + ), + tabletMode = false, + switchTab = {}, + ) + } +} + +@Preview(device = Devices.TABLET) +@Composable +@ShowkaseComposable(group = "Test") +internal fun HomeTabletPreview() { + PreviewTheme { + HomeScreenContent( + selectedContent = SelectedTabContent( + { + Box( + Modifier + .fillMaxSize() + .background(Color.Red) + ) + }, + Tab.WATCH_APPS, + "" + ), + tabletMode = true, + switchTab = {}, + ) + } +} diff --git a/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/WindowSize.android.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/WindowSize.android.kt new file mode 100644 index 00000000..56f139ed --- /dev/null +++ b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/home/WindowSize.android.kt @@ -0,0 +1,15 @@ +package com.matejdro.micropebble.home + +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import si.inova.kotlinova.core.activity.requireActivity + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@Composable +actual fun isExpandedWidth(): Boolean { + val sizeClass = calculateWindowSizeClass(activity = LocalContext.current.requireActivity()) + return sizeClass.widthSizeClass == WindowWidthSizeClass.Expanded +} diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreen.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreen.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreen.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreen.kt diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreenViewModel.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreenViewModel.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreenViewModel.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/onboarding/OnboardingScreenViewModel.kt diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreen.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsScreen.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreen.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsScreen.kt diff --git a/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt new file mode 100644 index 00000000..77b2713c --- /dev/null +++ b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt @@ -0,0 +1,11 @@ +package com.matejdro.micropebble.tools + +import com.matejdro.micropebble.navigation.keys.base.Tab +import com.matejdro.micropebble.navigation.keys.base.TabKey +import kotlinx.serialization.Serializable +import si.inova.kotlinova.navigation.screenkeys.ScreenKey + +@Serializable +data object ToolsScreenKey : ScreenKey(), TabKey { + override val tab get() = Tab.TOOLS +} diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsViewModel.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsViewModel.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsViewModel.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/tools/ToolsViewModel.kt diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsScreen.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsScreen.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsScreen.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsScreen.kt diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsViewModel.kt b/home/ui/src/androidMain/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsViewModel.kt similarity index 100% rename from home/ui/src/main/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsViewModel.kt rename to home/ui/src/androidMain/kotlin/com/matejdro/micropebble/watchsettings/WatchSettingsViewModel.kt diff --git a/home/ui/src/main/res/drawable/appstore_sources.xml b/home/ui/src/androidMain/res/drawable/appstore_sources.xml similarity index 100% rename from home/ui/src/main/res/drawable/appstore_sources.xml rename to home/ui/src/androidMain/res/drawable/appstore_sources.xml diff --git a/home/ui/src/main/res/drawable/calendar_settings.xml b/home/ui/src/androidMain/res/drawable/calendar_settings.xml similarity index 100% rename from home/ui/src/main/res/drawable/calendar_settings.xml rename to home/ui/src/androidMain/res/drawable/calendar_settings.xml diff --git a/home/ui/src/main/res/drawable/developer_connection.xml b/home/ui/src/androidMain/res/drawable/developer_connection.xml similarity index 100% rename from home/ui/src/main/res/drawable/developer_connection.xml rename to home/ui/src/androidMain/res/drawable/developer_connection.xml diff --git a/home/ui/src/main/res/drawable/logs.xml b/home/ui/src/androidMain/res/drawable/logs.xml similarity index 100% rename from home/ui/src/main/res/drawable/logs.xml rename to home/ui/src/androidMain/res/drawable/logs.xml diff --git a/home/ui/src/main/res/drawable/permissions.xml b/home/ui/src/androidMain/res/drawable/permissions.xml similarity index 100% rename from home/ui/src/main/res/drawable/permissions.xml rename to home/ui/src/androidMain/res/drawable/permissions.xml diff --git a/home/ui/src/main/res/drawable/watches.xml b/home/ui/src/androidMain/res/drawable/watches.xml similarity index 100% rename from home/ui/src/main/res/drawable/watches.xml rename to home/ui/src/androidMain/res/drawable/watches.xml diff --git a/home/ui/src/main/res/drawable/webservices_auth.xml b/home/ui/src/androidMain/res/drawable/webservices_auth.xml similarity index 100% rename from home/ui/src/main/res/drawable/webservices_auth.xml rename to home/ui/src/androidMain/res/drawable/webservices_auth.xml diff --git a/home/ui/src/main/res/values/strings.xml b/home/ui/src/androidMain/res/values/strings.xml similarity index 94% rename from home/ui/src/main/res/values/strings.xml rename to home/ui/src/androidMain/res/values/strings.xml index d266cb56..09cf81a9 100644 --- a/home/ui/src/main/res/values/strings.xml +++ b/home/ui/src/androidMain/res/values/strings.xml @@ -1,6 +1,5 @@ - Watchapps Grant Open Settings Permissions @@ -24,9 +23,6 @@ Calendar Needed to sync calendar to the Timeline Developer connection - Watches - Notifications - Tools MicroPebble version %1$s Save logs Voice recording diff --git a/home/ui/src/main/res/drawable/notifications.xml b/home/ui/src/commonMain/composeResources/drawable/notifications.xml similarity index 95% rename from home/ui/src/main/res/drawable/notifications.xml rename to home/ui/src/commonMain/composeResources/drawable/notifications.xml index c8c69fe3..6bbd3f68 100644 --- a/home/ui/src/main/res/drawable/notifications.xml +++ b/home/ui/src/commonMain/composeResources/drawable/notifications.xml @@ -1,7 +1,7 @@ - diff --git a/home/ui/src/main/res/drawable/tools.xml b/home/ui/src/commonMain/composeResources/drawable/tools.xml similarity index 100% rename from home/ui/src/main/res/drawable/tools.xml rename to home/ui/src/commonMain/composeResources/drawable/tools.xml diff --git a/home/ui/src/main/res/drawable/watchapps.xml b/home/ui/src/commonMain/composeResources/drawable/watchapps.xml similarity index 96% rename from home/ui/src/main/res/drawable/watchapps.xml rename to home/ui/src/commonMain/composeResources/drawable/watchapps.xml index b55ed7f7..86d1084f 100644 --- a/home/ui/src/main/res/drawable/watchapps.xml +++ b/home/ui/src/commonMain/composeResources/drawable/watchapps.xml @@ -1,7 +1,7 @@ - diff --git a/home/ui/src/commonMain/composeResources/drawable/watches.xml b/home/ui/src/commonMain/composeResources/drawable/watches.xml new file mode 100644 index 00000000..913d61ea --- /dev/null +++ b/home/ui/src/commonMain/composeResources/drawable/watches.xml @@ -0,0 +1,6 @@ + + + + diff --git a/home/ui/src/commonMain/composeResources/values/strings.xml b/home/ui/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 00000000..8a1d3aa2 --- /dev/null +++ b/home/ui/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,7 @@ + + + Watches + Watchapps + Notifications + Tools + diff --git a/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeScreenContent.kt b/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeScreenContent.kt new file mode 100644 index 00000000..de85b0a1 --- /dev/null +++ b/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeScreenContent.kt @@ -0,0 +1,135 @@ +package com.matejdro.micropebble.home + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.matejdro.micropebble.home.resources.Res +import com.matejdro.micropebble.home.resources.notifications +import com.matejdro.micropebble.home.resources.tools +import com.matejdro.micropebble.home.resources.watch_apps +import com.matejdro.micropebble.home.resources.watchapps +import com.matejdro.micropebble.home.resources.watches +import com.matejdro.micropebble.navigation.keys.base.SelectedTabContent +import com.matejdro.micropebble.navigation.keys.base.Tab +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +@Composable +fun HomeScreenContent( + selectedContent: SelectedTabContent, + switchTab: (Tab) -> Unit, + tabletMode: Boolean = isExpandedWidth(), +) { + val tabs = homeTabs() + val animatedMainContent: @Composable () -> Unit = { + AnimatedContent( + selectedContent, + contentKey = { entry -> entry.contentKey }, + transitionSpec = { fadeIn() togetherWith fadeOut() } + ) { + it.content() + } + } + if (tabletMode) { + NavigationRailContent(animatedMainContent, tabs, selectedContent.tab, switchTab) + } else { + NavigationBarContent(animatedMainContent, tabs, selectedContent.tab, switchTab) + } +} + +@Composable +private fun homeTabs(): List { + val watchesIcon = painterResource(Res.drawable.watches) + val watchAppsIcon = painterResource(Res.drawable.watchapps) + val notificationsIcon = painterResource(Res.drawable.notifications) + val toolsIcon = painterResource(Res.drawable.tools) + val watchesLabel = stringResource(Res.string.watches) + val watchAppsLabel = stringResource(Res.string.watch_apps) + val notificationsLabel = stringResource(Res.string.notifications) + val toolsLabel = stringResource(Res.string.tools) + + return remember( + watchesIcon, watchAppsIcon, notificationsIcon, toolsIcon, + watchesLabel, watchAppsLabel, notificationsLabel, toolsLabel, + ) { + listOf( + HomeTab(Tab.WATCHES, watchesIcon, watchesLabel), + HomeTab(Tab.WATCH_APPS, watchAppsIcon, watchAppsLabel), + HomeTab(Tab.NOTIFICATIONS, notificationsIcon, notificationsLabel), + HomeTab(Tab.TOOLS, toolsIcon, toolsLabel), + ) + } +} + +@Composable +private fun NavigationBarContent( + mainContent: @Composable () -> Unit, + tabs: List, + selectedTab: Tab?, + switchTab: (Tab) -> Unit, +) { + Column { + Box( + Modifier + .fillMaxWidth() + .weight(1f) + ) { + mainContent() + } + + NavigationBar { + tabs.forEach { tab -> + NavigationBarItem( + selected = selectedTab == tab.tab, + onClick = { switchTab(tab.tab) }, + icon = { Icon(painter = tab.icon, contentDescription = null) }, + label = { Text(tab.label) } + ) + } + } + } +} + +@Composable +private fun NavigationRailContent( + mainContent: @Composable () -> Unit, + tabs: List, + selectedTab: Tab?, + switchTab: (Tab) -> Unit, +) { + Row { + NavigationRail { + tabs.forEach { tab -> + NavigationRailItem( + selected = selectedTab == tab.tab, + onClick = { switchTab(tab.tab) }, + icon = { Icon(painter = tab.icon, contentDescription = null) }, + label = { Text(tab.label) } + ) + } + } + + Box( + Modifier + .fillMaxHeight() + .weight(1f) + ) { + mainContent() + } + } +} diff --git a/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeTab.kt b/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeTab.kt new file mode 100644 index 00000000..6052a0f3 --- /dev/null +++ b/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/HomeTab.kt @@ -0,0 +1,6 @@ +package com.matejdro.micropebble.home + +import androidx.compose.ui.graphics.painter.Painter +import com.matejdro.micropebble.navigation.keys.base.Tab + +data class HomeTab(val tab: Tab, val icon: Painter, val label: String) diff --git a/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/WindowSize.kt b/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/WindowSize.kt new file mode 100644 index 00000000..5d6b1056 --- /dev/null +++ b/home/ui/src/commonMain/kotlin/com/matejdro/micropebble/home/WindowSize.kt @@ -0,0 +1,7 @@ +package com.matejdro.micropebble.home + +import androidx.compose.runtime.Composable + +/** True when the available width is the Material3 "Expanded" class (tablet-style two-pane layout). */ +@Composable +expect fun isExpandedWidth(): Boolean diff --git a/home/ui/src/iosMain/kotlin/com/matejdro/micropebble/home/WindowSize.ios.kt b/home/ui/src/iosMain/kotlin/com/matejdro/micropebble/home/WindowSize.ios.kt new file mode 100644 index 00000000..ff17db67 --- /dev/null +++ b/home/ui/src/iosMain/kotlin/com/matejdro/micropebble/home/WindowSize.ios.kt @@ -0,0 +1,16 @@ +package com.matejdro.micropebble.home + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.unit.dp + +// Material3 "Expanded" starts at 840dp wide. +private val EXPANDED_WIDTH_BREAKPOINT = 840.dp + +@Composable +actual fun isExpandedWidth(): Boolean { + val containerSize = LocalWindowInfo.current.containerSize + val widthDp = with(LocalDensity.current) { containerSize.width.toDp() } + return widthDp >= EXPANDED_WIDTH_BREAKPOINT +} diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/home/HomeScreen.kt b/home/ui/src/main/kotlin/com/matejdro/micropebble/home/HomeScreen.kt deleted file mode 100644 index 39f5fd30..00000000 --- a/home/ui/src/main/kotlin/com/matejdro/micropebble/home/HomeScreen.kt +++ /dev/null @@ -1,229 +0,0 @@ -package com.matejdro.micropebble.home - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationRail -import androidx.compose.material3.NavigationRailItem -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import com.airbnb.android.showkase.annotation.ShowkaseComposable -import com.matejdro.micropebble.home.ui.R -import com.matejdro.micropebble.navigation.instructions.ReplaceTabContentWith -import com.matejdro.micropebble.navigation.keys.HomeScreenKey -import com.matejdro.micropebble.navigation.keys.NotificationAppListKey -import com.matejdro.micropebble.navigation.keys.WatchListKey -import com.matejdro.micropebble.navigation.keys.WatchappListKey -import com.matejdro.micropebble.navigation.keys.base.LocalSelectedTabContent -import com.matejdro.micropebble.navigation.keys.base.SelectedTabContent -import com.matejdro.micropebble.tools.ToolsScreenKey -import com.matejdro.micropebble.ui.debugging.FullScreenPreviews -import com.matejdro.micropebble.ui.debugging.PreviewTheme -import si.inova.kotlinova.core.activity.requireActivity -import si.inova.kotlinova.navigation.navigator.Navigator -import si.inova.kotlinova.navigation.screenkeys.ScreenKey -import si.inova.kotlinova.navigation.screens.InjectNavigationScreen -import si.inova.kotlinova.navigation.screens.Screen - -@InjectNavigationScreen -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) -class HomeScreen( - private val navigator: Navigator, -) : Screen() { - @Composable - override fun Content(key: HomeScreenKey) { - val sizeClass = calculateWindowSizeClass(activity = LocalContext.current.requireActivity()) - - Surface { - HomeScreenContent( - LocalSelectedTabContent.current, - tabletMode = sizeClass.widthSizeClass == WindowWidthSizeClass.Expanded, - switchScreen = { navigator.navigate(ReplaceTabContentWith(it)) }, - ) - } - } -} - -@Composable -private fun HomeScreenContent( - selectedTab: SelectedTabContent, - tabletMode: Boolean, - switchScreen: (ScreenKey) -> Unit, -) { - val animatedMainContent: @Composable () -> Unit = { - AnimatedContent( - selectedTab, - contentKey = { entry -> entry.contentKey }, - transitionSpec = { fadeIn() togetherWith fadeOut() } - ) { - it.content() - } - } - if (tabletMode) { - NavigationRailContent(animatedMainContent, selectedTab.key, switchScreen) - } else { - NavigationBarContent(animatedMainContent, selectedTab.key, switchScreen) - } -} - -@Composable -private fun NavigationBarContent( - mainContent: @Composable () -> Unit, - selectedScreen: ScreenKey, - switchScreen: (ScreenKey) -> Unit, -) { - Column { - Box( - Modifier - .fillMaxWidth() - .weight(1f) - ) { - mainContent() - } - - NavigationBar { - NavigationBarItem( - selected = selectedScreen is WatchListKey, - onClick = { switchScreen(WatchListKey) }, - icon = { Icon(painter = painterResource(id = R.drawable.watches), contentDescription = null) }, - label = { Text(stringResource(R.string.watches)) } - ) - - NavigationBarItem( - selected = selectedScreen is WatchappListKey, - onClick = { switchScreen(WatchappListKey()) }, - icon = { Icon(painter = painterResource(id = R.drawable.watchapps), contentDescription = null) }, - label = { Text(stringResource(R.string.watch_apps)) } - ) - - NavigationBarItem( - selected = selectedScreen is NotificationAppListKey, - onClick = { switchScreen(NotificationAppListKey) }, - icon = { Icon(painter = painterResource(id = R.drawable.notifications), contentDescription = null) }, - label = { Text(stringResource(R.string.notifications)) } - ) - - NavigationBarItem( - selected = selectedScreen is ToolsScreenKey, - onClick = { switchScreen(ToolsScreenKey) }, - icon = { Icon(painter = painterResource(id = R.drawable.tools), contentDescription = null) }, - label = { Text(stringResource(R.string.tools)) } - ) - } - } -} - -@Composable -private fun NavigationRailContent( - mainContent: @Composable () -> Unit, - selectedScreen: ScreenKey, - switchScreen: (ScreenKey) -> Unit, -) { - Row { - NavigationRail { - NavigationRailItem( - selected = selectedScreen is WatchListKey, - onClick = { switchScreen(WatchListKey) }, - icon = { Icon(painter = painterResource(id = R.drawable.watches), contentDescription = null) }, - label = { Text(stringResource(R.string.watches)) } - ) - - NavigationRailItem( - selected = selectedScreen is WatchappListKey, - onClick = { switchScreen(WatchappListKey()) }, - icon = { Icon(painter = painterResource(id = R.drawable.watchapps), contentDescription = null) }, - label = { Text(stringResource(R.string.watch_apps)) } - ) - - NavigationRailItem( - selected = selectedScreen is NotificationAppListKey, - onClick = { switchScreen(NotificationAppListKey) }, - icon = { Icon(painter = painterResource(id = R.drawable.notifications), contentDescription = null) }, - label = { Text(stringResource(R.string.notifications)) } - ) - - NavigationRailItem( - selected = selectedScreen is ToolsScreenKey, - onClick = { switchScreen(ToolsScreenKey) }, - icon = { Icon(painter = painterResource(id = R.drawable.tools), contentDescription = null) }, - label = { Text(stringResource(R.string.tools)) } - ) - } - - Box( - Modifier - .fillMaxHeight() - .weight(1f) - ) { - mainContent() - } - } -} - -@FullScreenPreviews -@Composable -@ShowkaseComposable(group = "Test") -internal fun HomePhonePreview() { - PreviewTheme { - HomeScreenContent( - tabletMode = false, - selectedTab = SelectedTabContent( - { - Box( - Modifier - .fillMaxSize() - .background(Color.Red) - ) - }, - WatchappListKey(), - "" - ), - switchScreen = {}, - ) - } -} - -@Preview(device = Devices.TABLET) -@Composable -@ShowkaseComposable(group = "Test") -internal fun HomeTabletPreview() { - PreviewTheme { - HomeScreenContent( - tabletMode = true, - selectedTab = SelectedTabContent( - { - Box( - Modifier - .fillMaxSize() - .background(Color.Red) - ) - }, - WatchappListKey(), - "" - ), - switchScreen = {}, - ) - } -} diff --git a/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt b/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt deleted file mode 100644 index af47c378..00000000 --- a/home/ui/src/main/kotlin/com/matejdro/micropebble/tools/ToolsScreenKey.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.matejdro.micropebble.tools - -import kotlinx.serialization.Serializable -import si.inova.kotlinova.navigation.screenkeys.ScreenKey - -@Serializable -data object ToolsScreenKey : ScreenKey() diff --git a/iosApp/.gitignore b/iosApp/.gitignore new file mode 100644 index 00000000..48840646 --- /dev/null +++ b/iosApp/.gitignore @@ -0,0 +1,4 @@ +build/ +xcuserdata/ +*.xcuserstate +.DS_Store diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000..c25fb2c4 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,630 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXContainerItemProxy section */ + BB7907F82EBA6DE8000ADD3D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BB7907E22EBA6DE5000ADD3D /* Project object */; + proxyType = 1; + remoteGlobalIDString = BB7907E92EBA6DE5000ADD3D; + remoteInfo = iosApp; + }; + BB7908022EBA6DE8000ADD3D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BB7907E22EBA6DE5000ADD3D /* Project object */; + proxyType = 1; + remoteGlobalIDString = BB7907E92EBA6DE5000ADD3D; + remoteInfo = iosApp; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + BB7907EA2EBA6DE5000ADD3D /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + BB7907F72EBA6DE8000ADD3D /* iosAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iosAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BB7908012EBA6DE8000ADD3D /* iosAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iosAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + BB7907ED2EBA6DF0000ADD3D /* Exceptions for "iosApp" folder in "iosApp" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = BB7907E92EBA6DE5000ADD3D /* iosApp */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + BB7907EC2EBA6DE5000ADD3D /* iosApp */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + BB7907ED2EBA6DF0000ADD3D /* Exceptions for "iosApp" folder in "iosApp" target */, + ); + path = iosApp; + sourceTree = ""; + }; + BB7907FA2EBA6DE8000ADD3D /* iosAppTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = iosAppTests; + sourceTree = ""; + }; + BB7908042EBA6DE8000ADD3D /* iosAppUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = iosAppUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + BB7907E72EBA6DE5000ADD3D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BB7907F42EBA6DE8000ADD3D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BB7907FE2EBA6DE8000ADD3D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + BB7907E12EBA6DE5000ADD3D = { + isa = PBXGroup; + children = ( + BB7907EC2EBA6DE5000ADD3D /* iosApp */, + BB7907FA2EBA6DE8000ADD3D /* iosAppTests */, + BB7908042EBA6DE8000ADD3D /* iosAppUITests */, + BB7907EB2EBA6DE5000ADD3D /* Products */, + ); + sourceTree = ""; + }; + BB7907EB2EBA6DE5000ADD3D /* Products */ = { + isa = PBXGroup; + children = ( + BB7907EA2EBA6DE5000ADD3D /* iosApp.app */, + BB7907F72EBA6DE8000ADD3D /* iosAppTests.xctest */, + BB7908012EBA6DE8000ADD3D /* iosAppUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + BB7907E92EBA6DE5000ADD3D /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = BB79080B2EBA6DE8000ADD3D /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + BB7908142EBA6E49000ADD3D /* ShellScript */, + BB7907E62EBA6DE5000ADD3D /* Sources */, + BB7907E72EBA6DE5000ADD3D /* Frameworks */, + BB7907E82EBA6DE5000ADD3D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + BB7907EC2EBA6DE5000ADD3D /* iosApp */, + ); + name = iosApp; + packageProductDependencies = ( + ); + productName = iosApp; + productReference = BB7907EA2EBA6DE5000ADD3D /* iosApp.app */; + productType = "com.apple.product-type.application"; + }; + BB7907F62EBA6DE8000ADD3D /* iosAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BB79080E2EBA6DE8000ADD3D /* Build configuration list for PBXNativeTarget "iosAppTests" */; + buildPhases = ( + BB7907F32EBA6DE8000ADD3D /* Sources */, + BB7907F42EBA6DE8000ADD3D /* Frameworks */, + BB7907F52EBA6DE8000ADD3D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BB7907F92EBA6DE8000ADD3D /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + BB7907FA2EBA6DE8000ADD3D /* iosAppTests */, + ); + name = iosAppTests; + packageProductDependencies = ( + ); + productName = iosAppTests; + productReference = BB7907F72EBA6DE8000ADD3D /* iosAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + BB7908002EBA6DE8000ADD3D /* iosAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BB7908112EBA6DE8000ADD3D /* Build configuration list for PBXNativeTarget "iosAppUITests" */; + buildPhases = ( + BB7907FD2EBA6DE8000ADD3D /* Sources */, + BB7907FE2EBA6DE8000ADD3D /* Frameworks */, + BB7907FF2EBA6DE8000ADD3D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BB7908032EBA6DE8000ADD3D /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + BB7908042EBA6DE8000ADD3D /* iosAppUITests */, + ); + name = iosAppUITests; + packageProductDependencies = ( + ); + productName = iosAppUITests; + productReference = BB7908012EBA6DE8000ADD3D /* iosAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BB7907E22EBA6DE5000ADD3D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + BB7907E92EBA6DE5000ADD3D = { + CreatedOnToolsVersion = 16.4; + }; + BB7907F62EBA6DE8000ADD3D = { + CreatedOnToolsVersion = 16.4; + TestTargetID = BB7907E92EBA6DE5000ADD3D; + }; + BB7908002EBA6DE8000ADD3D = { + CreatedOnToolsVersion = 16.4; + TestTargetID = BB7907E92EBA6DE5000ADD3D; + }; + }; + }; + buildConfigurationList = BB7907E52EBA6DE5000ADD3D /* Build configuration list for PBXProject "iosApp" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = BB7907E12EBA6DE5000ADD3D; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = BB7907EB2EBA6DE5000ADD3D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + BB7907E92EBA6DE5000ADD3D /* iosApp */, + BB7907F62EBA6DE8000ADD3D /* iosAppTests */, + BB7908002EBA6DE8000ADD3D /* iosAppUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + BB7907E82EBA6DE5000ADD3D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BB7907F52EBA6DE8000ADD3D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BB7907FF2EBA6DE8000ADD3D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + BB7908142EBA6E49000ADD3D /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/../app-ios/build.gradle.kts", + ); + outputFileListPaths = ( + ); + outputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Shared.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$SRCROOT/..\"\n./gradlew :app-ios:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + BB7907E62EBA6DE5000ADD3D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BB7907F32EBA6DE8000ADD3D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BB7907FD2EBA6DE8000ADD3D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + BB7907F92EBA6DE8000ADD3D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BB7907E92EBA6DE5000ADD3D /* iosApp */; + targetProxy = BB7907F82EBA6DE8000ADD3D /* PBXContainerItemProxy */; + }; + BB7908032EBA6DE8000ADD3D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BB7907E92EBA6DE5000ADD3D /* iosApp */; + targetProxy = BB7908022EBA6DE8000ADD3D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + BB7908092EBA6DE8000ADD3D /* Debug */ = { + isa = XCBuildConfiguration; + 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; + DEVELOPMENT_TEAM = 457FBQ4469; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + 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.5; + 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; + }; + BB79080A2EBA6DE8000ADD3D /* Release */ = { + isa = XCBuildConfiguration; + 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"; + DEVELOPMENT_TEAM = 457FBQ4469; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + 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.5; + 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; + }; + BB79080C2EBA6DE8000ADD3D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 457FBQ4469; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../app-ios/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "$(SRCROOT)/iosApp/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + KOTLIN_FRAMEWORK_BUILD_TYPE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + /usr/lib/swift, + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + Shared, + ); + PRODUCT_BUNDLE_IDENTIFIER = com.matejdro.micropebble.iosApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BB79080D2EBA6DE8000ADD3D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 457FBQ4469; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../app-ios/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "$(SRCROOT)/iosApp/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + KOTLIN_FRAMEWORK_BUILD_TYPE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + /usr/lib/swift, + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + Shared, + ); + PRODUCT_BUNDLE_IDENTIFIER = com.matejdro.micropebble.iosApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + BB79080F2EBA6DE8000ADD3D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 457FBQ4469; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "$(SRCROOT)/iosApp/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.matejdro.micropebble.iosAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iosApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iosApp"; + }; + name = Debug; + }; + BB7908102EBA6DE8000ADD3D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 457FBQ4469; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "$(SRCROOT)/iosApp/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.matejdro.micropebble.iosAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iosApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iosApp"; + }; + name = Release; + }; + BB7908122EBA6DE8000ADD3D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 457FBQ4469; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "$(SRCROOT)/iosApp/Info.plist"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.matejdro.micropebble.iosAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = iosApp; + }; + name = Debug; + }; + BB7908132EBA6DE8000ADD3D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 457FBQ4469; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "$(SRCROOT)/iosApp/Info.plist"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.matejdro.micropebble.iosAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = iosApp; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BB7907E52EBA6DE5000ADD3D /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BB7908092EBA6DE8000ADD3D /* Debug */, + BB79080A2EBA6DE8000ADD3D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BB79080B2EBA6DE8000ADD3D /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BB79080C2EBA6DE8000ADD3D /* Debug */, + BB79080D2EBA6DE8000ADD3D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BB79080E2EBA6DE8000ADD3D /* Build configuration list for PBXNativeTarget "iosAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BB79080F2EBA6DE8000ADD3D /* Debug */, + BB7908102EBA6DE8000ADD3D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BB7908112EBA6DE8000ADD3D /* Build configuration list for PBXNativeTarget "iosAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BB7908122EBA6DE8000ADD3D /* Debug */, + BB7908132EBA6DE8000ADD3D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BB7907E22EBA6DE5000ADD3D /* Project object */; +} diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/iosApp/iosApp/AppDelegate.swift b/iosApp/iosApp/AppDelegate.swift new file mode 100644 index 00000000..e19aa4a7 --- /dev/null +++ b/iosApp/iosApp/AppDelegate.swift @@ -0,0 +1,26 @@ +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return true + } + + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + let configuration = UISceneConfiguration( + name: "Default Configuration", + sessionRole: connectingSceneSession.role + ) + configuration.delegateClass = SceneDelegate.self + return configuration + } +} + diff --git a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "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/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist new file mode 100644 index 00000000..e4d077a4 --- /dev/null +++ b/iosApp/iosApp/Info.plist @@ -0,0 +1,81 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + CADisableMinimumFrameDurationOnPhone + + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSBluetoothAlwaysUsageDescription + This app needs Bluetooth access to connect to your Pebble watch + NSBluetoothPeripheralUsageDescription + This app needs Bluetooth access to connect to your Pebble watch + UIBackgroundModes + + bluetooth-central + bluetooth-peripheral + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + boot.rebble.io + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + NSExceptionRequiresForwardSecrecy + + + + + + + diff --git a/iosApp/iosApp/SceneDelegate.swift b/iosApp/iosApp/SceneDelegate.swift new file mode 100644 index 00000000..96b28ac5 --- /dev/null +++ b/iosApp/iosApp/SceneDelegate.swift @@ -0,0 +1,24 @@ +import UIKit +import Shared + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = (scene as? UIWindowScene) else { return } + + let window = UIWindow(windowScene: windowScene) + + let rootViewController = MainViewControllerKt.MainViewController() + + window.rootViewController = rootViewController + window.makeKeyAndVisible() + + self.window = window + } +} + diff --git a/iosApp/iosApp/SwiftDummy.swift b/iosApp/iosApp/SwiftDummy.swift new file mode 100644 index 00000000..9ac10955 --- /dev/null +++ b/iosApp/iosApp/SwiftDummy.swift @@ -0,0 +1,7 @@ +import Foundation + +@objc class SwiftDummy: NSObject { + // This class exists only to make Xcode recognize that this app uses Swift + // and should embed the Swift standard libraries +} + diff --git a/iosApp/iosAppTests/iosAppTests.swift b/iosApp/iosAppTests/iosAppTests.swift new file mode 100644 index 00000000..5d920a92 --- /dev/null +++ b/iosApp/iosAppTests/iosAppTests.swift @@ -0,0 +1,10 @@ +import XCTest +@testable import iosApp + +final class iosAppTests: XCTestCase { + + /// Smoke check that the app shell links; the shared UI is covered by iosAppUITests. + func testAppShellLinks() { + XCTAssertNotNil(SwiftDummy()) + } +} diff --git a/iosApp/iosAppUITests/iosAppUITests.swift b/iosApp/iosAppUITests/iosAppUITests.swift new file mode 100644 index 00000000..5eec1fe1 --- /dev/null +++ b/iosApp/iosAppUITests/iosAppUITests.swift @@ -0,0 +1,24 @@ +import XCTest + +final class iosAppUITests: XCTestCase { + + override func setUpWithError() throws { + continueAfterFailure = false + } + + override func tearDownWithError() throws { + } + + @MainActor + func testExample() throws { + let app = XCUIApplication() + app.launch() + } + + @MainActor + func testLaunchPerformance() throws { + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } +} diff --git a/navigation-impl/src/main/kotlin/com/matejdro/micropebble/navigation/scenes/TabListScene.kt b/navigation-impl/src/main/kotlin/com/matejdro/micropebble/navigation/scenes/TabListScene.kt index 283bc5d5..5454db73 100644 --- a/navigation-impl/src/main/kotlin/com/matejdro/micropebble/navigation/scenes/TabListScene.kt +++ b/navigation-impl/src/main/kotlin/com/matejdro/micropebble/navigation/scenes/TabListScene.kt @@ -10,6 +10,7 @@ import androidx.navigation3.scene.SceneStrategyScope import com.matejdro.micropebble.navigation.keys.base.LocalSelectedTabContent import com.matejdro.micropebble.navigation.keys.base.SelectedTabContent import com.matejdro.micropebble.navigation.keys.base.TabContainerKey +import com.matejdro.micropebble.navigation.keys.base.TabKey import si.inova.kotlinova.navigation.navigation3.key import si.inova.kotlinova.navigation.screenkeys.ScreenKey @@ -24,7 +25,7 @@ class TabListScene( val selectedTabContent = SelectedTabContent( displayedEntry::Content, - displayedEntry.key(), + (displayedEntry.key() as? TabKey)?.tab, contentKey = displayedEntry.contentKey ) diff --git a/settings.gradle.kts b/settings.gradle.kts index 97901b3b..eeeeaf99 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -61,3 +61,8 @@ include(":voice:data") include(":webservices:api") include(":webservices:data") include(":webservices:ui") + +// The iOS app framework only builds on macOS; skip it elsewhere (e.g. Linux CI) so configuration succeeds. +if (System.getProperty("os.name").contains("mac", ignoreCase = true)) { + include(":app-ios") +} diff --git a/shared-resources/src/main/res/values/strings.xml b/shared-resources/src/main/res/values/strings.xml index f4a20e36..044e9ba5 100644 --- a/shared-resources/src/main/res/values/strings.xml +++ b/shared-resources/src/main/res/values/strings.xml @@ -1,11 +1,13 @@ - + Connecting Micro Pebble OK Cancel Appstore sources - Webservices + + Webservices Pebble Classic Pebble Steel