From 05004d91815698087b6153286dcdb1b1894c55a0 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sat, 22 Mar 2025 18:26:30 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20WIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 18 +++++- .github/workflows/instrumented_tests.yml | 33 +++++++++- .../com/escodro/datastore/DesktopDataStore.kt | 11 ++++ .../datastore/di/DataStoreModule.desktop.kt | 11 ++++ data/local/build.gradle.kts | 7 ++ .../local/provider/AndroidDriverFactory.kt | 3 - .../local/provider/DatabaseProvider.kt | 13 +++- .../escodro/local/provider/DriverFactory.kt | 9 --- .../escodro/local/di/LocalModule.desktop.kt | 14 ++++ .../local/provider/DesktopDriverFactory.kt | 21 ++++++ .../local/provider/IosDriverFactory.kt | 40 ------------ desktop-app/.gitignore | 1 + desktop-app/build.gradle.kts | 61 ++++++++++++++++++ .../kotlin/com/escodro/desktopapp/main.kt | 22 +++++++ .../desktopMain/resources/ic_launcher.icns | Bin 0 -> 19307 bytes .../src/desktopMain/resources/ic_launcher.ico | Bin 0 -> 167702 bytes .../src/desktopMain/resources/ic_launcher.png | Bin .../escodro/alarm/di/PlatformAlarmModule.kt | 17 +++++ .../DesktopNotificationScheduler.kt | 18 ++++++ .../notification/DesktopTaskNotification.kt | 18 ++++++ .../permission/DesktopAlarmPermission.kt | 19 ++++++ features/preference/build.gradle.kts | 6 ++ .../preference/di/PreferenceModule.desktop.kt | 17 +++++ .../provider/DesktopAppInfoProvider.kt | 9 +++ .../provider/DesktopTrackerProvider.kt | 12 ++++ .../com/escodro/task/di/PlatformTaskModule.kt | 14 ++++ .../task/extension/LocalDateTimeExtensions.kt | 23 +++++++ .../DesktopRelativeDateTimeProvider.kt | 52 +++++++++++++++ gradle/libs.versions.toml | 8 +++ .../di/PlatformDesignSystemModule.kt | 11 ++++ .../provider/DesktopThemeProvider.kt | 14 ++++ .../com/escodro/di/ViewModelDefinition.kt | 15 +++++ .../parcelable/CommonParcelable.desktop.kt | 3 + .../api/BindPermissionEffect.desktop.kt | 10 +++ .../api/DesktopPermissionController.kt | 15 +++++ .../permission/di/PlatformPermissionModule.kt | 11 ++++ libraries/test/build.gradle.kts | 7 ++ .../src/main/java/extension/KmpExtensions.kt | 2 +- .../values-pt-rBR/strings.xml | 9 +++ .../composeResources/values/strings.xml | 9 +++ settings.gradle.kts | 1 + shared/build.gradle.kts | 8 +++ .../escodro/shared/di/PlatformSharedModule.kt | 8 +++ .../alkaa/test/PlatformAnimation.desktop.kt | 17 +++++ .../com/escodro/alkaa/test/UiTest.desktop.kt | 10 +++ 45 files changed, 571 insertions(+), 56 deletions(-) create mode 100644 data/datastore/src/desktopMain/kotlin/com/escodro/datastore/DesktopDataStore.kt create mode 100644 data/datastore/src/desktopMain/kotlin/com/escodro/datastore/di/DataStoreModule.desktop.kt create mode 100644 data/local/src/desktopMain/kotlin/com/escodro/local/di/LocalModule.desktop.kt create mode 100644 data/local/src/desktopMain/kotlin/com/escodro/local/provider/DesktopDriverFactory.kt create mode 100644 desktop-app/.gitignore create mode 100644 desktop-app/build.gradle.kts create mode 100644 desktop-app/src/desktopMain/kotlin/com/escodro/desktopapp/main.kt create mode 100644 desktop-app/src/desktopMain/resources/ic_launcher.icns create mode 100644 desktop-app/src/desktopMain/resources/ic_launcher.ico rename app/src/main/ic_launcher-web.png => desktop-app/src/desktopMain/resources/ic_launcher.png (100%) create mode 100644 features/alarm/src/desktopMain/kotlin/com/escodro/alarm/di/PlatformAlarmModule.kt create mode 100644 features/alarm/src/desktopMain/kotlin/com/escodro/alarm/notification/DesktopNotificationScheduler.kt create mode 100644 features/alarm/src/desktopMain/kotlin/com/escodro/alarm/notification/DesktopTaskNotification.kt create mode 100644 features/alarm/src/desktopMain/kotlin/com/escodro/alarm/permission/DesktopAlarmPermission.kt create mode 100644 features/preference/src/desktopMain/kotlin/com/escodro/preference/di/PreferenceModule.desktop.kt create mode 100644 features/preference/src/desktopMain/kotlin/com/escodro/preference/provider/DesktopAppInfoProvider.kt create mode 100644 features/preference/src/desktopMain/kotlin/com/escodro/preference/provider/DesktopTrackerProvider.kt create mode 100644 features/task/src/desktopMain/kotlin/com/escodro/task/di/PlatformTaskModule.kt create mode 100644 features/task/src/desktopMain/kotlin/com/escodro/task/extension/LocalDateTimeExtensions.kt create mode 100644 features/task/src/desktopMain/kotlin/com/escodro/task/provider/DesktopRelativeDateTimeProvider.kt create mode 100644 libraries/designsystem/src/desktopMain/kotlin/com/escodro/designsystem/di/PlatformDesignSystemModule.kt create mode 100644 libraries/designsystem/src/desktopMain/kotlin/com/escodro/designsystem/provider/DesktopThemeProvider.kt create mode 100644 libraries/di/src/desktopMain/kotlin/com/escodro/di/ViewModelDefinition.kt create mode 100644 libraries/parcelable/src/desktopMain/kotlin/com/escodro/parcelable/CommonParcelable.desktop.kt create mode 100644 libraries/permission/src/desktopMain/kotlin/com/escodro/permission/api/BindPermissionEffect.desktop.kt create mode 100644 libraries/permission/src/desktopMain/kotlin/com/escodro/permission/api/DesktopPermissionController.kt create mode 100644 libraries/permission/src/desktopMain/kotlin/com/escodro/permission/di/PlatformPermissionModule.kt create mode 100644 shared/src/desktopMain/kotlin/com/escodro/shared/di/PlatformSharedModule.kt create mode 100644 shared/src/desktopTest/kotlin/com/escodro/alkaa/test/PlatformAnimation.desktop.kt create mode 100644 shared/src/desktopTest/kotlin/com/escodro/alkaa/test/UiTest.desktop.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 92cfa374..11909fb4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: distribution: 'adopt' - name: Build with Gradle - run: ./gradlew clean build test -x shared:testDebugUnitTest -x shared:testReleaseUnitTest --parallel + run: ./gradlew clean build test -x shared:testDebugUnitTest -x shared:testReleaseUnitTest -x shared:desktopTest --parallel env: ALKAA_KEY_ALIAS: ${{ secrets.ALKAA_KEY_ALIAS }} ALKAA_KEY_PASSWORD: ${{ secrets.ALKAA_KEY_PASSWORD }} @@ -49,3 +49,19 @@ jobs: - name: Build with Xcode run: xcodebuild -workspace ios-app/alkaa.xcodeproj/project.xcworkspace -configuration Debug -scheme alkaa -sdk iphonesimulator + + build-desktop: + runs-on: macos-14 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '19' + distribution: 'temurin' + + - name: Build with Gradle + run: ./gradlew desktop-app:createDistributable diff --git a/.github/workflows/instrumented_tests.yml b/.github/workflows/instrumented_tests.yml index b9846b1e..db2be950 100644 --- a/.github/workflows/instrumented_tests.yml +++ b/.github/workflows/instrumented_tests.yml @@ -90,6 +90,37 @@ jobs: with: name: test-results path: | - /Users/runner/work/alkaa/alkaa/app/build/reports/androidTests/ + /Users/runner/work/alkaa/alkaa/app/build/reports/tests/ + ./logcat.txt + retention-days: 7 + + desktop-test: + name: "Desktop" + runs-on: macos-latest + timeout-minutes: 80 + + strategy: + fail-fast: false + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'zulu' + + - name: Run instrumented tests + run: ./gradlew :shared:desktopTest + + - name: Save Test Results + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: test-results + path: | + /Users/runner/work/alkaa/alkaa/app/build/reports/tests/ ./logcat.txt retention-days: 7 diff --git a/data/datastore/src/desktopMain/kotlin/com/escodro/datastore/DesktopDataStore.kt b/data/datastore/src/desktopMain/kotlin/com/escodro/datastore/DesktopDataStore.kt new file mode 100644 index 00000000..597a8148 --- /dev/null +++ b/data/datastore/src/desktopMain/kotlin/com/escodro/datastore/DesktopDataStore.kt @@ -0,0 +1,11 @@ +package com.escodro.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences + +internal class DesktopDataStore { + + fun getDataStore(): DataStore = getDataStore( + producePath = { dataStoreFileName }, + ) +} diff --git a/data/datastore/src/desktopMain/kotlin/com/escodro/datastore/di/DataStoreModule.desktop.kt b/data/datastore/src/desktopMain/kotlin/com/escodro/datastore/di/DataStoreModule.desktop.kt new file mode 100644 index 00000000..7deafdee --- /dev/null +++ b/data/datastore/src/desktopMain/kotlin/com/escodro/datastore/di/DataStoreModule.desktop.kt @@ -0,0 +1,11 @@ +package com.escodro.datastore.di + +import com.escodro.datastore.DesktopDataStore +import org.koin.dsl.module + +/** + * Provides the platform-specific dependencies. + */ +internal actual val platformDataStoreModule = module { + single { DesktopDataStore().getDataStore() } +} diff --git a/data/local/build.gradle.kts b/data/local/build.gradle.kts index 0de6a2a9..4e7e6cd5 100644 --- a/data/local/build.gradle.kts +++ b/data/local/build.gradle.kts @@ -11,6 +11,8 @@ kotlin { setFrameworkBaseName("local") sourceSets { + val desktopMain by getting + commonMain.dependencies { implementation(projects.libraries.coroutines) implementation(projects.data.repository) @@ -37,6 +39,11 @@ kotlin { implementation(kotlin("test")) implementation(libs.kotlinx.coroutines.test) } + + desktopMain.dependencies { + implementation(libs.sqldelight.jvm) + implementation(libs.multiplatform.path) + } } } diff --git a/data/local/src/androidMain/kotlin/com/escodro/local/provider/AndroidDriverFactory.kt b/data/local/src/androidMain/kotlin/com/escodro/local/provider/AndroidDriverFactory.kt index 72d9baf8..e0c5e5f2 100644 --- a/data/local/src/androidMain/kotlin/com/escodro/local/provider/AndroidDriverFactory.kt +++ b/data/local/src/androidMain/kotlin/com/escodro/local/provider/AndroidDriverFactory.kt @@ -14,7 +14,4 @@ internal class AndroidDriverFactory( override fun createDriver(databaseName: String): SqlDriver = AndroidSqliteDriver(AlkaaDatabase.Schema, context, databaseName) - - override fun shouldPrepopulateDatabase(databaseName: String): Boolean = - !context.getDatabasePath(databaseName).exists() } diff --git a/data/local/src/commonMain/kotlin/com/escodro/local/provider/DatabaseProvider.kt b/data/local/src/commonMain/kotlin/com/escodro/local/provider/DatabaseProvider.kt index 3e7edaad..dd753ba5 100644 --- a/data/local/src/commonMain/kotlin/com/escodro/local/provider/DatabaseProvider.kt +++ b/data/local/src/commonMain/kotlin/com/escodro/local/provider/DatabaseProvider.kt @@ -47,7 +47,7 @@ internal class DatabaseProvider( } private fun prepopulateDatabase(database: AlkaaDatabase) { - if (driverFactory.shouldPrepopulateDatabase(DATABASE_NAME)) { + if (isDatabaseEmpty(database)) { appCoroutineScope.launch { for (category in getPrepopulateData()) { database.categoryQueries.insert( @@ -59,6 +59,17 @@ internal class DatabaseProvider( } } + private fun isDatabaseEmpty(database: AlkaaDatabase): Boolean = with(database) { + categoryQueries + .selectAll() + .executeAsList() + .isEmpty() && + taskQueries + .selectAllTasksWithDueDate() + .executeAsList() + .isEmpty() + } + private suspend fun getPrepopulateData(): List = listOf( Category( diff --git a/data/local/src/commonMain/kotlin/com/escodro/local/provider/DriverFactory.kt b/data/local/src/commonMain/kotlin/com/escodro/local/provider/DriverFactory.kt index d6e416e5..5df4a7e7 100644 --- a/data/local/src/commonMain/kotlin/com/escodro/local/provider/DriverFactory.kt +++ b/data/local/src/commonMain/kotlin/com/escodro/local/provider/DriverFactory.kt @@ -15,13 +15,4 @@ internal interface DriverFactory { * @return the [SqlDriver] to be used in the database */ fun createDriver(databaseName: String): SqlDriver - - /** - * Checks if the database is opening for the first time and should be prepopulated. - * - * @param databaseName the database name - * - * @return true if the database should be prepopulated, false otherwise - */ - fun shouldPrepopulateDatabase(databaseName: String): Boolean } diff --git a/data/local/src/desktopMain/kotlin/com/escodro/local/di/LocalModule.desktop.kt b/data/local/src/desktopMain/kotlin/com/escodro/local/di/LocalModule.desktop.kt new file mode 100644 index 00000000..f651a0a4 --- /dev/null +++ b/data/local/src/desktopMain/kotlin/com/escodro/local/di/LocalModule.desktop.kt @@ -0,0 +1,14 @@ +package com.escodro.local.di + +import com.escodro.local.provider.DesktopDriverFactory +import com.escodro.local.provider.DriverFactory +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module + +/** + * Provides the platform-specific dependencies. + */ +internal actual val platformLocalModule = module { + singleOf(::DesktopDriverFactory) bind DriverFactory::class +} diff --git a/data/local/src/desktopMain/kotlin/com/escodro/local/provider/DesktopDriverFactory.kt b/data/local/src/desktopMain/kotlin/com/escodro/local/provider/DesktopDriverFactory.kt new file mode 100644 index 00000000..0e6618c6 --- /dev/null +++ b/data/local/src/desktopMain/kotlin/com/escodro/local/provider/DesktopDriverFactory.kt @@ -0,0 +1,21 @@ +package com.escodro.local.provider + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import com.escodro.local.AlkaaDatabase +import me.sujanpoudel.utils.paths.appCacheDirectory + +internal class DesktopDriverFactory : DriverFactory { + + override fun createDriver(databaseName: String): SqlDriver { + val appCacheDirectory = appCacheDirectory(appId = PACKAGE_NAME, createDir = true) + val jdbcUrl = "jdbc:sqlite:$appCacheDirectory$databaseName" + return JdbcSqliteDriver(jdbcUrl).apply { + AlkaaDatabase.Schema.create(this) + } + } + + private companion object { + private const val PACKAGE_NAME = "com.escodro.alkaa" + } +} diff --git a/data/local/src/iosMain/kotlin/com/escodro/local/provider/IosDriverFactory.kt b/data/local/src/iosMain/kotlin/com/escodro/local/provider/IosDriverFactory.kt index bfc3c1c1..e9c5f3ea 100644 --- a/data/local/src/iosMain/kotlin/com/escodro/local/provider/IosDriverFactory.kt +++ b/data/local/src/iosMain/kotlin/com/escodro/local/provider/IosDriverFactory.kt @@ -3,51 +3,11 @@ package com.escodro.local.provider import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver import com.escodro.local.AlkaaDatabase -import kotlinx.cinterop.CPointer -import kotlinx.cinterop.DoubleVarOf -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.get -import platform.CoreGraphics.CGColorGetComponents -import platform.CoreGraphics.CGFloat -import platform.Foundation.NSFileManager -import platform.Foundation.NSLibraryDirectory -import platform.Foundation.NSURL -import platform.Foundation.NSUserDomainMask -import platform.UIKit.UIColor internal class IosDriverFactory : DriverFactory { override fun createDriver(databaseName: String): SqlDriver = NativeSqliteDriver(AlkaaDatabase.Schema, databaseName) - override fun shouldPrepopulateDatabase(databaseName: String): Boolean = - !databaseExists(databaseName) - - private fun databaseExists(databaseName: String): Boolean { - val fileManager = NSFileManager.defaultManager - val documentDirectory = NSFileManager - .defaultManager - .URLsForDirectory( - NSLibraryDirectory, - NSUserDomainMask, - ).last() as NSURL - - val file = documentDirectory - .URLByAppendingPathComponent("$DATABASE_PATH$databaseName") - ?.path - - return fileManager.fileExistsAtPath(file ?: "") - } - - @Suppress("ktlint:standard:chain-method-continuation") - @OptIn(ExperimentalForeignApi::class) - private fun UIColor.toHex(): String { - val components: CPointer>? = CGColorGetComponents(CGColor) - val r = components?.get(0)?.times(255)?.toInt()?.toString(16)?.padStart(2, '0') ?: "00" - val g = components?.get(1)?.times(255)?.toInt()?.toString(16)?.padStart(2, '0') ?: "00" - val b = components?.get(2)?.times(255)?.toInt()?.toString(16)?.padStart(2, '0') ?: "00" - return "#$r$g$b" - } - private companion object { private const val DATABASE_PATH = "Application Support/databases/" } diff --git a/desktop-app/.gitignore b/desktop-app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/desktop-app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/desktop-app/build.gradle.kts b/desktop-app/build.gradle.kts new file mode 100644 index 00000000..4affcaef --- /dev/null +++ b/desktop-app/build.gradle.kts @@ -0,0 +1,61 @@ +import extension.setFrameworkBaseName +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + id("com.escodro.multiplatform") + alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) +} + +kotlin { + setFrameworkBaseName("desktop-app") + + sourceSets { + val desktopMain by getting + + commonMain.dependencies { + implementation(projects.shared) + implementation(projects.resources) + implementation(projects.libraries.appstate) + + implementation(compose.runtime) + implementation(compose.components.resources) + implementation(libs.kotlinx.coroutines.swing) + } + + desktopMain.dependencies { + implementation(compose.desktop.currentOs) + } + } +} + +compose.desktop { + application { + mainClass = "com.escodro.desktopapp.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "com.escodro.alkaa" + packageVersion = libs.versions.version.name.get() + modules("java.sql") + + macOS { + iconFile.set(project.file("src/desktopMain/resources/ic_launcher.icns")) + } + windows { + iconFile.set(project.file("src/desktopMain/resources/ic_launcher.ico")) + } + linux { + iconFile.set(project.file("src/desktopMain/resources/ic_launcher.png")) + } + } + + jvmArgs( + "-DpackageVersion=${libs.versions.version.name.get()}" + ) + } +} + +android { + namespace = "desktop" +} diff --git a/desktop-app/src/desktopMain/kotlin/com/escodro/desktopapp/main.kt b/desktop-app/src/desktopMain/kotlin/com/escodro/desktopapp/main.kt new file mode 100644 index 00000000..332f4946 --- /dev/null +++ b/desktop-app/src/desktopMain/kotlin/com/escodro/desktopapp/main.kt @@ -0,0 +1,22 @@ +@file:Suppress("ktlint:standard:filename") + +package com.escodro.desktopapp + +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import com.escodro.resources.Res +import com.escodro.resources.content_app_name +import com.escodro.shared.AlkaaMultiplatformApp +import com.escodro.shared.di.initKoin +import org.jetbrains.compose.resources.stringResource + +fun main() = application { + initKoin() + + Window( + onCloseRequest = ::exitApplication, + title = stringResource(Res.string.content_app_name), + ) { + AlkaaMultiplatformApp() + } +} diff --git a/desktop-app/src/desktopMain/resources/ic_launcher.icns b/desktop-app/src/desktopMain/resources/ic_launcher.icns new file mode 100644 index 0000000000000000000000000000000000000000..663965ed6c2c1ce8d3815424d58d2a75c2ad4b36 GIT binary patch literal 19307 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
    rI`?gSA1&J)v*$z7v*L#R0Hw2( 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 PQyE;oeyK>;KJxznWaWA1 literal 0 HcmV?d00001 diff --git a/desktop-app/src/desktopMain/resources/ic_launcher.ico b/desktop-app/src/desktopMain/resources/ic_launcher.ico new file mode 100644 index 0000000000000000000000000000000000000000..e0cf3ef939076fb19835cc7d554950f265d15f66 GIT binary patch literal 167702 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 P5B-RtV~1QFY_RVC>mrU8 literal 0 HcmV?d00001 diff --git a/app/src/main/ic_launcher-web.png b/desktop-app/src/desktopMain/resources/ic_launcher.png similarity index 100% rename from app/src/main/ic_launcher-web.png rename to desktop-app/src/desktopMain/resources/ic_launcher.png diff --git a/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/di/PlatformAlarmModule.kt b/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/di/PlatformAlarmModule.kt new file mode 100644 index 00000000..cd5e3c38 --- /dev/null +++ b/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/di/PlatformAlarmModule.kt @@ -0,0 +1,17 @@ +package com.escodro.alarm.di + +import com.escodro.alarm.notification.DesktopNotificationScheduler +import com.escodro.alarm.notification.DesktopTaskNotification +import com.escodro.alarm.notification.NotificationScheduler +import com.escodro.alarm.notification.TaskNotification +import com.escodro.alarm.permission.DesktopAlarmPermission +import com.escodro.alarmapi.AlarmPermission +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.bind +import org.koin.dsl.module + +actual val platformAlarmModule = module { + factoryOf(::DesktopNotificationScheduler) bind NotificationScheduler::class + factoryOf(::DesktopTaskNotification) bind TaskNotification::class + factoryOf(::DesktopAlarmPermission) bind AlarmPermission::class +} diff --git a/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/notification/DesktopNotificationScheduler.kt b/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/notification/DesktopNotificationScheduler.kt new file mode 100644 index 00000000..047d60be --- /dev/null +++ b/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/notification/DesktopNotificationScheduler.kt @@ -0,0 +1,18 @@ +package com.escodro.alarm.notification + +import com.escodro.alarm.model.Task + +internal class DesktopNotificationScheduler : NotificationScheduler { + + override fun scheduleTaskNotification(task: Task, timeInMillis: Long) { + // TODO: Implement scheduleTaskNotification + } + + override fun cancelTaskNotification(task: Task) { + // TODO: Implement cancelTaskNotification + } + + override fun updateTaskNotification(task: Task) { + // TODO: Implement updateTaskNotification + } +} diff --git a/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/notification/DesktopTaskNotification.kt b/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/notification/DesktopTaskNotification.kt new file mode 100644 index 00000000..92d161eb --- /dev/null +++ b/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/notification/DesktopTaskNotification.kt @@ -0,0 +1,18 @@ +package com.escodro.alarm.notification + +import com.escodro.alarm.model.Task + +internal class DesktopTaskNotification : TaskNotification { + + override fun show(task: Task) { + // TODO: Implement show + } + + override fun showRepeating(task: Task) { + // TODO: Implement showRepeating + } + + override fun dismiss(taskId: Long) { + // TODO: Implement dismiss + } +} diff --git a/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/permission/DesktopAlarmPermission.kt b/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/permission/DesktopAlarmPermission.kt new file mode 100644 index 00000000..f038e4a4 --- /dev/null +++ b/features/alarm/src/desktopMain/kotlin/com/escodro/alarm/permission/DesktopAlarmPermission.kt @@ -0,0 +1,19 @@ +package com.escodro.alarm.permission + +import com.escodro.alarmapi.AlarmPermission + +internal class DesktopAlarmPermission : AlarmPermission { + + override fun hasExactAlarmPermission(): Boolean { + // TODO: Implement hasExactAlarmPermission + return true + } + + override fun openExactAlarmPermissionScreen() { + // TODO: Implement openExactAlarmPermissionScreen + } + + override fun openAppSettings() { + // TODO: Implement openAppSettings + } +} diff --git a/features/preference/build.gradle.kts b/features/preference/build.gradle.kts index 7a5ee9e3..0eee5092 100644 --- a/features/preference/build.gradle.kts +++ b/features/preference/build.gradle.kts @@ -10,6 +10,8 @@ kotlin { setFrameworkBaseName("preference") sourceSets { + val desktopMain by getting + commonMain.dependencies { implementation(projects.domain) implementation(projects.libraries.coroutines) @@ -36,6 +38,10 @@ kotlin { iosMain.dependencies { implementation(projects.features.tracker) } + + desktopMain.dependencies{ + implementation(projects.features.tracker) + } } } android { diff --git a/features/preference/src/desktopMain/kotlin/com/escodro/preference/di/PreferenceModule.desktop.kt b/features/preference/src/desktopMain/kotlin/com/escodro/preference/di/PreferenceModule.desktop.kt new file mode 100644 index 00000000..68648ecf --- /dev/null +++ b/features/preference/src/desktopMain/kotlin/com/escodro/preference/di/PreferenceModule.desktop.kt @@ -0,0 +1,17 @@ +package com.escodro.preference.di + +import com.escodro.preference.provider.AppInfoProvider +import com.escodro.preference.provider.DesktopAppInfoProvider +import com.escodro.preference.provider.DesktopTrackerProvider +import com.escodro.preference.provider.TrackerProvider +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.bind +import org.koin.dsl.module + +/** + * Provides the platform-specific dependencies. + */ +internal actual val platformPreferenceModule = module { + factoryOf(::DesktopAppInfoProvider) bind AppInfoProvider::class + factoryOf(::DesktopTrackerProvider) bind TrackerProvider::class +} diff --git a/features/preference/src/desktopMain/kotlin/com/escodro/preference/provider/DesktopAppInfoProvider.kt b/features/preference/src/desktopMain/kotlin/com/escodro/preference/provider/DesktopAppInfoProvider.kt new file mode 100644 index 00000000..923c371a --- /dev/null +++ b/features/preference/src/desktopMain/kotlin/com/escodro/preference/provider/DesktopAppInfoProvider.kt @@ -0,0 +1,9 @@ +package com.escodro.preference.provider + +internal class DesktopAppInfoProvider : AppInfoProvider { + + override fun getAppVersion(): String { + val version = System.getProperty("packageVersion") ?: "0.0.0" + return "$version-alpha" + } +} diff --git a/features/preference/src/desktopMain/kotlin/com/escodro/preference/provider/DesktopTrackerProvider.kt b/features/preference/src/desktopMain/kotlin/com/escodro/preference/provider/DesktopTrackerProvider.kt new file mode 100644 index 00000000..69ff5fd2 --- /dev/null +++ b/features/preference/src/desktopMain/kotlin/com/escodro/preference/provider/DesktopTrackerProvider.kt @@ -0,0 +1,12 @@ +package com.escodro.preference.provider + +import androidx.compose.runtime.Composable +import com.escodro.tracker.presentation.TrackerScreen + +internal class DesktopTrackerProvider : TrackerProvider { + + @Composable + override fun Content(onUpPress: () -> Unit) { + TrackerScreen(onUpPress = onUpPress) + } +} diff --git a/features/task/src/desktopMain/kotlin/com/escodro/task/di/PlatformTaskModule.kt b/features/task/src/desktopMain/kotlin/com/escodro/task/di/PlatformTaskModule.kt new file mode 100644 index 00000000..e626a5f0 --- /dev/null +++ b/features/task/src/desktopMain/kotlin/com/escodro/task/di/PlatformTaskModule.kt @@ -0,0 +1,14 @@ +package com.escodro.task.di + +import com.escodro.task.provider.DesktopRelativeDateTimeProvider +import com.escodro.task.provider.RelativeDateTimeProvider +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.bind +import org.koin.dsl.module + +/** + * Provides the platform-specific dependencies. + */ +actual val platformTaskModule = module { + factoryOf(::DesktopRelativeDateTimeProvider) bind RelativeDateTimeProvider::class +} diff --git a/features/task/src/desktopMain/kotlin/com/escodro/task/extension/LocalDateTimeExtensions.kt b/features/task/src/desktopMain/kotlin/com/escodro/task/extension/LocalDateTimeExtensions.kt new file mode 100644 index 00000000..c79e7038 --- /dev/null +++ b/features/task/src/desktopMain/kotlin/com/escodro/task/extension/LocalDateTimeExtensions.kt @@ -0,0 +1,23 @@ +package com.escodro.task.extension + +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import java.text.DateFormat +import java.util.Calendar +import java.util.Locale + +/** + * Formats the [LocalDateTime] to a user-friendly string. + */ +actual fun LocalDateTime.format(): String { + val dateFormat = DateFormat.getDateTimeInstance( + DateFormat.LONG, + DateFormat.SHORT, + Locale.getDefault(), + ) + val calendar = Calendar.getInstance().apply { + timeInMillis = toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() + } + return dateFormat.format(calendar.time) +} diff --git a/features/task/src/desktopMain/kotlin/com/escodro/task/provider/DesktopRelativeDateTimeProvider.kt b/features/task/src/desktopMain/kotlin/com/escodro/task/provider/DesktopRelativeDateTimeProvider.kt new file mode 100644 index 00000000..e19e6cfd --- /dev/null +++ b/features/task/src/desktopMain/kotlin/com/escodro/task/provider/DesktopRelativeDateTimeProvider.kt @@ -0,0 +1,52 @@ +package com.escodro.task.provider + +import com.escodro.resources.Res +import com.escodro.resources.relative_date_time_days +import com.escodro.resources.relative_date_time_hours +import com.escodro.resources.relative_date_time_just_now +import com.escodro.resources.relative_date_time_minutes +import com.escodro.resources.relative_date_time_one_hour +import com.escodro.resources.relative_date_time_one_minute +import com.escodro.resources.relative_date_time_yesterday +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import org.jetbrains.compose.resources.getString + +internal class DesktopRelativeDateTimeProvider : RelativeDateTimeProvider { + + override fun toRelativeDateTimeString(dateTime: LocalDateTime): String { + val currentTime = Clock.System.now() + val targetInstant = dateTime.toInstant(TimeZone.currentSystemDefault()) + val duration = currentTime - targetInstant + + return runBlocking { + when { + duration.inWholeMinutes < 1 -> + getString(Res.string.relative_date_time_just_now) + + duration.inWholeMinutes == 1L -> + getString(Res.string.relative_date_time_one_minute) + + duration.inWholeMinutes < 60 -> + getString(Res.string.relative_date_time_minutes, duration.inWholeMinutes) + + duration.inWholeHours == 1L -> + getString(Res.string.relative_date_time_one_hour) + + duration.inWholeHours < 24 -> + getString(Res.string.relative_date_time_hours, duration.inWholeHours) + + duration.inWholeDays == 1L -> + getString(Res.string.relative_date_time_yesterday) + + duration.inWholeDays < 7 -> + getString(Res.string.relative_date_time_days, duration.inWholeDays) + + else -> dateTime.toString() + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a6df3466..8780ba3b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,6 +54,9 @@ sqldelight = "2.0.2" moko = "0.16.1" moko_permissions = "0.19.1" +# Multiplatform Utils +multiplatform_paths = "0.2.2" + # Test test_junit = "4.13.2" test_uiautomator = "2.3.0" @@ -81,6 +84,7 @@ stately = { module = "co.touchlab:stately-common", version.ref = "stately" } # KotlinX kotlinx_coroutines_core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx_coroutines" } kotlinx_coroutines_test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx_coroutines" } +kotlinx_coroutines_swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx_coroutines" } kotlinx_serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx_serialization" } kotlinx_collections_immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx_collections_immutable" } kotlinx_datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx_datetime" } @@ -118,6 +122,7 @@ koin_compose_jb = { module = "io.insert-koin:koin-compose", version.ref = "koin_ # SQLDelight sqldelight_driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight_native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" } +sqldelight_jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight_coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } # Moko @@ -126,6 +131,9 @@ moko_mvvm_compose = { module = "dev.icerock.moko:mvvm-flow-compose", version.ref moko_permissions_compose = { module = "dev.icerock.moko:permissions-compose", version.ref = "moko_permissions" } moko_permissions_notifications = { module = "dev.icerock.moko:permissions-notifications", version.ref = "moko_permissions" } +## Multiplatform Utils +multiplatform_path = { module = "me.sujanpoudel.multiplatform.utils:multiplatform-paths", version.ref = "multiplatform_paths" } + # Test test_junit = { module = "junit:junit", version.ref = "test_junit" } test_uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "test_uiautomator" } diff --git a/libraries/designsystem/src/desktopMain/kotlin/com/escodro/designsystem/di/PlatformDesignSystemModule.kt b/libraries/designsystem/src/desktopMain/kotlin/com/escodro/designsystem/di/PlatformDesignSystemModule.kt new file mode 100644 index 00000000..495b7850 --- /dev/null +++ b/libraries/designsystem/src/desktopMain/kotlin/com/escodro/designsystem/di/PlatformDesignSystemModule.kt @@ -0,0 +1,11 @@ +package com.escodro.designsystem.di + +import com.escodro.designsystem.provider.DesktopThemeProvider +import com.escodro.designsystem.provider.ThemeProvider +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.bind +import org.koin.dsl.module + +actual val platformDesignSystemModule = module { + factoryOf(::DesktopThemeProvider) bind ThemeProvider::class +} diff --git a/libraries/designsystem/src/desktopMain/kotlin/com/escodro/designsystem/provider/DesktopThemeProvider.kt b/libraries/designsystem/src/desktopMain/kotlin/com/escodro/designsystem/provider/DesktopThemeProvider.kt new file mode 100644 index 00000000..46f6eff4 --- /dev/null +++ b/libraries/designsystem/src/desktopMain/kotlin/com/escodro/designsystem/provider/DesktopThemeProvider.kt @@ -0,0 +1,14 @@ +package com.escodro.designsystem.provider + +import androidx.compose.material3.ColorScheme + +internal class DesktopThemeProvider : ThemeProvider { + override val isDynamicColorSupported: Boolean + get() = false + + override val dynamicDarkColorScheme: ColorScheme + get() = throw UnsupportedOperationException("Dynamic Theme not supported on Desktop") + + override val dynamicLightColorScheme: ColorScheme + get() = throw UnsupportedOperationException("Dynamic Theme not supported on Desktop") +} diff --git a/libraries/di/src/desktopMain/kotlin/com/escodro/di/ViewModelDefinition.kt b/libraries/di/src/desktopMain/kotlin/com/escodro/di/ViewModelDefinition.kt new file mode 100644 index 00000000..239243c6 --- /dev/null +++ b/libraries/di/src/desktopMain/kotlin/com/escodro/di/ViewModelDefinition.kt @@ -0,0 +1,15 @@ +package com.escodro.di + +import dev.icerock.moko.mvvm.viewmodel.ViewModel +import org.koin.core.definition.Definition +import org.koin.core.definition.KoinDefinition +import org.koin.core.module.Module +import org.koin.core.qualifier.Qualifier + +/** + * Defines an Desktop [ViewModel] in the Koin module. + */ +actual inline fun Module.viewModelDefinition( + qualifier: Qualifier?, + noinline definition: Definition, +): KoinDefinition = factory(qualifier = qualifier, definition = definition) diff --git a/libraries/parcelable/src/desktopMain/kotlin/com/escodro/parcelable/CommonParcelable.desktop.kt b/libraries/parcelable/src/desktopMain/kotlin/com/escodro/parcelable/CommonParcelable.desktop.kt new file mode 100644 index 00000000..7f330c21 --- /dev/null +++ b/libraries/parcelable/src/desktopMain/kotlin/com/escodro/parcelable/CommonParcelable.desktop.kt @@ -0,0 +1,3 @@ +package com.escodro.parcelable + +actual interface CommonParcelable diff --git a/libraries/permission/src/desktopMain/kotlin/com/escodro/permission/api/BindPermissionEffect.desktop.kt b/libraries/permission/src/desktopMain/kotlin/com/escodro/permission/api/BindPermissionEffect.desktop.kt new file mode 100644 index 00000000..36901d9b --- /dev/null +++ b/libraries/permission/src/desktopMain/kotlin/com/escodro/permission/api/BindPermissionEffect.desktop.kt @@ -0,0 +1,10 @@ +package com.escodro.permission.api + +import androidx.compose.runtime.Composable + +@Composable +actual fun BindPermissionEffect( + permissionController: PermissionController, +) { + // Do nothing - not required on Desktop +} diff --git a/libraries/permission/src/desktopMain/kotlin/com/escodro/permission/api/DesktopPermissionController.kt b/libraries/permission/src/desktopMain/kotlin/com/escodro/permission/api/DesktopPermissionController.kt new file mode 100644 index 00000000..f81a0b55 --- /dev/null +++ b/libraries/permission/src/desktopMain/kotlin/com/escodro/permission/api/DesktopPermissionController.kt @@ -0,0 +1,15 @@ +package com.escodro.permission.api + +internal class DesktopPermissionController : PermissionController { + + override val controller: Any = Any() + + override suspend fun requestPermission(permission: Permission) { + // TODO: Implement requestPermission + } + + override suspend fun isPermissionGranted(permission: Permission): Boolean { + // TODO: Implement isPermissionGranted + return false + } +} diff --git a/libraries/permission/src/desktopMain/kotlin/com/escodro/permission/di/PlatformPermissionModule.kt b/libraries/permission/src/desktopMain/kotlin/com/escodro/permission/di/PlatformPermissionModule.kt new file mode 100644 index 00000000..24ecd909 --- /dev/null +++ b/libraries/permission/src/desktopMain/kotlin/com/escodro/permission/di/PlatformPermissionModule.kt @@ -0,0 +1,11 @@ +package com.escodro.permission.di + +import com.escodro.permission.api.DesktopPermissionController +import com.escodro.permission.api.PermissionController +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module + +actual val platformPermissionModule = module { + singleOf(::DesktopPermissionController) bind PermissionController::class +} diff --git a/libraries/test/build.gradle.kts b/libraries/test/build.gradle.kts index ef51188d..76225d40 100644 --- a/libraries/test/build.gradle.kts +++ b/libraries/test/build.gradle.kts @@ -8,6 +8,8 @@ kotlin { setFrameworkBaseName("test") sourceSets { + val desktopMain by getting + commonMain.dependencies { implementation(kotlin("test")) api(libs.kotlinx.coroutines.test) @@ -16,6 +18,11 @@ kotlin { androidMain.dependencies { implementation(kotlin("test-junit")) } + + desktopMain.dependencies { + implementation(kotlin("test-junit")) + } + } tasks.withType { diff --git a/plugins/src/main/java/extension/KmpExtensions.kt b/plugins/src/main/java/extension/KmpExtensions.kt index 04e2541b..accc939b 100644 --- a/plugins/src/main/java/extension/KmpExtensions.kt +++ b/plugins/src/main/java/extension/KmpExtensions.kt @@ -1,6 +1,5 @@ package extension -import org.gradle.kotlin.dsl.get import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension /** @@ -18,4 +17,5 @@ fun KotlinMultiplatformExtension.setFrameworkBaseName(name: String) { baseName = name } } + jvm("desktop") } diff --git a/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml b/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml index cfe330d0..1d42b1a5 100644 --- a/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -119,4 +119,13 @@ Confirmar OK + + + agora + há 1 minuto + há %d minutos + há 1 hora + há %d horas + ontem + há %d dias diff --git a/resources/src/commonMain/composeResources/values/strings.xml b/resources/src/commonMain/composeResources/values/strings.xml index ab1b8636..78fe0676 100644 --- a/resources/src/commonMain/composeResources/values/strings.xml +++ b/resources/src/commonMain/composeResources/values/strings.xml @@ -123,4 +123,13 @@ OK + + just now + 1 minute ago + %d minutes ago + 1 hour ago + %d hours ago + yesterday + %d days ago + diff --git a/settings.gradle.kts b/settings.gradle.kts index c6f7ad33..51848b69 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,5 @@ include(":app") +include(":desktop-app") include(":features:alarm-api") include(":features:alarm") include(":features:task") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 81da9c17..8f557d3b 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -24,9 +24,12 @@ kotlin { baseName = "shared" isStatic = true } + jvm("desktop") } sourceSets { + val desktopTest by getting + commonMain.dependencies { implementation(projects.data.local) implementation(projects.data.datastore) @@ -74,6 +77,11 @@ kotlin { implementation(libs.koin.test) implementation(libs.kotlinx.datetime) } + + desktopTest.dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.kotlinx.coroutines.swing) + } } androidTarget { diff --git a/shared/src/desktopMain/kotlin/com/escodro/shared/di/PlatformSharedModule.kt b/shared/src/desktopMain/kotlin/com/escodro/shared/di/PlatformSharedModule.kt new file mode 100644 index 00000000..1b7769e7 --- /dev/null +++ b/shared/src/desktopMain/kotlin/com/escodro/shared/di/PlatformSharedModule.kt @@ -0,0 +1,8 @@ +package com.escodro.shared.di + +import org.koin.core.module.Module +import org.koin.dsl.module + +internal actual val platformSharedModule: Module = module { + // No specific modules for Desktop +} diff --git a/shared/src/desktopTest/kotlin/com/escodro/alkaa/test/PlatformAnimation.desktop.kt b/shared/src/desktopTest/kotlin/com/escodro/alkaa/test/PlatformAnimation.desktop.kt new file mode 100644 index 00000000..6657bed0 --- /dev/null +++ b/shared/src/desktopTest/kotlin/com/escodro/alkaa/test/PlatformAnimation.desktop.kt @@ -0,0 +1,17 @@ +package com.escodro.alkaa.test + +actual class PlatformAnimation actual constructor() { + /** + * Disable the animations. + */ + actual fun disable() { + // Do nothing + } + + /** + * Enable the animations. + */ + actual fun enable() { + // Do nothing + } +} diff --git a/shared/src/desktopTest/kotlin/com/escodro/alkaa/test/UiTest.desktop.kt b/shared/src/desktopTest/kotlin/com/escodro/alkaa/test/UiTest.desktop.kt new file mode 100644 index 00000000..e2b7cb1f --- /dev/null +++ b/shared/src/desktopTest/kotlin/com/escodro/alkaa/test/UiTest.desktop.kt @@ -0,0 +1,10 @@ +package com.escodro.alkaa.test + +import org.koin.core.module.Module +import org.koin.dsl.module + +/** + * Koin module to provide the platform dependencies. + */ +actual val platformModule: Module + get() = module { }