diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index b3631159..d622f598 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -10,16 +10,21 @@ on: jobs: build: - name: 🔨 Build - runs-on: ubuntu-24.04 + name: 🔨 Build - ${{ matrix.name }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - name: 📱 Android App gradle_module: tasks-app-android + os: ubuntu-24.04 - name: 🖥️ Desktop App gradle_module: tasks-app-desktop + os: ubuntu-24.04 + - name: 🍎 iOS App + gradle_module: tasks-app-ios + os: macos-15 permissions: contents: write checks: write @@ -30,12 +35,24 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-jdk-gradle + - name: Cache Gradle + if: ${{ matrix.gradle_module == 'tasks-app-ios' }} + uses: actions/cache@v4 + with: + path: | + .gradle + $HOME/.m2/repository + $HOME/.konan + key: gradle-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties', '**/*.gradle.kts', '**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}- + - name: 🔓 Decrypt secrets env: PLAYSTORE_SECRET_PASSPHRASE: ${{ secrets.PLAYSTORE_SECRET_PASSPHRASE }} run: ./_ci/decrypt_secrets.sh - - name: ${{ matrix.name }} + - name: ${{ matrix.gradle_module }} env: PLAYSTORE_SECRET_PASSPHRASE: ${{ secrets.PLAYSTORE_SECRET_PASSPHRASE }} KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} @@ -50,4 +67,6 @@ jobs: -Pplaystore.keystore.file="${PWD}/_ci/tasksApp.keystore" \ -Pplaystore.keystore.password="${KEYSTORE_PASSWORD}" \ -Pplaystore.keystore.key_password="${KEYSTORE_KEY_PASSWORD}" + elif [ "${gradle_module}" = "tasks-app-ios" ]; then + IOS_TARGET=simulator ./gradlew tasks-app-shared:linkDebugFrameworkIosSimulatorArm64 fi diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 58f9be45..3bf1d1f4 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -7,7 +7,7 @@ on: jobs: check-changes: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: changes-detected: ${{ steps.check.outputs.changes_detected }} steps: diff --git a/.github/workflows/ios-app-nightly.yml b/.github/workflows/ios-app-nightly.yml new file mode 100644 index 00000000..1a9e0a37 --- /dev/null +++ b/.github/workflows/ios-app-nightly.yml @@ -0,0 +1,73 @@ +name: 🍎 iOS App Nightly + +on: + schedule: + - cron: '0 2 * * *' + workflow_dispatch: + +jobs: + check-changes: + runs-on: macos-15 + outputs: + changes-detected: ${{ steps.check.outputs.changes_detected }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for changes + id: check + run: | + git fetch origin main + if git log --since="24 hours ago" --pretty=format:%H -- . \ + ':(exclude)website/' \ + ':(exclude)fastlane/' \ + ':(exclude)assets/' \ + ':(exclude)**/*.md' \ + | grep .; then + echo "changes_detected=true" >> "$GITHUB_OUTPUT" + else + echo "changes_detected=false" >> "$GITHUB_OUTPUT" + fi + + build-ios-app: + timeout-minutes: 15 + needs: check-changes + if: needs.check-changes.outputs.changes-detected == 'true' || github.event_name == 'workflow_dispatch' + name: 🍎 Build iOS App + runs-on: macos-15 + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-jdk-gradle + + - name: Cache Gradle + if: ${{ matrix.gradle_module == 'tasks-app-ios' }} + uses: actions/cache@v4 + with: + path: | + .gradle + $HOME/.m2/repository + $HOME/.konan + key: gradle-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties', '**/*.gradle.kts', '**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}- + + - name: 🔓 Decrypt secrets + env: + PLAYSTORE_SECRET_PASSPHRASE: ${{ secrets.PLAYSTORE_SECRET_PASSPHRASE }} + run: ./_ci/decrypt_secrets.sh + + - name: 🔨 Build + run: | + cd tasks-app-ios + IOS_TARGET=simulator xcodebuild \ + -project Taskfolio.xcodeproj \ + -scheme Taskfolio \ + -sdk iphonesimulator \ + -arch arm64 \ + -configuration Debug \ + build \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO diff --git a/.gitignore b/.gitignore index 903c6d3f..ec8b5345 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,11 @@ local.properties .kotlin/metadata/ tasks-app-android/google-services.json +tasks-app-desktop/bin/ +tasks-app-ios/Taskfolio.xcodeproj/xcuserdata/*.xcuserdatad +tasks-app-ios/Taskfolio.xcodeproj/project.xcworkspace/contents.xcworkspacedata +tasks-app-ios/Taskfolio.xcodeproj/project.xcworkspace/xcuserdata/*.xcuserdatad +tasks-app-ios/Taskfolio.xcodeproj/project.xcworkspace/xcshareddata/* *token_cache.* client_secret_*.apps.googleusercontent.com.json @@ -42,4 +47,3 @@ _ci/api-*.json bundletool-*.jar _site/ -tasks-app-desktop/bin/ diff --git a/README.md b/README.md index ad83a9cb..40e5ca39 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The coverage report excludes code not intended to be covered. -This avoids the [“broken window” effect](https://blog.codinghorror.com/the-broken-window-theory/): whether coverage is at 43% or 56%, it’s perceived as equally low—so efforts to improve it are often dismissed. In contrast, high or near-100% coverage is seen as achievable and worth tracking. +This avoids the [“broken window” effect](https://blog.codinghorror.com/the-broken-window-theory/): whether coverage is at 43% or 56%, it's perceived as equally low—so efforts to improve it are often dismissed. In contrast, high or near-100% coverage is seen as achievable and worth tracking. Refer to the root project's [`build.gradle.kts`](build.gradle.kts#L55-L90) for details. @@ -36,13 +36,16 @@ Refer to the root project's [`build.gradle.kts`](build.gradle.kts#L55-L90) for d [**Taskfolio**](https://opatry.github.io/taskfolio) is an Android task management app built using [Google Tasks API](https://developers.google.com/tasks/reference/rest). Developed to demonstrate my expertise in modern Android development, it highlights my skills in architecture, UI design with Jetpack Compose, OAuth authentication, and more—all packaged in a sleek, user-friendly interface. -> I set out to revisit the classical TODO app, ‘local-first’ syncing with Google Tasks—aiming for an MVE in 2 weeks, focusing on the 80/20 rule to nail the essentials. +> I set out to revisit the classical TODO app, 'local-first' syncing with Google Tasks—aiming for an MVE in 2 weeks, focusing on the 80/20 rule to nail the essentials. | ![](assets/screens/task_lists_light.png) | ![](assets/screens/groceries_light.png) | ![](assets/screens/add_task_light.png) | ![](assets/screens/home_dark.png) | | --------------------------------------- |--------------------------------------- | ---------------------------------- | ---------------------------------- | [![Taskfolio on Play Store](assets/GetItOnGooglePlay_Badge_Web_color_English.png)](https://play.google.com/store/apps/details?id=net.opatry.tasks.app) +> [!NOTE] +> The application is also available as a desktop (Jvm) application and an iOS application as well (using [Compose Multi Platform (aka CMP)](https://www.jetbrains.com/compose-multiplatform/) as UI Toolkit). + ## 🎯 Project intentions - [x] Showcase my expertise in Android application development @@ -76,9 +79,10 @@ I do not aim to implement advanced features beyond what is supported by the Goog ## 🛠️ Tech stack -- [Kotlin](https://kotlinlang.org/), [Multiplatform (aka KMP)](https://kotlinlang.org/docs/multiplatform.html) (currently Desktop & Android are supported) - - iOS wasn’t initially planned, but I bootstrapped a [PR to evaluate the feasibility of the iOS target]((https://github.com/opatry/taskfolio/pull/269)). It turned out to be quite achievable and just needs some polishing. - - Web is not planned any time soon (contribution are welcome 🤝) +- [Kotlin](https://kotlinlang.org/), [Multiplatform (aka KMP)](https://kotlinlang.org/docs/multiplatform.html) + - Android and Desktop are fully supported. + - iOS wasn't initially planned, but a draft version is available (use it at your own risk, there might be dragons 🐉). + - Web is not planned any time soon (contributions are welcome 🤝) - [Kotlin coroutines](https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html) - [Ktor client](https://ktor.io/) (+ [Kotlinx serialization](https://kotlinlang.org/docs/serialization.html)) - [Room](https://developer.android.com/training/data-storage/room) for local persistence @@ -123,6 +127,9 @@ I do not aim to implement advanced features beyond what is supported by the Goog - The Desktop application (thin layer fully reusing `:tasks-app-shared`) - [`:tasks-app-android`](tasks-app-android) ■■■■■■■■□□ 80% - The Android application (thin layer fully reusing `:tasks-app-shared`) +- [`:tasks-app-ios/Taskfolio`](tasks-app-ios/Taskfolio) ■■■■■■■■■□ 90% + - The iOS application (thin layer fully reusing `:tasks-app-shared`) + - Xcode project, written in Swift - [`website/`](website) ■■■■■■■■■■ 100% - The [static site](https://opatry.github.io/taskfolio/) presenting the project - Made with [Jekyll](https://jekyllrb.com/) and served by [Github pages](https://pages.github.com/) @@ -177,6 +184,68 @@ When clicking on it, it will open a new window with the hot reload status. ![](assets/compose-hot-reload-console.png) +## 🍎 Build for iOS target + +The support of iOS works more or less _as-is_ and gets the job done. It's provided without guarantees, use at your own risk. +Feedback and contributions are welcome though 🤝. + +> [!NOTE] +> iOS support is _opt-in_ and disabled by default to avoid unnecessary time and disk usage during the initial Gradle sync when the iOS target isn't required. +> You can enable it by setting `ios.target` Gradle property to `all`, `simulator` or `device` from either `local.properties` or CLI using `-P`. +> When building from Xcode, it automatically sets `-Pios.target=simulator` based on `Config.xcconfig`. + +
+See details… + +You can build the `:tasks-app-shared` code for iOS using Gradle (to check if everything compiles on Kotlin side): + +```bash +./gradlew tasks-app-shared:linkDebugFrameworkIosSimulatorArm64 -Pios.target=simulator +``` + +### Building & Running from IntelliJ/Android Studio + +You can also use the incubating [Kotlin Multiplatform IntelliJ plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform) to build and launch the iOS app directly from IntelliJ/Android Studio (starting from Narwhal | 2025.1.1). +This plugin allows you to choose whether to run the app on a device or simulator, and enables debugging of Kotlin code even when called from iOS/Swift. + +It builds the Kotlin code as a native framework, then triggers the appropriate Gradle task to build Kotlin first, followed by `xcodebuild` for the Xcode and iOS-specific parts, ensuring a seamless integration between Kotlin and Swift code (see next section for details). + +### Building & Running from Xcode + +For full XCFramework build (to be consumed by the iOS application), you'll have to rely on `xcodebuild` (or build directly from Xcode): + +```bash +cd tasks-app-ios +IOS_TARGET=simulator xcodebuild -project Taskfolio.xcodeproj \ + -scheme Taskfolio \ + -sdk iphonesimulator \ + -arch arm64 \ + -configuration Debug \ + build \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO +``` +This triggers the `:tasks-app-shared:embedAndSignAppleFrameworkForXcode` Gradle task under the hood. + +For Xcode integration, it's recommended to install the [Xcode Kotlin plugin](https://touchlab.co/xcodekotlin): + +```bash +brew install xcode-kotlin +xcode-kotlin install +``` + +When you update Xcode, you'll have to sync the plugin: + +```bash +xcode-kotlin sync +``` + +If you want to debug the Kotlin code from Xcode, you'll have to add the needed source sets in Xcode: +Add Group > Add folders as **reference** > `tasks-app-shared/{commonMain,iosMain}` (or any other module you want to debug). +If you properly installed the Xcode Kotlin plugin, you'll be able to set a breakpoint in the Kotlin code and see syntax coloring as well. +
+ ## ⚖️ License ``` diff --git a/build.gradle.kts b/build.gradle.kts index 731d5d86..2c1b25d0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,6 +22,7 @@ import kotlinx.kover.gradle.plugin.dsl.CoverageUnit import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension plugins { alias(libs.plugins.jetbrains.kotlin.multiplatform) apply false @@ -135,7 +136,32 @@ kover { } } +private val kmpPluginId = libs.plugins.jetbrains.kotlin.multiplatform.get().pluginId subprojects { + plugins.withId(kmpPluginId) { + if (project == project(":google:oauth-http")) return@withId + + extensions.configure { + // foo-bar-zorg → FooBarZorg + val frameworkBaseName = project.name.split('-').joinToString("") { part -> + part.replaceFirstChar(Char::uppercase) + } + iosTargets.mapNotNull { + when (it) { + "iosX64" -> iosX64() + "iosArm64" -> iosArm64() + "iosSimulatorArm64" -> iosSimulatorArm64() + else -> null + } + }.forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = frameworkBaseName + isStatic = true + } + } + } + } + tasks { findByName("test") ?: return@tasks named("test") { @@ -174,3 +200,12 @@ subprojects { } } } + +gradle.projectsEvaluated { + val xcFrameworkTask = project(":tasks-app-shared").tasks.findByName("embedAndSignAppleFrameworkForXcode") + val updateVersionTask = project(":tasks-app-ios").tasks.findByName("updateXcodeVersionConfig") + + if (xcFrameworkTask != null && updateVersionTask != null) { + xcFrameworkTask.dependsOn(updateVersionTask) + } +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..fc6cbb8d --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +kotlin { + jvmToolchain(17) +} + diff --git a/buildSrc/src/main/kotlin/IosTargetsExt.kt b/buildSrc/src/main/kotlin/IosTargetsExt.kt new file mode 100644 index 00000000..1c70c196 --- /dev/null +++ b/buildSrc/src/main/kotlin/IosTargetsExt.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import org.gradle.api.Project +import java.io.File +import java.util.* + +// can't use by lazy, we need Project.findProperty not accessible there +@Suppress("ObjectPropertyName") +private lateinit var _iosTargets: List + +private val localProperties = Properties() +private fun Project.getIosTargetedConfiguration(): String? { + return findProperty("ios.target") as? String + ?: System.getenv("IOS_TARGET") + ?: run { + if (localProperties.isEmpty) { + val localPropertiesFile = File(rootDir, "local.properties") + if (localPropertiesFile.isFile) { + localPropertiesFile.inputStream().use { reader -> + localProperties.load(reader) + } + } + } + localProperties.getProperty("ios.target") + } +} + +val Project.iosTargets: List + get() { + if (!::_iosTargets.isInitialized) { + _iosTargets = when (getIosTargetedConfiguration()) { + // We ignore "iosX64", not considered as a use case + "all" -> listOf("iosArm64", "iosSimulatorArm64") + "simulator" -> listOf("iosSimulatorArm64") + "device" -> listOf("iosArm64") + "none" -> emptyList() + else -> emptyList() + } + } + return _iosTargets + } diff --git a/google/oauth/build.gradle.kts b/google/oauth/build.gradle.kts index d31e5d4b..4cabd280 100644 --- a/google/oauth/build.gradle.kts +++ b/google/oauth/build.gradle.kts @@ -28,6 +28,8 @@ plugins { kotlin { jvm() + // Note: iOS targets are conditionally added dynamically in the root build.gradle.kts + jvmToolchain(17) sourceSets.all { diff --git a/google/oauth/src/commonMain/kotlin/net/opatry/google/auth/GoogleAuthenticator.kt b/google/oauth/src/commonMain/kotlin/net/opatry/google/auth/GoogleAuthenticator.kt index d0228aad..7e49e6b1 100644 --- a/google/oauth/src/commonMain/kotlin/net/opatry/google/auth/GoogleAuthenticator.kt +++ b/google/oauth/src/commonMain/kotlin/net/opatry/google/auth/GoogleAuthenticator.kt @@ -25,11 +25,9 @@ package net.opatry.google.auth import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import net.opatry.google.auth.GoogleAuthenticator.OAuthToken.TokenType.Bearer -import kotlin.jvm.JvmInline interface GoogleAuthenticator { - @JvmInline - value class Scope(val value: String) { + data class Scope(val value: String) { companion object { val Profile = Scope("https://www.googleapis.com/auth/userinfo.profile") val Email = Scope("https://www.googleapis.com/auth/userinfo.email") diff --git a/google/tasks/build.gradle.kts b/google/tasks/build.gradle.kts index bc6303d8..4428015a 100644 --- a/google/tasks/build.gradle.kts +++ b/google/tasks/build.gradle.kts @@ -28,6 +28,8 @@ plugins { kotlin { jvm() + // Note: iOS targets are conditionally added dynamically in the root build.gradle.kts + jvmToolchain(17) sourceSets.all { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 628fc82f..9ffb0247 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,7 @@ mockito = "5.20.0" mockito-kotlin = "6.1.0" kover = "0.9.3" androidx-test-runner = "1.7.0" +bignum = "0.3.10" [libraries] kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } @@ -101,6 +102,8 @@ mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = " androidx-ui-tooling-preview-android = { module = "androidx.compose.ui:ui-tooling-preview-android", version.ref = "compose" } androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } +bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "bignum" } + [bundles] ktor-server = ["ktor-server-core", "ktor-server-cio"] ktor-client = [ diff --git a/lucide-icons/build.gradle.kts b/lucide-icons/build.gradle.kts index 7339c27f..88f0392f 100644 --- a/lucide-icons/build.gradle.kts +++ b/lucide-icons/build.gradle.kts @@ -29,6 +29,8 @@ plugins { kotlin { jvm() + // Note: iOS targets are conditionally added dynamically in the root build.gradle.kts + jvmToolchain(17) sourceSets { diff --git a/settings.gradle.kts b/settings.gradle.kts index 889c1c01..eacbd781 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -64,3 +64,6 @@ include(":tasks-core") include(":tasks-app-shared") include(":tasks-app-desktop") include(":tasks-app-android") +// useful to isolate/ease license_ios.json file generation +// and also sync version from Gradle to Xcode +include(":tasks-app-ios") diff --git a/tasks-app-ios/Configuration/Config.dev.xcconfig b/tasks-app-ios/Configuration/Config.dev.xcconfig new file mode 100644 index 00000000..32b98ef5 --- /dev/null +++ b/tasks-app-ios/Configuration/Config.dev.xcconfig @@ -0,0 +1,7 @@ +#include? "Versions.xcconfig" + +APP_DISPLAY_NAME=T4skf0l10 d3v +APP_ID=net.opatry.tasks.app.dev +IOS_TARGET=simulator +GCP_CLIENT_ID=191682949161-ockace96gikfsif7hoa9h80p2r096iu4.apps.googleusercontent.com +GCP_REVERSED_CLIENT_ID=com.googleusercontent.apps.191682949161-ockace96gikfsif7hoa9h80p2r096iu4 diff --git a/tasks-app-ios/Configuration/Config.xcconfig b/tasks-app-ios/Configuration/Config.xcconfig new file mode 100644 index 00000000..22717fcc --- /dev/null +++ b/tasks-app-ios/Configuration/Config.xcconfig @@ -0,0 +1,7 @@ +#include? "Versions.xcconfig" + +APP_DISPLAY_NAME=Taskfolio +APP_ID=net.opatry.tasks.app +IOS_TARGET=all +GCP_CLIENT_ID=191682949161-79vl4dcpf9lppj5cj5k79tpqhv5ab10u.apps.googleusercontent.com +GCP_REVERSED_CLIENT_ID=com.googleusercontent.apps.191682949161-79vl4dcpf9lppj5cj5k79tpqhv5ab10u diff --git a/tasks-app-ios/Configuration/Versions.xcconfig b/tasks-app-ios/Configuration/Versions.xcconfig new file mode 100644 index 00000000..8ee1c7cf --- /dev/null +++ b/tasks-app-ios/Configuration/Versions.xcconfig @@ -0,0 +1,2 @@ +BUNDLE_VERSION=1 +BUNDLE_SHORT_VERSION_STRING=1.4.3 \ No newline at end of file diff --git a/tasks-app-ios/Taskfolio.xcodeproj/project.pbxproj b/tasks-app-ios/Taskfolio.xcodeproj/project.pbxproj new file mode 100644 index 00000000..71cb38aa --- /dev/null +++ b/tasks-app-ios/Taskfolio.xcodeproj/project.pbxproj @@ -0,0 +1,461 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 4705DD9C2E07520D0008E5F7 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4705DD972E07520D0008E5F7 /* ContentView.swift */; }; + 4705DD9D2E07520D0008E5F7 /* TaskfolioApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4705DD992E07520D0008E5F7 /* TaskfolioApp.swift */; }; + 4705DD9F2E07520D0008E5F7 /* licenses_ios.json in Resources */ = {isa = PBXBuildFile; fileRef = 4705DD922E07520D0008E5F7 /* licenses_ios.json */; }; + 4705DDA02E07520D0008E5F7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4705DD942E07520D0008E5F7 /* Assets.xcassets */; }; + 47629B9A2E173EC100391C27 /* IOSGoogleAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47629B992E173EB900391C27 /* IOSGoogleAuthenticator.swift */; }; + 47629B9D2E17407D00391C27 /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = 47629B9C2E17407D00391C27 /* GoogleSignIn */; }; + 47629B9F2E17407D00391C27 /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 47629B9E2E17407D00391C27 /* GoogleSignInSwift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 4705DD922E07520D0008E5F7 /* licenses_ios.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = licenses_ios.json; sourceTree = ""; }; + 4705DD942E07520D0008E5F7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4705DD952E07520D0008E5F7 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; + 4705DD972E07520D0008E5F7 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 4705DD982E07520D0008E5F7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4705DD992E07520D0008E5F7 /* TaskfolioApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskfolioApp.swift; sourceTree = ""; }; + 4705DD9A2E07520D0008E5F7 /* Versions.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Versions.xcconfig; sourceTree = ""; }; + 4731FF7D2E1307C0005A081E /* Config.dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.dev.xcconfig; sourceTree = ""; }; + 4734A08F2E05CE9D00175C75 /* T4skf0l10 d3v.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "T4skf0l10 d3v.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 47629B992E173EB900391C27 /* IOSGoogleAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSGoogleAuthenticator.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4734A08C2E05CE9D00175C75 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 47629B9D2E17407D00391C27 /* GoogleSignIn in Frameworks */, + 47629B9F2E17407D00391C27 /* GoogleSignInSwift in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4705DD932E07520D0008E5F7 /* Resources */ = { + isa = PBXGroup; + children = ( + 4705DD922E07520D0008E5F7 /* licenses_ios.json */, + ); + path = Resources; + sourceTree = ""; + }; + 4705DD9B2E07520D0008E5F7 /* Taskfolio */ = { + isa = PBXGroup; + children = ( + 47629B992E173EB900391C27 /* IOSGoogleAuthenticator.swift */, + 4705DD932E07520D0008E5F7 /* Resources */, + 4705DD942E07520D0008E5F7 /* Assets.xcassets */, + 4705DD972E07520D0008E5F7 /* ContentView.swift */, + 4705DD982E07520D0008E5F7 /* Info.plist */, + 4705DD992E07520D0008E5F7 /* TaskfolioApp.swift */, + ); + path = Taskfolio; + sourceTree = ""; + }; + 4731FF612E127E5C005A081E /* Configuration */ = { + isa = PBXGroup; + children = ( + 4705DD952E07520D0008E5F7 /* Config.xcconfig */, + 4731FF7D2E1307C0005A081E /* Config.dev.xcconfig */, + 4705DD9A2E07520D0008E5F7 /* Versions.xcconfig */, + ); + path = Configuration; + sourceTree = ""; + }; + 4734A0862E05CE9D00175C75 = { + isa = PBXGroup; + children = ( + 4731FF612E127E5C005A081E /* Configuration */, + 4705DD9B2E07520D0008E5F7 /* Taskfolio */, + 4734A0902E05CE9D00175C75 /* Products */, + ); + sourceTree = ""; + }; + 4734A0902E05CE9D00175C75 /* Products */ = { + isa = PBXGroup; + children = ( + 4734A08F2E05CE9D00175C75 /* T4skf0l10 d3v.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4734A08E2E05CE9D00175C75 /* Taskfolio */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4734A0B32E05CED900175C75 /* Build configuration list for PBXNativeTarget "Taskfolio" */; + buildPhases = ( + 473FF7702E05F9E6002B3B0F /* Compile Kotlin Framework */, + 4734A08B2E05CE9D00175C75 /* Sources */, + 4734A08C2E05CE9D00175C75 /* Frameworks */, + 4734A08D2E05CE9D00175C75 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Taskfolio; + packageProductDependencies = ( + 47629B9C2E17407D00391C27 /* GoogleSignIn */, + 47629B9E2E17407D00391C27 /* GoogleSignInSwift */, + ); + productName = Taskfolio; + productReference = 4734A08F2E05CE9D00175C75 /* T4skf0l10 d3v.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4734A0872E05CE9D00175C75 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 4734A08E2E05CE9D00175C75 = { + CreatedOnToolsVersion = 16.2; + LastSwiftMigration = 1620; + }; + }; + }; + buildConfigurationList = 4734A08A2E05CE9D00175C75 /* Build configuration list for PBXProject "Taskfolio" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4734A0862E05CE9D00175C75; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 47629B9B2E17407D00391C27 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 4734A0902E05CE9D00175C75 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4734A08E2E05CE9D00175C75 /* Taskfolio */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4734A08D2E05CE9D00175C75 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4705DD9F2E07520D0008E5F7 /* licenses_ios.json in Resources */, + 4705DDA02E07520D0008E5F7 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 473FF7702E05F9E6002B3B0F /* 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 [ \"${OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED}\" = \"YES\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to 'YES'.\"\n exit 0\nfi\n\nif [ -z \"${IOS_TARGET}\" ]; then\n echo \"You must define IOS_TARGET to 'all', 'simulator' or 'device' to allow building for iOS.\"\n exit 1\nfi\n\necho \"Building for '${IOS_TARGET}' target\"\n\ncd \"${SRCROOT}/..\"\n./gradlew :tasks-app-shared:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4734A08B2E05CE9D00175C75 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4705DD9C2E07520D0008E5F7 /* ContentView.swift in Sources */, + 47629B9A2E173EC100391C27 /* IOSGoogleAuthenticator.swift in Sources */, + 4705DD9D2E07520D0008E5F7 /* TaskfolioApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 4734A0B12E05CED900175C75 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4731FF7D2E1307C0005A081E /* Config.dev.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; + CURRENT_PROJECT_VERSION = "$(BUNDLE_VERSION)"; + DEBUG_INFORMATION_FORMAT = dwarf; + 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.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = "$(BUNDLE_SHORT_VERSION_STRING)"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(APP_DISPLAY_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 4734A0B22E05CED900175C75 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4705DD952E07520D0008E5F7 /* 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; + CURRENT_PROJECT_VERSION = "$(BUNDLE_VERSION)"; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = "$(BUNDLE_SHORT_VERSION_STRING)"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(APP_DISPLAY_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4734A0B42E05CED900175C75 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4731FF7D2E1307C0005A081E /* Config.dev.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon.dev; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor.dev; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_TEAM = 9J8LQ9R327; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + "FRAMEWORK_SEARCH_PATHS[arch=*]" = ( + "$(inherited)", + "$(SRCROOT)/../tasks-app-shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Taskfolio/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME)"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIStatusBarStyle = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID)"; + PRODUCT_NAME = "$(APP_DISPLAY_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4734A0B52E05CED900175C75 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4705DD952E07520D0008E5F7 /* Config.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 9J8LQ9R327; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + "FRAMEWORK_SEARCH_PATHS[arch=*]" = ( + "$(inherited)", + "$(SRCROOT)/../tasks-app-shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Taskfolio/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME)"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIStatusBarStyle = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID)"; + PRODUCT_NAME = "$(APP_DISPLAY_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4734A08A2E05CE9D00175C75 /* Build configuration list for PBXProject "Taskfolio" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4734A0B12E05CED900175C75 /* Debug */, + 4734A0B22E05CED900175C75 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4734A0B32E05CED900175C75 /* Build configuration list for PBXNativeTarget "Taskfolio" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4734A0B42E05CED900175C75 /* Debug */, + 4734A0B52E05CED900175C75 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 47629B9B2E17407D00391C27 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/google/GoogleSignIn-iOS"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 9.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 47629B9C2E17407D00391C27 /* GoogleSignIn */ = { + isa = XCSwiftPackageProductDependency; + package = 47629B9B2E17407D00391C27 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */; + productName = GoogleSignIn; + }; + 47629B9E2E17407D00391C27 /* GoogleSignInSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 47629B9B2E17407D00391C27 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */; + productName = GoogleSignInSwift; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 4734A0872E05CE9D00175C75 /* Project object */; +} diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AccentColor.colorset/Contents.json b/tasks-app-ios/Taskfolio/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..03aef6fd --- /dev/null +++ b/tasks-app-ios/Taskfolio/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.345", + "green" : "0.420", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.345", + "green" : "0.420", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.345", + "green" : "0.420", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AccentColor.dev.colorset/Contents.json b/tasks-app-ios/Taskfolio/Assets.xcassets/AccentColor.dev.colorset/Contents.json new file mode 100644 index 00000000..0e94f79e --- /dev/null +++ b/tasks-app-ios/Taskfolio/Assets.xcassets/AccentColor.dev.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.871", + "green" : "1.000", + "red" : "0.506" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.871", + "green" : "1.000", + "red" : "0.506" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.871", + "green" : "1.000", + "red" : "0.506" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Contents.json b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..1e2d7086 --- /dev/null +++ b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-60x60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-60x60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-20x20@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-76x76@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-76x76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-83.5x83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "ItunesArtwork@2x.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..9553a495 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..d2cab0a2 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..3fb3a7fb Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..66a54643 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..caaac082 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..b49fed01 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..d2cab0a2 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..0c270963 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..e575b2fd Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..e575b2fd Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..3827675f Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..ecf1bcbc Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..12d614cf Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..288ab03a Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png new file mode 100644 index 00000000..712ee259 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Contents.json b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Contents.json new file mode 100644 index 00000000..1e2d7086 --- /dev/null +++ b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-60x60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-60x60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-20x20@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-76x76@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-76x76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-83.5x83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "ItunesArtwork@2x.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-20x20@1x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..8574d97c Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-20x20@1x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-20x20@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..db8d8978 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-20x20@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-20x20@3x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..fa34697b Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-20x20@3x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-29x29@1x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..ce337d44 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-29x29@1x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-29x29@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..846913a7 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-29x29@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-29x29@3x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..03450ac8 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-29x29@3x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-40x40@1x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..db8d8978 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-40x40@1x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-40x40@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..7bd9b822 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-40x40@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-40x40@3x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..6280576a Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-40x40@3x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-60x60@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..6280576a Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-60x60@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-60x60@3x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..661b859e Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-60x60@3x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-76x76@1x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..a0d12aa8 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-76x76@1x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-76x76@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..33741028 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-76x76@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-83.5x83.5@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..54f933d8 Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/ItunesArtwork@2x.png b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/ItunesArtwork@2x.png new file mode 100644 index 00000000..643fe67b Binary files /dev/null and b/tasks-app-ios/Taskfolio/Assets.xcassets/AppIcon.dev.appiconset/ItunesArtwork@2x.png differ diff --git a/tasks-app-ios/Taskfolio/Assets.xcassets/Contents.json b/tasks-app-ios/Taskfolio/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/tasks-app-ios/Taskfolio/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tasks-app-ios/Taskfolio/Configuration/Versions.xcconfig b/tasks-app-ios/Taskfolio/Configuration/Versions.xcconfig new file mode 100644 index 00000000..8ee1c7cf --- /dev/null +++ b/tasks-app-ios/Taskfolio/Configuration/Versions.xcconfig @@ -0,0 +1,2 @@ +BUNDLE_VERSION=1 +BUNDLE_SHORT_VERSION_STRING=1.4.3 \ No newline at end of file diff --git a/tasks-app-ios/Taskfolio/ContentView.swift b/tasks-app-ios/Taskfolio/ContentView.swift new file mode 100644 index 00000000..4db5dd51 --- /dev/null +++ b/tasks-app-ios/Taskfolio/ContentView.swift @@ -0,0 +1,17 @@ +import SwiftUI +import TasksAppShared + +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(.keyboard) + } +} diff --git a/tasks-app-ios/Taskfolio/IOSGoogleAuthenticator.swift b/tasks-app-ios/Taskfolio/IOSGoogleAuthenticator.swift new file mode 100644 index 00000000..781b8d44 --- /dev/null +++ b/tasks-app-ios/Taskfolio/IOSGoogleAuthenticator.swift @@ -0,0 +1,140 @@ +import TasksAppShared +import GoogleSignIn +import UIKit + +@MainActor +class IOSGoogleAuthenticator: OauthGoogleAuthenticator { + + func authorize(scopes: [OauthGoogleAuthenticatorScope], force: Bool, requestUserAuthorization: @escaping (Any) -> Void) async throws -> String { + let stringScopes = scopes.compactMap { $0.value } + + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first, + let rootViewController = window.rootViewController else { + throw GoogleSignInError.noRootViewController + } + + guard let clientId = Bundle.main.object(forInfoDictionaryKey: "GIDClientID") as? String else { + throw GoogleSignInError.missingConfiguration + } + + if GIDSignIn.sharedInstance.configuration == nil { + let config: GIDConfiguration + if !stringScopes.isEmpty { + config = GIDConfiguration(clientID: clientId, serverClientID: clientId) + } else { + config = GIDConfiguration(clientID: clientId) + } + GIDSignIn.sharedInstance.configuration = config + } + + return try await withCheckedThrowingContinuation { continuation in + // FIXME in Jvm impl, the force means &prompt=consent&access_type=offline + // is it needed here? at least no need to sign-out + // or on Android .requestOfflineAccess(config.clientId, force) + if force { + GIDSignIn.sharedInstance.signOut() + } + + if !force, let currentUser = GIDSignIn.sharedInstance.currentUser { + if !stringScopes.isEmpty { + currentUser.addScopes(stringScopes, presenting: rootViewController) { result, error in + if let error = error { + continuation.resume(throwing: error) + } else if let user = result { + continuation.resume(returning: user.user.userID ?? "") + } else { + continuation.resume(throwing: GoogleSignInError.unknownError) + } + } + } else { + continuation.resume(returning: currentUser.userID ?? "") + } + return + } + + GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) { result, error in + if let error = error { + continuation.resume(throwing: error) + } else if let user = result?.user { + if !stringScopes.isEmpty { + user.addScopes(stringScopes, presenting: rootViewController) { scopeResult, scopeError in + if let scopeError = scopeError { + continuation.resume(throwing: scopeError) + } else { + continuation.resume(returning: user.userID ?? "") + } + } + } else { + continuation.resume(returning: user.userID ?? "") + } + } else { + continuation.resume(throwing: GoogleSignInError.unknownError) + } + } + } + } + + func getToken(grant: any OauthGoogleAuthenticatorGrant) async throws -> OauthGoogleAuthenticatorOAuthToken { + guard let currentUser = GIDSignIn.sharedInstance.currentUser else { + throw GoogleSignInError.userNotSignedIn + } + + return try await withCheckedThrowingContinuation { continuation in + currentUser.refreshTokensIfNeeded { user, error in + if let error = error { + continuation.resume(throwing: error) + return + } + + guard let user = user else { + continuation.resume(throwing: GoogleSignInError.tokenNotAvailable) + return + } + let accessToken = user.accessToken.tokenString + + let expirationDate = user.accessToken.expirationDate + let currentDate = Date() + let expiresIn = Int64(expirationDate?.timeIntervalSince(currentDate) ?? 0) + + let token = OauthGoogleAuthenticatorOAuthToken( + accessToken: accessToken, + expiresIn: expiresIn, + idToken: user.idToken?.tokenString, + refreshToken: user.refreshToken.tokenString, + scope: user.grantedScopes?.joined(separator: " ") ?? "", + tokenType: OauthGoogleAuthenticatorOAuthToken.TokenType.bearer + ) + + continuation.resume(returning: token) + } + } + } +} + +// MARK: - Error Types +enum GoogleSignInError: Error, LocalizedError { + case noRootViewController + case missingConfiguration + case configurationFailed + case userNotSignedIn + case tokenNotAvailable + case unknownError + + var errorDescription: String? { + switch self { + case .noRootViewController: + return "Unable to find root view controller" + case .missingConfiguration: + return "No GIDClientID found in Info.plist" + case .configurationFailed: + return "Failed to configure Google Sign-In" + case .userNotSignedIn: + return "User is not signed in" + case .tokenNotAvailable: + return "Access token not available" + case .unknownError: + return "An unknown error occurred" + } + } +} diff --git a/tasks-app-ios/Taskfolio/Info.plist b/tasks-app-ios/Taskfolio/Info.plist new file mode 100644 index 00000000..c2096d84 --- /dev/null +++ b/tasks-app-ios/Taskfolio/Info.plist @@ -0,0 +1,27 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleShortVersionString + $(BUNDLE_SHORT_VERSION_STRING) + CFBundleVersion + $(BUNDLE_VERSION) + CFBundleDisplayName + $(APP_DISPLAY_NAME) + GIDClientID + $(GCP_CLIENT_ID) + CFBundleURLTypes + + + CFBundleURLName + google-sign-in + CFBundleURLSchemes + + $(GCP_REVERSED_CLIENT_ID) + + + + + diff --git a/tasks-app-ios/Taskfolio/Resources/licenses_ios.json b/tasks-app-ios/Taskfolio/Resources/licenses_ios.json new file mode 100644 index 00000000..26a4f170 --- /dev/null +++ b/tasks-app-ios/Taskfolio/Resources/licenses_ios.json @@ -0,0 +1,1652 @@ +{ + "libraries": [ + { + "uniqueId": "androidx.annotation:annotation", + "artifactVersion": "1.9.1", + "name": "Annotation", + "description": "Provides source annotations for tooling and readability.", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.arch.core:core-common", + "artifactVersion": "2.2.0", + "name": "Android Arch-Common", + "description": "Android Arch-Common", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.collection:collection", + "artifactVersion": "1.5.0", + "name": "collections", + "description": "Standalone efficient collections.", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.compose.runtime:runtime", + "artifactVersion": "1.10.0-alpha02", + "name": "Compose Runtime", + "description": "Tree composition support for code generated by the Compose compiler plugin and corresponding public API", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.compose.runtime:runtime-annotation", + "artifactVersion": "1.10.0-alpha02", + "name": "Compose Runtime Annotation", + "description": "Provides Compose-specific annotations used by the compiler and tooling", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.compose.runtime:runtime-saveable", + "artifactVersion": "1.10.0-alpha02", + "name": "Compose Saveable", + "description": "Compose components that allow saving and restoring the local ui state", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.lifecycle:lifecycle-common", + "artifactVersion": "2.10.0-alpha03", + "name": "Lifecycle-Common", + "description": "Android Lifecycle-Common", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.lifecycle:lifecycle-runtime", + "artifactVersion": "2.10.0-alpha03", + "name": "Lifecycle Runtime", + "description": "Android Lifecycle Runtime", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.lifecycle:lifecycle-runtime-compose", + "artifactVersion": "2.10.0-alpha03", + "name": "Lifecycle Runtime Compose", + "description": "Compose integration with Lifecycle", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.lifecycle:lifecycle-viewmodel", + "artifactVersion": "2.10.0-alpha03", + "name": "Lifecycle ViewModel", + "description": "Android Lifecycle ViewModel", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.lifecycle:lifecycle-viewmodel-savedstate", + "artifactVersion": "2.10.0-alpha03", + "name": "Lifecycle ViewModel with SavedState", + "description": "Android Lifecycle ViewModel", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.room:room-common", + "artifactVersion": "2.8.3", + "name": "Room-Common", + "description": "Android Room-Common", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.room:room-runtime", + "artifactVersion": "2.8.3", + "name": "Room-Runtime", + "description": "Android Room-Runtime", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.savedstate:savedstate", + "artifactVersion": "1.4.0-alpha03", + "name": "Saved State", + "description": "Android Lifecycle Saved State", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.savedstate:savedstate-compose", + "artifactVersion": "1.4.0-alpha03", + "name": "Saved State Compose", + "description": "Compose integration with Saved State", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.sqlite:sqlite", + "artifactVersion": "2.6.1", + "name": "SQLite", + "description": "SQLite API", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "androidx.sqlite:sqlite-bundled", + "artifactVersion": "2.6.1", + "name": "SQLite Bundled Integration", + "description": "The implementation of SQLite library using the bundled SQLite.", + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "organization": { + "name": "The Android Open Source Project" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "co.touchlab:stately-concurrency", + "artifactVersion": "2.1.0", + "name": "Stately", + "description": "Multithreaded Kotlin Multiplatform Utilities", + "developers": [ + { + "name": "Kevin Galligan" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "com.mikepenz:aboutlibraries-core", + "artifactVersion": "13.1.0", + "name": "AboutLibraries Core Library", + "description": "AboutLibraries automatically detects all dependencies of a project and collects their information including the license. Optionally visualising it via the provided ui components.", + "developers": [ + { + "name": "Mike Penz" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "com.squareup.okio:okio", + "artifactVersion": "3.15.0", + "name": "okio", + "description": "A modern I/O library for Android, Java, and Kotlin Multiplatform.", + "developers": [ + { + "name": "Square, Inc." + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "com.typesafe:config", + "artifactVersion": "1.4.4", + "name": "config", + "description": "configuration library for JVM languages using HOCON files", + "developers": [ + { + "name": "Havoc Pennington" + } + ], + "organization": { + "name": "com.typesafe" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.coil-kt.coil3:coil", + "artifactVersion": "3.3.0", + "name": "coil", + "description": "An image loading library for Android and Compose Multiplatform.", + "developers": [ + { + "name": "Coil Contributors" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.coil-kt.coil3:coil-compose", + "artifactVersion": "3.3.0", + "name": "coil-compose", + "description": "An image loading library for Android and Compose Multiplatform.", + "developers": [ + { + "name": "Coil Contributors" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.coil-kt.coil3:coil-compose-core", + "artifactVersion": "3.3.0", + "name": "coil-compose-core", + "description": "An image loading library for Android and Compose Multiplatform.", + "developers": [ + { + "name": "Coil Contributors" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.coil-kt.coil3:coil-core", + "artifactVersion": "3.3.0", + "name": "coil-core", + "description": "An image loading library for Android and Compose Multiplatform.", + "developers": [ + { + "name": "Coil Contributors" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.coil-kt.coil3:coil-network-core", + "artifactVersion": "3.3.0", + "name": "coil-network-core", + "description": "An image loading library for Android and Compose Multiplatform.", + "developers": [ + { + "name": "Coil Contributors" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.coil-kt.coil3:coil-network-ktor3", + "artifactVersion": "3.3.0", + "name": "coil-network-ktor3", + "description": "An image loading library for Android and Compose Multiplatform.", + "developers": [ + { + "name": "Coil Contributors" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.insert-koin:koin-core", + "artifactVersion": "4.1.1", + "name": "Koin", + "description": "KOIN - Kotlin simple Dependency Injection Framework", + "developers": [ + { + "name": "Arnaud Giuliani" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-client-auth", + "artifactVersion": "3.3.1", + "name": "ktor-client-auth", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-client-cio", + "artifactVersion": "3.3.1", + "name": "ktor-client-cio", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-client-content-negotiation", + "artifactVersion": "3.3.1", + "name": "ktor-client-content-negotiation", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-client-core", + "artifactVersion": "3.3.1", + "name": "ktor-client-core", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-client-encoding", + "artifactVersion": "3.3.1", + "name": "ktor-client-encoding", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-client-logging", + "artifactVersion": "3.3.1", + "name": "ktor-client-logging", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-events", + "artifactVersion": "3.3.1", + "name": "ktor-events", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-http", + "artifactVersion": "3.3.1", + "name": "ktor-http", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-http-cio", + "artifactVersion": "3.3.1", + "name": "ktor-http-cio", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-io", + "artifactVersion": "3.3.1", + "name": "ktor-io", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-network", + "artifactVersion": "3.3.1", + "name": "ktor-network", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-network-tls", + "artifactVersion": "3.3.1", + "name": "ktor-network-tls", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-serialization", + "artifactVersion": "3.3.1", + "name": "ktor-serialization", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-serialization-kotlinx", + "artifactVersion": "3.3.1", + "name": "ktor-serialization-kotlinx", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-serialization-kotlinx-json", + "artifactVersion": "3.3.1", + "name": "ktor-serialization-kotlinx-json", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-server-cio", + "artifactVersion": "3.3.1", + "name": "ktor-server-cio", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-server-core-jvm", + "artifactVersion": "3.3.1", + "name": "ktor-server-core", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-server-html-builder", + "artifactVersion": "3.3.1", + "name": "ktor-server-html-builder", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-server-status-pages", + "artifactVersion": "3.3.1", + "name": "ktor-server-status-pages", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-sse", + "artifactVersion": "3.3.1", + "name": "ktor-sse", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-utils", + "artifactVersion": "3.3.1", + "name": "ktor-utils", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-websocket-serialization", + "artifactVersion": "3.3.1", + "name": "ktor-websocket-serialization", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "io.ktor:ktor-websockets", + "artifactVersion": "3.3.1", + "name": "ktor-websockets", + "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", + "developers": [ + { + "name": "Jetbrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.fusesource.jansi:jansi", + "artifactVersion": "2.4.2", + "name": "Jansi", + "description": "Jansi is a java library for generating and interpreting ANSI escape sequences.", + "developers": [ + { + "name": "Guillaume Nodet" + }, + { + "name": "Hiram Chirino" + } + ], + "organization": { + "name": "FuseSource, Corp." + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.lifecycle:lifecycle-common", + "artifactVersion": "2.10.0-alpha01", + "name": "Lifecycle-Common", + "description": "Android Lifecycle-Common", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.lifecycle:lifecycle-runtime", + "artifactVersion": "2.10.0-alpha01", + "name": "Lifecycle Runtime", + "description": "Android Lifecycle Runtime", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", + "artifactVersion": "2.10.0-alpha01", + "name": "Lifecycle Runtime Compose", + "description": "Compose integration with Lifecycle", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", + "artifactVersion": "2.10.0-alpha01", + "name": "Lifecycle ViewModel", + "description": "Android Lifecycle ViewModel", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", + "artifactVersion": "2.9.5", + "name": "Lifecycle ViewModel Compose", + "description": "Compose integration with Lifecycle ViewModel", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate", + "artifactVersion": "2.10.0-alpha01", + "name": "Lifecycle ViewModel with SavedState", + "description": "Android Lifecycle ViewModel", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.navigation:navigation-common", + "artifactVersion": "2.9.1", + "name": "Navigation Common", + "description": "Android Navigation-Common", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.navigation:navigation-compose", + "artifactVersion": "2.9.1", + "name": "Compose Navigation", + "description": "Compose integration with Navigation", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.navigation:navigation-runtime", + "artifactVersion": "2.9.1", + "name": "Navigation Runtime", + "description": "Android Navigation-Runtime", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.savedstate:savedstate", + "artifactVersion": "1.4.0-alpha01", + "name": "Saved State", + "description": "Android Lifecycle Saved State", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.savedstate:savedstate-compose", + "artifactVersion": "1.4.0-alpha01", + "name": "Saved State Compose", + "description": "Compose integration with Saved State", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.androidx.window:window-core", + "artifactVersion": "1.4.0", + "name": "WindowManager Core", + "description": "WindowManager Core Library.", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.animation:animation", + "artifactVersion": "1.9.1", + "name": "Compose Animation", + "description": "Compose animation library", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.animation:animation-core", + "artifactVersion": "1.9.1", + "name": "Compose Animation Core", + "description": "Animation engine and animation primitives that are the building blocks of the Compose animation library", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.annotation-internal:annotation", + "artifactVersion": "1.10.0-alpha01", + "name": "Annotation", + "description": "Provides source annotations for tooling and readability.", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.collection-internal:collection", + "artifactVersion": "1.10.0-alpha01", + "name": "collections", + "description": "Standalone efficient collections.", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.components:components-resources", + "artifactVersion": "1.9.1", + "name": "Resources for Compose JB", + "description": "Resources for Compose JB", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.components:components-ui-tooling-preview", + "artifactVersion": "1.9.1", + "name": "Experimental Compose Multiplatform tooling library API. This library provides the API required to declare @Preview composables in user apps.", + "description": "Experimental Compose Multiplatform tooling library API. This library provides the API required to declare @Preview composables in user apps.", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.foundation:foundation", + "artifactVersion": "1.9.1", + "name": "Compose Foundation", + "description": "Higher level abstractions of the Compose UI primitives. This library is design system agnostic, providing the high-level building blocks for both application and design-system developers", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.foundation:foundation-layout", + "artifactVersion": "1.9.1", + "name": "Compose Layouts", + "description": "Compose layout implementations", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.material3.adaptive:adaptive", + "artifactVersion": "1.1.2", + "name": "Material Adaptive", + "description": "Compose Material Design Adaptive Library", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.material3:material3", + "artifactVersion": "1.9.0", + "name": "Compose Material3 Components", + "description": "Compose Material You Design Components library", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.material3:material3-adaptive-navigation-suite", + "artifactVersion": "1.9.0", + "name": "Material Adaptive Navigation Suite", + "description": "Compose Material Design Adaptive Navigation Suite Library", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.material:material-ripple", + "artifactVersion": "1.9.1", + "name": "Compose Material Ripple", + "description": "Material ripple used to build interactive components", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.runtime:runtime", + "artifactVersion": "1.10.0-alpha01", + "name": "Compose Runtime", + "description": "Tree composition support for code generated by the Compose compiler plugin and corresponding public API", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.runtime:runtime-saveable", + "artifactVersion": "1.10.0-alpha01", + "name": "Compose Saveable", + "description": "Compose components that allow saving and restoring the local ui state", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.ui:ui", + "artifactVersion": "1.10.0-alpha01", + "name": "Compose UI primitives", + "description": "Compose UI primitives. This library contains the primitives that form the Compose UI Toolkit, such as drawing, measurement and layout.", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.ui:ui-backhandler", + "artifactVersion": "1.10.0-alpha01", + "name": "Compose Multiplatform BackHandler", + "description": "Provides BackHandler in Compose Multiplatform projects", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.ui:ui-geometry", + "artifactVersion": "1.10.0-alpha01", + "name": "Compose Geometry", + "description": "Compose classes related to dimensions without units", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.ui:ui-graphics", + "artifactVersion": "1.10.0-alpha01", + "name": "Compose Graphics", + "description": "Compose graphics", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.ui:ui-text", + "artifactVersion": "1.10.0-alpha01", + "name": "Compose UI Text", + "description": "Compose Text primitives and utilities", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.ui:ui-uikit", + "artifactVersion": "1.9.1", + "name": "Compose UIKit Utils", + "description": "Internal iOS UIKit utilities including Objective-C library.", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.ui:ui-unit", + "artifactVersion": "1.10.0-alpha01", + "name": "Compose Unit", + "description": "Compose classes for simple units", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.ui:ui-util", + "artifactVersion": "1.10.0-alpha01", + "name": "Compose Util", + "description": "Internal Compose utilities used by other modules", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlin:kotlin-reflect", + "artifactVersion": "2.2.20", + "name": "Kotlin Reflect", + "description": "Kotlin Full Reflection Library", + "developers": [ + { + "name": "Kotlin Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlin:kotlin-stdlib", + "artifactVersion": "2.2.21", + "name": "Kotlin Stdlib", + "description": "Kotlin Standard Library", + "developers": [ + { + "name": "Kotlin Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlin:kotlin-stdlib-common", + "artifactVersion": "2.2.21", + "name": "Kotlin Stdlib Common", + "description": "Kotlin Common Standard Library (legacy, use kotlin-stdlib instead)", + "developers": [ + { + "name": "Kotlin Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlin:kotlin-stdlib-jdk7", + "artifactVersion": "2.2.10", + "name": "Kotlin Stdlib Jdk7", + "description": "Kotlin Standard Library JDK 7 extension", + "developers": [ + { + "name": "Kotlin Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlin:kotlin-stdlib-jdk8", + "artifactVersion": "2.2.10", + "name": "Kotlin Stdlib Jdk8", + "description": "Kotlin Standard Library JDK 8 extension", + "developers": [ + { + "name": "Kotlin Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlinx:atomicfu", + "artifactVersion": "0.23.2", + "name": "atomicfu", + "description": "AtomicFU utilities", + "developers": [ + { + "name": "JetBrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlinx:kotlinx-collections-immutable", + "artifactVersion": "0.4.0", + "name": "kotlinx-collections-immutable", + "description": "Kotlin Immutable Collections multiplatform library", + "developers": [ + { + "name": "JetBrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlinx:kotlinx-coroutines-core", + "artifactVersion": "1.10.2", + "name": "kotlinx-coroutines-core", + "description": "Coroutines support libraries for Kotlin", + "developers": [ + { + "name": "JetBrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", + "artifactVersion": "1.10.2", + "name": "kotlinx-coroutines-slf4j", + "description": "Coroutines support libraries for Kotlin", + "developers": [ + { + "name": "JetBrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlinx:kotlinx-datetime", + "artifactVersion": "0.7.1", + "name": "kotlinx-datetime", + "description": "Kotlin Datetime Library", + "developers": [ + { + "name": "JetBrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlinx:kotlinx-html", + "artifactVersion": "0.12.0", + "name": "kotlinx.html", + "description": "A kotlinx.html library provides DSL to build HTML to Writer/Appendable or DOM at JVM and browser (or other JavaScript engine) for better Kotlin programming for Web.", + "developers": [ + { + "name": "Sergey Mashkov" + }, + { + "name": "Anton Dmitriev" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlinx:kotlinx-io-bytestring", + "artifactVersion": "0.8.0", + "name": "kotlinx-io-bytestring", + "description": "IO support for Kotlin", + "developers": [ + { + "name": "JetBrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlinx:kotlinx-io-core", + "artifactVersion": "0.8.0", + "name": "kotlinx-io-core", + "description": "IO support for Kotlin", + "developers": [ + { + "name": "JetBrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlinx:kotlinx-serialization-core", + "artifactVersion": "1.9.0", + "name": "kotlinx-serialization-core", + "description": "Kotlin multiplatform serialization runtime library", + "developers": [ + { + "name": "JetBrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlinx:kotlinx-serialization-json-io-jvm", + "artifactVersion": "1.9.0", + "name": "kotlinx-serialization-json-io", + "description": "Kotlin multiplatform serialization runtime library", + "developers": [ + { + "name": "JetBrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", + "artifactVersion": "1.9.0", + "name": "kotlinx-serialization-json", + "description": "Kotlin multiplatform serialization runtime library", + "developers": [ + { + "name": "JetBrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.runtime:jbr-api", + "artifactVersion": "1.5.0", + "name": "jbr-api", + "description": "Interface for the functionality specific to https://github.com/JetBrains/JetBrainsRuntime", + "developers": [ + { + "name": "Nikita Gubarkov" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.skiko:skiko", + "artifactVersion": "0.9.24", + "name": "Skiko MPP", + "description": "Kotlin Skia bindings", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.skiko:skiko-awt", + "artifactVersion": "0.9.24", + "name": "Skiko Awt", + "description": "Kotlin Skia bindings", + "developers": [ + { + "name": "Compose Multiplatform Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains:annotations", + "artifactVersion": "13.0", + "name": "IntelliJ IDEA Annotations", + "description": "A set of annotations used for code inspection support and code documentation.", + "developers": [ + { + "name": "JetBrains Team" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jspecify:jspecify", + "artifactVersion": "1.0.0", + "name": "JSpecify annotations", + "description": "An artifact of well-named and well-specified annotations to power static analysis checks", + "developers": [ + { + "name": "Kevin Bourrillion" + } + ], + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.slf4j:slf4j-api", + "artifactVersion": "2.0.17", + "name": "SLF4J API Module", + "description": "The slf4j API", + "developers": [ + { + "name": "Ceki Gulcu" + } + ], + "organization": { + "name": "QOS.ch" + }, + "licenses": [ + "MIT" + ] + }, + { + "uniqueId": "org.slf4j:slf4j-nop", + "artifactVersion": "2.0.17", + "name": "SLF4J NOP Provider", + "description": "SLF4J NOP Provider", + "developers": [ + { + "name": "Ceki Gulcu" + } + ], + "organization": { + "name": "QOS.ch" + }, + "licenses": [ + "MIT" + ] + }, + { + "uniqueId": "ro.cosminmihu.ktor:ktor-monitor-logging-no-op", + "artifactVersion": "1.7.4", + "name": "Ktor Monitor", + "description": "Powerful tools to log Ktor Client requests and responses, making it easier to debug and analyze network communication.", + "developers": [ + { + "name": "Cosmin Mihu" + } + ], + "licenses": [ + "Apache-2.0" + ] + } + ], + "licenses": { + "ASDKL": { + "name": "Android Software Development Kit License Agreement", + "url": "https://developer.android.com/studio/terms.html", + "content": "This is the Android Software Development Kit License Agreement\n
\n1. Introduction\n
\n1.1 The Android Software Development Kit (referred to in the License Agreement as the \"SDK\" and specifically including the Android system files, packaged APIs, and Google APIs add-ons) is licensed to you subject to the terms of the License Agreement. The License Agreement forms a legally binding contract between you and Google in relation to your use of the SDK.\n
\n1.2 \"Android\" means the Android software stack for devices, as made available under the Android Open Source Project, which is located at the following URL: http://source.android.com/, as updated from time to time.\n
\n1.3 A \"compatible implementation\" means any Android device that (i) complies with the Android Compatibility Definition document, which can be found at the Android compatibility website (http://source.android.com/compatibility) and which may be updated from time to time; and (ii) successfully passes the Android Compatibility Test Suite (CTS).\n
\n1.4 \"Google\" means Google LLC, a Delaware corporation with principal place of business at 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States.\n
\n2. Accepting this License Agreement\n
\n2.1 In order to use the SDK, you must first agree to the License Agreement. You may not use the SDK if you do not accept the License Agreement.\n
\n2.2 By clicking to accept, you hereby agree to the terms of the License Agreement.\n
\n2.3 You may not use the SDK and may not accept the License Agreement if you are a person barred from receiving the SDK under the laws of the United States or other countries, including the country in which you are resident or from which you use the SDK.\n
\n2.4 If you are agreeing to be bound by the License Agreement on behalf of your employer or other entity, you represent and warrant that you have full legal authority to bind your employer or such entity to the License Agreement. If you do not have the requisite authority, you may not accept the License Agreement or use the SDK on behalf of your employer or other entity.\n
\n3. SDK License from Google\n
\n3.1 Subject to the terms of the License Agreement, Google grants you a limited, worldwide, royalty-free, non-assignable, non-exclusive, and non-sublicensable license to use the SDK solely to develop applications for compatible implementations of Android.\n
\n3.2 You may not use this SDK to develop applications for other platforms (including non-compatible implementations of Android) or to develop another SDK. You are of course free to develop applications for other platforms, including non-compatible implementations of Android, provided that this SDK is not used for that purpose.\n
\n3.3 You agree that Google or third parties own all legal right, title and interest in and to the SDK, including any Intellectual Property Rights that subsist in the SDK. \"Intellectual Property Rights\" means any and all rights under patent law, copyright law, trade secret law, trademark law, and any and all other proprietary rights. Google reserves all rights not expressly granted to you.\n
\n3.4 You may not use the SDK for any purpose not expressly permitted by the License Agreement. Except to the extent required by applicable third party licenses, you may not copy (except for backup purposes), modify, adapt, redistribute, decompile, reverse engineer, disassemble, or create derivative works of the SDK or any part of the SDK.\n
\n3.5 Use, reproduction and distribution of components of the SDK licensed under an open source software license are governed solely by the terms of that open source software license and not the License Agreement.\n
\n3.6 You agree that the form and nature of the SDK that Google provides may change without prior notice to you and that future versions of the SDK may be incompatible with applications developed on previous versions of the SDK. You agree that Google may stop (permanently or temporarily) providing the SDK (or any features within the SDK) to you or to users generally at Google's sole discretion, without prior notice to you.\n
\n3.7 Nothing in the License Agreement gives you a right to use any of Google's trade names, trademarks, service marks, logos, domain names, or other distinctive brand features.\n
\n3.8 You agree that you will not remove, obscure, or alter any proprietary rights notices (including copyright and trademark notices) that may be affixed to or contained within the SDK.\n
\n4. Use of the SDK by You\n
\n4.1 Google agrees that it obtains no right, title or interest from you (or your licensors) under the License Agreement in or to any software applications that you develop using the SDK, including any intellectual property rights that subsist in those applications.\n
\n4.2 You agree to use the SDK and write applications only for purposes that are permitted by (a) the License Agreement and (b) any applicable law, regulation or generally accepted practices or guidelines in the relevant jurisdictions (including any laws regarding the export of data or software to and from the United States or other relevant countries).\n
\n4.3 You agree that if you use the SDK to develop applications for general public users, you will protect the privacy and legal rights of those users. If the users provide you with user names, passwords, or other login information or personal information, you must make the users aware that the information will be available to your application, and you must provide legally adequate privacy notice and protection for those users. If your application stores personal or sensitive information provided by users, it must do so securely. If the user provides your application with Google Account information, your application may only use that information to access the user's Google Account when, and for the limited purposes for which, the user has given you permission to do so.\n
\n4.4 You agree that you will not engage in any activity with the SDK, including the development or distribution of an application, that interferes with, disrupts, damages, or accesses in an unauthorized manner the servers, networks, or other properties or services of any third party including, but not limited to, Google or any mobile communications carrier.\n
\n4.5 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) any data, content, or resources that you create, transmit or display through Android and/or applications for Android, and for the consequences of your actions (including any loss or damage which Google may suffer) by doing so.\n
\n4.6 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) any breach of your obligations under the License Agreement, any applicable third party contract or Terms of Service, or any applicable law or regulation, and for the consequences (including any loss or damage which Google or any third party may suffer) of any such breach.\n
\n5. Your Developer Credentials\n
\n5.1 You agree that you are responsible for maintaining the confidentiality of any developer credentials that may be issued to you by Google or which you may choose yourself and that you will be solely responsible for all applications that are developed under your developer credentials.\n
\n6. Privacy and Information\n
\n6.1 In order to continually innovate and improve the SDK, Google may collect certain usage statistics from the software including but not limited to a unique identifier, associated IP address, version number of the software, and information on which tools and/or services in the SDK are being used and how they are being used. Before any of this information is collected, the SDK will notify you and seek your consent. If you withhold consent, the information will not be collected.\n
\n6.2 The data collected is examined in the aggregate to improve the SDK and is maintained in accordance with Google's Privacy Policy.\n
\n7. Third Party Applications\n
\n7.1 If you use the SDK to run applications developed by a third party or that access data, content or resources provided by a third party, you agree that Google is not responsible for those applications, data, content, or resources. You understand that all data, content or resources which you may access through such third party applications are the sole responsibility of the person from which they originated and that Google is not liable for any loss or damage that you may experience as a result of the use or access of any of those third party applications, data, content, or resources.\n
\n7.2 You should be aware the data, content, and resources presented to you through such a third party application may be protected by intellectual property rights which are owned by the providers (or by other persons or companies on their behalf). You may not modify, rent, lease, loan, sell, distribute or create derivative works based on these data, content, or resources (either in whole or in part) unless you have been specifically given permission to do so by the relevant owners.\n
\n7.3 You acknowledge that your use of such third party applications, data, content, or resources may be subject to separate terms between you and the relevant third party. In that case, the License Agreement does not affect your legal relationship with these third parties.\n
\n8. Using Android APIs\n
\n8.1 Google Data APIs\n
\n8.1.1 If you use any API to retrieve data from Google, you acknowledge that the data may be protected by intellectual property rights which are owned by Google or those parties that provide the data (or by other persons or companies on their behalf). Your use of any such API may be subject to additional Terms of Service. You may not modify, rent, lease, loan, sell, distribute or create derivative works based on this data (either in whole or in part) unless allowed by the relevant Terms of Service.\n
\n8.1.2 If you use any API to retrieve a user's data from Google, you acknowledge and agree that you shall retrieve data only with the user's explicit consent and only when, and for the limited purposes for which, the user has given you permission to do so. If you use the Android Recognition Service API, documented at the following URL: https://developer.android.com/reference/android/speech/RecognitionService, as updated from time to time, you acknowledge that the use of the API is subject to the Data Processing Addendum for Products where Google is a Data Processor, which is located at the following URL: https://privacy.google.com/businesses/gdprprocessorterms/, as updated from time to time. By clicking to accept, you hereby agree to the terms of the Data Processing Addendum for Products where Google is a Data Processor.\n
\n9. Terminating this License Agreement\n
\n9.1 The License Agreement will continue to apply until terminated by either you or Google as set out below.\n
\n9.2 If you want to terminate the License Agreement, you may do so by ceasing your use of the SDK and any relevant developer credentials.\n
\n9.3 Google may at any time, terminate the License Agreement with you if:
\n(A) you have breached any provision of the License Agreement; or
\n(B) Google is required to do so by law; or
\n(C) the partner with whom Google offered certain parts of SDK (such as APIs) to you has terminated its relationship with Google or ceased to offer certain parts of the SDK to you; or
\n(D) Google decides to no longer provide the SDK or certain parts of the SDK to users in the country in which you are resident or from which you use the service, or the provision of the SDK or certain SDK services to you by Google is, in Google's sole discretion, no longer commercially viable.
\n
\n9.4 When the License Agreement comes to an end, all of the legal rights, obligations and liabilities that you and Google have benefited from, been subject to (or which have accrued over time whilst the License Agreement has been in force) or which are expressed to continue indefinitely, shall be unaffected by this cessation, and the provisions of paragraph 14.7 shall continue to apply to such rights, obligations and liabilities indefinitely.\n
\n10. DISCLAIMER OF WARRANTIES\n
\n10.1 YOU EXPRESSLY UNDERSTAND AND AGREE THAT YOUR USE OF THE SDK IS AT YOUR SOLE RISK AND THAT THE SDK IS PROVIDED \"AS IS\" AND \"AS AVAILABLE\" WITHOUT WARRANTY OF ANY KIND FROM GOOGLE.\n
\n10.2 YOUR USE OF THE SDK AND ANY MATERIAL DOWNLOADED OR OTHERWISE OBTAINED THROUGH THE USE OF THE SDK IS AT YOUR OWN DISCRETION AND RISK AND YOU ARE SOLELY RESPONSIBLE FOR ANY DAMAGE TO YOUR COMPUTER SYSTEM OR OTHER DEVICE OR LOSS OF DATA THAT RESULTS FROM SUCH USE.\n
\n10.3 GOOGLE FURTHER EXPRESSLY DISCLAIMS ALL WARRANTIES AND CONDITIONS OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.\n
\n11. LIMITATION OF LIABILITY\n
\n11.1 YOU EXPRESSLY UNDERSTAND AND AGREE THAT GOOGLE, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS SHALL NOT BE LIABLE TO YOU UNDER ANY THEORY OF LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL OR EXEMPLARY DAMAGES THAT MAY BE INCURRED BY YOU, INCLUDING ANY LOSS OF DATA, WHETHER OR NOT GOOGLE OR ITS REPRESENTATIVES HAVE BEEN ADVISED OF OR SHOULD HAVE BEEN AWARE OF THE POSSIBILITY OF ANY SUCH LOSSES ARISING.\n
\n12. Indemnification\n
\n12.1 To the maximum extent permitted by law, you agree to defend, indemnify and hold harmless Google, its affiliates and their respective directors, officers, employees and agents from and against any and all claims, actions, suits or proceedings, as well as any and all losses, liabilities, damages, costs and expenses (including reasonable attorneys fees) arising out of or accruing from (a) your use of the SDK, (b) any application you develop on the SDK that infringes any copyright, trademark, trade secret, trade dress, patent or other intellectual property right of any person or defames any person or violates their rights of publicity or privacy, and (c) any non-compliance by you with the License Agreement.\n
\n13. Changes to the License Agreement\n
\n13.1 Google may make changes to the License Agreement as it distributes new versions of the SDK. When these changes are made, Google will make a new version of the License Agreement available on the website where the SDK is made available.\n
\n14. General Legal Terms\n
\n14.1 The License Agreement constitutes the whole legal agreement between you and Google and governs your use of the SDK (excluding any services which Google may provide to you under a separate written agreement), and completely replaces any prior agreements between you and Google in relation to the SDK.\n
\n14.2 You agree that if Google does not exercise or enforce any legal right or remedy which is contained in the License Agreement (or which Google has the benefit of under any applicable law), this will not be taken to be a formal waiver of Google's rights and that those rights or remedies will still be available to Google.\n
\n14.3 If any court of law, having the jurisdiction to decide on this matter, rules that any provision of the License Agreement is invalid, then that provision will be removed from the License Agreement without affecting the rest of the License Agreement. The remaining provisions of the License Agreement will continue to be valid and enforceable.\n
\n14.4 You acknowledge and agree that each member of the group of companies of which Google is the parent shall be third party beneficiaries to the License Agreement and that such other companies shall be entitled to directly enforce, and rely upon, any provision of the License Agreement that confers a benefit on (or rights in favor of) them. Other than this, no other person or company shall be third party beneficiaries to the License Agreement.\n
\n14.5 EXPORT RESTRICTIONS. THE SDK IS SUBJECT TO UNITED STATES EXPORT LAWS AND REGULATIONS. YOU MUST COMPLY WITH ALL DOMESTIC AND INTERNATIONAL EXPORT LAWS AND REGULATIONS THAT APPLY TO THE SDK. THESE LAWS INCLUDE RESTRICTIONS ON DESTINATIONS, END USERS AND END USE.\n
\n14.6 The rights granted in the License Agreement may not be assigned or transferred by either you or Google without the prior written approval of the other party. Neither you nor Google shall be permitted to delegate their responsibilities or obligations under the License Agreement without the prior written approval of the other party.\n
\n14.7 The License Agreement, and your relationship with Google under the License Agreement, shall be governed by the laws of the State of California without regard to its conflict of laws provisions. You and Google agree to submit to the exclusive jurisdiction of the courts located within the county of Santa Clara, California to resolve any legal matter arising from the License Agreement. Notwithstanding this, you agree that Google shall still be allowed to apply for injunctive remedies (or an equivalent type of urgent legal relief) in any jurisdiction.", + "internalHash": "ASDKL", + "spdxId": "ASDKL", + "hash": "ASDKL" + }, + "Apache-2.0": { + "name": "Apache License 2.0", + "url": "https://spdx.org/licenses/Apache-2.0.html", + "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:\n\n (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.\n\n You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\nTo apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets \"[]\" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \"printed page\" as the copyright notice for easier identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.", + "internalHash": "Apache-2.0", + "spdxId": "Apache-2.0", + "hash": "Apache-2.0" + }, + "BSD-3-Clause": { + "name": "BSD 3-Clause \"New\" or \"Revised\" License", + "url": "https://spdx.org/licenses/BSD-3-Clause.html", + "content": "Copyright (c) < ;match=.+>>. All rights reserved. \n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. \n\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. \n\n3. Neither the name of <> nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY <> \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <> BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ", + "internalHash": "BSD-3-Clause", + "spdxId": "BSD-3-Clause", + "hash": "BSD-3-Clause" + }, + "EPL-1.0": { + "name": "Eclipse Public License 1.0", + "url": "https://spdx.org/licenses/EPL-1.0.html", + "content": "Eclipse Public License - v 1.0\n\nTHE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n1. DEFINITIONS\n\n\"Contribution\" means:\n a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and\n b) in the case of each subsequent Contributor:\n i) changes to the Program, and\n ii) additions to the Program;\n\nwhere such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program.\n\"Contributor\" means any person or entity that distributes the Program.\n\n\"Licensed Patents\" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program.\n\n\"Program\" means the Contributions distributed in accordance with this Agreement.\n\n\"Recipient\" means anyone who receives the Program under this Agreement, including all Contributors.\n\n2. GRANT OF RIGHTS\n\n a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form.\n \n b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder.\n\n c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program.\n\n d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement.\n\n3. REQUIREMENTS\nA Contributor may choose to distribute the Program in object code form under its own license agreement, provided that:\n\n a) it complies with the terms and conditions of this Agreement; and\n \n b) its license agreement:\n i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose;\n ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits;\n iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and\n iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange.\n\nWhen the Program is made available in source code form:\n\n a) it must be made available under this Agreement; and\n\n b) a copy of this Agreement must be included with each copy of the Program.\nContributors may not remove or alter any copyright notices contained within the Program.\n\nEach Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution.\n\n4. COMMERCIAL DISTRIBUTION\nCommercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor (\"Commercial Contributor\") hereby agrees to defend and indemnify every other Contributor (\"Indemnified Contributor\") against any losses, damages and costs (collectively \"Losses\") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense.\n\nFor example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages.\n\n5. NO WARRANTY\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations.\n\n6. DISCLAIMER OF LIABILITY\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n7. GENERAL\n\nIf any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.\n\nIf Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed.\n\nAll Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive.\n\nEveryone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved.\n\nThis Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation.", + "internalHash": "EPL-1.0", + "spdxId": "EPL-1.0", + "hash": "EPL-1.0" + }, + "MIT": { + "name": "MIT License", + "url": "https://spdx.org/licenses/MIT.html", + "content": "MIT License\n\nCopyright (c) \n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.", + "internalHash": "MIT", + "spdxId": "MIT", + "hash": "MIT" + } + } +} \ No newline at end of file diff --git a/tasks-app-ios/Taskfolio/TaskfolioApp.swift b/tasks-app-ios/Taskfolio/TaskfolioApp.swift new file mode 100644 index 00000000..f6766a85 --- /dev/null +++ b/tasks-app-ios/Taskfolio/TaskfolioApp.swift @@ -0,0 +1,15 @@ +import SwiftUI +import TasksAppShared + +@main +struct TaskfolioApp: App { + init() { + InitKoinKt.doInitKoin(googleAuthenticator: IOSGoogleAuthenticator()) + } + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/tasks-app-ios/build.gradle.kts b/tasks-app-ios/build.gradle.kts new file mode 100644 index 00000000..ac718cee --- /dev/null +++ b/tasks-app-ios/build.gradle.kts @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import com.mikepenz.aboutlibraries.plugin.DuplicateMode +import com.mikepenz.aboutlibraries.plugin.DuplicateRule +import com.mikepenz.aboutlibraries.plugin.StrictMode + +plugins { + alias(libs.plugins.jetbrains.kotlin.multiplatform) + alias(libs.plugins.about.libraries) +} + +val appVersion = libs.versions.tasksApp.name.get() +val appVersionCode = System.getenv("CI_BUILD_NUMBER")?.toIntOrNull() ?: 1 + +kotlin { + // kinda useless but need a target to allow sync in IntelliJ + // and don't want this target to be forced to an iOS one + // to avoid downloading too much stuff when not needed (no iOS target by default) + jvm() + + // Note: iOS targets are conditionally added dynamically in the root build.gradle.kts + + jvmToolchain(17) + + sourceSets { + commonMain.dependencies { + implementation(project(":tasks-app-shared")) + } + } +} + +tasks.register("updateXcodeVersionConfig") { + val configFile = file("${projectDir}/Taskfolio/Configuration/Versions.xcconfig") + outputs.file(configFile) + val content = """ + BUNDLE_VERSION=$appVersionCode + BUNDLE_SHORT_VERSION_STRING=$appVersion + """.trimIndent() + + outputs.upToDateWhen { + configFile.takeIf(File::exists)?.readText() == content + } + doLast { + configFile.writeText(content) + } +} + +aboutLibraries { + // - If the automatic registered android tasks are disabled, a similar thing can be achieved manually + // - `./gradlew :tasks-app-ios:exportLibraryDefinitions -Pci=true` + // - the resulting file can for example be added as part of the SCM + collect { + configPath = file("$rootDir/license_config") + offlineMode = true + fetchRemoteLicense = true + fetchRemoteFunding = false + // no need of BOM + includePlatform = false + } + export { + outputPath = file("$projectDir/Taskfolio/Resources/licenses_ios.json") + excludeFields.addAll("metadata", "funding", "scm", "associated", "website", "Developer.organisationUrl", "Organization.url") + prettyPrint = true + } + license { + strictMode = StrictMode.FAIL + allowedLicenses.addAll("Apache-2.0", "asdkl", "MIT", "EPL-1.0", "BSD-3-Clause") + } + library { + duplicationMode = DuplicateMode.MERGE + duplicationRule = DuplicateRule.SIMPLE + } +} \ No newline at end of file diff --git a/tasks-app-shared/build.gradle.kts b/tasks-app-shared/build.gradle.kts index 27a9dbc0..e0ec072b 100644 --- a/tasks-app-shared/build.gradle.kts +++ b/tasks-app-shared/build.gradle.kts @@ -47,14 +47,16 @@ val ciBuild = (findProperty("ci") as? String).toBoolean() kotlin { jvm() - jvmToolchain(17) - androidTarget { // useful to allow using commonTest in Android instrumentation tests @OptIn(ExperimentalKotlinGradlePluginApi::class) instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test) } + // Note: iOS targets are conditionally added dynamically in the root build.gradle.kts + + jvmToolchain(17) + compilerOptions { // Common compiler options applied to all Kotlin source sets freeCompilerArgs.add("-Xexpect-actual-classes") @@ -161,6 +163,9 @@ room { dependencies { add("kspJvm", libs.androidx.room.compiler) add("kspAndroid", libs.androidx.room.compiler) + iosTargets.forEach { iosTarget -> + add("ksp${iosTarget.replaceFirstChar(Char::uppercase)}", libs.androidx.room.compiler) + } debugImplementation(compose.uiTooling) } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt index af4ca0ae..93bf8fa4 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt @@ -60,6 +60,7 @@ object Converters { AutoMigration(from = 1, to = 2), // add user table AutoMigration(from = 2, to = 3), // add sorting column in task_list table ], + exportSchema = true, ) @ConstructedBy(TasksAppDatabaseConstructor::class) @TypeConverters(Converters::class) diff --git a/tasks-app-shared/src/iosMain/kotlin/net/opatry/network/NetworkStatusNotifier.ios.kt b/tasks-app-shared/src/iosMain/kotlin/net/opatry/network/NetworkStatusNotifier.ios.kt new file mode 100644 index 00000000..843e7092 --- /dev/null +++ b/tasks-app-shared/src/iosMain/kotlin/net/opatry/network/NetworkStatusNotifier.ios.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.network + +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import platform.Network.nw_path_get_status +import platform.Network.nw_path_monitor_cancel +import platform.Network.nw_path_monitor_create +import platform.Network.nw_path_monitor_set_queue +import platform.Network.nw_path_monitor_set_update_handler +import platform.Network.nw_path_monitor_start +import platform.Network.nw_path_status_satisfied +import platform.darwin.dispatch_queue_create + +actual fun networkStateFlow(): Flow = callbackFlow { + val monitor = nw_path_monitor_create() + val queue = dispatch_queue_create("NetworkMonitorQueue", null) + + nw_path_monitor_set_update_handler(monitor) { path -> + val hasInternet = nw_path_get_status(path) == nw_path_status_satisfied + trySend(hasInternet).isSuccess + } + + nw_path_monitor_set_queue(monitor, queue) + nw_path_monitor_start(monitor) + + awaitClose { + nw_path_monitor_cancel(monitor) + } +}.distinctUntilChanged() \ No newline at end of file diff --git a/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/FileCredentialsStorage.ios.kt b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/FileCredentialsStorage.ios.kt new file mode 100644 index 00000000..59a2dc9f --- /dev/null +++ b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/FileCredentialsStorage.ios.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks + +import kotlinx.cinterop.BetaInteropApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import platform.Foundation.NSData +import platform.Foundation.NSFileManager +import platform.Foundation.NSString +import platform.Foundation.NSURL +import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.create +import platform.Foundation.dataUsingEncoding +import platform.Foundation.dataWithContentsOfFile +import platform.Foundation.writeToURL + +actual class FileCredentialsStorage actual constructor(private val filepath: String) : CredentialsStorage { + @OptIn(BetaInteropApi::class) + actual override suspend fun load(): TokenCache? { + return withContext(Dispatchers.IO) { + val fileManager = NSFileManager.defaultManager + if (!fileManager.fileExistsAtPath(filepath)) return@withContext null + + val data = NSData.dataWithContentsOfFile(filepath) + ?: return@withContext null + + val content = NSString.create(data, NSUTF8StringEncoding)?.toString() + ?: return@withContext null + + runCatching { + Json.decodeFromString(content) + }.getOrNull() + } + } + + @OptIn(BetaInteropApi::class) + actual override suspend fun store(tokenCache: TokenCache) { + val json = Json { prettyPrint = true } + + val success = withContext(Dispatchers.IO) { + val nsString = NSString.create(string = json.encodeToString(tokenCache)) + val data = nsString.dataUsingEncoding(NSUTF8StringEncoding) + ?: error("Failed to encode JSON to NSData") + + val url = NSURL.fileURLWithPath(filepath) + data.writeToURL(url, atomically = true) + } + + if (!success) { + error("Failed to write token cache to file at $filepath") + } + } +} + diff --git a/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/MainViewController.kt b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/MainViewController.kt new file mode 100644 index 00000000..d76b7aa4 --- /dev/null +++ b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/MainViewController.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks.app + +import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeUIViewController +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.cinterop.ExperimentalForeignApi +import net.opatry.tasks.app.presentation.TaskListsViewModel +import net.opatry.tasks.app.presentation.UserState +import net.opatry.tasks.app.presentation.UserViewModel +import net.opatry.tasks.app.ui.TasksApp +import net.opatry.tasks.app.ui.component.AuthorizeGoogleTasksButton +import net.opatry.tasks.app.ui.component.LoadingPane +import net.opatry.tasks.app.ui.screen.AboutApp +import net.opatry.tasks.app.ui.screen.AuthorizationScreen +import net.opatry.tasks.app.ui.theme.TaskfolioTheme +import org.koin.compose.viewmodel.koinViewModel +import platform.Foundation.stringWithContentsOfFile + +@OptIn( + ExperimentalForeignApi::class, + ExperimentalComposeUiApi::class, +) +@Suppress( + "unused", + "FunctionName", +) +fun MainViewController() = ComposeUIViewController( + configure = { + parallelRendering = true + } +) { + val mainBundle = platform.Foundation.NSBundle.mainBundle + val appName = mainBundle.objectForInfoDictionaryKey("CFBundleDisplayName")?.toString() + ?: mainBundle.objectForInfoDictionaryKey("CFBundleName")?.toString() + ?: "Taskfolio" + + val shortVersion = mainBundle.objectForInfoDictionaryKey("CFBundleShortVersionString") + ?.toString() + ?.takeUnless(String::isEmpty) + val versionCode = mainBundle.objectForInfoDictionaryKey("CFBundleVersion") + ?.toString() + ?.takeUnless(String::isEmpty) + val fullVersion = listOfNotNull( + shortVersion, + versionCode, + ).joinToString(separator = ".") + .ifEmpty { "0.0.0.0" } + + val userViewModel = koinViewModel() + val userState by userViewModel.state.collectAsStateWithLifecycle(null) + + if (userState == null) { + LaunchedEffect(userState) { + userViewModel.refreshUserState() + } + } + + TaskfolioTheme { + Surface { + when (userState) { + null -> LoadingPane() + + UserState.Unsigned, + is UserState.SignedIn -> { + val aboutApp = AboutApp( + name = appName, + version = fullVersion + ) { + val path = mainBundle.pathForResource("licenses_ios", "json") + ?: error("licenses_ios.json not found in bundle") + + platform.Foundation.NSString.stringWithContentsOfFile( + path, + encoding = platform.Foundation.NSUTF8StringEncoding, + error = null + ) ?: error("Failed to load licenses_ios.json from bundle path: $path") + } + val tasksViewModel = koinViewModel() + TasksApp(aboutApp, userViewModel, tasksViewModel) + } + + UserState.Newcomer -> AuthorizationScreen(userViewModel::skipSignIn) { + AuthorizeGoogleTasksButton(onSuccess = userViewModel::signIn) + } + } + } + } +} diff --git a/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/di/authModule.ios.kt b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/di/authModule.ios.kt new file mode 100644 index 00000000..16ac47e7 --- /dev/null +++ b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/di/authModule.ios.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks.app.di + +import org.koin.core.module.Module +import org.koin.dsl.module + +// Not needed for iOS, the concrete instance of GoogleAuthenticator is provided by the iOS app in Swift. +actual fun authModule(gcpClientId: String): Module = module {} diff --git a/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/di/platformModule.ios.kt b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/di/platformModule.ios.kt new file mode 100644 index 00000000..6cd1426e --- /dev/null +++ b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/di/platformModule.ios.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks.app.di + +import androidx.room.Room +import io.ktor.client.engine.HttpClientEngineFactory +import io.ktor.client.engine.darwin.Darwin +import kotlinx.cinterop.ExperimentalForeignApi +import net.opatry.tasks.CredentialsStorage +import net.opatry.tasks.FileCredentialsStorage +import net.opatry.tasks.data.TasksAppDatabase +import org.koin.core.module.Module +import org.koin.core.qualifier.named +import org.koin.dsl.module +import platform.Foundation.NSDocumentDirectory +import platform.Foundation.NSFileManager +import platform.Foundation.NSUserDomainMask + +actual fun platformModule(target: String): Module = module { + single> { + Darwin + } + + @OptIn(ExperimentalForeignApi::class) + single(named("app_root_dir")) { + val fileManager = NSFileManager.defaultManager + val documentDirectoryPath = fileManager.URLForDirectory( + directory = NSDocumentDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = true, + error = null, + )?.path ?: throw IllegalStateException("Could not find document directory") + + ("$documentDirectoryPath/.taskfolio").also { appRootDirPath -> + if (!fileManager.fileExistsAtPath(appRootDirPath)) { + val success = fileManager.createDirectoryAtPath( + path = appRootDirPath, + withIntermediateDirectories = true, + attributes = null, + error = null + ) + check(success) { "Failed to create directory at $appRootDirPath" } + } + } + } + + single { + val dbFilePath = get(named("app_root_dir")) + "/tasks.db" + Room.databaseBuilder(dbFilePath) + } + + single { + // TODO store in database + val credentialsFilePath = get(named("app_root_dir")) + "/google_auth_token_cache.json" + FileCredentialsStorage(credentialsFilePath) + } +} diff --git a/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/initKoin.kt b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/initKoin.kt new file mode 100644 index 00000000..7c3a0645 --- /dev/null +++ b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/initKoin.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks.app + +import net.opatry.google.auth.GoogleAuthenticator +import net.opatry.tasks.app.di.dataModule +import net.opatry.tasks.app.di.loggingModule +import net.opatry.tasks.app.di.networkModule +import net.opatry.tasks.app.di.platformModule +import net.opatry.tasks.app.di.tasksAppModule +import net.opatry.tasks.app.di.utilModule +import org.koin.core.context.startKoin +import org.koin.dsl.module + +@Suppress("unused") +fun initKoin(googleAuthenticator: GoogleAuthenticator) { + startKoin { + modules( + utilModule, + loggingModule, + platformModule("ios"), + dataModule, + module { + single { googleAuthenticator } + }, + networkModule, + tasksAppModule, + ) + } +} diff --git a/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/ui/component/AuthorizeGoogleTasksButton.ios.kt b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/ui/component/AuthorizeGoogleTasksButton.ios.kt new file mode 100644 index 00000000..c9705773 --- /dev/null +++ b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/ui/component/AuthorizeGoogleTasksButton.ios.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks.app.ui.component + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import net.opatry.google.auth.GoogleAuthenticator +import net.opatry.google.tasks.TasksScopes +import net.opatry.tasks.resources.Res +import net.opatry.tasks.resources.onboarding_screen_authorize_cta +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.koinInject + +@Composable +actual fun AuthorizeGoogleTasksButton( + modifier: Modifier, + onSuccess: (GoogleAuthenticator.OAuthToken) -> Unit +) { + val coroutineScope = rememberCoroutineScope() + val authenticator = koinInject() + var ongoingAuth by remember { mutableStateOf(false) } + var error by remember { mutableStateOf(null) } + + Column(modifier, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) { + Button( + onClick = { + ongoingAuth = true + coroutineScope.launch { + val scopes = listOf( + GoogleAuthenticator.Scope.Profile, + GoogleAuthenticator.Scope(TasksScopes.Tasks), + ) + try { + val authCode = authenticator.authorize( + scopes = scopes, + force = true, + requestUserAuthorization = {}, + ).let(GoogleAuthenticator.Grant::AuthorizationCode) + val oauthToken = authenticator.getToken(authCode) + onSuccess(oauthToken) + } catch (e: Exception) { + error = e.message + ongoingAuth = false + } + } + }, + enabled = !ongoingAuth + ) { + Box(modifier, contentAlignment = Alignment.Center) { + AnimatedContent(ongoingAuth, label = "authorize_button_content") { ongoing -> + if (ongoing) { + LoadingIndicator(Modifier.size(24.dp)) + } else { + Text(stringResource(Res.string.onboarding_screen_authorize_cta)) + } + } + } + } + + AnimatedContent(error, label = "authorize_error_message") { message -> + Text(message ?: "", color = MaterialTheme.colorScheme.error) + } + } +} \ No newline at end of file diff --git a/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/ui/component/backHandler.ios.kt b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/ui/component/backHandler.ios.kt new file mode 100644 index 00000000..31cfbc0c --- /dev/null +++ b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/ui/component/backHandler.ios.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks.app.ui.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.backhandler.BackHandler + +@ExperimentalComposeUiApi +@Composable +actual fun MyBackHandler(canNavigateBack: () -> Boolean, navigateBack: () -> Unit) { + BackHandler(canNavigateBack()) { + navigateBack() + } +} \ No newline at end of file diff --git a/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/ui/theme/Type.ios.kt b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/ui/theme/Type.ios.kt new file mode 100644 index 00000000..8a3e0c22 --- /dev/null +++ b/tasks-app-shared/src/iosMain/kotlin/net/opatry/tasks/app/ui/theme/Type.ios.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks.app.ui.theme + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.sp + +internal actual val Typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography( + displayLarge = TextStyle(fontSize = 57.sp), // TODO adjust + displayMedium = TextStyle(fontSize = 45.sp), // TODO adjust + displaySmall = TextStyle(fontSize = 36.sp), // TODO adjust + headlineLarge = TextStyle(fontSize = 32.sp), // TODO adjust + headlineMedium = TextStyle(fontSize = 28.sp), // TODO adjust + headlineSmall = TextStyle(fontSize = 24.sp), // TODO adjust + titleLarge = TextStyle(fontSize = 22.sp), // TODO adjust + titleMedium = TextStyle(fontSize = 16.sp), // TODO adjust + titleSmall = TextStyle(fontSize = 14.sp), // TODO adjust + bodyLarge = TextStyle(fontSize = 14.sp), + bodyMedium = TextStyle(fontSize = 12.sp), + bodySmall = TextStyle(fontSize = 10.sp), + labelLarge = TextStyle(fontSize = 12.sp), + labelMedium = TextStyle(fontSize = 10.sp), + labelSmall = TextStyle(fontSize = 9.sp), +) diff --git a/tasks-app-shared/src/iosTest/kotlin/net/opatry/tasks/data/util/inMemoryTasksAppDatabaseBuilder.ios.kt b/tasks-app-shared/src/iosTest/kotlin/net/opatry/tasks/data/util/inMemoryTasksAppDatabaseBuilder.ios.kt new file mode 100644 index 00000000..b135b06f --- /dev/null +++ b/tasks-app-shared/src/iosTest/kotlin/net/opatry/tasks/data/util/inMemoryTasksAppDatabaseBuilder.ios.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks.data.util + +import androidx.room.Room +import androidx.room.RoomDatabase +import net.opatry.tasks.data.TasksAppDatabase + +actual fun inMemoryTasksAppDatabaseBuilder(): RoomDatabase.Builder { + return Room.inMemoryDatabaseBuilder() +} diff --git a/tasks-core/build.gradle.kts b/tasks-core/build.gradle.kts index c75ec784..366d300a 100644 --- a/tasks-core/build.gradle.kts +++ b/tasks-core/build.gradle.kts @@ -29,6 +29,8 @@ plugins { kotlin { jvm() + // Note: iOS targets are conditionally added dynamically in the root build.gradle.kts + jvmToolchain(17) compilerOptions { @@ -55,5 +57,11 @@ kotlin { commonTest.dependencies { implementation(kotlin("test")) } + + if (iosTargets.isNotEmpty()) { + iosMain.dependencies { + implementation(libs.bignum) + } + } } } \ No newline at end of file diff --git a/tasks-core/src/iosMain/kotlin/net/opatry/tasks/TaskPosition.ios.kt b/tasks-core/src/iosMain/kotlin/net/opatry/tasks/TaskPosition.ios.kt new file mode 100644 index 00000000..7e4a085b --- /dev/null +++ b/tasks-core/src/iosMain/kotlin/net/opatry/tasks/TaskPosition.ios.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks + +import com.ionspin.kotlin.bignum.integer.BigInteger +import com.ionspin.kotlin.bignum.integer.toBigInteger +import kotlin.time.Instant + +actual class TodoTaskPosition private constructor(internal val rawValue: BigInteger) : TaskPosition { + actual override val value: String + get() = rawValue.toString().padStart(20, '0') + + actual companion object { + actual fun fromIndex(index: Int): TodoTaskPosition { + return TodoTaskPosition(BigInteger.fromInt(index)) + } + + actual fun fromPosition(position: String): TodoTaskPosition { + return TodoTaskPosition(BigInteger.parseString(position)) + } + } + + actual override fun compareTo(other: TaskPosition): Int { + return when (other) { + is TodoTaskPosition -> rawValue.compareTo(other.rawValue) + is DoneTaskPosition -> rawValue.compareTo(other.rawValue) + else -> throw IllegalArgumentException("Only TodoTaskPosition and DoneTaskPosition are supported") + } + } + + actual override fun hashCode(): Int { + return rawValue.hashCode() + } + + actual override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TodoTaskPosition) return false + return rawValue == other.rawValue + } + + actual override fun toString(): String = value +} + +actual class DoneTaskPosition private constructor(internal val rawValue: BigInteger) : TaskPosition { + actual companion object { + private val UpperBound = BigInteger.parseString("9999999999999999999") + actual fun fromCompletionDate(completionDate: Instant): DoneTaskPosition { + return DoneTaskPosition(UpperBound - completionDate.toEpochMilliseconds().toBigInteger()) + } + + actual fun fromPosition(position: String): DoneTaskPosition { + return DoneTaskPosition(BigInteger.parseString(position)) + } + } + + actual override val value: String + get() = rawValue.toString().padStart(20, '0') + + actual override fun compareTo(other: TaskPosition): Int { + return when (other) { + is TodoTaskPosition -> rawValue.compareTo(other.rawValue) + is DoneTaskPosition -> rawValue.compareTo(other.rawValue) + else -> throw IllegalArgumentException("Only TodoTaskPosition and DoneTaskPosition are supported") + } + } + + actual override fun hashCode(): Int { + return rawValue.hashCode() + } + + actual override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DoneTaskPosition) return false + return rawValue == other.rawValue + } + + actual override fun toString(): String = value +} diff --git a/website/index.md b/website/index.md index 34ea7941..2a2a4a6b 100644 --- a/website/index.md +++ b/website/index.md @@ -14,11 +14,13 @@ This project highlights the breadth of my Android knowledge, from API integratio It’s designed not just as a functioning task manager, but as a demonstration of my ability to deliver well-structured, maintainable, and scalable Android apps. -[![Taskfolio Android Application](assets/GetItOnGooglePlay_Badge_Web_color_English.png)](https://play.google.com/store/apps/details?id=net.opatry.tasks.app) +[![Taskfolio on Play Store](assets/GetItOnGooglePlay_Badge_Web_color_English.png)](https://play.google.com/store/apps/details?id=net.opatry.tasks.app) -| --------------------------------------- |--------------------------------------- |--------------------------------------- | ---------------------------------- | +| --------------------------------------- |--------------------------------------- | ---------------------------------- | ---------------------------------- | | ![](assets/screens/task_lists_light.png) | ![](assets/screens/groceries_light.png) | ![](assets/screens/add_task_light.png) | ![](assets/screens/home_dark.png) | +> ℹ️ The application is also available as a desktop (Jvm) application and an iOS application as well (using [Compose Multi Platform (aka CMP)](https://www.jetbrains.com/compose-multiplatform/) as UI Toolkit). + ## 🎯 Project intentions - [x] Showcase my expertise in Android application development @@ -52,11 +54,13 @@ I do not aim to implement advanced features beyond what is supported by the Goog ## 🛠️ Tech stack -- [Kotlin](https://kotlinlang.org/), [Multiplatform (aka KMP)](https://kotlinlang.org/docs/multiplatform.html) (currently Desktop & Android are supported) - - iOS & Web are not planned any time soon (contribution are welcome 🤝) +- [Kotlin](https://kotlinlang.org/), [Multiplatform (aka KMP)](https://kotlinlang.org/docs/multiplatform.html) + - Android and Desktop are fully supported. + - iOS wasn't initially planned, but a draft version is available (use it at your own risk, there might be dragons 🐉). + - Web is not planned any time soon (contributions are welcome 🤝) - [Kotlin coroutines](https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html) - [Ktor client](https://ktor.io/) (+ [Kotlinx serialization](https://kotlinlang.org/docs/serialization.html)) -- [Room](https://developer.android.com/training/data-storage/room) for local persistance +- [Room](https://developer.android.com/training/data-storage/room) for local persistence - [Koin](https://insert-koin.io/) for dependency injection - [Material Design 3 Components](https://developer.android.com/develop/ui/compose/designsystems/material3) - [Jetpack Compose](https://developer.android.com/jetpack/compose), [Multiplatform (aka CMP)](https://www.jetbrains.com/compose-multiplatform/) diff --git a/website/styles.css b/website/styles.css index 28d00b40..f2ff5515 100644 --- a/website/styles.css +++ b/website/styles.css @@ -152,4 +152,10 @@ td { width: 50%; box-sizing: border-box; } -} \ No newline at end of file +} + +blockquote { + margin-left: 0; + padding-left: 1.5rem; + border-left: 4px solid #6BEACA; +}