Merge pull request 'Milestone/lot 1 architecture' (#31) from milestone/lot-1-architecture into main
Reviewed-on: Gato/Planify-mobile#31
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
|
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".PlanifyApp"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.PlanifyMobile">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.PlanifyMobile">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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<List<ProjectEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM projects WHERE is_favorite = 1 AND is_deleted = 0 ORDER BY child_order ASC")
|
||||||
|
fun getFavoriteProjects(): Flow<List<ProjectEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM projects WHERE parent_id = :parentId AND is_deleted = 0 ORDER BY child_order ASC")
|
||||||
|
fun getSubProjects(parentId: String): Flow<List<ProjectEntity>>
|
||||||
|
|
||||||
|
@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)
|
||||||
|
}
|
||||||
@@ -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<List<SectionEntity>>
|
||||||
|
|
||||||
|
@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)
|
||||||
|
}
|
||||||
@@ -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<List<TaskEntity>>
|
||||||
|
|
||||||
|
@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<List<TaskEntity>>
|
||||||
|
|
||||||
|
@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<List<TaskEntity>>
|
||||||
|
|
||||||
|
@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<List<TaskEntity>>
|
||||||
|
|
||||||
|
@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<List<TaskEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tasks WHERE parent_id = :parentId AND is_deleted = 0 ORDER BY child_order ASC")
|
||||||
|
fun getSubTasks(parentId: String): Flow<List<TaskEntity>>
|
||||||
|
|
||||||
|
@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)
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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 = "{}",
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.planify.mobile.domain.model
|
||||||
|
|
||||||
|
enum class BackendType { LOCAL, CALDAV, TODOIST }
|
||||||
@@ -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<Int> = emptyList(),
|
||||||
|
val recurrencyCount: Int? = null,
|
||||||
|
val recurrencyEnd: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class RecurrencyType {
|
||||||
|
NONE, MINUTELY, HOURLY, EVERY_DAY, EVERY_WEEK, EVERY_MONTH, EVERY_YEAR
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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 }
|
||||||
@@ -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 }
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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 }
|
||||||
@@ -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<String> = 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 }
|
||||||
@@ -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<List<Project>>
|
||||||
|
fun getFavoriteProjects(): Flow<List<Project>>
|
||||||
|
fun getSubProjects(parentId: String): Flow<List<Project>>
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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<List<Task>>
|
||||||
|
fun getTasksBySection(sectionId: String): Flow<List<Task>>
|
||||||
|
fun getInboxTasks(): Flow<List<Task>>
|
||||||
|
fun getTodayTasks(): Flow<List<Task>>
|
||||||
|
fun getOverdueTasks(): Flow<List<Task>>
|
||||||
|
fun getSubTasks(parentId: String): Flow<List<Task>>
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
@@ -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")
|
||||||
Reference in New Issue
Block a user