From b0b073c8ec803b2f41fd2295a269c8f4a9432edc Mon Sep 17 00:00:00 2001 From: Gato Date: Sat, 6 Jun 2026 05:54:24 +0200 Subject: [PATCH 1/3] feat: [#3] setup projet Android (Kotlin, Compose, Hilt, MVVM) --- app/build.gradle.kts | 93 +++++++++++++++++++ app/src/main/AndroidManifest.xml | 31 +++++++ .../java/com/planify/mobile/PlanifyApp.kt | 19 ++++ .../java/com/planify/mobile/di/AppModule.kt | 10 ++ .../com/planify/mobile/ui/MainActivity.kt | 22 +++++ .../java/com/planify/mobile/ui/theme/Theme.kt | 36 +++++++ .../java/com/planify/mobile/ui/theme/Type.kt | 15 +++ build.gradle.kts | 6 ++ gradle/libs.versions.toml | 58 ++++++++++++ settings.gradle.kts | 23 +++++ 10 files changed, 313 insertions(+) create mode 100644 app/build.gradle.kts create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/planify/mobile/PlanifyApp.kt create mode 100644 app/src/main/java/com/planify/mobile/di/AppModule.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/MainActivity.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/theme/Type.kt create mode 100644 build.gradle.kts create mode 100644 gradle/libs.versions.toml create mode 100644 settings.gradle.kts diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..201015e --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,93 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.hilt) + alias(libs.plugins.ksp) +} + +android { + namespace = "com.planify.mobile" + compileSdk = 34 + + defaultConfig { + applicationId = "com.planify.mobile" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "0.1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.14" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.material.icons.extended) + implementation(libs.androidx.navigation.compose) + + // Hilt + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + implementation(libs.hilt.navigation.compose) + implementation(libs.hilt.work) + ksp(libs.hilt.work.compiler) + + // Room + implementation(libs.room.runtime) + implementation(libs.room.ktx) + ksp(libs.room.compiler) + + // Coroutines + implementation(libs.kotlinx.coroutines.android) + + // Network + implementation(libs.okhttp) + implementation(libs.okhttp.logging) + + // Storage + implementation(libs.datastore.preferences) + implementation(libs.security.crypto) + + // WorkManager + implementation(libs.work.runtime.ktx) + + // Tests + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8c6cf7a --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/planify/mobile/PlanifyApp.kt b/app/src/main/java/com/planify/mobile/PlanifyApp.kt new file mode 100644 index 0000000..f087ff7 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/PlanifyApp.kt @@ -0,0 +1,19 @@ +package com.planify.mobile + +import android.app.Application +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration +import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject + +@HiltAndroidApp +class PlanifyApp : Application(), Configuration.Provider { + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() +} diff --git a/app/src/main/java/com/planify/mobile/di/AppModule.kt b/app/src/main/java/com/planify/mobile/di/AppModule.kt new file mode 100644 index 0000000..f42ae78 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/di/AppModule.kt @@ -0,0 +1,10 @@ +package com.planify.mobile.di + +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object AppModule +// TODO #5 : fournir AppDatabase, repositories, et client CalDAV diff --git a/app/src/main/java/com/planify/mobile/ui/MainActivity.kt b/app/src/main/java/com/planify/mobile/ui/MainActivity.kt new file mode 100644 index 0000000..f866341 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/MainActivity.kt @@ -0,0 +1,22 @@ +package com.planify.mobile.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.planify.mobile.ui.theme.PlanifyTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + PlanifyTheme { + // TODO #6 : PlanifyNavHost() + } + } + } +} diff --git a/app/src/main/java/com/planify/mobile/ui/theme/Theme.kt b/app/src/main/java/com/planify/mobile/ui/theme/Theme.kt new file mode 100644 index 0000000..594ef8e --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/theme/Theme.kt @@ -0,0 +1,36 @@ +package com.planify.mobile.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val LightColorScheme = lightColorScheme() +private val DarkColorScheme = darkColorScheme() + +@Composable +fun PlanifyTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/app/src/main/java/com/planify/mobile/ui/theme/Type.kt b/app/src/main/java/com/planify/mobile/ui/theme/Type.kt new file mode 100644 index 0000000..e1e0b28 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/theme/Type.kt @@ -0,0 +1,15 @@ +package com.planify.mobile.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + bodyLarge = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..0adef57 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,6 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.jetbrains.kotlin.android) apply false + alias(libs.plugins.hilt) apply false + alias(libs.plugins.ksp) apply false +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d308142 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,58 @@ +[versions] +agp = "8.4.0" +kotlin = "1.9.24" +coreKtx = "1.13.1" +lifecycleRuntimeKtx = "2.8.3" +activityCompose = "1.9.0" +composeBom = "2024.06.00" +hilt = "2.51.1" +hiltNavigationCompose = "1.2.0" +navigationCompose = "2.7.7" +room = "2.6.1" +coroutines = "1.8.1" +okhttp = "4.12.0" +datastore = "1.1.1" +securityCrypto = "1.1.0-alpha06" +workManager = "2.9.0" +junit = "4.13.2" +junitExt = "1.2.1" +espressoCore = "3.6.1" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } +hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltNavigationCompose" } +hilt-work-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltNavigationCompose" } +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } +datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } +security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "securityCrypto" } +work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitExt" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +ksp = { id = "com.google.devtools.ksp", version = "1.9.24-1.0.20" } diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..654bdd7 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,23 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Planify Mobile" +include(":app") From c5fffb93b4c7b1734df6eabbd383631cd4d45999 Mon Sep 17 00:00:00 2001 From: Gato Date: Sat, 6 Jun 2026 05:55:12 +0200 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20[#4]=20mod=C3=A8les=20de=20donn?= =?UTF-8?q?=C3=A9es=20du=20domaine=20(Task,=20Project,=20Section,=20Label,?= =?UTF-8?q?=20Source)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/domain/model/BackendType.kt | 3 ++ .../planify/mobile/domain/model/DueDate.kt | 16 +++++++++++ .../com/planify/mobile/domain/model/Label.kt | 12 ++++++++ .../planify/mobile/domain/model/Project.kt | 25 +++++++++++++++++ .../planify/mobile/domain/model/Reminder.kt | 11 ++++++++ .../planify/mobile/domain/model/Section.kt | 13 +++++++++ .../com/planify/mobile/domain/model/Source.kt | 26 +++++++++++++++++ .../com/planify/mobile/domain/model/Task.kt | 28 +++++++++++++++++++ .../domain/repository/ProjectRepository.kt | 15 ++++++++++ .../domain/repository/TaskRepository.kt | 18 ++++++++++++ 10 files changed, 167 insertions(+) create mode 100644 app/src/main/java/com/planify/mobile/domain/model/BackendType.kt create mode 100644 app/src/main/java/com/planify/mobile/domain/model/DueDate.kt create mode 100644 app/src/main/java/com/planify/mobile/domain/model/Label.kt create mode 100644 app/src/main/java/com/planify/mobile/domain/model/Project.kt create mode 100644 app/src/main/java/com/planify/mobile/domain/model/Reminder.kt create mode 100644 app/src/main/java/com/planify/mobile/domain/model/Section.kt create mode 100644 app/src/main/java/com/planify/mobile/domain/model/Source.kt create mode 100644 app/src/main/java/com/planify/mobile/domain/model/Task.kt create mode 100644 app/src/main/java/com/planify/mobile/domain/repository/ProjectRepository.kt create mode 100644 app/src/main/java/com/planify/mobile/domain/repository/TaskRepository.kt diff --git a/app/src/main/java/com/planify/mobile/domain/model/BackendType.kt b/app/src/main/java/com/planify/mobile/domain/model/BackendType.kt new file mode 100644 index 0000000..a37643b --- /dev/null +++ b/app/src/main/java/com/planify/mobile/domain/model/BackendType.kt @@ -0,0 +1,3 @@ +package com.planify.mobile.domain.model + +enum class BackendType { LOCAL, CALDAV, TODOIST } diff --git a/app/src/main/java/com/planify/mobile/domain/model/DueDate.kt b/app/src/main/java/com/planify/mobile/domain/model/DueDate.kt new file mode 100644 index 0000000..72e7508 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/domain/model/DueDate.kt @@ -0,0 +1,16 @@ +package com.planify.mobile.domain.model + +data class DueDate( + val date: String, + val timezone: String? = null, + val isRecurring: Boolean = false, + val recurrencyType: RecurrencyType = RecurrencyType.NONE, + val recurrencyInterval: Int = 1, + val recurrencyWeekDays: List = emptyList(), + val recurrencyCount: Int? = null, + val recurrencyEnd: String? = null, +) + +enum class RecurrencyType { + NONE, MINUTELY, HOURLY, EVERY_DAY, EVERY_WEEK, EVERY_MONTH, EVERY_YEAR +} diff --git a/app/src/main/java/com/planify/mobile/domain/model/Label.kt b/app/src/main/java/com/planify/mobile/domain/model/Label.kt new file mode 100644 index 0000000..ebfe368 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/domain/model/Label.kt @@ -0,0 +1,12 @@ +package com.planify.mobile.domain.model + +data class Label( + val id: String, + val name: String, + val color: String, + val order: Int = 0, + val sourceId: String? = null, + val backendType: BackendType = BackendType.LOCAL, + val isFavorite: Boolean = false, + val isDeleted: Boolean = false, +) diff --git a/app/src/main/java/com/planify/mobile/domain/model/Project.kt b/app/src/main/java/com/planify/mobile/domain/model/Project.kt new file mode 100644 index 0000000..9bed99c --- /dev/null +++ b/app/src/main/java/com/planify/mobile/domain/model/Project.kt @@ -0,0 +1,25 @@ +package com.planify.mobile.domain.model + +data class Project( + val id: String, + val name: String, + val color: String = "#4CAF50", + val emoji: String? = null, + val parentId: String? = null, + val sourceId: String? = null, + val backendType: BackendType = BackendType.LOCAL, + val isInbox: Boolean = false, + val isFavorite: Boolean = false, + val isArchived: Boolean = false, + val isDeleted: Boolean = false, + val viewStyle: ViewStyle = ViewStyle.LIST, + val sortedBy: SortBy = SortBy.MANUAL, + val sortAscending: Boolean = true, + val childOrder: Int = 0, + val calendarUrl: String? = null, + val syncId: String? = null, +) + +enum class ViewStyle { LIST, BOARD } + +enum class SortBy { MANUAL, NAME, DUE_DATE, ADDED_DATE, PRIORITY } diff --git a/app/src/main/java/com/planify/mobile/domain/model/Reminder.kt b/app/src/main/java/com/planify/mobile/domain/model/Reminder.kt new file mode 100644 index 0000000..8d415c7 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/domain/model/Reminder.kt @@ -0,0 +1,11 @@ +package com.planify.mobile.domain.model + +data class Reminder( + val id: String, + val taskId: String, + val type: ReminderType, + val dueDate: DueDate? = null, + val minutesOffset: Int? = null, +) + +enum class ReminderType { ABSOLUTE, RELATIVE } diff --git a/app/src/main/java/com/planify/mobile/domain/model/Section.kt b/app/src/main/java/com/planify/mobile/domain/model/Section.kt new file mode 100644 index 0000000..c4215c3 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/domain/model/Section.kt @@ -0,0 +1,13 @@ +package com.planify.mobile.domain.model + +data class Section( + val id: String, + val name: String, + val projectId: String, + val order: Int = 0, + val isArchived: Boolean = false, + val isDeleted: Boolean = false, + val collapsed: Boolean = false, + val icalUrl: String? = null, + val etag: String? = null, +) diff --git a/app/src/main/java/com/planify/mobile/domain/model/Source.kt b/app/src/main/java/com/planify/mobile/domain/model/Source.kt new file mode 100644 index 0000000..9b8487b --- /dev/null +++ b/app/src/main/java/com/planify/mobile/domain/model/Source.kt @@ -0,0 +1,26 @@ +package com.planify.mobile.domain.model + +data class Source( + val id: String, + val type: SourceType, + val displayName: String, + val addedAt: String, + val updatedAt: String, + val isVisible: Boolean = true, + val lastSync: String? = null, + val caldavData: SourceCalDavData? = null, +) + +enum class SourceType { LOCAL, CALDAV } + +data class SourceCalDavData( + val serverUrl: String, + val username: String, + val calendarHomeUrl: String? = null, + val userDisplayName: String? = null, + val userEmail: String? = null, + val caldavType: CalDavType = CalDavType.GENERIC, + val ignoreSsl: Boolean = false, +) + +enum class CalDavType { NEXTCLOUD, GENERIC } diff --git a/app/src/main/java/com/planify/mobile/domain/model/Task.kt b/app/src/main/java/com/planify/mobile/domain/model/Task.kt new file mode 100644 index 0000000..0632479 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/domain/model/Task.kt @@ -0,0 +1,28 @@ +package com.planify.mobile.domain.model + +data class Task( + val id: String, + val content: String, + val description: String = "", + val projectId: String, + val sectionId: String? = null, + val parentId: String? = null, + val priority: Int = 4, + val checked: Boolean = false, + val dueDate: DueDate? = null, + val deadlineDate: String? = null, + val labels: List = emptyList(), + val pinned: Boolean = false, + val collapsed: Boolean = false, + val childOrder: Int = 0, + val addedAt: String = "", + val updatedAt: String = "", + val completedAt: String? = null, + val itemType: ItemType = ItemType.TASK, + val calendarEventUid: String? = null, + val icalUrl: String? = null, + val etag: String? = null, + val responsibleUid: String? = null, +) + +enum class ItemType { TASK, NOTE } diff --git a/app/src/main/java/com/planify/mobile/domain/repository/ProjectRepository.kt b/app/src/main/java/com/planify/mobile/domain/repository/ProjectRepository.kt new file mode 100644 index 0000000..152dc82 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/domain/repository/ProjectRepository.kt @@ -0,0 +1,15 @@ +package com.planify.mobile.domain.repository + +import com.planify.mobile.domain.model.Project +import kotlinx.coroutines.flow.Flow + +interface ProjectRepository { + fun getAllProjects(): Flow> + fun getFavoriteProjects(): Flow> + fun getSubProjects(parentId: String): Flow> + suspend fun getProjectById(id: String): Project? + suspend fun getInboxProject(): Project? + suspend fun insertProject(project: Project) + suspend fun updateProject(project: Project) + suspend fun deleteProject(id: String) +} diff --git a/app/src/main/java/com/planify/mobile/domain/repository/TaskRepository.kt b/app/src/main/java/com/planify/mobile/domain/repository/TaskRepository.kt new file mode 100644 index 0000000..90556dd --- /dev/null +++ b/app/src/main/java/com/planify/mobile/domain/repository/TaskRepository.kt @@ -0,0 +1,18 @@ +package com.planify.mobile.domain.repository + +import com.planify.mobile.domain.model.Task +import kotlinx.coroutines.flow.Flow + +interface TaskRepository { + fun getTasksByProject(projectId: String): Flow> + fun getTasksBySection(sectionId: String): Flow> + fun getInboxTasks(): Flow> + fun getTodayTasks(): Flow> + fun getOverdueTasks(): Flow> + fun getSubTasks(parentId: String): Flow> + suspend fun getTaskById(id: String): Task? + suspend fun insertTask(task: Task) + suspend fun updateTask(task: Task) + suspend fun deleteTask(id: String) + suspend fun checkTask(id: String, checked: Boolean) +} From c83a15c1b1d52b7824bcb6713733792f4b7b25dd Mon Sep 17 00:00:00 2001 From: Gato Date: Sat, 6 Jun 2026 05:56:23 +0200 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20[#5]=20base=20de=20donn=C3=A9es=20R?= =?UTF-8?q?oom=20(entit=C3=A9s,=20DAOs,=20AppDatabase,=20module=20Hilt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../planify/mobile/data/local/AppDatabase.kt | 31 ++++++++++ .../mobile/data/local/dao/ProjectDao.kt | 40 ++++++++++++ .../mobile/data/local/dao/SectionDao.kt | 28 +++++++++ .../planify/mobile/data/local/dao/TaskDao.kt | 62 +++++++++++++++++++ .../mobile/data/local/entity/LabelEntity.kt | 17 +++++ .../mobile/data/local/entity/ProjectEntity.kt | 26 ++++++++ .../data/local/entity/ReminderEntity.kt | 27 ++++++++ .../mobile/data/local/entity/SectionEntity.kt | 31 ++++++++++ .../mobile/data/local/entity/SourceEntity.kt | 17 +++++ .../mobile/data/local/entity/TaskEntity.kt | 48 ++++++++++++++ .../com/planify/mobile/di/DatabaseModule.kt | 27 ++++++++ 11 files changed, 354 insertions(+) create mode 100644 app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt create mode 100644 app/src/main/java/com/planify/mobile/data/local/dao/ProjectDao.kt create mode 100644 app/src/main/java/com/planify/mobile/data/local/dao/SectionDao.kt create mode 100644 app/src/main/java/com/planify/mobile/data/local/dao/TaskDao.kt create mode 100644 app/src/main/java/com/planify/mobile/data/local/entity/LabelEntity.kt create mode 100644 app/src/main/java/com/planify/mobile/data/local/entity/ProjectEntity.kt create mode 100644 app/src/main/java/com/planify/mobile/data/local/entity/ReminderEntity.kt create mode 100644 app/src/main/java/com/planify/mobile/data/local/entity/SectionEntity.kt create mode 100644 app/src/main/java/com/planify/mobile/data/local/entity/SourceEntity.kt create mode 100644 app/src/main/java/com/planify/mobile/data/local/entity/TaskEntity.kt create mode 100644 app/src/main/java/com/planify/mobile/di/DatabaseModule.kt diff --git a/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt b/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt new file mode 100644 index 0000000..0c86eaf --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt @@ -0,0 +1,31 @@ +package com.planify.mobile.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.planify.mobile.data.local.dao.ProjectDao +import com.planify.mobile.data.local.dao.SectionDao +import com.planify.mobile.data.local.dao.TaskDao +import com.planify.mobile.data.local.entity.LabelEntity +import com.planify.mobile.data.local.entity.ProjectEntity +import com.planify.mobile.data.local.entity.ReminderEntity +import com.planify.mobile.data.local.entity.SectionEntity +import com.planify.mobile.data.local.entity.SourceEntity +import com.planify.mobile.data.local.entity.TaskEntity + +@Database( + entities = [ + TaskEntity::class, + ProjectEntity::class, + SectionEntity::class, + LabelEntity::class, + ReminderEntity::class, + SourceEntity::class, + ], + version = 1, + exportSchema = true, +) +abstract class AppDatabase : RoomDatabase() { + abstract fun taskDao(): TaskDao + abstract fun projectDao(): ProjectDao + abstract fun sectionDao(): SectionDao +} diff --git a/app/src/main/java/com/planify/mobile/data/local/dao/ProjectDao.kt b/app/src/main/java/com/planify/mobile/data/local/dao/ProjectDao.kt new file mode 100644 index 0000000..09fc679 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/dao/ProjectDao.kt @@ -0,0 +1,40 @@ +package com.planify.mobile.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.planify.mobile.data.local.entity.ProjectEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ProjectDao { + + @Query("SELECT * FROM projects WHERE is_deleted = 0 ORDER BY child_order ASC") + fun getAllProjects(): Flow> + + @Query("SELECT * FROM projects WHERE is_favorite = 1 AND is_deleted = 0 ORDER BY child_order ASC") + fun getFavoriteProjects(): Flow> + + @Query("SELECT * FROM projects WHERE parent_id = :parentId AND is_deleted = 0 ORDER BY child_order ASC") + fun getSubProjects(parentId: String): Flow> + + @Query("SELECT * FROM projects WHERE id = :id") + suspend fun getById(id: String): ProjectEntity? + + @Query("SELECT * FROM projects WHERE is_inbox = 1 LIMIT 1") + suspend fun getInboxProject(): ProjectEntity? + + @Query("SELECT * FROM projects WHERE calendar_url = :calendarUrl LIMIT 1") + suspend fun getByCalendarUrl(calendarUrl: String): ProjectEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(project: ProjectEntity) + + @Update + suspend fun update(project: ProjectEntity) + + @Query("UPDATE projects SET is_deleted = 1 WHERE id = :id") + suspend fun softDelete(id: String) +} diff --git a/app/src/main/java/com/planify/mobile/data/local/dao/SectionDao.kt b/app/src/main/java/com/planify/mobile/data/local/dao/SectionDao.kt new file mode 100644 index 0000000..d51e3bf --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/dao/SectionDao.kt @@ -0,0 +1,28 @@ +package com.planify.mobile.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.planify.mobile.data.local.entity.SectionEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface SectionDao { + + @Query("SELECT * FROM sections WHERE project_id = :projectId AND is_deleted = 0 ORDER BY `order` ASC") + fun getSectionsByProject(projectId: String): Flow> + + @Query("SELECT * FROM sections WHERE id = :id") + suspend fun getById(id: String): SectionEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(section: SectionEntity) + + @Update + suspend fun update(section: SectionEntity) + + @Query("UPDATE sections SET is_deleted = 1 WHERE id = :id") + suspend fun softDelete(id: String) +} diff --git a/app/src/main/java/com/planify/mobile/data/local/dao/TaskDao.kt b/app/src/main/java/com/planify/mobile/data/local/dao/TaskDao.kt new file mode 100644 index 0000000..f4a05b0 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/dao/TaskDao.kt @@ -0,0 +1,62 @@ +package com.planify.mobile.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.planify.mobile.data.local.entity.TaskEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface TaskDao { + + @Query("SELECT * FROM tasks WHERE project_id = :projectId AND parent_id IS NULL AND is_deleted = 0 ORDER BY child_order ASC") + fun getTasksByProject(projectId: String): Flow> + + @Query("SELECT * FROM tasks WHERE section_id = :sectionId AND parent_id IS NULL AND is_deleted = 0 ORDER BY child_order ASC") + fun getTasksBySection(sectionId: String): Flow> + + @Query(""" + SELECT t.* FROM tasks t + INNER JOIN projects p ON t.project_id = p.id + WHERE p.is_inbox = 1 AND t.parent_id IS NULL AND t.is_deleted = 0 + ORDER BY t.child_order ASC + """) + fun getInboxTasks(): Flow> + + @Query(""" + SELECT * FROM tasks + WHERE date(due_date) = date('now') AND checked = 0 AND is_deleted = 0 + ORDER BY child_order ASC + """) + fun getTodayTasks(): Flow> + + @Query(""" + SELECT * FROM tasks + WHERE date(due_date) < date('now') AND checked = 0 AND is_deleted = 0 + ORDER BY due_date ASC + """) + fun getOverdueTasks(): Flow> + + @Query("SELECT * FROM tasks WHERE parent_id = :parentId AND is_deleted = 0 ORDER BY child_order ASC") + fun getSubTasks(parentId: String): Flow> + + @Query("SELECT * FROM tasks WHERE id = :id") + suspend fun getTaskById(id: String): TaskEntity? + + @Query("SELECT * FROM tasks WHERE ical_url = :icalUrl LIMIT 1") + suspend fun getTaskByIcalUrl(icalUrl: String): TaskEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(task: TaskEntity) + + @Update + suspend fun update(task: TaskEntity) + + @Query("UPDATE tasks SET checked = :checked, completed_at = :completedAt, updated_at = :updatedAt WHERE id = :id") + suspend fun setChecked(id: String, checked: Boolean, completedAt: String?, updatedAt: String) + + @Query("UPDATE tasks SET is_deleted = 1, updated_at = :updatedAt WHERE id = :id") + suspend fun softDelete(id: String, updatedAt: String) +} diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/LabelEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/LabelEntity.kt new file mode 100644 index 0000000..ae052bd --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/LabelEntity.kt @@ -0,0 +1,17 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "labels") +data class LabelEntity( + @PrimaryKey val id: String, + val name: String, + val color: String, + val order: Int = 0, + @ColumnInfo(name = "source_id") val sourceId: String? = null, + @ColumnInfo(name = "backend_type") val backendType: String = "LOCAL", + @ColumnInfo(name = "is_favorite") val isFavorite: Boolean = false, + @ColumnInfo(name = "is_deleted") val isDeleted: Boolean = false, +) diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/ProjectEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/ProjectEntity.kt new file mode 100644 index 0000000..242ad55 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/ProjectEntity.kt @@ -0,0 +1,26 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "projects") +data class ProjectEntity( + @PrimaryKey val id: String, + val name: String, + val color: String = "#4CAF50", + val emoji: String? = null, + @ColumnInfo(name = "parent_id") val parentId: String? = null, + @ColumnInfo(name = "source_id") val sourceId: String? = null, + @ColumnInfo(name = "backend_type") val backendType: String = "LOCAL", + @ColumnInfo(name = "is_inbox") val isInbox: Boolean = false, + @ColumnInfo(name = "is_favorite") val isFavorite: Boolean = false, + @ColumnInfo(name = "is_archived") val isArchived: Boolean = false, + @ColumnInfo(name = "is_deleted") val isDeleted: Boolean = false, + @ColumnInfo(name = "view_style") val viewStyle: String = "LIST", + @ColumnInfo(name = "sorted_by") val sortedBy: String = "MANUAL", + @ColumnInfo(name = "sort_ascending") val sortAscending: Boolean = true, + @ColumnInfo(name = "child_order") val childOrder: Int = 0, + @ColumnInfo(name = "calendar_url") val calendarUrl: String? = null, + @ColumnInfo(name = "sync_id") val syncId: String? = null, +) diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/ReminderEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/ReminderEntity.kt new file mode 100644 index 0000000..2f20dde --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/ReminderEntity.kt @@ -0,0 +1,27 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "reminders", + foreignKeys = [ + ForeignKey( + entity = TaskEntity::class, + parentColumns = ["id"], + childColumns = ["task_id"], + onDelete = ForeignKey.CASCADE + ) + ], + indices = [Index("task_id")] +) +data class ReminderEntity( + @PrimaryKey val id: String, + @ColumnInfo(name = "task_id") val taskId: String, + val type: String, + @ColumnInfo(name = "due_date") val dueDate: String? = null, + @ColumnInfo(name = "minutes_offset") val minutesOffset: Int? = null, +) diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/SectionEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/SectionEntity.kt new file mode 100644 index 0000000..b91b1c1 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/SectionEntity.kt @@ -0,0 +1,31 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "sections", + foreignKeys = [ + ForeignKey( + entity = ProjectEntity::class, + parentColumns = ["id"], + childColumns = ["project_id"], + onDelete = ForeignKey.CASCADE + ) + ], + indices = [Index("project_id")] +) +data class SectionEntity( + @PrimaryKey val id: String, + val name: String, + @ColumnInfo(name = "project_id") val projectId: String, + val order: Int = 0, + @ColumnInfo(name = "is_archived") val isArchived: Boolean = false, + @ColumnInfo(name = "is_deleted") val isDeleted: Boolean = false, + val collapsed: Boolean = false, + @ColumnInfo(name = "ical_url") val icalUrl: String? = null, + val etag: String? = null, +) diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/SourceEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/SourceEntity.kt new file mode 100644 index 0000000..5522b04 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/SourceEntity.kt @@ -0,0 +1,17 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "sources") +data class SourceEntity( + @PrimaryKey val id: String, + val type: String, + @ColumnInfo(name = "display_name") val displayName: String, + @ColumnInfo(name = "added_at") val addedAt: String, + @ColumnInfo(name = "updated_at") val updatedAt: String, + @ColumnInfo(name = "is_visible") val isVisible: Boolean = true, + @ColumnInfo(name = "last_sync") val lastSync: String? = null, + val data: String = "{}", +) diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/TaskEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/TaskEntity.kt new file mode 100644 index 0000000..868c8ff --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/TaskEntity.kt @@ -0,0 +1,48 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "tasks", + foreignKeys = [ + ForeignKey( + entity = ProjectEntity::class, + parentColumns = ["id"], + childColumns = ["project_id"], + onDelete = ForeignKey.CASCADE + ) + ], + indices = [ + Index("project_id"), + Index("section_id"), + Index("parent_id") + ] +) +data class TaskEntity( + @PrimaryKey val id: String, + val content: String, + val description: String = "", + @ColumnInfo(name = "project_id") val projectId: String, + @ColumnInfo(name = "section_id") val sectionId: String? = null, + @ColumnInfo(name = "parent_id") val parentId: String? = null, + val priority: Int = 4, + val checked: Boolean = false, + @ColumnInfo(name = "due_date") val dueDate: String? = null, + @ColumnInfo(name = "deadline_date") val deadlineDate: String? = null, + val labels: String = "[]", + val pinned: Boolean = false, + val collapsed: Boolean = false, + @ColumnInfo(name = "child_order") val childOrder: Int = 0, + @ColumnInfo(name = "added_at") val addedAt: String = "", + @ColumnInfo(name = "updated_at") val updatedAt: String = "", + @ColumnInfo(name = "completed_at") val completedAt: String? = null, + @ColumnInfo(name = "item_type") val itemType: String = "TASK", + @ColumnInfo(name = "calendar_event_uid") val calendarEventUid: String? = null, + @ColumnInfo(name = "ical_url") val icalUrl: String? = null, + val etag: String? = null, + @ColumnInfo(name = "responsible_uid") val responsibleUid: String? = null, +) diff --git a/app/src/main/java/com/planify/mobile/di/DatabaseModule.kt b/app/src/main/java/com/planify/mobile/di/DatabaseModule.kt new file mode 100644 index 0000000..e8d5238 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/di/DatabaseModule.kt @@ -0,0 +1,27 @@ +package com.planify.mobile.di + +import android.content.Context +import androidx.room.Room +import com.planify.mobile.data.local.AppDatabase +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 provideDatabase(@ApplicationContext context: Context): AppDatabase = + Room.databaseBuilder(context, AppDatabase::class.java, "planify.db") + .fallbackToDestructiveMigration() + .build() + + @Provides fun provideTaskDao(db: AppDatabase) = db.taskDao() + @Provides fun provideProjectDao(db: AppDatabase) = db.projectDao() + @Provides fun provideSectionDao(db: AppDatabase) = db.sectionDao() +}