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/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/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/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()
+}
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)
+}
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")