diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1a28ecb..843dd91 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.util.Properties -// Load signing config from keystore.properties (not committed to VCS) val keystorePropertiesFile = rootProject.file("keystore.properties") val keystoreProperties = Properties().apply { if (keystorePropertiesFile.exists()) load(keystorePropertiesFile.inputStream()) @@ -11,8 +10,6 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) - alias(libs.plugins.ksp) - alias(libs.plugins.hilt) } android { @@ -25,12 +22,8 @@ android { targetSdk = 36 versionCode = 1 versionName = "1.0.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - vectorDrawables { - useSupportLibrary = true - } + vectorDrawables { useSupportLibrary = true } } signingConfigs { @@ -47,10 +40,7 @@ android { isMinifyEnabled = true isShrinkResources = true signingConfig = signingConfigs.getByName("release") - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } @@ -59,9 +49,7 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - buildFeatures { - compose = true - } + buildFeatures { compose = true } packaging { resources { @@ -73,13 +61,10 @@ android { } kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) - } + compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } dependencies { - // Modules implementation(project(":domain")) implementation(project(":data")) implementation(project(":feature:tasklist")) @@ -88,7 +73,7 @@ dependencies { implementation(project(":core")) implementation(project(":ui")) - // Core + // Core Android implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) @@ -102,20 +87,16 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.navigation.compose) - // Hilt - implementation(libs.hilt.android) - ksp(libs.hilt.compiler) - implementation(libs.hilt.navigation.compose) - - // Room - implementation(libs.room.runtime) + // Koin + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) - // DataStore - implementation(libs.androidx.datastore.preferences) + // SQLDelight Android driver + implementation(libs.sqldelight.android.driver) // Coroutines implementation(libs.kotlinx.coroutines.android) - // Debug debugImplementation(libs.androidx.compose.ui.tooling) } diff --git a/app/src/main/java/fr/benju/tasks/MainActivity.kt b/app/src/main/java/fr/benju/tasks/MainActivity.kt index 0f6bb76..5eae624 100644 --- a/app/src/main/java/fr/benju/tasks/MainActivity.kt +++ b/app/src/main/java/fr/benju/tasks/MainActivity.kt @@ -4,7 +4,6 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -18,16 +17,14 @@ import fr.benju.tasks.feature.settings.SettingsViewModel import fr.benju.tasks.feature.taskeditor.TaskEditorScreen import fr.benju.tasks.feature.tasklist.TaskListScreen import fr.benju.tasks.ui.theme.TaskManagerTheme -import dagger.hilt.android.AndroidEntryPoint +import org.koin.compose.viewmodel.koinViewModel -@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - - val settingsViewModel: SettingsViewModel by viewModels() setContent { + val settingsViewModel: SettingsViewModel = koinViewModel() val isDarkMode by settingsViewModel.darkModeFlow.collectAsState(initial = false) TaskManagerTheme(isDarkMode) { TaskManagerMainApp(settingsViewModel) @@ -39,25 +36,14 @@ class MainActivity : ComponentActivity() { @Composable fun TaskManagerMainApp(settingsViewModel: SettingsViewModel) { val navController = rememberNavController() - - NavHost( - navController = navController, - startDestination = "task_list" - ) { + NavHost(navController = navController, startDestination = "task_list") { composable("task_list") { TaskListScreen( - onAddTaskClick = { - navController.navigate("task_editor") - }, - onSettingsClick = { - navController.navigate("settings") - }, - onTaskClick = { taskId -> - navController.navigate("task_editor?taskId=$taskId") - } + onAddTaskClick = { navController.navigate("task_editor") }, + onSettingsClick = { navController.navigate("settings") }, + onTaskClick = { taskId -> navController.navigate("task_editor?taskId=$taskId") } ) } - composable( route = "task_editor?taskId={taskId}", arguments = listOf( @@ -67,24 +53,15 @@ fun TaskManagerMainApp(settingsViewModel: SettingsViewModel) { } ) ) { backStackEntry -> - val taskId = backStackEntry.arguments?.getLong("taskId") - ?.takeIf { it != -1L } + val taskId = backStackEntry.arguments?.getLong("taskId")?.takeIf { it != -1L } TaskEditorScreen( taskId = taskId, - onTaskSaved = { - navController.popBackStack() - }, - onDismiss = { - navController.popBackStack() - } + onTaskSaved = { navController.popBackStack() }, + onDismiss = { navController.popBackStack() } ) } - composable("settings") { - SettingsScreen( - viewModel = settingsViewModel, - onBack = { navController.popBackStack() } - ) + SettingsScreen(viewModel = settingsViewModel, onBack = { navController.popBackStack() }) } } } diff --git a/app/src/main/java/fr/benju/tasks/TaskManagerApp.kt b/app/src/main/java/fr/benju/tasks/TaskManagerApp.kt index b7118d9..556a483 100644 --- a/app/src/main/java/fr/benju/tasks/TaskManagerApp.kt +++ b/app/src/main/java/fr/benju/tasks/TaskManagerApp.kt @@ -1,7 +1,31 @@ package fr.benju.tasks import android.app.Application -import dagger.hilt.android.HiltAndroidApp +import fr.benju.tasks.data.database.DatabaseDriverFactory +import fr.benju.tasks.data.di.dataModule +import fr.benju.tasks.data.preferences.SettingsFactory +import fr.benju.tasks.feature.settings.di.settingsModule +import fr.benju.tasks.feature.taskeditor.di.taskEditorModule +import fr.benju.tasks.feature.tasklist.di.taskListModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin +import org.koin.dsl.module -@HiltAndroidApp -class TaskManagerApp : Application() +class TaskManagerApp : Application() { + override fun onCreate() { + super.onCreate() + startKoin { + androidContext(this@TaskManagerApp) + modules( + dataModule, + module { + single { DatabaseDriverFactory(androidContext()) } + single { SettingsFactory(androidContext()) } + }, + taskListModule, + taskEditorModule, + settingsModule, + ) + } + } +} diff --git a/app/src/main/java/fr/benju/tasks/di/AppModule.kt b/app/src/main/java/fr/benju/tasks/di/AppModule.kt deleted file mode 100644 index 2f28540..0000000 --- a/app/src/main/java/fr/benju/tasks/di/AppModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package fr.benju.tasks.di - -import fr.benju.tasks.data.repository.TaskRepositoryImpl -import fr.benju.tasks.data.repository.UserPreferencesRepositoryImpl -import fr.benju.tasks.domain.repository.TaskRepository -import fr.benju.tasks.domain.repository.UserPreferencesRepository -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -abstract class AppModule { - - @Binds - @Singleton - abstract fun bindTaskRepository( - impl: TaskRepositoryImpl - ): TaskRepository - - @Binds - @Singleton - abstract fun bindUserPreferencesRepository( - impl: UserPreferencesRepositoryImpl - ): UserPreferencesRepository -} diff --git a/app/src/main/java/fr/benju/tasks/di/CoroutineModule.kt b/app/src/main/java/fr/benju/tasks/di/CoroutineModule.kt deleted file mode 100644 index 07b7cd1..0000000 --- a/app/src/main/java/fr/benju/tasks/di/CoroutineModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package fr.benju.tasks.di - -import fr.benju.tasks.core.dispatchers.CoroutineDispatchers -import fr.benju.tasks.core.dispatchers.ICoroutineDispatchers -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object CoroutineModule { - - @Provides - @Singleton - fun provideCoroutineDispatchers(): ICoroutineDispatchers { - return CoroutineDispatchers() - } -} diff --git a/app/src/main/java/fr/benju/tasks/di/DatabaseModule.kt b/app/src/main/java/fr/benju/tasks/di/DatabaseModule.kt deleted file mode 100644 index b088e27..0000000 --- a/app/src/main/java/fr/benju/tasks/di/DatabaseModule.kt +++ /dev/null @@ -1,35 +0,0 @@ -package fr.benju.tasks.di - -import android.content.Context -import androidx.room.Room -import fr.benju.tasks.data.database.TaskDatabase -import fr.benju.tasks.data.database.dao.TaskDao -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object DatabaseModule { - - @Provides - @Singleton - fun provideTaskDatabase( - @ApplicationContext context: Context - ): TaskDatabase { - return Room.databaseBuilder( - context, - TaskDatabase::class.java, - TaskDatabase.DATABASE_NAME - ).build() - } - - @Provides - @Singleton - fun provideTaskDao(database: TaskDatabase): TaskDao { - return database.taskDao() - } -} diff --git a/app/src/main/java/fr/benju/tasks/di/UserPreferencesModule.kt b/app/src/main/java/fr/benju/tasks/di/UserPreferencesModule.kt deleted file mode 100644 index 82a4b46..0000000 --- a/app/src/main/java/fr/benju/tasks/di/UserPreferencesModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package fr.benju.tasks.di - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.preferencesDataStore -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -private val Context.dataStore: DataStore by preferencesDataStore( - name = "user_preferences" -) - -@Module -@InstallIn(SingletonComponent::class) -object UserPreferencesModule { - - @Provides - @Singleton - fun provideDataStore( - @ApplicationContext context: Context - ): DataStore = context.dataStore -} - diff --git a/build.gradle.kts b/build.gradle.kts index ecefdfd..49682e1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,9 +3,12 @@ plugins { alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.compose.multiplatform) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.hilt) apply false + alias(libs.plugins.sqldelight) apply false } tasks.register("clean", Delete::class) { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index e15fe32..e081ed5 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,40 +1,26 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { - alias(libs.plugins.kotlin.jvm) -} - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + alias(libs.plugins.kotlin.multiplatform) } -tasks.withType { - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) +kotlin { + jvm() + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(project(":domain")) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlin.stdlib) + } + commonTest.dependencies { + api(libs.junit.jupiter.api) + api(libs.kotlinx.coroutines.test) + implementation(libs.junit.jupiter.params) + implementation(libs.mockk) + implementation(libs.kluent) + implementation(libs.turbine) } } } - -dependencies { - implementation(project(":domain")) - - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlin.stdlib) - - // Testing - api for test utilities in main source - api(libs.junit.jupiter.api) - api(libs.kotlinx.coroutines.test) - - // Testing - only for tests, not exposed - testImplementation(libs.junit.jupiter.params) - testRuntimeOnly(libs.junit.jupiter.engine) - testImplementation(libs.mockk) - testImplementation(libs.kluent) - testImplementation(libs.turbine) -} - -tasks.withType { - useJUnitPlatform() -} diff --git a/core/src/main/java/fr/benju/tasks/core/dispatchers/CoroutineDispatchers.kt b/core/src/commonMain/kotlin/fr/benju/tasks/core/dispatchers/CoroutineDispatchers.kt similarity index 100% rename from core/src/main/java/fr/benju/tasks/core/dispatchers/CoroutineDispatchers.kt rename to core/src/commonMain/kotlin/fr/benju/tasks/core/dispatchers/CoroutineDispatchers.kt diff --git a/core/src/main/java/fr/benju/tasks/core/dispatchers/ICoroutineDispatchers.kt b/core/src/commonMain/kotlin/fr/benju/tasks/core/dispatchers/ICoroutineDispatchers.kt similarity index 100% rename from core/src/main/java/fr/benju/tasks/core/dispatchers/ICoroutineDispatchers.kt rename to core/src/commonMain/kotlin/fr/benju/tasks/core/dispatchers/ICoroutineDispatchers.kt diff --git a/core/src/main/java/fr/benju/tasks/core/test/CoroutineTestExtension.kt b/core/src/commonMain/kotlin/fr/benju/tasks/core/test/CoroutineTestExtension.kt similarity index 100% rename from core/src/main/java/fr/benju/tasks/core/test/CoroutineTestExtension.kt rename to core/src/commonMain/kotlin/fr/benju/tasks/core/test/CoroutineTestExtension.kt diff --git a/core/src/main/java/fr/benju/tasks/core/test/TaskTestFactory.kt b/core/src/commonMain/kotlin/fr/benju/tasks/core/test/TaskTestFactory.kt similarity index 90% rename from core/src/main/java/fr/benju/tasks/core/test/TaskTestFactory.kt rename to core/src/commonMain/kotlin/fr/benju/tasks/core/test/TaskTestFactory.kt index fd85624..621f534 100644 --- a/core/src/main/java/fr/benju/tasks/core/test/TaskTestFactory.kt +++ b/core/src/commonMain/kotlin/fr/benju/tasks/core/test/TaskTestFactory.kt @@ -2,6 +2,7 @@ package fr.benju.tasks.core.test import fr.benju.tasks.domain.model.Priority import fr.benju.tasks.domain.model.Task +import kotlinx.datetime.Clock object TaskTestFactory { @@ -11,7 +12,7 @@ object TaskTestFactory { description: String = "Test Description", priority: Priority = Priority.MEDIUM, isCompleted: Boolean = false, - createdAt: Long = System.currentTimeMillis() + createdAt: Long = Clock.System.now().toEpochMilliseconds() ): Task { return Task( id = id, diff --git a/data/build.gradle.kts b/data/build.gradle.kts index ac07ab1..7a0c9b6 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -1,65 +1,76 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.ksp) - alias(libs.plugins.hilt) + alias(libs.plugins.sqldelight) +} + +kotlin { + androidTarget() + jvm() + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(project(":domain")) + implementation(project(":core")) + + // SQLDelight + implementation(libs.sqldelight.runtime) + implementation(libs.sqldelight.coroutines) + + // Multiplatform Settings + implementation(libs.multiplatform.settings) + implementation(libs.multiplatform.settings.coroutines) + + // Koin + implementation(libs.koin.core) + + // Coroutines + implementation(libs.kotlinx.coroutines.core) + } + androidMain.dependencies { + implementation(libs.sqldelight.android.driver) + implementation(libs.koin.android) + } + val iosMain by creating { + dependsOn(commonMain.get()) + dependencies { + implementation(libs.sqldelight.native.driver) + } + } + val iosX64Main by getting { dependsOn(iosMain) } + val iosArm64Main by getting { dependsOn(iosMain) } + val iosSimulatorArm64Main by getting { dependsOn(iosMain) } + jvmMain.dependencies { + implementation(libs.sqldelight.sqlite.driver) + } + commonTest.dependencies { + implementation(libs.junit.jupiter.api) + implementation(libs.mockk) + implementation(libs.kluent) + implementation(libs.kotlinx.coroutines.test) + } + } } android { namespace = "fr.benju.tasks.data" compileSdk = 36 - defaultConfig { minSdk = 26 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } } -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) +sqldelight { + databases { + create("TaskDatabase") { + packageName.set("fr.benju.tasks.data.database") + } } } - -dependencies { - implementation(project(":domain")) - implementation(project(":core")) - - // Core - implementation(libs.androidx.core.ktx) - - // Room - implementation(libs.room.runtime) - implementation(libs.room.ktx) - ksp(libs.room.compiler) - - // DataStore - implementation(libs.androidx.datastore.preferences) - - // Coroutines - implementation(libs.kotlinx.coroutines.android) - - // Hilt - implementation(libs.hilt.android) - ksp(libs.hilt.compiler) - - // Testing - testImplementation(libs.junit.jupiter.api) - testRuntimeOnly(libs.junit.jupiter.engine) - testRuntimeOnly(libs.junit.platform.launcher) - testImplementation(libs.mockk) - testImplementation(libs.kluent) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.room.testing) -} - -tasks.withType { - useJUnitPlatform() -} diff --git a/data/src/main/AndroidManifest.xml b/data/src/androidMain/AndroidManifest.xml similarity index 100% rename from data/src/main/AndroidManifest.xml rename to data/src/androidMain/AndroidManifest.xml diff --git a/data/src/androidMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt b/data/src/androidMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt new file mode 100644 index 0000000..5fedabd --- /dev/null +++ b/data/src/androidMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt @@ -0,0 +1,11 @@ +package fr.benju.tasks.data.database + +import android.content.Context +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.android.AndroidSqliteDriver + +actual class DatabaseDriverFactory(private val context: Context) { + actual fun createDriver(): SqlDriver { + return AndroidSqliteDriver(TaskDatabase.Schema, context, "tasks.db") + } +} diff --git a/data/src/androidMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt b/data/src/androidMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt new file mode 100644 index 0000000..060366e --- /dev/null +++ b/data/src/androidMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt @@ -0,0 +1,14 @@ +package fr.benju.tasks.data.preferences + +import android.content.Context +import com.russhwolf.settings.SharedPreferencesSettings +import com.russhwolf.settings.coroutines.FlowSettings +import com.russhwolf.settings.coroutines.toFlowSettings + +actual class SettingsFactory(private val context: Context) { + actual fun createSettings(): FlowSettings { + return SharedPreferencesSettings( + context.getSharedPreferences("user_preferences", Context.MODE_PRIVATE) + ).toFlowSettings() + } +} diff --git a/data/src/commonMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt new file mode 100644 index 0000000..9d3ec59 --- /dev/null +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt @@ -0,0 +1,7 @@ +package fr.benju.tasks.data.database + +import app.cash.sqldelight.db.SqlDriver + +expect class DatabaseDriverFactory { + fun createDriver(): SqlDriver +} diff --git a/data/src/commonMain/kotlin/fr/benju/tasks/data/di/DataModule.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/di/DataModule.kt new file mode 100644 index 0000000..de93801 --- /dev/null +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/di/DataModule.kt @@ -0,0 +1,58 @@ +package fr.benju.tasks.data.di + +import fr.benju.tasks.core.dispatchers.CoroutineDispatchers +import fr.benju.tasks.core.dispatchers.ICoroutineDispatchers +import fr.benju.tasks.data.database.DatabaseDriverFactory +import fr.benju.tasks.data.database.TaskDatabase +import fr.benju.tasks.data.mapper.TaskMapper +import fr.benju.tasks.data.preferences.SettingsFactory +import fr.benju.tasks.data.repository.TaskRepositoryImpl +import fr.benju.tasks.data.repository.UserPreferencesRepositoryImpl +import fr.benju.tasks.data.usecase.AddTaskUseCaseImpl +import fr.benju.tasks.data.usecase.DeleteTaskUseCaseImpl +import fr.benju.tasks.data.usecase.GetDarkModeUseCaseImpl +import fr.benju.tasks.data.usecase.GetTaskByIdUseCaseImpl +import fr.benju.tasks.data.usecase.GetTasksUseCaseImpl +import fr.benju.tasks.data.usecase.SetDarkModeUseCaseImpl +import fr.benju.tasks.data.usecase.ToggleTaskStatusUseCaseImpl +import fr.benju.tasks.data.usecase.UpdateTaskUseCaseImpl +import fr.benju.tasks.domain.repository.TaskRepository +import fr.benju.tasks.domain.repository.UserPreferencesRepository +import fr.benju.tasks.domain.usecase.AddTaskUseCase +import fr.benju.tasks.domain.usecase.DeleteTaskUseCase +import fr.benju.tasks.domain.usecase.GetDarkModeUseCase +import fr.benju.tasks.domain.usecase.GetTaskByIdUseCase +import fr.benju.tasks.domain.usecase.GetTasksUseCase +import fr.benju.tasks.domain.usecase.SetDarkModeUseCase +import fr.benju.tasks.domain.usecase.ToggleTaskStatusUseCase +import fr.benju.tasks.domain.usecase.UpdateTaskUseCase +import org.koin.dsl.module + +val dataModule = module { + // Database + single { get().createDriver() } + single { TaskDatabase(get()) } + + // Settings + single { get().createSettings() } + + // Mapper + single { TaskMapper() } + + // Repositories + single { TaskRepositoryImpl(get(), get(), get()) } + single { UserPreferencesRepositoryImpl(get()) } + + // Dispatchers + single { CoroutineDispatchers() } + + // Use cases + factory { GetTasksUseCaseImpl(get(), get()) } + factory { GetTaskByIdUseCaseImpl(get()) } + factory { AddTaskUseCaseImpl(get(), get()) } + factory { UpdateTaskUseCaseImpl(get(), get()) } + factory { DeleteTaskUseCaseImpl(get(), get()) } + factory { ToggleTaskStatusUseCaseImpl(get(), get()) } + factory { GetDarkModeUseCaseImpl(get()) } + factory { SetDarkModeUseCaseImpl(get()) } +} diff --git a/data/src/main/java/fr/benju/tasks/data/mapper/TaskMapper.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/mapper/TaskMapper.kt similarity index 50% rename from data/src/main/java/fr/benju/tasks/data/mapper/TaskMapper.kt rename to data/src/commonMain/kotlin/fr/benju/tasks/data/mapper/TaskMapper.kt index 9eeddec..f956a82 100644 --- a/data/src/main/java/fr/benju/tasks/data/mapper/TaskMapper.kt +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/mapper/TaskMapper.kt @@ -1,12 +1,10 @@ package fr.benju.tasks.data.mapper -import fr.benju.tasks.data.database.entity.TaskEntity +import fr.benju.tasks.data.database.TaskEntity import fr.benju.tasks.domain.model.Priority import fr.benju.tasks.domain.model.Task -import javax.inject.Inject - -class TaskMapper @Inject constructor() { +class TaskMapper { fun toDomain(entity: TaskEntity): Task { return Task( id = entity.id, @@ -17,15 +15,4 @@ class TaskMapper @Inject constructor() { createdAt = entity.createdAt ) } - - fun toEntity(domain: Task): TaskEntity { - return TaskEntity( - id = domain.id, - title = domain.title, - description = domain.description, - priority = domain.priority.name, - isCompleted = domain.isCompleted, - createdAt = domain.createdAt - ) - } } diff --git a/data/src/commonMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt new file mode 100644 index 0000000..f4469a4 --- /dev/null +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt @@ -0,0 +1,7 @@ +package fr.benju.tasks.data.preferences + +import com.russhwolf.settings.coroutines.FlowSettings + +expect class SettingsFactory { + fun createSettings(): FlowSettings +} diff --git a/data/src/commonMain/kotlin/fr/benju/tasks/data/repository/TaskRepositoryImpl.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/repository/TaskRepositoryImpl.kt new file mode 100644 index 0000000..0095bb6 --- /dev/null +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/repository/TaskRepositoryImpl.kt @@ -0,0 +1,77 @@ +package fr.benju.tasks.data.repository + +import app.cash.sqldelight.coroutines.asFlow +import app.cash.sqldelight.coroutines.mapToList +import fr.benju.tasks.core.dispatchers.ICoroutineDispatchers +import fr.benju.tasks.data.database.TaskDatabase +import fr.benju.tasks.data.mapper.TaskMapper +import fr.benju.tasks.domain.model.Task +import fr.benju.tasks.domain.repository.TaskRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class TaskRepositoryImpl( + private val database: TaskDatabase, + private val taskMapper: TaskMapper, + private val dispatchers: ICoroutineDispatchers, +) : TaskRepository { + + @Volatile + private var cache: Map = emptyMap() + + override fun getTasks(): Flow> { + return database.tasksQueries.getAllTasks() + .asFlow() + .mapToList(dispatchers.default) + .map { entities -> + val tasks = entities.map { taskMapper.toDomain(it) } + cache = tasks.associateBy { it.id } + tasks + } + } + + override suspend fun getTaskById(id: Long): Task? { + return cache[id] ?: database.tasksQueries.getTaskById(id).executeAsOneOrNull() + ?.let { taskMapper.toDomain(it) } + } + + override suspend fun addTask(task: Task): Result { + return try { + database.tasksQueries.insertTask( + title = task.title, + description = task.description, + priority = task.priority.name, + isCompleted = task.isCompleted, + createdAt = task.createdAt, + ) + val id = database.tasksQueries.getLastInsertId().executeAsOne() + Result.success(id) + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun updateTask(task: Task): Result { + return try { + database.tasksQueries.updateTask( + title = task.title, + description = task.description, + priority = task.priority.name, + isCompleted = task.isCompleted, + id = task.id, + ) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun deleteTask(id: Long): Result { + return try { + database.tasksQueries.deleteTask(id) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } +} diff --git a/data/src/commonMain/kotlin/fr/benju/tasks/data/repository/UserPreferencesRepositoryImpl.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/repository/UserPreferencesRepositoryImpl.kt new file mode 100644 index 0000000..421bea7 --- /dev/null +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/repository/UserPreferencesRepositoryImpl.kt @@ -0,0 +1,22 @@ +package fr.benju.tasks.data.repository + +import com.russhwolf.settings.coroutines.FlowSettings +import fr.benju.tasks.domain.repository.UserPreferencesRepository +import kotlinx.coroutines.flow.Flow + +class UserPreferencesRepositoryImpl( + private val settings: FlowSettings, +) : UserPreferencesRepository { + + companion object { + private const val DARK_MODE_KEY = "dark_mode" + } + + override fun getDarkMode(): Flow { + return settings.getBooleanFlow(DARK_MODE_KEY, false) + } + + override suspend fun setDarkMode(enabled: Boolean) { + settings.putBoolean(DARK_MODE_KEY, enabled) + } +} diff --git a/data/src/main/java/fr/benju/tasks/data/usecase/AddTaskUseCaseImpl.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/AddTaskUseCaseImpl.kt similarity index 88% rename from data/src/main/java/fr/benju/tasks/data/usecase/AddTaskUseCaseImpl.kt rename to data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/AddTaskUseCaseImpl.kt index 0ea4682..d3a7684 100644 --- a/data/src/main/java/fr/benju/tasks/data/usecase/AddTaskUseCaseImpl.kt +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/AddTaskUseCaseImpl.kt @@ -5,13 +5,11 @@ import fr.benju.tasks.domain.model.Task import fr.benju.tasks.domain.repository.TaskRepository import fr.benju.tasks.domain.usecase.AddTaskUseCase import kotlinx.coroutines.withContext -import javax.inject.Inject -class AddTaskUseCaseImpl @Inject constructor( +class AddTaskUseCaseImpl( private val repository: TaskRepository, private val dispatchers: ICoroutineDispatchers ) : AddTaskUseCase { - override suspend fun invoke(task: Task): Result { return withContext(dispatchers.io) { repository.addTask(task) diff --git a/data/src/main/java/fr/benju/tasks/data/usecase/DeleteTaskUseCaseImpl.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/DeleteTaskUseCaseImpl.kt similarity index 87% rename from data/src/main/java/fr/benju/tasks/data/usecase/DeleteTaskUseCaseImpl.kt rename to data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/DeleteTaskUseCaseImpl.kt index 50b27f6..1bcad3e 100644 --- a/data/src/main/java/fr/benju/tasks/data/usecase/DeleteTaskUseCaseImpl.kt +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/DeleteTaskUseCaseImpl.kt @@ -4,13 +4,11 @@ import fr.benju.tasks.core.dispatchers.ICoroutineDispatchers import fr.benju.tasks.domain.repository.TaskRepository import fr.benju.tasks.domain.usecase.DeleteTaskUseCase import kotlinx.coroutines.withContext -import javax.inject.Inject -class DeleteTaskUseCaseImpl @Inject constructor( +class DeleteTaskUseCaseImpl( private val repository: TaskRepository, private val dispatchers: ICoroutineDispatchers ) : DeleteTaskUseCase { - override suspend fun invoke(taskId: Long): Result { return withContext(dispatchers.io) { repository.deleteTask(taskId) diff --git a/data/src/main/java/fr/benju/tasks/data/usecase/GetDarkModeUseCaseImpl.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/GetDarkModeUseCaseImpl.kt similarity index 81% rename from data/src/main/java/fr/benju/tasks/data/usecase/GetDarkModeUseCaseImpl.kt rename to data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/GetDarkModeUseCaseImpl.kt index 011db2a..60e65f4 100644 --- a/data/src/main/java/fr/benju/tasks/data/usecase/GetDarkModeUseCaseImpl.kt +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/GetDarkModeUseCaseImpl.kt @@ -3,12 +3,9 @@ package fr.benju.tasks.data.usecase import fr.benju.tasks.domain.repository.UserPreferencesRepository import fr.benju.tasks.domain.usecase.GetDarkModeUseCase import kotlinx.coroutines.flow.Flow -import javax.inject.Inject -class GetDarkModeUseCaseImpl @Inject constructor( +class GetDarkModeUseCaseImpl( private val repository: UserPreferencesRepository ) : GetDarkModeUseCase { - override fun invoke(): Flow = repository.getDarkMode() } - diff --git a/data/src/main/java/fr/benju/tasks/data/usecase/GetTaskByIdUseCaseImpl.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/GetTaskByIdUseCaseImpl.kt similarity index 81% rename from data/src/main/java/fr/benju/tasks/data/usecase/GetTaskByIdUseCaseImpl.kt rename to data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/GetTaskByIdUseCaseImpl.kt index 96c2e50..13ab005 100644 --- a/data/src/main/java/fr/benju/tasks/data/usecase/GetTaskByIdUseCaseImpl.kt +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/GetTaskByIdUseCaseImpl.kt @@ -3,14 +3,11 @@ package fr.benju.tasks.data.usecase import fr.benju.tasks.domain.model.Task import fr.benju.tasks.domain.repository.TaskRepository import fr.benju.tasks.domain.usecase.GetTaskByIdUseCase -import javax.inject.Inject -class GetTaskByIdUseCaseImpl @Inject constructor( +class GetTaskByIdUseCaseImpl( private val repository: TaskRepository ) : GetTaskByIdUseCase { - override suspend fun invoke(id: Long): Task? { return repository.getTaskById(id) } } - diff --git a/data/src/main/java/fr/benju/tasks/data/usecase/GetTasksUseCaseImpl.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/GetTasksUseCaseImpl.kt similarity index 93% rename from data/src/main/java/fr/benju/tasks/data/usecase/GetTasksUseCaseImpl.kt rename to data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/GetTasksUseCaseImpl.kt index b50ca4d..ee8dba1 100644 --- a/data/src/main/java/fr/benju/tasks/data/usecase/GetTasksUseCaseImpl.kt +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/GetTasksUseCaseImpl.kt @@ -8,13 +8,11 @@ import fr.benju.tasks.domain.usecase.GetTasksUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import javax.inject.Inject -class GetTasksUseCaseImpl @Inject constructor( +class GetTasksUseCaseImpl( private val repository: TaskRepository, private val dispatchers: ICoroutineDispatchers ) : GetTasksUseCase { - override fun invoke(filter: TaskFilter): Flow> { return repository.getTasks() .map { tasks -> applyFilter(tasks, filter) } diff --git a/data/src/main/java/fr/benju/tasks/data/usecase/SetDarkModeUseCaseImpl.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/SetDarkModeUseCaseImpl.kt similarity index 80% rename from data/src/main/java/fr/benju/tasks/data/usecase/SetDarkModeUseCaseImpl.kt rename to data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/SetDarkModeUseCaseImpl.kt index 6635fba..7479e51 100644 --- a/data/src/main/java/fr/benju/tasks/data/usecase/SetDarkModeUseCaseImpl.kt +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/SetDarkModeUseCaseImpl.kt @@ -2,9 +2,8 @@ package fr.benju.tasks.data.usecase import fr.benju.tasks.domain.repository.UserPreferencesRepository import fr.benju.tasks.domain.usecase.SetDarkModeUseCase -import javax.inject.Inject -class SetDarkModeUseCaseImpl @Inject constructor( +class SetDarkModeUseCaseImpl( private val repository: UserPreferencesRepository ) : SetDarkModeUseCase { override suspend fun invoke(enabled: Boolean) = repository.setDarkMode(enabled) diff --git a/data/src/main/java/fr/benju/tasks/data/usecase/ToggleTaskStatusUseCaseImpl.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/ToggleTaskStatusUseCaseImpl.kt similarity index 91% rename from data/src/main/java/fr/benju/tasks/data/usecase/ToggleTaskStatusUseCaseImpl.kt rename to data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/ToggleTaskStatusUseCaseImpl.kt index 61d489a..6574776 100644 --- a/data/src/main/java/fr/benju/tasks/data/usecase/ToggleTaskStatusUseCaseImpl.kt +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/ToggleTaskStatusUseCaseImpl.kt @@ -4,13 +4,11 @@ import fr.benju.tasks.core.dispatchers.ICoroutineDispatchers import fr.benju.tasks.domain.repository.TaskRepository import fr.benju.tasks.domain.usecase.ToggleTaskStatusUseCase import kotlinx.coroutines.withContext -import javax.inject.Inject -class ToggleTaskStatusUseCaseImpl @Inject constructor( +class ToggleTaskStatusUseCaseImpl( private val repository: TaskRepository, private val dispatchers: ICoroutineDispatchers ) : ToggleTaskStatusUseCase { - override suspend fun invoke(taskId: Long): Result { return withContext(dispatchers.io) { try { diff --git a/data/src/main/java/fr/benju/tasks/data/usecase/UpdateTaskUseCaseImpl.kt b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/UpdateTaskUseCaseImpl.kt similarity index 88% rename from data/src/main/java/fr/benju/tasks/data/usecase/UpdateTaskUseCaseImpl.kt rename to data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/UpdateTaskUseCaseImpl.kt index 73aee5a..45ac8ad 100644 --- a/data/src/main/java/fr/benju/tasks/data/usecase/UpdateTaskUseCaseImpl.kt +++ b/data/src/commonMain/kotlin/fr/benju/tasks/data/usecase/UpdateTaskUseCaseImpl.kt @@ -5,13 +5,11 @@ import fr.benju.tasks.domain.model.Task import fr.benju.tasks.domain.repository.TaskRepository import fr.benju.tasks.domain.usecase.UpdateTaskUseCase import kotlinx.coroutines.withContext -import javax.inject.Inject -class UpdateTaskUseCaseImpl @Inject constructor( +class UpdateTaskUseCaseImpl( private val repository: TaskRepository, private val dispatchers: ICoroutineDispatchers ) : UpdateTaskUseCase { - override suspend fun invoke(task: Task): Result { return withContext(dispatchers.io) { repository.updateTask(task) diff --git a/data/src/commonMain/sqldelight/fr/benju/tasks/data/database/Tasks.sq b/data/src/commonMain/sqldelight/fr/benju/tasks/data/database/Tasks.sq new file mode 100644 index 0000000..36a5ad1 --- /dev/null +++ b/data/src/commonMain/sqldelight/fr/benju/tasks/data/database/Tasks.sq @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS TaskEntity ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + priority TEXT NOT NULL, + isCompleted INTEGER AS Boolean NOT NULL DEFAULT 0, + createdAt INTEGER NOT NULL +); + +getAllTasks: +SELECT * FROM TaskEntity ORDER BY createdAt DESC; + +getTaskById: +SELECT * FROM TaskEntity WHERE id = :taskId; + +insertTask: +INSERT INTO TaskEntity (title, description, priority, isCompleted, createdAt) +VALUES (:title, :description, :priority, :isCompleted, :createdAt); + +updateTask: +UPDATE TaskEntity SET title = :title, description = :description, priority = :priority, isCompleted = :isCompleted WHERE id = :id; + +deleteTask: +DELETE FROM TaskEntity WHERE id = :taskId; + +deleteAllTasks: +DELETE FROM TaskEntity; + +getLastInsertId: +SELECT last_insert_rowid(); diff --git a/data/src/iosMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt b/data/src/iosMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt new file mode 100644 index 0000000..0e605c5 --- /dev/null +++ b/data/src/iosMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt @@ -0,0 +1,10 @@ +package fr.benju.tasks.data.database + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.native.NativeSqliteDriver + +actual class DatabaseDriverFactory { + actual fun createDriver(): SqlDriver { + return NativeSqliteDriver(TaskDatabase.Schema, "tasks.db") + } +} diff --git a/data/src/iosMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt b/data/src/iosMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt new file mode 100644 index 0000000..e439b8a --- /dev/null +++ b/data/src/iosMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt @@ -0,0 +1,12 @@ +package fr.benju.tasks.data.preferences + +import com.russhwolf.settings.NSUserDefaultsSettings +import com.russhwolf.settings.coroutines.FlowSettings +import com.russhwolf.settings.coroutines.toFlowSettings +import platform.Foundation.NSUserDefaults + +actual class SettingsFactory { + actual fun createSettings(): FlowSettings { + return NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults).toFlowSettings() + } +} diff --git a/data/src/jvmMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt b/data/src/jvmMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt new file mode 100644 index 0000000..69ab8c2 --- /dev/null +++ b/data/src/jvmMain/kotlin/fr/benju/tasks/data/database/DatabaseDriverFactory.kt @@ -0,0 +1,15 @@ +package fr.benju.tasks.data.database + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import java.io.File + +actual class DatabaseDriverFactory { + actual fun createDriver(): SqlDriver { + val dbPath = File(System.getProperty("user.home"), "task-manager/tasks.db") + dbPath.parentFile?.mkdirs() + val driver = JdbcSqliteDriver("jdbc:sqlite:${dbPath.absolutePath}") + TaskDatabase.Schema.create(driver) + return driver + } +} diff --git a/data/src/jvmMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt b/data/src/jvmMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt new file mode 100644 index 0000000..b5d6fcd --- /dev/null +++ b/data/src/jvmMain/kotlin/fr/benju/tasks/data/preferences/SettingsFactory.kt @@ -0,0 +1,12 @@ +package fr.benju.tasks.data.preferences + +import com.russhwolf.settings.PreferencesSettings +import com.russhwolf.settings.coroutines.FlowSettings +import com.russhwolf.settings.coroutines.toFlowSettings +import java.util.prefs.Preferences + +actual class SettingsFactory { + actual fun createSettings(): FlowSettings { + return PreferencesSettings(Preferences.userRoot().node("task-manager")).toFlowSettings() + } +} diff --git a/data/src/main/java/fr/benju/tasks/data/database/TaskDatabase.kt b/data/src/main/java/fr/benju/tasks/data/database/TaskDatabase.kt deleted file mode 100644 index 3115584..0000000 --- a/data/src/main/java/fr/benju/tasks/data/database/TaskDatabase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package fr.benju.tasks.data.database - -import androidx.room.Database -import androidx.room.RoomDatabase -import fr.benju.tasks.data.database.dao.TaskDao -import fr.benju.tasks.data.database.entity.TaskEntity - -@Database( - entities = [TaskEntity::class], - version = 1, - exportSchema = false -) -abstract class TaskDatabase : RoomDatabase() { - abstract fun taskDao(): TaskDao - - companion object { - const val DATABASE_NAME = "task_manager_db" - } -} diff --git a/data/src/main/java/fr/benju/tasks/data/database/dao/TaskDao.kt b/data/src/main/java/fr/benju/tasks/data/database/dao/TaskDao.kt deleted file mode 100644 index dea1005..0000000 --- a/data/src/main/java/fr/benju/tasks/data/database/dao/TaskDao.kt +++ /dev/null @@ -1,27 +0,0 @@ -package fr.benju.tasks.data.database.dao - -import androidx.room.* -import fr.benju.tasks.data.database.entity.TaskEntity -import kotlinx.coroutines.flow.Flow - -@Dao -interface TaskDao { - - @Query("SELECT * FROM tasks ORDER BY createdAt DESC") - fun getAllTasks(): Flow> - - @Query("SELECT * FROM tasks WHERE id = :taskId") - suspend fun getTaskById(taskId: Long): TaskEntity? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertTask(task: TaskEntity): Long - - @Update - suspend fun updateTask(task: TaskEntity) - - @Query("DELETE FROM tasks WHERE id = :taskId") - suspend fun deleteTask(taskId: Long) - - @Query("DELETE FROM tasks") - suspend fun deleteAllTasks() -} diff --git a/data/src/main/java/fr/benju/tasks/data/database/entity/TaskEntity.kt b/data/src/main/java/fr/benju/tasks/data/database/entity/TaskEntity.kt deleted file mode 100644 index 3a82e9b..0000000 --- a/data/src/main/java/fr/benju/tasks/data/database/entity/TaskEntity.kt +++ /dev/null @@ -1,15 +0,0 @@ -package fr.benju.tasks.data.database.entity - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "tasks") -data class TaskEntity( - @PrimaryKey(autoGenerate = true) - val id: Long = 0, - val title: String, - val description: String, - val priority: String, // "LOW", "MEDIUM", "HIGH" - val isCompleted: Boolean = false, - val createdAt: Long = System.currentTimeMillis() -) diff --git a/data/src/main/java/fr/benju/tasks/data/di/UseCaseModule.kt b/data/src/main/java/fr/benju/tasks/data/di/UseCaseModule.kt deleted file mode 100644 index f3f0dfb..0000000 --- a/data/src/main/java/fr/benju/tasks/data/di/UseCaseModule.kt +++ /dev/null @@ -1,68 +0,0 @@ -package fr.benju.tasks.data.di - -import fr.benju.tasks.data.usecase.DeleteTaskUseCaseImpl -import fr.benju.tasks.data.usecase.GetDarkModeUseCaseImpl -import fr.benju.tasks.data.usecase.GetTaskByIdUseCaseImpl -import fr.benju.tasks.data.usecase.GetTasksUseCaseImpl -import fr.benju.tasks.data.usecase.SetDarkModeUseCaseImpl -import fr.benju.tasks.data.usecase.ToggleTaskStatusUseCaseImpl -import fr.benju.tasks.data.usecase.UpdateTaskUseCaseImpl -import fr.benju.tasks.domain.usecase.DeleteTaskUseCase -import fr.benju.tasks.domain.usecase.GetDarkModeUseCase -import fr.benju.tasks.domain.usecase.GetTaskByIdUseCase -import fr.benju.tasks.domain.usecase.GetTasksUseCase -import fr.benju.tasks.domain.usecase.SetDarkModeUseCase -import fr.benju.tasks.domain.usecase.ToggleTaskStatusUseCase -import fr.benju.tasks.domain.usecase.UpdateTaskUseCase -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped -import javax.inject.Singleton - -@Module -@InstallIn(ViewModelComponent::class) -abstract class UseCaseModule { - - @Binds - @ViewModelScoped - abstract fun bindGetTasksUseCase( - impl: GetTasksUseCaseImpl - ): GetTasksUseCase - - @Binds - @ViewModelScoped - abstract fun bindGetTaskByIdUseCase( - impl: GetTaskByIdUseCaseImpl - ): GetTaskByIdUseCase - - @Binds - @ViewModelScoped - abstract fun bindUpdateTaskUseCase( - impl: UpdateTaskUseCaseImpl - ): UpdateTaskUseCase - - @Binds - @ViewModelScoped - abstract fun bindDeleteTaskUseCase( - impl: DeleteTaskUseCaseImpl - ): DeleteTaskUseCase - - @Binds - abstract fun bindToggleTaskStatusUseCase( - impl: ToggleTaskStatusUseCaseImpl - ): ToggleTaskStatusUseCase - - @Binds - @ViewModelScoped - abstract fun bindGetDarkModeUseCase( - impl: GetDarkModeUseCaseImpl - ): GetDarkModeUseCase - - @Binds - @ViewModelScoped - abstract fun bindSetDarkModeUseCase( - impl: SetDarkModeUseCaseImpl - ): SetDarkModeUseCase -} diff --git a/data/src/main/java/fr/benju/tasks/data/di/di/TaskEditorModule.kt b/data/src/main/java/fr/benju/tasks/data/di/di/TaskEditorModule.kt deleted file mode 100644 index af56115..0000000 --- a/data/src/main/java/fr/benju/tasks/data/di/di/TaskEditorModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package fr.benju.tasks.feature.taskeditor.di - -import fr.benju.tasks.data.usecase.AddTaskUseCaseImpl -import fr.benju.tasks.domain.usecase.AddTaskUseCase -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -abstract class TaskEditorModule { - - @Binds - @Singleton - abstract fun bindAddTaskUseCase( - impl: AddTaskUseCaseImpl - ): AddTaskUseCase -} diff --git a/data/src/main/java/fr/benju/tasks/data/repository/TaskRepositoryImpl.kt b/data/src/main/java/fr/benju/tasks/data/repository/TaskRepositoryImpl.kt deleted file mode 100644 index a5b69c6..0000000 --- a/data/src/main/java/fr/benju/tasks/data/repository/TaskRepositoryImpl.kt +++ /dev/null @@ -1,57 +0,0 @@ -package fr.benju.tasks.data.repository - -import fr.benju.tasks.data.database.dao.TaskDao -import fr.benju.tasks.data.mapper.TaskMapper -import fr.benju.tasks.domain.model.Task -import fr.benju.tasks.domain.repository.TaskRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import javax.inject.Inject - -class TaskRepositoryImpl @Inject constructor( - private val taskDao: TaskDao, - private val taskMapper: TaskMapper, -) : TaskRepository { - - @Volatile - private var cache: Map = emptyMap() - - override fun getTasks(): Flow> { - return taskDao.getAllTasks().map { entities -> - val tasks = entities.map { taskMapper.toDomain(it) } - cache = tasks.associateBy { it.id } - tasks - } - } - - override suspend fun getTaskById(id: Long): Task? { - return cache[id] ?: taskDao.getTaskById(id)?.let { taskMapper.toDomain(it) } - } - - override suspend fun addTask(task: Task): Result { - return try { - val id = taskDao.insertTask(taskMapper.toEntity(task)) - Result.success(id) - } catch (e: Exception) { - Result.failure(e) - } - } - - override suspend fun updateTask(task: Task): Result { - return try { - taskDao.updateTask(taskMapper.toEntity(task)) - Result.success(Unit) - } catch (e: Exception) { - Result.failure(e) - } - } - - override suspend fun deleteTask(id: Long): Result { - return try { - taskDao.deleteTask(id) - Result.success(Unit) - } catch (e: Exception) { - Result.failure(e) - } - } -} diff --git a/data/src/main/java/fr/benju/tasks/data/repository/UserPreferencesRepositoryImpl.kt b/data/src/main/java/fr/benju/tasks/data/repository/UserPreferencesRepositoryImpl.kt deleted file mode 100644 index d752c70..0000000 --- a/data/src/main/java/fr/benju/tasks/data/repository/UserPreferencesRepositoryImpl.kt +++ /dev/null @@ -1,32 +0,0 @@ -package fr.benju.tasks.data.repository - -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import fr.benju.tasks.domain.repository.UserPreferencesRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import javax.inject.Inject - -class UserPreferencesRepositoryImpl @Inject constructor( - private val dataStore: DataStore -) : UserPreferencesRepository { - - companion object { - private val DARK_MODE_KEY = booleanPreferencesKey("dark_mode") - } - - override fun getDarkMode(): Flow { - return dataStore.data.map { preferences -> - preferences[DARK_MODE_KEY] ?: false - } - } - - override suspend fun setDarkMode(enabled: Boolean) { - dataStore.edit { preferences -> - preferences[DARK_MODE_KEY] = enabled - } - } -} - diff --git a/data/src/test/java/fr/benju/tasks/data/mapper/TaskMapperTest.kt b/data/src/test/java/fr/benju/tasks/data/mapper/TaskMapperTest.kt deleted file mode 100644 index f0e5e1b..0000000 --- a/data/src/test/java/fr/benju/tasks/data/mapper/TaskMapperTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package fr.benju.tasks.data.mapper - -import fr.benju.tasks.data.database.entity.TaskEntity -import fr.benju.tasks.domain.model.Priority -import fr.benju.tasks.domain.model.Task -import org.amshove.kluent.shouldBeEqualTo -import org.junit.jupiter.api.Test - -class TaskMapperTest { - - private val mapper = TaskMapper() - - @Test - fun `toDomain should map entity to domain model correctly`() { - val entity = TaskEntity( - id = 1L, - title = "Test Task", - description = "Test Description", - priority = "HIGH", - isCompleted = false, - createdAt = 123456789L - ) - - val result = mapper.toDomain(entity) - - result.id shouldBeEqualTo 1L - result.title shouldBeEqualTo "Test Task" - result.description shouldBeEqualTo "Test Description" - result.priority shouldBeEqualTo Priority.HIGH - result.isCompleted shouldBeEqualTo false - result.createdAt shouldBeEqualTo 123456789L - } - - @Test - fun `toEntity should map domain to entity correctly`() { - val domain = Task( - id = 1L, - title = "Test Task", - description = "Test Description", - priority = Priority.MEDIUM, - isCompleted = true, - createdAt = 987654321L - ) - - val result = mapper.toEntity(domain) - - result.id shouldBeEqualTo 1L - result.title shouldBeEqualTo "Test Task" - result.description shouldBeEqualTo "Test Description" - result.priority shouldBeEqualTo "MEDIUM" - result.isCompleted shouldBeEqualTo true - result.createdAt shouldBeEqualTo 987654321L - } -} diff --git a/data/src/test/java/fr/benju/tasks/data/repository/TaskRepositoryImplTest.kt b/data/src/test/java/fr/benju/tasks/data/repository/TaskRepositoryImplTest.kt deleted file mode 100644 index 5e1d964..0000000 --- a/data/src/test/java/fr/benju/tasks/data/repository/TaskRepositoryImplTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package fr.benju.tasks.data.repository - -import fr.benju.tasks.data.database.dao.TaskDao -import fr.benju.tasks.data.database.entity.TaskEntity -import fr.benju.tasks.data.mapper.TaskMapper -import fr.benju.tasks.domain.model.Priority -import fr.benju.tasks.domain.model.Task -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.jupiter.api.Test - -class TaskRepositoryImplTest { - - private val taskDao: TaskDao = mockk() - private val taskMapper = TaskMapper() - private val repository = TaskRepositoryImpl(taskDao, taskMapper) - - @Test - fun `addTask should insert task and return success with id`() = runTest { - // Given - val task = Task( - id = 0, - title = "New Task", - description = "Description", - priority = Priority.HIGH, - isCompleted = false - ) - coEvery { taskDao.insertTask(any()) } returns 1L - - // When - val result = repository.addTask(task) - - // Then - result.isSuccess shouldBeEqualTo true - result.getOrNull() shouldBeEqualTo 1L - coVerify { taskDao.insertTask(any()) } - } - - @Test - fun `updateTask should call dao update`() = runTest { - // Given - val task = Task( - id = 1L, - title = "Updated Task", - description = "Updated Description", - priority = Priority.LOW, - isCompleted = true - ) - coEvery { taskDao.updateTask(any()) } returns Unit - - // When - val result = repository.updateTask(task) - - // Then - result.isSuccess shouldBeEqualTo true - coVerify { taskDao.updateTask(any()) } - } - - @Test - fun `deleteTask should call dao delete`() = runTest { - // Given - val taskId = 1L - coEvery { taskDao.deleteTask(taskId) } returns Unit - - // When - val result = repository.deleteTask(taskId) - - // Then - result.isSuccess shouldBeEqualTo true - coVerify { taskDao.deleteTask(taskId) } - } -} diff --git a/data/src/test/java/fr/benju/tasks/data/usecase/AddTaskUseCaseImplTest.kt b/data/src/test/java/fr/benju/tasks/data/usecase/AddTaskUseCaseImplTest.kt deleted file mode 100644 index 3e03a3d..0000000 --- a/data/src/test/java/fr/benju/tasks/data/usecase/AddTaskUseCaseImplTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package fr.benju.tasks.data.usecase - -import fr.benju.tasks.core.test.CoroutineTestExtension -import fr.benju.tasks.core.test.TaskTestFactory -import fr.benju.tasks.domain.repository.TaskRepository -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.RegisterExtension - -class AddTaskUseCaseImplTest { - - @JvmField - @RegisterExtension - val coroutineExtension = CoroutineTestExtension() - - private val repository: TaskRepository = mockk() - private val useCase = AddTaskUseCaseImpl(repository, coroutineExtension.dispatchers) - - @Test - fun `invoke should call repository and return success with id`() = runTest { - // Given - val task = TaskTestFactory.createTask() - coEvery { repository.addTask(task) } returns Result.success(1L) - - // When - val result = useCase(task) - - // Then - result.isSuccess shouldBeEqualTo true - result.getOrNull() shouldBeEqualTo 1L - coVerify { repository.addTask(task) } - } -} diff --git a/data/src/test/java/fr/benju/tasks/data/usecase/GetTasksUseCaseImplTest.kt b/data/src/test/java/fr/benju/tasks/data/usecase/GetTasksUseCaseImplTest.kt deleted file mode 100644 index 34acb1b..0000000 --- a/data/src/test/java/fr/benju/tasks/data/usecase/GetTasksUseCaseImplTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -package fr.benju.tasks.data.usecase - -import fr.benju.tasks.core.test.CoroutineTestExtension -import fr.benju.tasks.core.test.TaskTestFactory -import fr.benju.tasks.domain.model.TaskFilter -import fr.benju.tasks.domain.repository.TaskRepository -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.RegisterExtension - -class GetTasksUseCaseImplTest { - - @JvmField - @RegisterExtension - val coroutineExtension = CoroutineTestExtension() - - private val repository: TaskRepository = mockk() - private val useCase = GetTasksUseCaseImpl(repository, coroutineExtension.dispatchers) - - @Test - fun `invoke with ALL filter should return all tasks`() = runTest { - // Given - val tasks = TaskTestFactory.createTasks(3) - every { repository.getTasks() } returns flowOf(tasks) - - // When - val result = useCase(TaskFilter.ALL).first() - - // Then - result.size shouldBeEqualTo 3 - } - - @Test - fun `invoke with ACTIVE filter should return only active tasks`() = runTest { - // Given - val tasks = listOf( - TaskTestFactory.createTask(id = 1, isCompleted = false), - TaskTestFactory.createTask(id = 2, isCompleted = true), - TaskTestFactory.createTask(id = 3, isCompleted = false) - ) - every { repository.getTasks() } returns flowOf(tasks) - - // When - val result = useCase(TaskFilter.ACTIVE).first() - - // Then - result.size shouldBeEqualTo 2 - result.all { !it.isCompleted } shouldBeEqualTo true - } - - @Test - fun `invoke with COMPLETED filter should return only completed tasks`() = runTest { - // Given - val tasks = listOf( - TaskTestFactory.createTask(id = 1, isCompleted = false), - TaskTestFactory.createTask(id = 2, isCompleted = true), - TaskTestFactory.createTask(id = 3, isCompleted = true) - ) - every { repository.getTasks() } returns flowOf(tasks) - - // When - val result = useCase(TaskFilter.COMPLETED).first() - - // Then - result.size shouldBeEqualTo 2 - result.all { it.isCompleted } shouldBeEqualTo true - } -} diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts new file mode 100644 index 0000000..b60c6dc --- /dev/null +++ b/desktopApp/build.gradle.kts @@ -0,0 +1,45 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + jvm() + + sourceSets { + jvmMain.dependencies { + implementation(project(":domain")) + implementation(project(":data")) + implementation(project(":feature:tasklist")) + implementation(project(":feature:taskeditor")) + implementation(project(":feature:settings")) + implementation(project(":core")) + implementation(project(":ui")) + + implementation(compose.desktop.currentOs) + implementation(libs.koin.core) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) + implementation(libs.sqldelight.sqlite.driver) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) + } + } +} + +compose.desktop { + application { + mainClass = "fr.benju.tasks.desktop.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "TaskManager" + packageVersion = "1.0.0" + } + } +} diff --git a/desktopApp/src/jvmMain/kotlin/fr/benju/tasks/desktop/DesktopApp.kt b/desktopApp/src/jvmMain/kotlin/fr/benju/tasks/desktop/DesktopApp.kt new file mode 100644 index 0000000..3c0d19a --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/fr/benju/tasks/desktop/DesktopApp.kt @@ -0,0 +1,45 @@ +package fr.benju.tasks.desktop + +import androidx.compose.runtime.Composable +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import fr.benju.tasks.feature.settings.SettingsScreen +import fr.benju.tasks.feature.settings.SettingsViewModel +import fr.benju.tasks.feature.taskeditor.TaskEditorScreen +import fr.benju.tasks.feature.tasklist.TaskListScreen + +@Composable +fun DesktopApp(settingsViewModel: SettingsViewModel) { + val navController = rememberNavController() + NavHost(navController = navController, startDestination = "task_list") { + composable("task_list") { + TaskListScreen( + onAddTaskClick = { navController.navigate("task_editor") }, + onSettingsClick = { navController.navigate("settings") }, + onTaskClick = { taskId -> navController.navigate("task_editor?taskId=$taskId") } + ) + } + composable( + route = "task_editor?taskId={taskId}", + arguments = listOf( + navArgument("taskId") { + type = NavType.LongType + defaultValue = -1L + } + ) + ) { backStackEntry -> + val taskId = backStackEntry.arguments?.getLong("taskId")?.takeIf { it != -1L } + TaskEditorScreen( + taskId = taskId, + onTaskSaved = { navController.popBackStack() }, + onDismiss = { navController.popBackStack() } + ) + } + composable("settings") { + SettingsScreen(viewModel = settingsViewModel, onBack = { navController.popBackStack() }) + } + } +} diff --git a/desktopApp/src/jvmMain/kotlin/fr/benju/tasks/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/fr/benju/tasks/desktop/Main.kt new file mode 100644 index 0000000..d154c53 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/fr/benju/tasks/desktop/Main.kt @@ -0,0 +1,43 @@ +package fr.benju.tasks.desktop + +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import fr.benju.tasks.data.database.DatabaseDriverFactory +import fr.benju.tasks.data.di.dataModule +import fr.benju.tasks.data.preferences.SettingsFactory +import fr.benju.tasks.feature.settings.SettingsViewModel +import fr.benju.tasks.feature.settings.di.settingsModule +import fr.benju.tasks.feature.taskeditor.di.taskEditorModule +import fr.benju.tasks.feature.tasklist.di.taskListModule +import fr.benju.tasks.ui.theme.TaskManagerTheme +import org.koin.compose.KoinApplication +import org.koin.compose.viewmodel.koinViewModel +import org.koin.dsl.module + +fun main() = application { + KoinApplication(application = { + modules( + dataModule, + module { + single { DatabaseDriverFactory() } + single { SettingsFactory() } + }, + taskListModule, + taskEditorModule, + settingsModule, + ) + }) { + Window( + onCloseRequest = ::exitApplication, + title = "Task Manager" + ) { + val settingsViewModel: SettingsViewModel = koinViewModel() + val isDarkMode by settingsViewModel.darkModeFlow.collectAsState(initial = false) + TaskManagerTheme(isDarkMode) { + DesktopApp(settingsViewModel) + } + } + } +} diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 71cd1ca..76d6621 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -1,33 +1,23 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { - alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.multiplatform) } -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} +kotlin { + jvm() + iosX64() + iosArm64() + iosSimulatorArm64() -tasks.withType { - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) + } + commonTest.dependencies { + implementation(libs.junit.jupiter.api) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.mockk) + implementation(libs.kluent) } } } - -dependencies { - implementation(libs.kotlinx.coroutines.core) - - // Testing - testImplementation(libs.junit.jupiter.api) - testRuntimeOnly(libs.junit.jupiter.engine) - testImplementation(libs.mockk) - testImplementation(libs.kluent) - testImplementation(libs.kotlinx.coroutines.test) -} - -tasks.withType { - useJUnitPlatform() -} diff --git a/domain/src/main/java/fr/benju/tasks/domain/model/Priority.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/model/Priority.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/model/Priority.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/model/Priority.kt diff --git a/domain/src/main/java/fr/benju/tasks/domain/model/Task.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/model/Task.kt similarity index 66% rename from domain/src/main/java/fr/benju/tasks/domain/model/Task.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/model/Task.kt index a2a0a24..2f12db6 100644 --- a/domain/src/main/java/fr/benju/tasks/domain/model/Task.kt +++ b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/model/Task.kt @@ -1,10 +1,12 @@ package fr.benju.tasks.domain.model +import kotlinx.datetime.Clock + data class Task( val id: Long = 0, val title: String, val description: String, val priority: Priority, val isCompleted: Boolean = false, - val createdAt: Long = System.currentTimeMillis(), + val createdAt: Long = Clock.System.now().toEpochMilliseconds(), ) diff --git a/domain/src/main/java/fr/benju/tasks/domain/model/TaskFilter.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/model/TaskFilter.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/model/TaskFilter.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/model/TaskFilter.kt diff --git a/domain/src/main/java/fr/benju/tasks/domain/repository/TaskRepository.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/repository/TaskRepository.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/repository/TaskRepository.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/repository/TaskRepository.kt diff --git a/domain/src/main/java/fr/benju/tasks/domain/repository/UserPreferencesRepository.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/repository/UserPreferencesRepository.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/repository/UserPreferencesRepository.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/repository/UserPreferencesRepository.kt diff --git a/domain/src/main/java/fr/benju/tasks/domain/usecase/AddTaskUseCase.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/AddTaskUseCase.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/usecase/AddTaskUseCase.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/AddTaskUseCase.kt diff --git a/domain/src/main/java/fr/benju/tasks/domain/usecase/DeleteTaskUseCase.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/DeleteTaskUseCase.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/usecase/DeleteTaskUseCase.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/DeleteTaskUseCase.kt diff --git a/domain/src/main/java/fr/benju/tasks/domain/usecase/GetDarkModeUseCase.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/GetDarkModeUseCase.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/usecase/GetDarkModeUseCase.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/GetDarkModeUseCase.kt diff --git a/domain/src/main/java/fr/benju/tasks/domain/usecase/GetTaskByIdUseCase.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/GetTaskByIdUseCase.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/usecase/GetTaskByIdUseCase.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/GetTaskByIdUseCase.kt diff --git a/domain/src/main/java/fr/benju/tasks/domain/usecase/GetTasksUseCase.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/GetTasksUseCase.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/usecase/GetTasksUseCase.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/GetTasksUseCase.kt diff --git a/domain/src/main/java/fr/benju/tasks/domain/usecase/SetDarkModeUseCase.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/SetDarkModeUseCase.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/usecase/SetDarkModeUseCase.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/SetDarkModeUseCase.kt diff --git a/domain/src/main/java/fr/benju/tasks/domain/usecase/ToggleTaskStatusUseCase.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/ToggleTaskStatusUseCase.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/usecase/ToggleTaskStatusUseCase.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/ToggleTaskStatusUseCase.kt diff --git a/domain/src/main/java/fr/benju/tasks/domain/usecase/UpdateTaskUseCase.kt b/domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/UpdateTaskUseCase.kt similarity index 100% rename from domain/src/main/java/fr/benju/tasks/domain/usecase/UpdateTaskUseCase.kt rename to domain/src/commonMain/kotlin/fr/benju/tasks/domain/usecase/UpdateTaskUseCase.kt diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 6dd9e0c..234c9c3 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -1,77 +1,62 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.multiplatform) alias(libs.plugins.kotlin.compose) - alias(libs.plugins.ksp) - alias(libs.plugins.hilt) +} + +kotlin { + androidTarget() + jvm() + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(project(":domain")) + implementation(project(":core")) + implementation(project(":ui")) + + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(libs.compose.material.icons.extended) + + // Koin + implementation(libs.koin.core) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) + + // Lifecycle ViewModel + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) + + // Coroutines + implementation(libs.kotlinx.coroutines.core) + } + androidMain.dependencies { + implementation(libs.androidx.core.ktx) + } + commonTest.dependencies { + implementation(libs.junit.jupiter.api) + implementation(libs.mockk) + implementation(libs.kluent) + implementation(libs.turbine) + implementation(libs.kotlinx.coroutines.test) + implementation(project(":core")) + } + } } android { namespace = "fr.benju.tasks.feature.settings" compileSdk = 36 - - defaultConfig { - minSdk = 26 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - + defaultConfig { minSdk = 26 } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - - buildFeatures { - compose = true - } -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) - freeCompilerArgs.addAll( - "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api" - ) - } -} - -dependencies { - implementation(project(":domain")) - implementation(project(":core")) - implementation(project(":ui")) - - // Core - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.viewmodel.ktx) - implementation(libs.androidx.lifecycle.runtime.compose) - - // Compose - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.compose.material3) - - // Hilt - implementation(libs.hilt.android) - ksp(libs.hilt.compiler) - implementation(libs.hilt.navigation.compose) - - // Coroutines - implementation(libs.kotlinx.coroutines.android) - - // Testing - testImplementation(libs.junit.jupiter.api) - testRuntimeOnly(libs.junit.jupiter.engine) - testRuntimeOnly(libs.junit.platform.launcher) - testImplementation(libs.mockk) - testImplementation(libs.kluent) - testImplementation(libs.turbine) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(project(":core")) -} - -tasks.withType { - useJUnitPlatform() + buildFeatures { compose = true } } diff --git a/feature/settings/src/main/AndroidManifest.xml b/feature/settings/src/androidMain/AndroidManifest.xml similarity index 100% rename from feature/settings/src/main/AndroidManifest.xml rename to feature/settings/src/androidMain/AndroidManifest.xml diff --git a/feature/settings/src/main/java/fr/benju/tasks/feature/settings/SettingsScreen.kt b/feature/settings/src/commonMain/kotlin/fr/benju/tasks/feature/settings/SettingsScreen.kt similarity index 76% rename from feature/settings/src/main/java/fr/benju/tasks/feature/settings/SettingsScreen.kt rename to feature/settings/src/commonMain/kotlin/fr/benju/tasks/feature/settings/SettingsScreen.kt index aae1f4b..4fe2091 100644 --- a/feature/settings/src/main/java/fr/benju/tasks/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/fr/benju/tasks/feature/settings/SettingsScreen.kt @@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material3.DividerDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider @@ -22,10 +24,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import fr.benju.tasks.ui.Res +import fr.benju.tasks.ui.cd_back +import fr.benju.tasks.ui.settings_dark_mode +import fr.benju.tasks.ui.settings_title +import fr.benju.tasks.ui.settings_version +import org.jetbrains.compose.resources.stringResource @Composable fun SettingsScreen( @@ -38,11 +44,13 @@ fun SettingsScreen( Scaffold( topBar = { TopAppBar( - title = { Text(stringResource(R.string.settings_title)) }, + title = { Text(stringResource(Res.string.settings_title)) }, navigationIcon = { IconButton(onClick = { onBack() }) { - val icon = painterResource(R.drawable.ic_arrow_back_rounded_24dp) - Icon(icon, contentDescription = stringResource(R.string.cd_back)) + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(Res.string.cd_back) + ) } } ) @@ -60,7 +68,7 @@ fun SettingsScreen( verticalAlignment = Alignment.CenterVertically ) { Text( - text = stringResource(R.string.settings_dark_mode), + text = stringResource(Res.string.settings_dark_mode), style = MaterialTheme.typography.titleMedium ) @@ -77,7 +85,7 @@ fun SettingsScreen( ) Text( - text = stringResource(R.string.settings_version), + text = stringResource(Res.string.settings_version), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/feature/settings/src/main/java/fr/benju/tasks/feature/settings/SettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/fr/benju/tasks/feature/settings/SettingsViewModel.kt similarity index 87% rename from feature/settings/src/main/java/fr/benju/tasks/feature/settings/SettingsViewModel.kt rename to feature/settings/src/commonMain/kotlin/fr/benju/tasks/feature/settings/SettingsViewModel.kt index 957016c..d678b06 100644 --- a/feature/settings/src/main/java/fr/benju/tasks/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/fr/benju/tasks/feature/settings/SettingsViewModel.kt @@ -2,17 +2,14 @@ package fr.benju.tasks.feature.settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import fr.benju.tasks.domain.usecase.GetDarkModeUseCase import fr.benju.tasks.domain.usecase.SetDarkModeUseCase import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class SettingsViewModel @Inject constructor( +class SettingsViewModel( private val getDarkModeUseCase: GetDarkModeUseCase, private val setDarkModeUseCase: SetDarkModeUseCase ) : ViewModel() { diff --git a/feature/settings/src/commonMain/kotlin/fr/benju/tasks/feature/settings/di/SettingsModule.kt b/feature/settings/src/commonMain/kotlin/fr/benju/tasks/feature/settings/di/SettingsModule.kt new file mode 100644 index 0000000..53cabfa --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/fr/benju/tasks/feature/settings/di/SettingsModule.kt @@ -0,0 +1,9 @@ +package fr.benju.tasks.feature.settings.di + +import fr.benju.tasks.feature.settings.SettingsViewModel +import org.koin.compose.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +val settingsModule = module { + viewModelOf(::SettingsViewModel) +} diff --git a/feature/settings/src/main/res/drawable/ic_arrow_back_rounded_24dp.xml b/feature/settings/src/main/res/drawable/ic_arrow_back_rounded_24dp.xml deleted file mode 100644 index 70aee9c..0000000 --- a/feature/settings/src/main/res/drawable/ic_arrow_back_rounded_24dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/feature/settings/src/main/res/values-de/strings.xml b/feature/settings/src/main/res/values-de/strings.xml deleted file mode 100644 index f06be5e..0000000 --- a/feature/settings/src/main/res/values-de/strings.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Einstellungen - Dunkelmodus - Version 1.0.0 - Zurück - - diff --git a/feature/settings/src/main/res/values-es/strings.xml b/feature/settings/src/main/res/values-es/strings.xml deleted file mode 100644 index 4ea8125..0000000 --- a/feature/settings/src/main/res/values-es/strings.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Ajustes - Modo oscuro - Versión 1.0.0 - Atrás - - diff --git a/feature/settings/src/main/res/values-fr/strings.xml b/feature/settings/src/main/res/values-fr/strings.xml deleted file mode 100644 index af24c3f..0000000 --- a/feature/settings/src/main/res/values-fr/strings.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Paramètres - Mode sombre - Version 1.0.0 - Retour - - diff --git a/feature/settings/src/main/res/values-it/strings.xml b/feature/settings/src/main/res/values-it/strings.xml deleted file mode 100644 index aca6372..0000000 --- a/feature/settings/src/main/res/values-it/strings.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Impostazioni - Modalità scura - Versione 1.0.0 - Indietro - - diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml deleted file mode 100644 index cb79452..0000000 --- a/feature/settings/src/main/res/values/strings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Settings - Dark Mode - Version 1.0.0 - - - Back - - diff --git a/feature/taskeditor/build.gradle.kts b/feature/taskeditor/build.gradle.kts index 8ab5340..fe0857f 100644 --- a/feature/taskeditor/build.gradle.kts +++ b/feature/taskeditor/build.gradle.kts @@ -1,77 +1,61 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.multiplatform) alias(libs.plugins.kotlin.compose) - alias(libs.plugins.ksp) - alias(libs.plugins.hilt) +} + +kotlin { + androidTarget() + jvm() + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(project(":domain")) + implementation(project(":core")) + implementation(project(":ui")) + + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + + // Koin + implementation(libs.koin.core) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) + + // Lifecycle ViewModel + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) + + // Coroutines + implementation(libs.kotlinx.coroutines.core) + } + androidMain.dependencies { + implementation(libs.androidx.core.ktx) + } + commonTest.dependencies { + implementation(libs.junit.jupiter.api) + implementation(libs.mockk) + implementation(libs.kluent) + implementation(libs.turbine) + implementation(libs.kotlinx.coroutines.test) + implementation(project(":core")) + } + } } android { namespace = "fr.benju.tasks.feature.taskeditor" compileSdk = 36 - - defaultConfig { - minSdk = 26 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - + defaultConfig { minSdk = 26 } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - - buildFeatures { - compose = true - } -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) - freeCompilerArgs.addAll( - "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api" - ) - } -} - -dependencies { - implementation(project(":domain")) - implementation(project(":core")) - implementation(project(":ui")) - - // Core - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.viewmodel.ktx) - implementation(libs.androidx.lifecycle.runtime.compose) - - // Compose - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.compose.material3) - - // Hilt - implementation(libs.hilt.android) - ksp(libs.hilt.compiler) - implementation(libs.hilt.navigation.compose) - - // Coroutines - implementation(libs.kotlinx.coroutines.android) - - // Testing - testImplementation(libs.junit.jupiter.api) - testRuntimeOnly(libs.junit.jupiter.engine) - testRuntimeOnly(libs.junit.platform.launcher) - testImplementation(libs.mockk) - testImplementation(libs.kluent) - testImplementation(libs.turbine) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(project(":core")) -} - -tasks.withType { - useJUnitPlatform() + buildFeatures { compose = true } } diff --git a/feature/taskeditor/src/main/AndroidManifest.xml b/feature/taskeditor/src/androidMain/AndroidManifest.xml similarity index 100% rename from feature/taskeditor/src/main/AndroidManifest.xml rename to feature/taskeditor/src/androidMain/AndroidManifest.xml diff --git a/feature/taskeditor/src/main/java/fr/benju/tasks/feature/taskeditor/TaskEditorScreen.kt b/feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/TaskEditorScreen.kt similarity index 59% rename from feature/taskeditor/src/main/java/fr/benju/tasks/feature/taskeditor/TaskEditorScreen.kt rename to feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/TaskEditorScreen.kt index 0200010..8230a52 100644 --- a/feature/taskeditor/src/main/java/fr/benju/tasks/feature/taskeditor/TaskEditorScreen.kt +++ b/feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/TaskEditorScreen.kt @@ -2,17 +2,45 @@ package fr.benju.tasks.feature.taskeditor -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import fr.benju.tasks.domain.model.Priority +import fr.benju.tasks.ui.Res +import fr.benju.tasks.ui.priority_high +import fr.benju.tasks.ui.priority_low +import fr.benju.tasks.ui.priority_medium +import fr.benju.tasks.ui.task_editor_cancel +import fr.benju.tasks.ui.task_editor_field_description +import fr.benju.tasks.ui.task_editor_field_priority +import fr.benju.tasks.ui.task_editor_field_title +import fr.benju.tasks.ui.task_editor_save +import fr.benju.tasks.ui.task_editor_title_edit +import fr.benju.tasks.ui.task_editor_title_new +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel @Composable fun TaskEditorScreen( @@ -20,7 +48,7 @@ fun TaskEditorScreen( onTaskSaved: () -> Unit, onDismiss: () -> Unit, taskId: Long? = null, - viewModel: TaskEditorViewModel = hiltViewModel() + viewModel: TaskEditorViewModel = koinViewModel() ) { val viewState by viewModel.viewState.collectAsStateWithLifecycle() val currentOnTaskSaved by rememberUpdatedState(onTaskSaved) @@ -33,7 +61,7 @@ fun TaskEditorScreen( try { titleFocusRequester.requestFocus() } catch (e: IllegalStateException) { - // Ignore focus request failures, e.g., if the view is not yet attached + // Ignore focus request failures } } } @@ -49,13 +77,13 @@ fun TaskEditorScreen( CenterAlignedTopAppBar( title = { Text( - if (taskId != null) stringResource(R.string.task_editor_title_edit) - else stringResource(R.string.task_editor_title_new) + if (taskId != null) stringResource(Res.string.task_editor_title_edit) + else stringResource(Res.string.task_editor_title_new) ) }, navigationIcon = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.task_editor_cancel)) + Text(stringResource(Res.string.task_editor_cancel)) } }, actions = { @@ -63,7 +91,7 @@ fun TaskEditorScreen( onClick = { viewModel.saveTask() }, enabled = !viewState.isSaving ) { - Text(stringResource(R.string.task_editor_save)) + Text(stringResource(Res.string.task_editor_save)) } } ) @@ -79,7 +107,7 @@ fun TaskEditorScreen( OutlinedTextField( value = viewState.title, onValueChange = { viewModel.updateTitle(it) }, - label = { Text(stringResource(R.string.task_editor_field_title)) }, + label = { Text(stringResource(Res.string.task_editor_field_title)) }, modifier = Modifier .fillMaxWidth() .focusRequester(titleFocusRequester), @@ -89,13 +117,13 @@ fun TaskEditorScreen( OutlinedTextField( value = viewState.description, onValueChange = { viewModel.updateDescription(it) }, - label = { Text(stringResource(R.string.task_editor_field_description)) }, + label = { Text(stringResource(Res.string.task_editor_field_description)) }, modifier = Modifier.fillMaxWidth(), minLines = 3, maxLines = 5 ) - Text(stringResource(R.string.task_editor_field_priority), style = MaterialTheme.typography.titleMedium) + Text(stringResource(Res.string.task_editor_field_priority), style = MaterialTheme.typography.titleMedium) Row( modifier = Modifier.fillMaxWidth(), @@ -107,9 +135,9 @@ fun TaskEditorScreen( onClick = { viewModel.updatePriority(priority) }, label = { val label = when (priority) { - Priority.LOW -> stringResource(R.string.priority_low) - Priority.MEDIUM -> stringResource(R.string.priority_medium) - Priority.HIGH -> stringResource(R.string.priority_high) + Priority.LOW -> stringResource(Res.string.priority_low) + Priority.MEDIUM -> stringResource(Res.string.priority_medium) + Priority.HIGH -> stringResource(Res.string.priority_high) } Text(label) } @@ -117,9 +145,9 @@ fun TaskEditorScreen( } } - viewState.error?.let { errorRes -> + viewState.error?.let { errorMessage -> Text( - text = stringResource(errorRes), + text = errorMessage, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall ) diff --git a/feature/taskeditor/src/main/java/fr/benju/tasks/feature/taskeditor/TaskEditorViewModel.kt b/feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/TaskEditorViewModel.kt similarity index 90% rename from feature/taskeditor/src/main/java/fr/benju/tasks/feature/taskeditor/TaskEditorViewModel.kt rename to feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/TaskEditorViewModel.kt index 16478db..eba653e 100644 --- a/feature/taskeditor/src/main/java/fr/benju/tasks/feature/taskeditor/TaskEditorViewModel.kt +++ b/feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/TaskEditorViewModel.kt @@ -8,16 +8,13 @@ import fr.benju.tasks.domain.model.Task import fr.benju.tasks.domain.usecase.AddTaskUseCase import fr.benju.tasks.domain.usecase.GetTaskByIdUseCase import fr.benju.tasks.domain.usecase.UpdateTaskUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class TaskEditorViewModel @Inject constructor( +class TaskEditorViewModel( private val addTaskUseCase: AddTaskUseCase, private val updateTaskUseCase: UpdateTaskUseCase, private val getTaskByIdUseCase: GetTaskByIdUseCase, @@ -57,7 +54,7 @@ class TaskEditorViewModel @Inject constructor( fun saveTask() { val state = _viewState.value if (state.title.isBlank()) { - _viewState.value = state.copy(error = R.string.task_editor_error_title_required) + _viewState.value = state.copy(error = "Title is required") return } @@ -84,7 +81,7 @@ class TaskEditorViewModel @Inject constructor( onFailure = { _viewState.value = _viewState.value.copy( isSaving = false, - error = R.string.task_editor_error_save_failed + error = "Failed to save the task. Please try again." ) } ) diff --git a/feature/taskeditor/src/main/java/fr/benju/tasks/feature/taskeditor/TaskEditorViewState.kt b/feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/TaskEditorViewState.kt similarity index 79% rename from feature/taskeditor/src/main/java/fr/benju/tasks/feature/taskeditor/TaskEditorViewState.kt rename to feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/TaskEditorViewState.kt index 175ade1..528d8a5 100644 --- a/feature/taskeditor/src/main/java/fr/benju/tasks/feature/taskeditor/TaskEditorViewState.kt +++ b/feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/TaskEditorViewState.kt @@ -1,6 +1,5 @@ package fr.benju.tasks.feature.taskeditor -import androidx.annotation.StringRes import fr.benju.tasks.domain.model.Priority data class TaskEditorViewState( @@ -9,5 +8,5 @@ data class TaskEditorViewState( val description: String = "", val priority: Priority = Priority.MEDIUM, val isSaving: Boolean = false, - @StringRes val error: Int? = null + val error: String? = null ) diff --git a/feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/di/TaskEditorModule.kt b/feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/di/TaskEditorModule.kt new file mode 100644 index 0000000..8f4f5c2 --- /dev/null +++ b/feature/taskeditor/src/commonMain/kotlin/fr/benju/tasks/feature/taskeditor/di/TaskEditorModule.kt @@ -0,0 +1,9 @@ +package fr.benju.tasks.feature.taskeditor.di + +import fr.benju.tasks.feature.taskeditor.TaskEditorViewModel +import org.koin.compose.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +val taskEditorModule = module { + viewModelOf(::TaskEditorViewModel) +} diff --git a/feature/taskeditor/src/main/res/values-de/strings.xml b/feature/taskeditor/src/main/res/values-de/strings.xml deleted file mode 100644 index c127626..0000000 --- a/feature/taskeditor/src/main/res/values-de/strings.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - Neue Aufgabe - Aufgabe bearbeiten - Abbrechen - Speichern - Titel - Beschreibung - Priorität - Titel ist erforderlich - Aufgabe konnte nicht gespeichert werden. Bitte erneut versuchen. - Niedrig - Mittel - Hoch - - diff --git a/feature/taskeditor/src/main/res/values-es/strings.xml b/feature/taskeditor/src/main/res/values-es/strings.xml deleted file mode 100644 index 86a75d1..0000000 --- a/feature/taskeditor/src/main/res/values-es/strings.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - Nueva tarea - Editar tarea - Cancelar - Guardar - Título - Descripción - Prioridad - El título es obligatorio - No se pudo guardar la tarea. Por favor, inténtalo de nuevo. - Baja - Media - Alta - - diff --git a/feature/taskeditor/src/main/res/values-fr/strings.xml b/feature/taskeditor/src/main/res/values-fr/strings.xml deleted file mode 100644 index 924f425..0000000 --- a/feature/taskeditor/src/main/res/values-fr/strings.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - Nouvelle tâche - Modifier la tâche - Annuler - Enregistrer - Titre - Description - Priorité - Le titre est obligatoire - Impossible d\'enregistrer la tâche. Veuillez réessayer. - Faible - Moyenne - Haute - - diff --git a/feature/taskeditor/src/main/res/values-it/strings.xml b/feature/taskeditor/src/main/res/values-it/strings.xml deleted file mode 100644 index 4899c5a..0000000 --- a/feature/taskeditor/src/main/res/values-it/strings.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - Nuova attività - Modifica attività - Annulla - Salva - Titolo - Descrizione - Priorità - Il titolo è obbligatorio - Impossibile salvare l\'attività. Riprova. - Bassa - Media - Alta - - diff --git a/feature/taskeditor/src/main/res/values/strings.xml b/feature/taskeditor/src/main/res/values/strings.xml deleted file mode 100644 index c5dc098..0000000 --- a/feature/taskeditor/src/main/res/values/strings.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - New Task - Edit Task - Cancel - Save - Title - Description - Priority - Title is required - Failed to save the task. Please try again. - - - Low - Medium - High - - diff --git a/feature/tasklist/build.gradle.kts b/feature/tasklist/build.gradle.kts index 8a21683..9ae6594 100644 --- a/feature/tasklist/build.gradle.kts +++ b/feature/tasklist/build.gradle.kts @@ -1,77 +1,62 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.multiplatform) alias(libs.plugins.kotlin.compose) - alias(libs.plugins.ksp) - alias(libs.plugins.hilt) +} + +kotlin { + androidTarget() + jvm() + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(project(":domain")) + implementation(project(":core")) + implementation(project(":ui")) + + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(libs.compose.material.icons.extended) + + // Koin + implementation(libs.koin.core) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) + + // Lifecycle ViewModel + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) + + // Coroutines + implementation(libs.kotlinx.coroutines.core) + } + androidMain.dependencies { + implementation(libs.androidx.core.ktx) + } + commonTest.dependencies { + implementation(libs.junit.jupiter.api) + implementation(libs.mockk) + implementation(libs.kluent) + implementation(libs.turbine) + implementation(libs.kotlinx.coroutines.test) + implementation(project(":core")) + } + } } android { namespace = "fr.benju.tasks.feature.tasklist" compileSdk = 36 - - defaultConfig { - minSdk = 26 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - + defaultConfig { minSdk = 26 } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - - buildFeatures { - compose = true - } -} -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) - freeCompilerArgs.addAll( - "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api" - ) - } -} - -dependencies { - implementation(project(":domain")) - implementation(project(":core")) - implementation(project(":ui")) - - // Core - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.viewmodel.ktx) - implementation(libs.androidx.lifecycle.runtime.compose) - - // Compose - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.navigation.compose) - - // Hilt - implementation(libs.hilt.android) - ksp(libs.hilt.compiler) - implementation(libs.hilt.navigation.compose) - - // Coroutines - implementation(libs.kotlinx.coroutines.android) - - // Testing - testImplementation(libs.junit.jupiter.api) - testRuntimeOnly(libs.junit.jupiter.engine) - testRuntimeOnly(libs.junit.platform.launcher) - testImplementation(libs.mockk) - testImplementation(libs.kluent) - testImplementation(libs.turbine) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(project(":core")) -} - -tasks.withType { - useJUnitPlatform() + buildFeatures { compose = true } } diff --git a/feature/tasklist/src/main/AndroidManifest.xml b/feature/tasklist/src/androidMain/AndroidManifest.xml similarity index 100% rename from feature/tasklist/src/main/AndroidManifest.xml rename to feature/tasklist/src/androidMain/AndroidManifest.xml diff --git a/feature/tasklist/src/main/java/fr/benju/tasks/feature/tasklist/TaskListScreen.kt b/feature/tasklist/src/commonMain/kotlin/fr/benju/tasks/feature/tasklist/TaskListScreen.kt similarity index 56% rename from feature/tasklist/src/main/java/fr/benju/tasks/feature/tasklist/TaskListScreen.kt rename to feature/tasklist/src/commonMain/kotlin/fr/benju/tasks/feature/tasklist/TaskListScreen.kt index 80593a9..fbb0cf4 100644 --- a/feature/tasklist/src/main/java/fr/benju/tasks/feature/tasklist/TaskListScreen.kt +++ b/feature/tasklist/src/commonMain/kotlin/fr/benju/tasks/feature/tasklist/TaskListScreen.kt @@ -10,6 +10,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Settings import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.FloatingActionButton @@ -18,7 +21,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -28,17 +30,20 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import fr.benju.tasks.domain.model.Priority -import fr.benju.tasks.domain.model.Task import fr.benju.tasks.domain.model.TaskFilter +import fr.benju.tasks.ui.Res +import fr.benju.tasks.ui.app_name +import fr.benju.tasks.ui.cd_add_task +import fr.benju.tasks.ui.cd_settings +import fr.benju.tasks.ui.task_list_empty +import fr.benju.tasks.ui.task_list_filter_active +import fr.benju.tasks.ui.task_list_filter_all +import fr.benju.tasks.ui.task_list_filter_completed import fr.benju.tasks.ui.components.TaskCard -import fr.benju.tasks.ui.theme.TaskManagerTheme +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -47,7 +52,7 @@ fun TaskListScreen( onTaskClick: (Long) -> Unit, onSettingsClick: () -> Unit, modifier: Modifier = Modifier, - viewModel: TaskListViewModel = hiltViewModel() + viewModel: TaskListViewModel = koinViewModel() ) { val viewState by viewModel.viewState.collectAsStateWithLifecycle() @@ -91,19 +96,23 @@ internal fun TaskListContent( snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { TopAppBar( - title = { Text(stringResource(R.string.app_name)) }, + title = { Text(stringResource(Res.string.app_name)) }, actions = { IconButton(onClick = onSettingsClick) { - val icon = painterResource(R.drawable.ic_settings_rounded_24dp) - Icon(icon, contentDescription = stringResource(R.string.cd_settings)) + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = stringResource(Res.string.cd_settings) + ) } } ) }, floatingActionButton = { FloatingActionButton(onClick = onAddTaskClick) { - val icon = painterResource(R.drawable.ic_add_task_rounded_24dp) - Icon(icon, contentDescription = stringResource(R.string.cd_add_task)) + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(Res.string.cd_add_task) + ) } } ) { paddingValues -> @@ -112,7 +121,6 @@ internal fun TaskListContent( .fillMaxSize() .padding(paddingValues) ) { - // Filter chips Row( modifier = Modifier .fillMaxWidth() @@ -122,27 +130,26 @@ internal fun TaskListContent( FilterChip( selected = viewState.filter == TaskFilter.ALL, onClick = { onFilterSelected(TaskFilter.ALL) }, - label = { Text(stringResource(R.string.task_list_filter_all)) } + label = { Text(stringResource(Res.string.task_list_filter_all)) } ) FilterChip( selected = viewState.filter == TaskFilter.ACTIVE, onClick = { onFilterSelected(TaskFilter.ACTIVE) }, - label = { Text(stringResource(R.string.task_list_filter_active)) } + label = { Text(stringResource(Res.string.task_list_filter_active)) } ) FilterChip( selected = viewState.filter == TaskFilter.COMPLETED, onClick = { onFilterSelected(TaskFilter.COMPLETED) }, - label = { Text(stringResource(R.string.task_list_filter_completed)) } + label = { Text(stringResource(Res.string.task_list_filter_completed)) } ) } - // Task list if (viewState.tasks.isEmpty() && !viewState.isLoading) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - Text(stringResource(R.string.task_list_empty)) + Text(stringResource(Res.string.task_list_empty)) } } else { LazyColumn( @@ -163,69 +170,3 @@ internal fun TaskListContent( } } } - -// ── Preview data ────────────────────────────────────────────────────────────── - -private val previewTasks = listOf( - Task(id = 1, title = "Grocery shopping", description = "Buy fruits, vegetables and milk", priority = Priority.HIGH, isCompleted = true), - Task(id = 2, title = "Call mom", description = "Catch up and schedule next family dinner", priority = Priority.MEDIUM), - Task(id = 3, title = "Finish the project", description = "Complete the final report and send it to the team", priority = Priority.HIGH), - Task(id = 4, title = "Read a book", description = "Continue reading \"Atomic Habits\" — chapter 5", priority = Priority.LOW), - Task(id = 5, title = "Clean the house", description = "Vacuum the living room and clean the bathroom", priority = Priority.MEDIUM), -) - -// ── Previews ────────────────────────────────────────────────────────────────── - -@Preview(name = "Task List – Light", showBackground = true, showSystemUi = true) -@Composable -private fun TaskListContentLightPreview() { - TaskManagerTheme(darkTheme = false) { - TaskListContent( - viewState = TaskListViewState(tasks = previewTasks, isLoading = false), - onAddTaskClick = {}, - onTaskClick = {}, - onSettingsClick = {}, - onFilterSelected = {}, - onToggleComplete = {}, - onDeleteTask = {}, - onErrorShown = {}, - ) - } -} - -@Preview(name = "Task List – Dark", showBackground = true, showSystemUi = true) -@Composable -private fun TaskListContentDarkPreview() { - TaskManagerTheme(darkTheme = true) { - Surface { - TaskListContent( - viewState = TaskListViewState(tasks = previewTasks, isLoading = false), - onAddTaskClick = {}, - onTaskClick = {}, - onSettingsClick = {}, - onFilterSelected = {}, - onToggleComplete = {}, - onDeleteTask = {}, - onErrorShown = {}, - ) - } - } -} - -@Preview(name = "Task List – Empty", showBackground = true, showSystemUi = true) -@Composable -private fun TaskListContentEmptyPreview() { - TaskManagerTheme(darkTheme = false) { - TaskListContent( - viewState = TaskListViewState(tasks = emptyList(), isLoading = false), - onAddTaskClick = {}, - onTaskClick = {}, - onSettingsClick = {}, - onFilterSelected = {}, - onToggleComplete = {}, - onDeleteTask = {}, - onErrorShown = {}, - ) - } -} - diff --git a/feature/tasklist/src/main/java/fr/benju/tasks/feature/tasklist/TaskListViewModel.kt b/feature/tasklist/src/commonMain/kotlin/fr/benju/tasks/feature/tasklist/TaskListViewModel.kt similarity index 87% rename from feature/tasklist/src/main/java/fr/benju/tasks/feature/tasklist/TaskListViewModel.kt rename to feature/tasklist/src/commonMain/kotlin/fr/benju/tasks/feature/tasklist/TaskListViewModel.kt index 74e88e9..093fe59 100644 --- a/feature/tasklist/src/main/java/fr/benju/tasks/feature/tasklist/TaskListViewModel.kt +++ b/feature/tasklist/src/commonMain/kotlin/fr/benju/tasks/feature/tasklist/TaskListViewModel.kt @@ -6,17 +6,14 @@ import fr.benju.tasks.domain.model.TaskFilter import fr.benju.tasks.domain.usecase.DeleteTaskUseCase import fr.benju.tasks.domain.usecase.GetTasksUseCase import fr.benju.tasks.domain.usecase.ToggleTaskStatusUseCase -import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch -import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel -class TaskListViewModel @Inject constructor( +class TaskListViewModel( private val getTasksUseCase: GetTasksUseCase, private val toggleTaskStatusUseCase: ToggleTaskStatusUseCase, private val deleteTaskUseCase: DeleteTaskUseCase @@ -25,8 +22,6 @@ class TaskListViewModel @Inject constructor( private val _viewState = MutableStateFlow(TaskListViewState()) val viewState: StateFlow = _viewState - // Un seul StateFlow de filtre : toute modification relance automatiquement - // flatMapLatest qui annule le flux précédent avant d'en démarrer un nouveau. private val _filter = MutableStateFlow(TaskFilter.ALL) init { @@ -69,5 +64,4 @@ class TaskListViewModel @Inject constructor( fun clearError() { _viewState.value = _viewState.value.copy(error = null) } - } diff --git a/feature/tasklist/src/main/java/fr/benju/tasks/feature/tasklist/TaskListViewState.kt b/feature/tasklist/src/commonMain/kotlin/fr/benju/tasks/feature/tasklist/TaskListViewState.kt similarity index 100% rename from feature/tasklist/src/main/java/fr/benju/tasks/feature/tasklist/TaskListViewState.kt rename to feature/tasklist/src/commonMain/kotlin/fr/benju/tasks/feature/tasklist/TaskListViewState.kt diff --git a/feature/tasklist/src/commonMain/kotlin/fr/benju/tasks/feature/tasklist/di/TaskListModule.kt b/feature/tasklist/src/commonMain/kotlin/fr/benju/tasks/feature/tasklist/di/TaskListModule.kt new file mode 100644 index 0000000..1169bde --- /dev/null +++ b/feature/tasklist/src/commonMain/kotlin/fr/benju/tasks/feature/tasklist/di/TaskListModule.kt @@ -0,0 +1,9 @@ +package fr.benju.tasks.feature.tasklist.di + +import fr.benju.tasks.feature.tasklist.TaskListViewModel +import org.koin.compose.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +val taskListModule = module { + viewModelOf(::TaskListViewModel) +} diff --git a/feature/tasklist/src/main/res/drawable/ic_add_task_rounded_24dp.xml b/feature/tasklist/src/main/res/drawable/ic_add_task_rounded_24dp.xml deleted file mode 100644 index f047a67..0000000 --- a/feature/tasklist/src/main/res/drawable/ic_add_task_rounded_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/feature/tasklist/src/main/res/drawable/ic_settings_rounded_24dp.xml b/feature/tasklist/src/main/res/drawable/ic_settings_rounded_24dp.xml deleted file mode 100644 index b04dbed..0000000 --- a/feature/tasklist/src/main/res/drawable/ic_settings_rounded_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/feature/tasklist/src/main/res/values-de/strings.xml b/feature/tasklist/src/main/res/values-de/strings.xml deleted file mode 100644 index cd5bbbf..0000000 --- a/feature/tasklist/src/main/res/values-de/strings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - Simple Tasks - Alle - Aktive - Erledigt - Noch keine Aufgaben - Einstellungen - Aufgabe hinzufügen - - diff --git a/feature/tasklist/src/main/res/values-es/strings.xml b/feature/tasklist/src/main/res/values-es/strings.xml deleted file mode 100644 index c01bada..0000000 --- a/feature/tasklist/src/main/res/values-es/strings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - Simple Tasks - Todas - Activas - Completadas - Aún no hay tareas - Ajustes - Añadir tarea - - diff --git a/feature/tasklist/src/main/res/values-fr/strings.xml b/feature/tasklist/src/main/res/values-fr/strings.xml deleted file mode 100644 index de8a4cc..0000000 --- a/feature/tasklist/src/main/res/values-fr/strings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - Simple Tasks - Toutes - Actives - Terminées - Aucune tâche pour l\'instant - Paramètres - Ajouter une tâche - - diff --git a/feature/tasklist/src/main/res/values-it/strings.xml b/feature/tasklist/src/main/res/values-it/strings.xml deleted file mode 100644 index 37a1f4c..0000000 --- a/feature/tasklist/src/main/res/values-it/strings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - Simple Tasks - Tutte - Attive - Completate - Nessuna attività - Impostazioni - Aggiungi attività - - diff --git a/feature/tasklist/src/main/res/values/strings.xml b/feature/tasklist/src/main/res/values/strings.xml deleted file mode 100644 index edfff5a..0000000 --- a/feature/tasklist/src/main/res/values/strings.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - Simple Tasks - - - All - Active - Completed - No tasks yet - - - Settings - Add Task - - diff --git a/gradle.properties b/gradle.properties index f60ae32..6e99aa1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,4 +23,4 @@ android.builtInKotlin=false android.newDsl=false # Compose -org.jetbrains.compose.experimental.uikit.enabled=false +org.jetbrains.compose.experimental.uikit.enabled=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5289d23..fdecf7e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,12 @@ coroutines = "1.10.2" # Navigation navigationCompose = "2.9.7" +composeMultiplatform = "1.7.3" +koin = "4.0.0" +sqldelight = "2.0.2" +multiplatformSettings = "1.2.0" +kotlinxDatetime = "0.6.1" + # Testing junit = "6.0.3" mockk = "1.14.9" @@ -86,6 +92,29 @@ turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } room-testing = { module = "androidx.room:room-testing", version.ref = "room" } +# Koin +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } + +# SQLDelight +sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" } +sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } +sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } +sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" } +sqldelight-sqlite-driver = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } + +# Multiplatform Settings +multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } +multiplatform-settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatformSettings" } + +# DateTime +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } + +# Compose Multiplatform Material Icons Extended +compose-material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "composeMultiplatform" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } @@ -94,3 +123,6 @@ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } +sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } diff --git a/iosApp/README.md b/iosApp/README.md new file mode 100644 index 0000000..b048134 --- /dev/null +++ b/iosApp/README.md @@ -0,0 +1,20 @@ +# iOS App + +This directory contains the iOS application entry point for the KMP Task Manager. + +## Setup + +1. Build the KMP shared framework: + ``` + ./gradlew embedAndSignAppleFrameworkForXcode + ``` + +2. Open `iosApp.xcodeproj` in Xcode + +3. The app uses the shared KMP framework built from the Kotlin modules + +## Architecture + +The iOS app uses Compose Multiplatform through a `UIViewController` bridge: +- Swift entry point: `iOSApp.swift` +- Bridge: `ContentView.swift` → `MainViewControllerKt.MainViewController()` diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift new file mode 100644 index 0000000..8261ab3 --- /dev/null +++ b/iosApp/iosApp/ContentView.swift @@ -0,0 +1,10 @@ +import SwiftUI +import ComposeApp + +struct ContentView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + return MainViewControllerKt.MainViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift new file mode 100644 index 0000000..927e0b9 --- /dev/null +++ b/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 1e18ff8..9786466 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,3 +27,4 @@ include(":feature:taskeditor") include(":feature:settings") include(":core") include(":ui") +include(":desktopApp") diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index 5b941be..27153b4 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -1,49 +1,44 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.multiplatform) alias(libs.plugins.kotlin.compose) } +kotlin { + androidTarget() + jvm() + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(project(":domain")) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(libs.compose.material.icons.extended) + } + androidMain.dependencies { + implementation(libs.androidx.core.ktx) + } + } +} + android { namespace = "fr.benju.tasks.ui" compileSdk = 36 - - defaultConfig { - minSdk = 26 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - + defaultConfig { minSdk = 26 } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - - buildFeatures { - compose = true - } -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) - } } -dependencies { - implementation(project(":domain")) - - // Core - implementation(libs.androidx.core.ktx) - - // Compose - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.compose.material3) - api(libs.androidx.compose.runtime) - - debugImplementation(libs.androidx.compose.ui.tooling) +compose.resources { + publicResClass = true + packageOfResClass = "fr.benju.tasks.ui" + generateResClass = always } diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/androidMain/AndroidManifest.xml similarity index 100% rename from ui/src/main/AndroidManifest.xml rename to ui/src/androidMain/AndroidManifest.xml diff --git a/ui/src/commonMain/composeResources/values-de/strings.xml b/ui/src/commonMain/composeResources/values-de/strings.xml new file mode 100644 index 0000000..c9995be --- /dev/null +++ b/ui/src/commonMain/composeResources/values-de/strings.xml @@ -0,0 +1,31 @@ + + + Simple Tasks + Alle + Aktive + Erledigt + Noch keine Aufgaben + Einstellungen + Aufgabe hinzufügen + Neue Aufgabe + Aufgabe bearbeiten + Abbrechen + Speichern + Titel + Beschreibung + Priorität + Titel ist erforderlich + Aufgabe konnte nicht gespeichert werden. Bitte erneut versuchen. + Niedrig + Mittel + Hoch + Einstellungen + Dunkelmodus + Version 1.0.0 + Zurück + Aufgabe löschen + Möchtest du \"%1$s\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. + Löschen + Abbrechen + Aufgabe löschen + diff --git a/ui/src/commonMain/composeResources/values-es/strings.xml b/ui/src/commonMain/composeResources/values-es/strings.xml new file mode 100644 index 0000000..79efcf6 --- /dev/null +++ b/ui/src/commonMain/composeResources/values-es/strings.xml @@ -0,0 +1,31 @@ + + + Simple Tasks + Todas + Activas + Completadas + Aún no hay tareas + Ajustes + Añadir tarea + Nueva tarea + Editar tarea + Cancelar + Guardar + Título + Descripción + Prioridad + El título es obligatorio + No se pudo guardar la tarea. Por favor, inténtalo de nuevo. + Baja + Media + Alta + Ajustes + Modo oscuro + Versión 1.0.0 + Atrás + Eliminar tarea + ¿Seguro que quieres eliminar \"%1$s\"? Esta acción no se puede deshacer. + Eliminar + Cancelar + Eliminar tarea + diff --git a/ui/src/commonMain/composeResources/values-fr/strings.xml b/ui/src/commonMain/composeResources/values-fr/strings.xml new file mode 100644 index 0000000..e23a003 --- /dev/null +++ b/ui/src/commonMain/composeResources/values-fr/strings.xml @@ -0,0 +1,31 @@ + + + Simple Tasks + Toutes + Actives + Terminées + Aucune tâche pour l\'instant + Paramètres + Ajouter une tâche + Nouvelle tâche + Modifier la tâche + Annuler + Enregistrer + Titre + Description + Priorité + Le titre est obligatoire + Impossible d\'enregistrer la tâche. Veuillez réessayer. + Faible + Moyenne + Haute + Paramètres + Mode sombre + Version 1.0.0 + Retour + Supprimer la tâche + Voulez-vous vraiment supprimer \"%1$s\" ? Cette action est irréversible. + Supprimer + Annuler + Supprimer la tâche + diff --git a/ui/src/commonMain/composeResources/values-it/strings.xml b/ui/src/commonMain/composeResources/values-it/strings.xml new file mode 100644 index 0000000..d5bd2e5 --- /dev/null +++ b/ui/src/commonMain/composeResources/values-it/strings.xml @@ -0,0 +1,31 @@ + + + Simple Tasks + Tutte + Attive + Completate + Nessuna attività + Impostazioni + Aggiungi attività + Nuova attività + Modifica attività + Annulla + Salva + Titolo + Descrizione + Priorità + Il titolo è obbligatorio + Impossibile salvare l\'attività. Riprova. + Bassa + Media + Alta + Impostazioni + Modalità scura + Versione 1.0.0 + Indietro + Elimina attività + Sei sicuro di voler eliminare \"%1$s\"? Questa azione non può essere annullata. + Elimina + Annulla + Elimina attività + diff --git a/ui/src/commonMain/composeResources/values/strings.xml b/ui/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000..4571838 --- /dev/null +++ b/ui/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,31 @@ + + + Simple Tasks + All + Active + Completed + No tasks yet + Settings + Add Task + New Task + Edit Task + Cancel + Save + Title + Description + Priority + Title is required + Failed to save the task. Please try again. + Low + Medium + High + Settings + Dark Mode + Version 1.0.0 + Back + Delete task + Are you sure you want to delete \"%1$s\"? This action cannot be undone. + Delete + Cancel + Delete task + diff --git a/ui/src/main/java/fr/benju/tasks/ui/components/PriorityChip.kt b/ui/src/commonMain/kotlin/fr/benju/tasks/ui/components/PriorityChip.kt similarity index 76% rename from ui/src/main/java/fr/benju/tasks/ui/components/PriorityChip.kt rename to ui/src/commonMain/kotlin/fr/benju/tasks/ui/components/PriorityChip.kt index 85a250a..ea3c682 100644 --- a/ui/src/main/java/fr/benju/tasks/ui/components/PriorityChip.kt +++ b/ui/src/commonMain/kotlin/fr/benju/tasks/ui/components/PriorityChip.kt @@ -7,14 +7,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.compose.ui.res.stringResource import fr.benju.tasks.domain.model.Priority -import fr.benju.tasks.ui.R +import fr.benju.tasks.ui.Res import fr.benju.tasks.ui.theme.PriorityHigh import fr.benju.tasks.ui.theme.PriorityLow import fr.benju.tasks.ui.theme.PriorityMedium +import org.jetbrains.compose.resources.stringResource @Composable fun PriorityChip( @@ -22,9 +21,9 @@ fun PriorityChip( modifier: Modifier = Modifier ) { val (color, labelRes) = when (priority) { - Priority.LOW -> PriorityLow to R.string.priority_low - Priority.MEDIUM -> PriorityMedium to R.string.priority_medium - Priority.HIGH -> PriorityHigh to R.string.priority_high + Priority.LOW -> PriorityLow to Res.string.priority_low + Priority.MEDIUM -> PriorityMedium to Res.string.priority_medium + Priority.HIGH -> PriorityHigh to Res.string.priority_high } Text( diff --git a/ui/src/main/java/fr/benju/tasks/ui/components/TaskCard.kt b/ui/src/commonMain/kotlin/fr/benju/tasks/ui/components/TaskCard.kt similarity index 78% rename from ui/src/main/java/fr/benju/tasks/ui/components/TaskCard.kt rename to ui/src/commonMain/kotlin/fr/benju/tasks/ui/components/TaskCard.kt index 7a62ff1..84afe9e 100644 --- a/ui/src/main/java/fr/benju/tasks/ui/components/TaskCard.kt +++ b/ui/src/commonMain/kotlin/fr/benju/tasks/ui/components/TaskCard.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -28,13 +30,16 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import fr.benju.tasks.domain.model.Task -import fr.benju.tasks.ui.R +import fr.benju.tasks.ui.Res +import fr.benju.tasks.ui.cancel +import fr.benju.tasks.ui.cd_delete_task +import fr.benju.tasks.ui.delete +import fr.benju.tasks.ui.delete_task_dialog_message +import fr.benju.tasks.ui.delete_task_dialog_title +import org.jetbrains.compose.resources.stringResource @Composable fun TaskCard( @@ -49,9 +54,9 @@ fun TaskCard( if (showDeleteConfirmation && onDeleteTask != null) { AlertDialog( onDismissRequest = { showDeleteConfirmation = false }, - title = { Text(stringResource(R.string.delete_task_dialog_title)) }, + title = { Text(stringResource(Res.string.delete_task_dialog_title)) }, text = { - Text(stringResource(R.string.delete_task_dialog_message, task.title)) + Text(stringResource(Res.string.delete_task_dialog_message, task.title)) }, confirmButton = { Button( @@ -63,12 +68,12 @@ fun TaskCard( containerColor = MaterialTheme.colorScheme.error ) ) { - Text(stringResource(R.string.delete)) + Text(stringResource(Res.string.delete)) } }, dismissButton = { TextButton(onClick = { showDeleteConfirmation = false }) { - Text(stringResource(R.string.cancel)) + Text(stringResource(Res.string.cancel)) } } ) @@ -81,7 +86,9 @@ fun TaskCard( elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Row( - modifier = Modifier.fillMaxWidth().padding(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), verticalAlignment = Alignment.CenterVertically, ) { Checkbox( @@ -118,8 +125,8 @@ fun TaskCard( if (onDeleteTask != null) { IconButton(onClick = { showDeleteConfirmation = true }) { Icon( - painter = painterResource(R.drawable.ic_delete_rounded_24dp), - contentDescription = stringResource(R.string.cd_delete_task), + imageVector = Icons.Rounded.Delete, + contentDescription = stringResource(Res.string.cd_delete_task), tint = Color.Red.copy(alpha = 0.7f), ) } @@ -127,20 +134,3 @@ fun TaskCard( } } } - -@Preview -@Composable -private fun TaskCardPreview() { - TaskCard( - task = Task( - id = 1L, - title = "Buy groceries", - description = "Milk, eggs, bread, and cheese", - isCompleted = false, - priority = fr.benju.tasks.domain.model.Priority.HIGH - ), - onTaskClick = {}, - onToggleComplete = {}, - onDeleteTask = {} - ) -} diff --git a/ui/src/main/java/fr/benju/tasks/ui/theme/Color.kt b/ui/src/commonMain/kotlin/fr/benju/tasks/ui/theme/Color.kt similarity index 100% rename from ui/src/main/java/fr/benju/tasks/ui/theme/Color.kt rename to ui/src/commonMain/kotlin/fr/benju/tasks/ui/theme/Color.kt diff --git a/ui/src/main/java/fr/benju/tasks/ui/theme/Theme.kt b/ui/src/commonMain/kotlin/fr/benju/tasks/ui/theme/Theme.kt similarity index 66% rename from ui/src/main/java/fr/benju/tasks/ui/theme/Theme.kt rename to ui/src/commonMain/kotlin/fr/benju/tasks/ui/theme/Theme.kt index c5cbe00..6fa897b 100644 --- a/ui/src/main/java/fr/benju/tasks/ui/theme/Theme.kt +++ b/ui/src/commonMain/kotlin/fr/benju/tasks/ui/theme/Theme.kt @@ -1,14 +1,10 @@ package fr.benju.tasks.ui.theme -import android.app.Activity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat private val LightColorScheme = lightColorScheme( primary = PrimaryLight, @@ -35,20 +31,7 @@ fun TaskManagerTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { - val colorScheme = if (darkTheme) { - DarkColorScheme - } else { - LightColorScheme - } - - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme - } - } - + val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme MaterialTheme( colorScheme = colorScheme, typography = Typography, diff --git a/ui/src/main/java/fr/benju/tasks/ui/theme/Type.kt b/ui/src/commonMain/kotlin/fr/benju/tasks/ui/theme/Type.kt similarity index 87% rename from ui/src/main/java/fr/benju/tasks/ui/theme/Type.kt rename to ui/src/commonMain/kotlin/fr/benju/tasks/ui/theme/Type.kt index a7692ff..2400c32 100644 --- a/ui/src/main/java/fr/benju/tasks/ui/theme/Type.kt +++ b/ui/src/commonMain/kotlin/fr/benju/tasks/ui/theme/Type.kt @@ -42,11 +42,18 @@ val Typography = Typography( lineHeight = 20.sp, letterSpacing = 0.25.sp ), + bodySmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp + ), labelMedium = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp - ) + ), ) diff --git a/ui/src/main/res/drawable/ic_delete_rounded_24dp.xml b/ui/src/main/res/drawable/ic_delete_rounded_24dp.xml deleted file mode 100644 index f468115..0000000 --- a/ui/src/main/res/drawable/ic_delete_rounded_24dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/ui/src/main/res/values-de/strings.xml b/ui/src/main/res/values-de/strings.xml deleted file mode 100644 index 8b996ce..0000000 --- a/ui/src/main/res/values-de/strings.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - Aufgabe löschen - Möchtest du \"%1$s\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. - Löschen - Abbrechen - Niedrig - Mittel - Hoch - Aufgabe löschen - - diff --git a/ui/src/main/res/values-es/strings.xml b/ui/src/main/res/values-es/strings.xml deleted file mode 100644 index ec61184..0000000 --- a/ui/src/main/res/values-es/strings.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - Eliminar tarea - ¿Seguro que quieres eliminar \"%1$s\"? Esta acción no se puede deshacer. - Eliminar - Cancelar - Baja - Media - Alta - Eliminar tarea - - diff --git a/ui/src/main/res/values-fr/strings.xml b/ui/src/main/res/values-fr/strings.xml deleted file mode 100644 index f19c7b9..0000000 --- a/ui/src/main/res/values-fr/strings.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - Supprimer la tâche - Voulez-vous vraiment supprimer \"%1$s\" ? Cette action est irréversible. - Supprimer - Annuler - Faible - Moyenne - Haute - Supprimer la tâche - - diff --git a/ui/src/main/res/values-it/strings.xml b/ui/src/main/res/values-it/strings.xml deleted file mode 100644 index 7f35276..0000000 --- a/ui/src/main/res/values-it/strings.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - Elimina attività - Sei sicuro di voler eliminare \"%1$s\"? Questa azione non può essere annullata. - Elimina - Annulla - Bassa - Media - Alta - Elimina attività - - diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml deleted file mode 100644 index 6d8639f..0000000 --- a/ui/src/main/res/values/strings.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - Delete task - Are you sure you want to delete \"%1$s\"? This action cannot be undone. - Delete - Cancel - - - Low - Medium - High - - - Delete task - -