diff --git a/app/schemas/com.shaarit.data.local.database.ShaarliDatabase/9.json b/app/schemas/com.shaarit.data.local.database.ShaarliDatabase/9.json new file mode 100644 index 0000000..7fd5afe --- /dev/null +++ b/app/schemas/com.shaarit.data.local.database.ShaarliDatabase/9.json @@ -0,0 +1,757 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "a77d5257e0e1717bb3899da878237658", + "entities": [ + { + "tableName": "links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `tags` TEXT NOT NULL, `is_private` INTEGER NOT NULL, `is_pinned` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `sync_status` TEXT NOT NULL, `local_modified_at` INTEGER NOT NULL, `thumbnail_url` TEXT, `reading_time_minutes` INTEGER, `content_type` TEXT NOT NULL, `site_name` TEXT, `excerpt` TEXT, `link_check_status` TEXT NOT NULL, `fail_count` INTEGER NOT NULL, `last_health_check` INTEGER NOT NULL, `excluded_from_health_check` INTEGER NOT NULL, `reader_content` TEXT, `reader_content_fetched_at` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrivate", + "columnName": "is_private", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPinned", + "columnName": "is_pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncStatus", + "columnName": "sync_status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localModifiedAt", + "columnName": "local_modified_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readingTimeMinutes", + "columnName": "reading_time_minutes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "siteName", + "columnName": "site_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "excerpt", + "columnName": "excerpt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "linkCheckStatus", + "columnName": "link_check_status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "failCount", + "columnName": "fail_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHealthCheck", + "columnName": "last_health_check", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excludedFromHealthCheck", + "columnName": "excluded_from_health_check", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readerContent", + "columnName": "reader_content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readerContentFetchedAt", + "columnName": "reader_content_fetched_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_links_sync_status", + "unique": false, + "columnNames": [ + "sync_status" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_links_sync_status` ON `${TABLE_NAME}` (`sync_status`)" + }, + { + "name": "index_links_is_private", + "unique": false, + "columnNames": [ + "is_private" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_links_is_private` ON `${TABLE_NAME}` (`is_private`)" + }, + { + "name": "index_links_created_at", + "unique": false, + "columnNames": [ + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_links_created_at` ON `${TABLE_NAME}` (`created_at`)" + }, + { + "name": "index_links_is_pinned", + "unique": false, + "columnNames": [ + "is_pinned" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_links_is_pinned` ON `${TABLE_NAME}` (`is_pinned`)" + }, + { + "name": "index_links_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_links_url` ON `${TABLE_NAME}` (`url`)" + }, + { + "name": "index_links_content_type", + "unique": false, + "columnNames": [ + "content_type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_links_content_type` ON `${TABLE_NAME}` (`content_type`)" + }, + { + "name": "index_links_site_name", + "unique": false, + "columnNames": [ + "site_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_links_site_name` ON `${TABLE_NAME}` (`site_name`)" + }, + { + "name": "index_links_link_check_status", + "unique": false, + "columnNames": [ + "link_check_status" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_links_link_check_status` ON `${TABLE_NAME}` (`link_check_status`)" + }, + { + "name": "index_links_last_health_check", + "unique": false, + "columnNames": [ + "last_health_check" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_links_last_health_check` ON `${TABLE_NAME}` (`last_health_check`)" + } + ], + "foreignKeys": [] + }, + { + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "links", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_links_fts_BEFORE_UPDATE BEFORE UPDATE ON `links` BEGIN DELETE FROM `links_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_links_fts_BEFORE_DELETE BEFORE DELETE ON `links` BEGIN DELETE FROM `links_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_links_fts_AFTER_UPDATE AFTER UPDATE ON `links` BEGIN INSERT INTO `links_fts`(`docid`, `url`, `title`, `description`, `tags`, `excerpt`) VALUES (NEW.`rowid`, NEW.`url`, NEW.`title`, NEW.`description`, NEW.`tags`, NEW.`excerpt`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_links_fts_AFTER_INSERT AFTER INSERT ON `links` BEGIN INSERT INTO `links_fts`(`docid`, `url`, `title`, `description`, `tags`, `excerpt`) VALUES (NEW.`rowid`, NEW.`url`, NEW.`title`, NEW.`description`, NEW.`tags`, NEW.`excerpt`); END" + ], + "tableName": "links_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`url` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `tags` TEXT NOT NULL, `excerpt` TEXT, content=`links`)", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "excerpt", + "columnName": "excerpt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `occurrences` INTEGER NOT NULL, `last_used_at` INTEGER NOT NULL, `color` INTEGER, `is_favorite` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "occurrences", + "columnName": "occurrences", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUsedAt", + "columnName": "last_used_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [ + { + "name": "index_tags_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_tags_occurrences", + "unique": false, + "columnNames": [ + "occurrences" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tags_occurrences` ON `${TABLE_NAME}` (`occurrences`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "link_tag_cross_ref", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`link_id` INTEGER NOT NULL, `tag_name` TEXT NOT NULL, PRIMARY KEY(`link_id`, `tag_name`))", + "fields": [ + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tagName", + "columnName": "tag_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "link_id", + "tag_name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "collections", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `description` TEXT, `icon` TEXT NOT NULL, `color` INTEGER, `is_smart` INTEGER NOT NULL, `query` TEXT, `sort_order` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isSmart", + "columnName": "is_smart", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortOrder", + "columnName": "sort_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collections_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collections_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_collections_is_smart", + "unique": false, + "columnNames": [ + "is_smart" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collections_is_smart` ON `${TABLE_NAME}` (`is_smart`)" + }, + { + "name": "index_collections_sort_order", + "unique": false, + "columnNames": [ + "sort_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collections_sort_order` ON `${TABLE_NAME}` (`sort_order`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "collection_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`collection_id` INTEGER NOT NULL, `link_id` INTEGER NOT NULL, `added_at` INTEGER NOT NULL, PRIMARY KEY(`collection_id`, `link_id`))", + "fields": [ + { + "fieldPath": "collectionId", + "columnName": "collection_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "added_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "collection_id", + "link_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "reading_reminders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `link_id` INTEGER NOT NULL, `remind_at` INTEGER NOT NULL, `repeat_interval` TEXT NOT NULL, `is_dismissed` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, FOREIGN KEY(`link_id`) REFERENCES `links`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remindAt", + "columnName": "remind_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatInterval", + "columnName": "repeat_interval", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDismissed", + "columnName": "is_dismissed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_reading_reminders_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reading_reminders_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_reading_reminders_remind_at", + "unique": false, + "columnNames": [ + "remind_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reading_reminders_remind_at` ON `${TABLE_NAME}` (`remind_at`)" + }, + { + "name": "index_reading_reminders_is_dismissed", + "unique": false, + "columnNames": [ + "is_dismissed" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reading_reminders_is_dismissed` ON `${TABLE_NAME}` (`is_dismissed`)" + } + ], + "foreignKeys": [ + { + "table": "links", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "link_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "todos", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `shaarli_link_url` TEXT NOT NULL, `content` TEXT NOT NULL, `is_done` INTEGER NOT NULL, `due_date` INTEGER, `start_date` INTEGER, `repeat_mode` TEXT, `priority` TEXT, `tags` TEXT NOT NULL, `is_synced` INTEGER NOT NULL, `group_name` TEXT, `subtasks` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shaarliLinkUrl", + "columnName": "shaarli_link_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueDate", + "columnName": "due_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "start_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "repeatMode", + "columnName": "repeat_mode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSynced", + "columnName": "is_synced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "group_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subtasks", + "columnName": "subtasks", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_todos_shaarli_link_url", + "unique": true, + "columnNames": [ + "shaarli_link_url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_todos_shaarli_link_url` ON `${TABLE_NAME}` (`shaarli_link_url`)" + }, + { + "name": "index_todos_due_date", + "unique": false, + "columnNames": [ + "due_date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_todos_due_date` ON `${TABLE_NAME}` (`due_date`)" + }, + { + "name": "index_todos_start_date", + "unique": false, + "columnNames": [ + "start_date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_todos_start_date` ON `${TABLE_NAME}` (`start_date`)" + }, + { + "name": "index_todos_is_done", + "unique": false, + "columnNames": [ + "is_done" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_todos_is_done` ON `${TABLE_NAME}` (`is_done`)" + }, + { + "name": "index_todos_group_name", + "unique": false, + "columnNames": [ + "group_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_todos_group_name` ON `${TABLE_NAME}` (`group_name`)" + }, + { + "name": "index_todos_priority", + "unique": false, + "columnNames": [ + "priority" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_todos_priority` ON `${TABLE_NAME}` (`priority`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a77d5257e0e1717bb3899da878237658')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shaarit/data/local/database/ShaarliDatabase.kt b/app/src/main/java/com/shaarit/data/local/database/ShaarliDatabase.kt index 088116c..76293a6 100644 --- a/app/src/main/java/com/shaarit/data/local/database/ShaarliDatabase.kt +++ b/app/src/main/java/com/shaarit/data/local/database/ShaarliDatabase.kt @@ -36,7 +36,7 @@ import com.shaarit.data.local.entity.TodoEntity ReadingReminderEntity::class, TodoEntity::class ], - version = 8, + version = 9, exportSchema = true ) @TypeConverters(Converters::class) @@ -131,6 +131,19 @@ abstract class ShaarliDatabase : RoomDatabase() { } } + /** + * Migration v8 → v9 : Ajout de start_date, repeat_mode et priority + */ + val MIGRATION_8_9 = object : Migration(8, 9) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `todos` ADD COLUMN `start_date` INTEGER DEFAULT NULL") + db.execSQL("ALTER TABLE `todos` ADD COLUMN `repeat_mode` TEXT DEFAULT NULL") + db.execSQL("ALTER TABLE `todos` ADD COLUMN `priority` TEXT DEFAULT NULL") + db.execSQL("CREATE INDEX IF NOT EXISTS `index_todos_start_date` ON `todos` (`start_date`)") + db.execSQL("CREATE INDEX IF NOT EXISTS `index_todos_priority` ON `todos` (`priority`)") + } + } + @Volatile private var instance: ShaarliDatabase? = null @@ -146,7 +159,7 @@ abstract class ShaarliDatabase : RoomDatabase() { ShaarliDatabase::class.java, DATABASE_NAME ) - .addMigrations(MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8) + .addMigrations(MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9) .fallbackToDestructiveMigrationFrom(1, 2, 3) .build() } diff --git a/app/src/main/java/com/shaarit/data/local/entity/TodoEntity.kt b/app/src/main/java/com/shaarit/data/local/entity/TodoEntity.kt index 7f03bf6..3075ed2 100644 --- a/app/src/main/java/com/shaarit/data/local/entity/TodoEntity.kt +++ b/app/src/main/java/com/shaarit/data/local/entity/TodoEntity.kt @@ -11,8 +11,10 @@ import com.shaarit.domain.model.SubTask indices = [ Index(value = ["shaarli_link_url"], unique = true), Index(value = ["due_date"]), + Index(value = ["start_date"]), Index(value = ["is_done"]), - Index(value = ["group_name"]) + Index(value = ["group_name"]), + Index(value = ["priority"]) ] ) data class TodoEntity( @@ -31,6 +33,15 @@ data class TodoEntity( @ColumnInfo(name = "due_date") val dueDate: Long? = null, + @ColumnInfo(name = "start_date") + val startDate: Long? = null, + + @ColumnInfo(name = "repeat_mode") + val repeatMode: String? = null, + + @ColumnInfo(name = "priority") + val priority: String? = null, + @ColumnInfo(name = "tags") val tags: List = emptyList(), diff --git a/app/src/main/java/com/shaarit/data/repository/TodoRepositoryImpl.kt b/app/src/main/java/com/shaarit/data/repository/TodoRepositoryImpl.kt index ab3ebe9..0c44d1e 100644 --- a/app/src/main/java/com/shaarit/data/repository/TodoRepositoryImpl.kt +++ b/app/src/main/java/com/shaarit/data/repository/TodoRepositoryImpl.kt @@ -181,7 +181,10 @@ class TodoRepositoryImpl @Inject constructor( shaarliLinkUrl = url, content = cleanedContent, isDone = todo.isDone, + startDate = todo.startDate, dueDate = todo.dueDate, + repeatMode = todo.repeatMode, + priority = todo.priority, tags = cleanedTags, isSynced = false, groupName = todo.groupName?.trim()?.takeIf { it.isNotBlank() }, @@ -207,7 +210,10 @@ class TodoRepositoryImpl @Inject constructor( shaarliLinkUrl = shaarliLinkUrl, content = content, isDone = isDone, + startDate = startDate, dueDate = dueDate, + repeatMode = repeatMode, + priority = priority, tags = tags, isSynced = isSynced, groupName = groupName, diff --git a/app/src/main/java/com/shaarit/domain/model/TodoItem.kt b/app/src/main/java/com/shaarit/domain/model/TodoItem.kt index 53d03fc..ca9616c 100644 --- a/app/src/main/java/com/shaarit/domain/model/TodoItem.kt +++ b/app/src/main/java/com/shaarit/domain/model/TodoItem.kt @@ -5,7 +5,10 @@ data class TodoItem( val shaarliLinkUrl: String, val content: String, val isDone: Boolean = false, + val startDate: Long? = null, val dueDate: Long? = null, + val repeatMode: String? = null, + val priority: String? = null, val tags: List = emptyList(), val isSynced: Boolean = false, val groupName: String? = null, diff --git a/app/src/main/java/com/shaarit/presentation/todo/CustomRepeatDialog.kt b/app/src/main/java/com/shaarit/presentation/todo/CustomRepeatDialog.kt new file mode 100644 index 0000000..1c48a97 --- /dev/null +++ b/app/src/main/java/com/shaarit/presentation/todo/CustomRepeatDialog.kt @@ -0,0 +1,218 @@ +package com.shaarit.presentation.todo + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import org.json.JSONArray +import org.json.JSONObject + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CustomRepeatDialog( + initialRepeatMode: String?, + onDismiss: () -> Unit, + onSave: (String) -> Unit +) { + var interval by remember { mutableStateOf("1") } + var unit by remember { mutableStateOf("semaine") } + val units = listOf("jour", "semaine", "mois", "an") + var expandedUnit by remember { mutableStateOf(false) } + + val daysOfWeek = listOf("D", "L", "M", "M", "J", "V", "S") + var selectedDays by remember { mutableStateOf(setOf()) } + + var endMode by remember { mutableStateOf("jamais") } // "jamais", "date", "occurrences" + var endDate by remember { mutableStateOf("20 mai") } + var endOccurrences by remember { mutableStateOf("1") } + + // Parse initialRepeatMode if it's CUSTOM + LaunchedEffect(initialRepeatMode) { + if (initialRepeatMode?.startsWith("CUSTOM:") == true) { + try { + val json = JSONObject(initialRepeatMode.substring(7)) + interval = json.optString("interval", "1") + unit = json.optString("unit", "semaine") + + val daysArr = json.optJSONArray("daysOfWeek") + if (daysArr != null) { + val days = mutableSetOf() + for (i in 0 until daysArr.length()) { + days.add(daysArr.getInt(i)) + } + selectedDays = days + } + + endMode = json.optString("endMode", "jamais") + endDate = json.optString("endDate", "") + endOccurrences = json.optString("endOccurrences", "1") + } catch (e: Exception) { + // Default fallback + } + } + } + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + usePlatformDefaultWidth = false, + decorFitsSystemWindows = true + ) + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Répétition personnalisée") }, + navigationIcon = { + IconButton(onClick = onDismiss) { + Icon(Icons.Default.ArrowBack, contentDescription = "Retour") + } + }, + actions = { + TextButton(onClick = { + val json = JSONObject() + json.put("interval", interval) + json.put("unit", unit) + if (unit == "semaine") { + json.put("daysOfWeek", JSONArray(selectedDays.toList())) + } + json.put("endMode", endMode) + if (endMode == "date") json.put("endDate", endDate) + if (endMode == "occurrences") json.put("endOccurrences", endOccurrences) + onSave("CUSTOM:${json.toString()}") + }) { + Text("ENREGISTRER", color = MaterialTheme.colorScheme.primary) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + ) { + Text("Répéter par intervalle de", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + Spacer(modifier = Modifier.height(8.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + value = interval, + onValueChange = { interval = it }, + modifier = Modifier.width(80.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true + ) + Spacer(modifier = Modifier.width(16.dp)) + Box { + OutlinedButton(onClick = { expandedUnit = true }) { + Text(unit) + } + DropdownMenu( + expanded = expandedUnit, + onDismissRequest = { expandedUnit = false } + ) { + units.forEach { u -> + DropdownMenuItem( + text = { Text(u) }, + onClick = { unit = u; expandedUnit = false } + ) + } + } + } + } + + if (unit == "semaine") { + Spacer(modifier = Modifier.height(24.dp)) + Text("Répéter le", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + daysOfWeek.forEachIndexed { index, day -> + val isSelected = selectedDays.contains(index) + Box( + modifier = Modifier + .size(40.dp) + .background( + color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent, + shape = CircleShape + ) + .clickable { + if (isSelected) selectedDays = selectedDays - index + else selectedDays = selectedDays + index + }, + contentAlignment = Alignment.Center + ) { + Text( + text = day, + color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface + ) + } + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + Text("Se termine", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + Spacer(modifier = Modifier.height(8.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = endMode == "jamais", + onClick = { endMode = "jamais" } + ) + Text("Jamais", modifier = Modifier.clickable { endMode = "jamais" }) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = endMode == "date", + onClick = { endMode = "date" } + ) + Text("Le ", modifier = Modifier.clickable { endMode = "date" }) + OutlinedTextField( + value = endDate, + onValueChange = { endDate = it }, + modifier = Modifier.width(120.dp).padding(start = 8.dp), + enabled = endMode == "date", + singleLine = true + ) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = endMode == "occurrences", + onClick = { endMode = "occurrences" } + ) + Text("Après ", modifier = Modifier.clickable { endMode = "occurrences" }) + OutlinedTextField( + value = endOccurrences, + onValueChange = { endOccurrences = it }, + modifier = Modifier.width(80.dp).padding(horizontal = 8.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + enabled = endMode == "occurrences", + singleLine = true + ) + Text("occurrence", modifier = Modifier.clickable { endMode = "occurrences" }) + } + } + } + } +} diff --git a/app/src/main/java/com/shaarit/presentation/todo/TodoScreen.kt b/app/src/main/java/com/shaarit/presentation/todo/TodoScreen.kt index 1283e99..e4bc344 100644 --- a/app/src/main/java/com/shaarit/presentation/todo/TodoScreen.kt +++ b/app/src/main/java/com/shaarit/presentation/todo/TodoScreen.kt @@ -49,8 +49,10 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Event +import androidx.compose.material.icons.filled.Flag import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.List +import androidx.compose.material.icons.filled.Repeat import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.Snooze import androidx.compose.material.icons.outlined.CheckBoxOutlineBlank @@ -443,6 +445,9 @@ fun TodoScreen( onDismiss = viewModel::closeEditDialog, onContentChanged = viewModel::onEditContentChanged, onDueDateChanged = viewModel::onEditDueDateChanged, + onStartDateChanged = viewModel::onEditStartDateChanged, + onRepeatModeChanged = viewModel::onEditRepeatModeChanged, + onPriorityChanged = viewModel::onEditPriorityChanged, onGroupChanged = viewModel::onEditGroupChanged, onNewSubtaskTextChanged = viewModel::onEditNewSubtaskTextChanged, onAddSubtask = viewModel::addSubtask, @@ -657,6 +662,9 @@ private fun EditTodoDialog( onDismiss: () -> Unit, onContentChanged: (String) -> Unit, onDueDateChanged: (Long?) -> Unit, + onStartDateChanged: (Long?) -> Unit, + onRepeatModeChanged: (String?) -> Unit, + onPriorityChanged: (String?) -> Unit, onGroupChanged: (String) -> Unit, onNewSubtaskTextChanged: (String) -> Unit, onAddSubtask: () -> Unit, @@ -669,6 +677,10 @@ private fun EditTodoDialog( onSave: () -> Unit ) { var showDatePicker by remember { mutableStateOf(false) } + var showStartDatePicker by remember { mutableStateOf(false) } + var showRepeatMenu by remember { mutableStateOf(false) } + var showCustomRepeatDialog by remember { mutableStateOf(false) } + var showPriorityMenu by remember { mutableStateOf(false) } androidx.compose.ui.window.Dialog( onDismissRequest = onDismiss, @@ -737,6 +749,17 @@ private fun EditTodoDialog( androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + // Start Date + val startDateText = state.startDate?.let { formatDateTime(it, timezoneId) } ?: "Pas de date de début" + TaskActionRow( + icon = Icons.Default.Event, + text = startDateText, + onClick = { showStartDatePicker = true }, + isSet = state.startDate != null + ) + + androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + // Due Date val dateText = state.dueDate?.let { formatDateTime(it, timezoneId) } ?: "Aucune date d'échéance" TaskActionRow( @@ -748,6 +771,105 @@ private fun EditTodoDialog( androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + // Repeat Mode + val repeatText = when { + state.repeatMode == null || state.repeatMode == "NONE" -> "Ne pas répéter" + state.repeatMode == "DAILY" -> "Tous les jours" + state.repeatMode == "WEEKLY" -> "Toutes les semaines" + state.repeatMode == "MONTHLY" -> "Tous les mois" + state.repeatMode == "YEARLY" -> "Tous les ans" + state.repeatMode?.startsWith("CUSTOM") == true -> "Personnalisé..." + else -> "Ne pas répéter" + } + androidx.compose.foundation.layout.Box { + TaskActionRow( + icon = Icons.Default.Repeat, + text = repeatText, + onClick = { showRepeatMenu = true }, + isSet = state.repeatMode != null && state.repeatMode != "NONE" + ) + androidx.compose.material3.DropdownMenu( + expanded = showRepeatMenu, + onDismissRequest = { showRepeatMenu = false } + ) { + androidx.compose.material3.DropdownMenuItem( + text = { Text("Ne pas répéter") }, + onClick = { onRepeatModeChanged("NONE"); showRepeatMenu = false } + ) + androidx.compose.material3.DropdownMenuItem( + text = { Text("Tous les jours") }, + onClick = { onRepeatModeChanged("DAILY"); showRepeatMenu = false } + ) + androidx.compose.material3.DropdownMenuItem( + text = { Text("Toutes les semaines") }, + onClick = { onRepeatModeChanged("WEEKLY"); showRepeatMenu = false } + ) + androidx.compose.material3.DropdownMenuItem( + text = { Text("Tous les mois") }, + onClick = { onRepeatModeChanged("MONTHLY"); showRepeatMenu = false } + ) + androidx.compose.material3.DropdownMenuItem( + text = { Text("Tous les ans") }, + onClick = { onRepeatModeChanged("YEARLY"); showRepeatMenu = false } + ) + androidx.compose.material3.DropdownMenuItem( + text = { Text("Personnalisé...") }, + onClick = { showRepeatMenu = false; showCustomRepeatDialog = true } + ) + } + } + + androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + + // Priority + val priorityText = when(state.priority) { + "RED" -> "Urgent (Rouge)" + "YELLOW" -> "Élevée (Jaune)" + "BLUE" -> "Normale (Bleu)" + "GREEN" -> "Basse (Vert)" + else -> "Priorité normale" + } + val priorityColor = when(state.priority) { + "RED" -> androidx.compose.ui.graphics.Color.Red + "YELLOW" -> androidx.compose.ui.graphics.Color.Yellow + "BLUE" -> androidx.compose.ui.graphics.Color.Blue + "GREEN" -> androidx.compose.ui.graphics.Color.Green + else -> MaterialTheme.colorScheme.onSurfaceVariant + } + androidx.compose.foundation.layout.Box { + TaskActionRow( + icon = Icons.Default.Flag, + iconTint = priorityColor, + textTint = priorityColor, + text = priorityText, + onClick = { showPriorityMenu = true }, + isSet = state.priority != null + ) + androidx.compose.material3.DropdownMenu( + expanded = showPriorityMenu, + onDismissRequest = { showPriorityMenu = false } + ) { + androidx.compose.material3.DropdownMenuItem( + text = { Text("Urgent (Rouge)", color = androidx.compose.ui.graphics.Color.Red) }, + onClick = { onPriorityChanged("RED"); showPriorityMenu = false } + ) + androidx.compose.material3.DropdownMenuItem( + text = { Text("Élevée (Jaune)", color = androidx.compose.ui.graphics.Color.Yellow) }, + onClick = { onPriorityChanged("YELLOW"); showPriorityMenu = false } + ) + androidx.compose.material3.DropdownMenuItem( + text = { Text("Normale (Bleu)", color = androidx.compose.ui.graphics.Color.Blue) }, + onClick = { onPriorityChanged("BLUE"); showPriorityMenu = false } + ) + androidx.compose.material3.DropdownMenuItem( + text = { Text("Basse (Vert)", color = androidx.compose.ui.graphics.Color.Green) }, + onClick = { onPriorityChanged("GREEN"); showPriorityMenu = false } + ) + } + } + + androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + // Group (Famille) TaskGroupRow( groupName = state.groupName, @@ -791,6 +913,26 @@ private fun EditTodoDialog( onDateSelected = onDueDateChanged ) } + + if (showStartDatePicker) { + DateTimePickerBottomSheet( + initialDate = state.startDate, + timezoneId = timezoneId, + onDismiss = { showStartDatePicker = false }, + onDateSelected = onStartDateChanged + ) + } + + if (showCustomRepeatDialog) { + CustomRepeatDialog( + initialRepeatMode = state.repeatMode, + onDismiss = { showCustomRepeatDialog = false }, + onSave = { + onRepeatModeChanged(it) + showCustomRepeatDialog = false + } + ) + } } } @@ -799,7 +941,9 @@ private fun TaskActionRow( icon: androidx.compose.ui.graphics.vector.ImageVector, text: String, onClick: () -> Unit = {}, - isSet: Boolean = false + isSet: Boolean = false, + iconTint: androidx.compose.ui.graphics.Color? = null, + textTint: androidx.compose.ui.graphics.Color? = null ) { Row( modifier = Modifier @@ -811,12 +955,12 @@ private fun TaskActionRow( Icon( icon, contentDescription = null, - tint = if (isSet) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant + tint = iconTint ?: if (isSet) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.width(16.dp)) Text( text, - color = if (isSet) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + color = textTint ?: if (isSet) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge ) } diff --git a/app/src/main/java/com/shaarit/presentation/todo/TodoViewModel.kt b/app/src/main/java/com/shaarit/presentation/todo/TodoViewModel.kt index ceffde7..f8239aa 100644 --- a/app/src/main/java/com/shaarit/presentation/todo/TodoViewModel.kt +++ b/app/src/main/java/com/shaarit/presentation/todo/TodoViewModel.kt @@ -44,7 +44,10 @@ data class EditTodoDialogUiState( val isCreateMode: Boolean = false, val todoId: Long = 0, val content: String = "", + val startDate: Long? = null, val dueDate: Long? = null, + val repeatMode: String? = null, + val priority: String? = null, val tags: List = emptyList(), val groupName: String = "", val subtasks: List = emptyList(), @@ -310,7 +313,10 @@ class TodoViewModel @Inject constructor( isVisible = true, todoId = todo.id, content = todo.content, + startDate = todo.startDate, dueDate = todo.dueDate, + repeatMode = todo.repeatMode, + priority = todo.priority, tags = todo.tags, groupName = todo.groupName ?: "", subtasks = todo.subtasks, @@ -332,6 +338,18 @@ class TodoViewModel @Inject constructor( _editDialogState.update { it.copy(dueDate = dueDate) } } + fun onEditStartDateChanged(startDate: Long?) { + _editDialogState.update { it.copy(startDate = startDate) } + } + + fun onEditRepeatModeChanged(repeatMode: String?) { + _editDialogState.update { it.copy(repeatMode = repeatMode) } + } + + fun onEditPriorityChanged(priority: String?) { + _editDialogState.update { it.copy(priority = priority) } + } + fun onEditGroupChanged(groupName: String) { _editDialogState.update { it.copy(groupName = groupName) } } @@ -411,7 +429,10 @@ class TodoViewModel @Inject constructor( shaarliLinkUrl = "", content = content, isDone = false, + startDate = state.startDate, dueDate = state.dueDate, + repeatMode = state.repeatMode, + priority = state.priority, tags = state.tags, isSynced = false, groupName = state.groupName.takeIf { it.isNotBlank() }, @@ -423,7 +444,10 @@ class TodoViewModel @Inject constructor( shaarliLinkUrl = state.shaarliLinkUrl, content = content, isDone = state.isDone, + startDate = state.startDate, dueDate = state.dueDate, + repeatMode = state.repeatMode, + priority = state.priority, tags = state.tags, isSynced = false, groupName = state.groupName.takeIf { it.isNotBlank() }, diff --git a/version.properties b/version.properties index bc818d0..37d5f8d 100644 --- a/version.properties +++ b/version.properties @@ -1,3 +1,3 @@ -#Thu Apr 23 19:02:26 2026 -VERSION_NAME=2.13.0 -VERSION_CODE=40 \ No newline at end of file +#Thu Apr 23 19:46:44 2026 +VERSION_NAME=2.14.0 +VERSION_CODE=41 \ No newline at end of file