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 00000000..663965ed Binary files /dev/null and b/desktop-app/src/desktopMain/resources/ic_launcher.icns differ 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 00000000..e0cf3ef9 Binary files /dev/null and b/desktop-app/src/desktopMain/resources/ic_launcher.ico differ 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 { }