diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/DiaryTheme.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/DiaryTheme.kt index a6d2bfd4..dd34a2ae 100644 --- a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/DiaryTheme.kt +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/DiaryTheme.kt @@ -42,6 +42,7 @@ public fun DiaryTheme( onSurface = colorScheme.onSurface, ), LocalDiaryTypography provides DiaryTypography( + headlineMedium = MaterialTheme.typography.headlineMedium, labelSmall = MaterialTheme.typography.labelSmall, labelMedium = MaterialTheme.typography.labelMedium, labelLarge = MaterialTheme.typography.labelLarge, diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/typography/DiaryTypography.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/typography/DiaryTypography.kt index 6b426e8c..9283849a 100644 --- a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/typography/DiaryTypography.kt +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/typography/DiaryTypography.kt @@ -3,6 +3,7 @@ package io.github.taetae98coding.diary.core.design.system.typography import androidx.compose.ui.text.TextStyle public data class DiaryTypography( + val headlineMedium: TextStyle, val labelSmall: TextStyle, val labelMedium: TextStyle, val labelLarge: TextStyle, diff --git a/app/core/diary-database-room/schemas/io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase/2.json b/app/core/diary-database-room/schemas/io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase/2.json new file mode 100644 index 00000000..10134c58 --- /dev/null +++ b/app/core/diary-database-room/schemas/io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase/2.json @@ -0,0 +1,209 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "8e6b08fa78bcbc993963c679fe1d6ce9", + "entities": [ + { + "tableName": "MemoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL DEFAULT '', `title` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', `start` TEXT DEFAULT null, `endInclusive` TEXT DEFAULT null, `color` INTEGER NOT NULL DEFAULT -16777216, `isFinish` INTEGER NOT NULL DEFAULT 0, `isDelete` INTEGER NOT NULL DEFAULT 0, `owner` TEXT DEFAULT null, `updateAt` INTEGER NOT NULL DEFAULT 0, `serverUpdateAt` INTEGER DEFAULT null, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "endInclusive", + "columnName": "endInclusive", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-16777216" + }, + { + "fieldPath": "isFinish", + "columnName": "isFinish", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isDelete", + "columnName": "isDelete", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "owner", + "columnName": "owner", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "updateAt", + "columnName": "updateAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "serverUpdateAt", + "columnName": "serverUpdateAt", + "affinity": "INTEGER", + "defaultValue": "null" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "TagEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL DEFAULT '', `title` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', `color` INTEGER NOT NULL DEFAULT -16777216, `isFinish` INTEGER NOT NULL DEFAULT 0, `isDelete` INTEGER NOT NULL DEFAULT 0, `owner` TEXT DEFAULT null, `updateAt` INTEGER NOT NULL DEFAULT 0, `serverUpdateAt` INTEGER DEFAULT null, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-16777216" + }, + { + "fieldPath": "isFinish", + "columnName": "isFinish", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isDelete", + "columnName": "isDelete", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "owner", + "columnName": "owner", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "updateAt", + "columnName": "updateAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "serverUpdateAt", + "columnName": "serverUpdateAt", + "affinity": "INTEGER", + "defaultValue": "null" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "MemoBackupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `memoId` TEXT NOT NULL, PRIMARY KEY(`uid`, `memoId`), FOREIGN KEY(`memoId`) REFERENCES `MemoEntity`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "memoId", + "columnName": "memoId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid", + "memoId" + ] + }, + "foreignKeys": [ + { + "table": "MemoEntity", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "memoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "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, '8e6b08fa78bcbc993963c679fe1d6ce9')" + ] + } +} \ No newline at end of file diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt index 29441afc..566243f4 100644 --- a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt @@ -6,8 +6,10 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import io.github.taetae98coding.diary.core.diary.database.room.dao.MemoBackupEntityDao import io.github.taetae98coding.diary.core.diary.database.room.dao.MemoEntityDao +import io.github.taetae98coding.diary.core.diary.database.room.dao.TagEntityDao import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoBackupEntity import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoEntity +import io.github.taetae98coding.diary.core.diary.database.room.entity.TagEntity import io.github.taetae98coding.diary.core.diary.database.room.internal.DiaryDatabaseConstructor import io.github.taetae98coding.diary.library.room.InstantConverter import io.github.taetae98coding.diary.library.room.LocalDataConverter @@ -15,9 +17,10 @@ import io.github.taetae98coding.diary.library.room.LocalDataConverter @Database( entities = [ MemoEntity::class, + TagEntity::class, MemoBackupEntity::class, ], - version = 1 + version = 2, ) @ConstructedBy(DiaryDatabaseConstructor::class) @TypeConverters( @@ -26,5 +29,7 @@ import io.github.taetae98coding.diary.library.room.LocalDataConverter ) internal abstract class DiaryDatabase : RoomDatabase() { abstract fun memo(): MemoEntityDao + abstract fun tag(): TagEntityDao + abstract fun memoBackup(): MemoBackupEntityDao } diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/TagEntityDao.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/TagEntityDao.kt new file mode 100644 index 00000000..fd0abd31 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/TagEntityDao.kt @@ -0,0 +1,29 @@ +package io.github.taetae98coding.diary.core.diary.database.room.dao + +import androidx.room.Dao +import androidx.room.Query +import io.github.taetae98coding.diary.core.diary.database.room.entity.TagEntity +import kotlinx.coroutines.flow.Flow + +@Dao +internal abstract class TagEntityDao : EntityDao() { + @Query( + """ + SELECT * + FROM TagEntity + WHERE id = :tagId + """, + ) + abstract fun find(tagId: String): Flow + + @Query( + """ + SELECT * + FROM TagEntity + WHERE isDelete = 0 + AND (owner = :owner OR (owner IS NULL AND :owner IS NULL)) + ORDER BY title + """, + ) + abstract fun page(owner: String?): Flow> +} diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/TagRoomDao.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/TagRoomDao.kt new file mode 100644 index 00000000..06376683 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/TagRoomDao.kt @@ -0,0 +1,32 @@ +package io.github.taetae98coding.diary.core.diary.database.room.dao + +import io.github.taetae98coding.diary.core.diary.database.TagDao +import io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase +import io.github.taetae98coding.diary.core.diary.database.room.entity.TagEntity +import io.github.taetae98coding.diary.core.diary.database.room.mapper.toDto +import io.github.taetae98coding.diary.core.diary.database.room.mapper.toEntity +import io.github.taetae98coding.diary.core.model.tag.TagDto +import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +internal class TagRoomDao( + private val database: DiaryDatabase, +) : TagDao { + override suspend fun upsert(tag: TagDto) { + database.tag().upsert(tag.toEntity()) + } + + override fun find(tagId: String): Flow { + return database.tag().find(tagId).mapLatest { it?.toDto() } + } + + override fun page(owner: String?): Flow> { + return database.tag().page(owner) + .mapCollectionLatest(TagEntity::toDto) + } +} diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/TagEntity.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/TagEntity.kt new file mode 100644 index 00000000..a7fce016 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/TagEntity.kt @@ -0,0 +1,29 @@ +package io.github.taetae98coding.diary.core.diary.database.room.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.datetime.Instant + +@Entity +internal data class TagEntity( + @PrimaryKey + @ColumnInfo(defaultValue = "") + val id: String, + @ColumnInfo(defaultValue = "") + val title: String, + @ColumnInfo(defaultValue = "") + val description: String, + @ColumnInfo(defaultValue = "-16777216") + val color: Int, + @ColumnInfo(defaultValue = "0") + val isFinish: Boolean, + @ColumnInfo(defaultValue = "0") + val isDelete: Boolean, + @ColumnInfo(defaultValue = "null") + val owner: String?, + @ColumnInfo(defaultValue = "0") + val updateAt: Instant, + @ColumnInfo(defaultValue = "null") + val serverUpdateAt: Instant?, +) diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/mapper/TagMapper.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/mapper/TagMapper.kt new file mode 100644 index 00000000..7731953d --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/mapper/TagMapper.kt @@ -0,0 +1,38 @@ +package io.github.taetae98coding.diary.core.diary.database.room.mapper + +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoEntity +import io.github.taetae98coding.diary.core.diary.database.room.entity.TagEntity +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.core.model.memo.MemoDto +import io.github.taetae98coding.diary.core.model.tag.TagDetail +import io.github.taetae98coding.diary.core.model.tag.TagDto + +internal fun TagDto.toEntity(): TagEntity { + return TagEntity( + id = id, + title = detail.title, + description = detail.description, + color = detail.color, + isFinish = isFinish, + isDelete = isDelete, + owner = owner, + updateAt = updateAt, + serverUpdateAt = serverUpdateAt, + ) +} + +internal fun TagEntity.toDto(): TagDto { + return TagDto( + id = id, + detail = TagDetail( + title = title, + description = description, + color = color, + ), + owner = owner, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + serverUpdateAt = serverUpdateAt, + ) +} diff --git a/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/TagDao.kt b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/TagDao.kt new file mode 100644 index 00000000..9a81d3b9 --- /dev/null +++ b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/TagDao.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.core.diary.database + +import io.github.taetae98coding.diary.core.model.tag.TagDto +import kotlinx.coroutines.flow.Flow + +public interface TagDao { + public suspend fun upsert(tag: TagDto) + + public fun find(tagId: String):Flow + public fun page(owner: String?): Flow> +} diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/mapper/MemoMapper.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/mapper/MemoMapper.kt index 1dae2193..33403ec5 100644 --- a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/mapper/MemoMapper.kt +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/mapper/MemoMapper.kt @@ -3,6 +3,18 @@ package io.github.taetae98coding.diary.core.model.mapper import io.github.taetae98coding.diary.core.model.memo.Memo import io.github.taetae98coding.diary.core.model.memo.MemoDto +public fun Memo.toDto(): MemoDto { + return MemoDto( + id = id, + detail = detail, + owner = owner, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + serverUpdateAt = null, + ) +} + public fun MemoDto.toMemo(): Memo { return Memo( id = id, diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/mapper/TagMapper.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/mapper/TagMapper.kt new file mode 100644 index 00000000..cae225b4 --- /dev/null +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/mapper/TagMapper.kt @@ -0,0 +1,27 @@ +package io.github.taetae98coding.diary.core.model.mapper + +import io.github.taetae98coding.diary.core.model.tag.Tag +import io.github.taetae98coding.diary.core.model.tag.TagDto + +public fun Tag.toDto(): TagDto { + return TagDto( + id = id, + detail = detail, + owner = owner, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + serverUpdateAt = null, + ) +} + +public fun TagDto.toTag(): Tag { + return Tag( + id = id, + detail = detail, + owner = owner, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + ) +} diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/tag/Tag.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/tag/Tag.kt new file mode 100644 index 00000000..95b2bcbf --- /dev/null +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/tag/Tag.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.core.model.tag + +import kotlinx.datetime.Instant + +public data class Tag( + val id: String, + val detail: TagDetail, + val owner: String?, + val isFinish: Boolean, + val isDelete: Boolean, + val updateAt: Instant, +) diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/tag/TagDetail.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/tag/TagDetail.kt new file mode 100644 index 00000000..bc03abb2 --- /dev/null +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/tag/TagDetail.kt @@ -0,0 +1,7 @@ +package io.github.taetae98coding.diary.core.model.tag + +public data class TagDetail( + val title: String, + val description: String, + val color: Int, +) diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/tag/TagDto.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/tag/TagDto.kt new file mode 100644 index 00000000..4d1b23af --- /dev/null +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/tag/TagDto.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.core.model.tag + +import kotlinx.datetime.Instant + +public data class TagDto( + val id: String, + val detail: TagDetail, + val owner: String?, + val isFinish: Boolean, + val isDelete: Boolean, + val updateAt: Instant, + val serverUpdateAt: Instant?, +) diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/tag/TagDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/tag/TagDestination.kt new file mode 100644 index 00000000..4f7be878 --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/tag/TagDestination.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.core.navigation.tag + +import kotlinx.serialization.Serializable + +@Serializable +public data object TagDestination diff --git a/app/core/resources/src/commonMain/composeResources/values-ko/strings.xml b/app/core/resources/src/commonMain/composeResources/values-ko/strings.xml index 67d216a0..a6548938 100644 --- a/app/core/resources/src/commonMain/composeResources/values-ko/strings.xml +++ b/app/core/resources/src/commonMain/composeResources/values-ko/strings.xml @@ -14,6 +14,8 @@ 제목 설명 날짜 + 태그 + 태그 추가 %1$d년 %2$d월 %1$d월 %2$d일 @@ -31,6 +33,7 @@ 비밀번호 확인 메모 추가 + 태그 추가 제목을 입력해 주세요 🥲 계정을 찾을 수 없습니다 🧐 diff --git a/app/core/resources/src/commonMain/composeResources/values/strings.xml b/app/core/resources/src/commonMain/composeResources/values/strings.xml index 837fae89..2652ad3e 100644 --- a/app/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/resources/src/commonMain/composeResources/values/strings.xml @@ -14,6 +14,8 @@ Title Description Date + Tag + Tag Add %1$d. %2$d. %1$d. %2$d. @@ -31,6 +33,7 @@ Check Password Memo add + Tag add Title is blank Account Not Found diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/TagIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/TagIcon.kt new file mode 100644 index 00000000..c88c9a9c --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/TagIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Tag +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun TagIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.Tag, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt index 334c4141..22058c75 100644 --- a/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt +++ b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt @@ -1,7 +1,7 @@ package io.github.taetae98coding.diary.data.memo.repository -import io.github.taetae98coding.diary.core.diary.database.MemoBackupDao import io.github.taetae98coding.diary.core.diary.database.MemoDao +import io.github.taetae98coding.diary.core.model.mapper.toDto import io.github.taetae98coding.diary.core.model.mapper.toMemo import io.github.taetae98coding.diary.core.model.memo.Memo import io.github.taetae98coding.diary.core.model.memo.MemoDetail @@ -18,20 +18,9 @@ import org.koin.core.annotation.Factory @Factory internal class MemoRepositoryImpl( private val localDataSource: MemoDao, - private val backupDataSource: MemoBackupDao, ) : MemoRepository { override suspend fun upsert(memo: Memo) { - val dto = MemoDto( - id = memo.id, - detail = memo.detail, - owner = memo.owner, - isFinish = memo.isFinish, - isDelete = memo.isDelete, - updateAt = memo.updateAt, - serverUpdateAt = null, - ) - - localDataSource.upsert(dto) + localDataSource.upsert(memo.toDto()) } override suspend fun update(memoId: String, detail: MemoDetail) { diff --git a/app/data/tag/build.gradle.kts b/app/data/tag/build.gradle.kts new file mode 100644 index 00000000..14641c87 --- /dev/null +++ b/app/data/tag/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("diary.app.data") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:diary-database")) + implementation(project(":app:domain:tag")) + } + } + } +} diff --git a/app/data/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/data/tag/TagDataModule.kt b/app/data/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/data/tag/TagDataModule.kt new file mode 100644 index 00000000..e7d81c0a --- /dev/null +++ b/app/data/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/data/tag/TagDataModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.data.tag + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class TagDataModule diff --git a/app/data/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/data/tag/repository/TagRepositoryImpl.kt b/app/data/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/data/tag/repository/TagRepositoryImpl.kt new file mode 100644 index 00000000..d84cf3ad --- /dev/null +++ b/app/data/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/data/tag/repository/TagRepositoryImpl.kt @@ -0,0 +1,32 @@ +package io.github.taetae98coding.diary.data.tag.repository + +import io.github.taetae98coding.diary.core.diary.database.TagDao +import io.github.taetae98coding.diary.core.model.mapper.toDto +import io.github.taetae98coding.diary.core.model.mapper.toTag +import io.github.taetae98coding.diary.core.model.tag.Tag +import io.github.taetae98coding.diary.core.model.tag.TagDto +import io.github.taetae98coding.diary.domain.tag.repository.TagRepository +import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +internal class TagRepositoryImpl( + private val tagDao: TagDao, +) : TagRepository { + override suspend fun upsert(tag: Tag) { + tagDao.upsert(tag.toDto()) + } + + override fun find(tagId: String): Flow { + return tagDao.find(tagId).mapLatest { it?.toTag() } + } + + override fun page(owner: String?): Flow> { + return tagDao.page(owner) + .mapCollectionLatest(TagDto::toTag) + } +} diff --git a/app/domain/tag/build.gradle.kts b/app/domain/tag/build.gradle.kts new file mode 100644 index 00000000..7385bf5b --- /dev/null +++ b/app/domain/tag/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("diary.app.domain") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:domain:account")) + implementation(project(":app:domain:backup")) + } + } + } +} diff --git a/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/TagDomainModule.kt b/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/TagDomainModule.kt new file mode 100644 index 00000000..9e7c32f6 --- /dev/null +++ b/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/TagDomainModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.domain.tag + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class TagDomainModule diff --git a/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/repository/TagRepository.kt b/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/repository/TagRepository.kt new file mode 100644 index 00000000..509070d4 --- /dev/null +++ b/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/repository/TagRepository.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.domain.tag.repository + +import io.github.taetae98coding.diary.core.model.tag.Tag +import kotlinx.coroutines.flow.Flow + +public interface TagRepository { + public suspend fun upsert(tag: Tag) + + public fun find(tagId: String): Flow + public fun page(owner: String?): Flow> +} diff --git a/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/usecase/AddTagUseCase.kt b/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/usecase/AddTagUseCase.kt new file mode 100644 index 00000000..cbc46706 --- /dev/null +++ b/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/usecase/AddTagUseCase.kt @@ -0,0 +1,40 @@ +package io.github.taetae98coding.diary.domain.tag.usecase + +import io.github.taetae98coding.diary.common.exception.tag.TagTitleBlankException +import io.github.taetae98coding.diary.core.model.tag.Tag +import io.github.taetae98coding.diary.core.model.tag.TagDetail +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.tag.repository.TagRepository +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid +import kotlinx.coroutines.flow.first +import kotlinx.datetime.Clock +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalUuidApi::class) +@Factory +public class AddTagUseCase internal constructor( + private val clock: Clock, + private val getAccountUseCase: GetAccountUseCase, + private val repository: TagRepository, +) { + public suspend operator fun invoke(detail: TagDetail): Result { + return runCatching { + if (detail.title.isBlank()) throw TagTitleBlankException() + + val account = getAccountUseCase().first().getOrThrow() + val id = Uuid.random().toString() + + val tag = Tag( + id = id, + detail = detail, + owner = account.uid, + isFinish = false, + isDelete = false, + updateAt = clock.now(), + ) + + repository.upsert(tag) + } + } +} diff --git a/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/usecase/FindTagUseCase.kt b/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/usecase/FindTagUseCase.kt new file mode 100644 index 00000000..8340a742 --- /dev/null +++ b/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/usecase/FindTagUseCase.kt @@ -0,0 +1,26 @@ +package io.github.taetae98coding.diary.domain.tag.usecase + +import io.github.taetae98coding.diary.core.model.tag.Tag +import io.github.taetae98coding.diary.domain.tag.repository.TagRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class FindTagUseCase internal constructor( + private val repository: TagRepository, +) { + public operator fun invoke(tagId: String?): Flow> { + if (tagId.isNullOrBlank()) return flowOf(Result.success(null)) + + return flow { emitAll(repository.find(tagId)) } + .mapLatest { Result.success(it) } + .catch { emit(Result.failure(it)) } + } +} diff --git a/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/usecase/PageTagUseCase.kt b/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/usecase/PageTagUseCase.kt new file mode 100644 index 00000000..7411dd08 --- /dev/null +++ b/app/domain/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/tag/usecase/PageTagUseCase.kt @@ -0,0 +1,32 @@ +package io.github.taetae98coding.diary.domain.tag.usecase + +import io.github.taetae98coding.diary.core.model.tag.Tag +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.tag.repository.TagRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.mapLatest +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class PageTagUseCase internal constructor( + private val getAccountUseCase: GetAccountUseCase, + private val repository: TagRepository, +) { + public operator fun invoke(): Flow>> { + return flow { + getAccountUseCase().mapLatest { it.getOrThrow() } + .flatMapLatest { repository.page(it.uid) } + .also { emitAll(it) } + }.mapLatest { + Result.success(it) + }.catch { + emit(Result.failure(it)) + } + } +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreen.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreen.kt index 9ff3134a..7cc9bd54 100644 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreen.kt +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreen.kt @@ -73,7 +73,7 @@ internal fun MemoDetailScreen( } } - else -> Unit + is MemoDetailNavigationButton.None -> Unit } }, actions = { @@ -111,7 +111,7 @@ internal fun MemoDetailScreen( } } - else -> Unit + is MemoDetailFloatingButton.None -> Unit } }, ) { diff --git a/app/feature/tag/build.gradle.kts b/app/feature/tag/build.gradle.kts new file mode 100644 index 00000000..f6887fa8 --- /dev/null +++ b/app/feature/tag/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("diary.app.feature") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:domain:tag")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.feature.tag" +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagFeatureModule.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagFeatureModule.kt new file mode 100644 index 00000000..9247bacd --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagFeatureModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.feature.tag + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class TagFeatureModule diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagNavigation.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagNavigation.kt new file mode 100644 index 00000000..fce4f59a --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagNavigation.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.feature.tag + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import io.github.taetae98coding.diary.core.navigation.tag.TagDestination + +public fun NavGraphBuilder.tagNavigation( + navController: NavController, +) { + composable { + TagRoute() + } +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagRoute.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagRoute.kt new file mode 100644 index 00000000..7ddcc933 --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagRoute.kt @@ -0,0 +1,191 @@ +package io.github.taetae98coding.diary.feature.tag + +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.taetae98coding.diary.core.design.system.diary.color.rememberDiaryColorState +import io.github.taetae98coding.diary.core.design.system.diary.component.rememberDiaryComponentState +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.tag_add +import io.github.taetae98coding.diary.feature.tag.add.TagAddViewModel +import io.github.taetae98coding.diary.feature.tag.detail.TagDetailFloatingButton +import io.github.taetae98coding.diary.feature.tag.detail.TagDetailNavigationButton +import io.github.taetae98coding.diary.feature.tag.detail.TagDetailScreen +import io.github.taetae98coding.diary.feature.tag.detail.TagDetailScreenState +import io.github.taetae98coding.diary.feature.tag.detail.TagDetailViewModel +import io.github.taetae98coding.diary.feature.tag.list.TagListFloatingButton +import io.github.taetae98coding.diary.feature.tag.list.TagListScreen +import io.github.taetae98coding.diary.feature.tag.list.TagListViewModel +import io.github.taetae98coding.library.compose.isDetailVisible +import io.github.taetae98coding.library.compose.isListVisible +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +internal fun TagRoute( + modifier: Modifier = Modifier, + listViewModel: TagListViewModel = koinViewModel(), + addViewModel: TagAddViewModel = koinViewModel(), + detailViewModel: TagDetailViewModel = koinViewModel(), +) { + val navigator = rememberListDetailPaneScaffoldNavigator() + + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + val isFloatingVisible by remember { + derivedStateOf { + val isAdd = navigator.currentDestination?.content == null + + isAdd && navigator.isListVisible() && !navigator.isDetailVisible() + } + } + + val list by listViewModel.list.collectAsStateWithLifecycle() + + TagListScreen( + floatingButtonProvider = { + if (isFloatingVisible) { + TagListFloatingButton.Add( + onAdd = { + navigator.navigateTo( + ThreePaneScaffoldRole.Primary, + ) + }, + ) + } else { + TagListFloatingButton.None + } + }, + listProvider = { list }, + onTag = { navigator.navigateTo(ThreePaneScaffoldRole.Primary, it) }, + ) + } + }, + detailPane = { + AnimatedPane { + val isAdd by remember { + derivedStateOf { + navigator.currentDestination?.content == null + } + } + + val state = if (isAdd) { + rememberTagDetailAddState() + } else { + rememberTagDetailDetailState() + } + + val tagAddTitle = stringResource(Res.string.tag_add) + val tag by detailViewModel.tag.collectAsStateWithLifecycle() + + val isNavigateUpVisible by remember { + derivedStateOf { + !navigator.isListVisible() && navigator.isDetailVisible() + } + } + + val isFloatingVisible by remember { + derivedStateOf { + isAdd && navigator.isDetailVisible() + } + } + + val uiState = if (isAdd) { + addViewModel.uiState.collectAsStateWithLifecycle() + } else { + addViewModel.uiState.collectAsStateWithLifecycle() + } + + TagDetailScreen( + state = state, + titleProvider = { + if (isAdd) { + tagAddTitle + } else { + tag?.detail?.title + } + }, + navigateButtonProvider = { + if (isNavigateUpVisible) { + TagDetailNavigationButton.NavigateUp(onNavigateUp = navigator::navigateBack) + } else { + TagDetailNavigationButton.None + } + }, + floatingButtonProvider = { + if (isFloatingVisible) { + TagDetailFloatingButton.Add(onAdd = { addViewModel.add(state.tagDetail) }) + } else { + TagDetailFloatingButton.None + } + }, + uiStateProvider = { uiState.value }, + ) + } + }, + modifier = modifier, + ) + + LaunchedFetch( + navigator = navigator, + detailViewModel = detailViewModel, + ) +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +private fun LaunchedFetch( + navigator: ThreePaneScaffoldNavigator, + detailViewModel: TagDetailViewModel, +) { + LaunchedEffect(navigator.currentDestination?.content, detailViewModel) { + detailViewModel.fetch(navigator.currentDestination?.content) + } +} + +@Composable +private fun rememberTagDetailAddState(): TagDetailScreenState.Add { + val coroutineScope = rememberCoroutineScope() + val componentState = rememberDiaryComponentState() + val colorState = rememberDiaryColorState() + + return remember { + TagDetailScreenState.Add( + coroutineScope = coroutineScope, + componentState = componentState, + colorState = colorState, + ) + } +} + +@Composable +private fun rememberTagDetailDetailState(): TagDetailScreenState.Detail { + val coroutineScope = rememberCoroutineScope() + val componentState = rememberDiaryComponentState() + val colorState = rememberDiaryColorState() + + return remember { + TagDetailScreenState.Detail( + onUpdate = {}, + onDelete = {}, + coroutineScope = coroutineScope, + componentState = componentState, + colorState = colorState, + ) + } +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddViewModel.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddViewModel.kt new file mode 100644 index 00000000..feb22c9c --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddViewModel.kt @@ -0,0 +1,47 @@ +package io.github.taetae98coding.diary.feature.tag.add + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.common.exception.tag.TagTitleBlankException +import io.github.taetae98coding.diary.core.model.tag.TagDetail +import io.github.taetae98coding.diary.domain.tag.usecase.AddTagUseCase +import io.github.taetae98coding.diary.feature.tag.detail.TagDetailScreenUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel + +@KoinViewModel +internal class TagAddViewModel( + private val addTagUseCase: AddTagUseCase, +) : ViewModel() { + private val _uiState = MutableStateFlow(TagDetailScreenUiState(onMessageShow = ::clearMessage)) + val uiState = _uiState.asStateFlow() + + fun add(detail: TagDetail) { + viewModelScope.launch { + _uiState.update { it.copy(isProgress = true) } + addTagUseCase(detail) + .onSuccess { _uiState.update { it.copy(isProgress = false, isAdd = true) } } + .onFailure { handleThrowable(it) } + } + } + + private fun handleThrowable(throwable: Throwable) { + when (throwable) { + is TagTitleBlankException -> _uiState.update { it.copy(isProgress = false, isTitleBlankError = true) } + else -> _uiState.update { it.copy(isProgress = false, isUnknownError = true) } + } + } + + private fun clearMessage() { + _uiState.update { + it.copy( + isAdd = false, + isTitleBlankError = false, + isUnknownError = false, + ) + } + } +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailFloatingButton.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailFloatingButton.kt new file mode 100644 index 00000000..2ecc5321 --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailFloatingButton.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.feature.tag.detail + +internal sealed class TagDetailFloatingButton { + data object None : TagDetailFloatingButton() + data class Add(val onAdd: () -> Unit) : TagDetailFloatingButton() +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailNavigationButton.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailNavigationButton.kt new file mode 100644 index 00000000..9bf91a40 --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailNavigationButton.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.feature.tag.detail + +internal sealed class TagDetailNavigationButton { + data object None : TagDetailNavigationButton() + data class NavigateUp(val onNavigateUp: () -> Unit) : TagDetailNavigationButton() +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreen.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreen.kt new file mode 100644 index 00000000..a0eaa4cb --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreen.kt @@ -0,0 +1,173 @@ +package io.github.taetae98coding.diary.feature.tag.detail + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.design.system.diary.color.DiaryColor +import io.github.taetae98coding.diary.core.design.system.diary.component.DiaryComponent +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.icon.AddIcon +import io.github.taetae98coding.diary.core.resources.icon.NavigateUpIcon +import io.github.taetae98coding.diary.core.resources.tag_add_message +import io.github.taetae98coding.diary.core.resources.title_blank_error +import io.github.taetae98coding.diary.core.resources.unknown_error +import org.jetbrains.compose.resources.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun TagDetailScreen( + state: TagDetailScreenState, + titleProvider: () -> String?, + navigateButtonProvider: () -> TagDetailNavigationButton, + floatingButtonProvider: () -> TagDetailFloatingButton, + uiStateProvider: () -> TagDetailScreenUiState, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { titleProvider()?.let { Text(text = it) } }, + navigationIcon = { + when (val button = navigateButtonProvider()) { + is TagDetailNavigationButton.NavigateUp -> { + IconButton(onClick = button.onNavigateUp) { + NavigateUpIcon() + } + } + + is TagDetailNavigationButton.None -> Unit + } + }, + ) + }, + snackbarHost = { SnackbarHost(hostState = state.hostState) }, + floatingActionButton = { + when (val button = floatingButtonProvider()) { + is TagDetailFloatingButton.Add -> { + FloatingActionButton(onClick = button.onAdd) { + AddIcon() + } + } + + is TagDetailFloatingButton.None -> Unit + } + }, + ) { + Content( + state = state, + modifier = Modifier.fillMaxSize() + .padding(DiaryTheme.dimen.screenPaddingValues) + .padding(it), + ) + } + + Message( + state = state, + uiStateProvider = uiStateProvider, + ) + + LaunchedFocus(state = state) +} + +@Composable +private fun Message( + state: TagDetailScreenState, + uiStateProvider: () -> TagDetailScreenUiState, +) { + val uiState = uiStateProvider() + val addMessage = stringResource(Res.string.tag_add_message) + val titleBlankMessage = stringResource(Res.string.title_blank_error) + val unknownErrorMessage = stringResource(Res.string.unknown_error) + + LaunchedEffect( + uiState.isAdd, + uiState.isDelete, + uiState.isUpdate, + uiState.isTitleBlankError, + uiState.isUnknownError, + ) { + if (!uiState.hasMessage) return@LaunchedEffect + + when { + uiState.isAdd -> { + state.showMessage(addMessage) + state.clearInput() + state.requestTitleFocus() + } + + uiState.isDelete -> { + if (state is TagDetailScreenState.Detail) { + state.onDelete() + } + } + + uiState.isUpdate -> { + if (state is TagDetailScreenState.Detail) { + state.onUpdate() + } + } + + uiState.isTitleBlankError -> { + state.showMessage(titleBlankMessage) + state.titleError() + } + + uiState.isUnknownError -> state.showMessage(unknownErrorMessage) + } + + uiState.onMessageShow() + } +} + +@Composable +private fun LaunchedFocus( + state: TagDetailScreenState, +) { + LaunchedEffect(state) { + if (state is TagDetailScreenState.Add) { + state.requestTitleFocus() + } + } +} + +@Composable +private fun Content( + state: TagDetailScreenState, + modifier: Modifier = Modifier, +) { + Column( + modifier = Modifier.verticalScroll(state = rememberScrollState()) + .then(modifier), + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + DiaryComponent(state = state.componentState) + Row { + DiaryColor( + state = state.colorState, + modifier = Modifier.weight(1F) + .height(100.dp), + ) + + Spacer(modifier = Modifier.weight(1F)) + } + } +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenState.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenState.kt new file mode 100644 index 00000000..fa5a6222 --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenState.kt @@ -0,0 +1,60 @@ +package io.github.taetae98coding.diary.feature.tag.detail + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.ui.graphics.toArgb +import io.github.taetae98coding.diary.core.design.system.diary.color.DiaryColorState +import io.github.taetae98coding.diary.core.design.system.diary.component.DiaryComponentState +import io.github.taetae98coding.diary.core.model.tag.TagDetail +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +internal sealed class TagDetailScreenState { + abstract val coroutineScope: CoroutineScope + abstract val componentState: DiaryComponentState + abstract val colorState: DiaryColorState + + private var messageJob: Job? = null + + val hostState: SnackbarHostState = SnackbarHostState() + + data class Add( + override val coroutineScope: CoroutineScope, + override val componentState: DiaryComponentState, + override val colorState: DiaryColorState, + ) : TagDetailScreenState() + + data class Detail( + val onUpdate: () -> Unit, + val onDelete: () -> Unit, + override val coroutineScope: CoroutineScope, + override val componentState: DiaryComponentState, + override val colorState: DiaryColorState, + ) : TagDetailScreenState() + + val tagDetail: TagDetail + get() { + return TagDetail( + title = componentState.title, + description = componentState.description, + color = colorState.color.toArgb(), + ) + } + + fun requestTitleFocus() { + componentState.requestTitleFocus() + } + + fun clearInput() { + componentState.clearInput() + } + + fun titleError() { + componentState.titleError() + } + + fun showMessage(message: String) { + messageJob?.cancel() + messageJob = coroutineScope.launch { hostState.showSnackbar(message) } + } +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenUiState.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenUiState.kt new file mode 100644 index 00000000..e219ccbf --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenUiState.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.feature.tag.detail + +internal data class TagDetailScreenUiState( + val isProgress: Boolean = false, + val isAdd: Boolean = false, + val isDelete: Boolean = false, + val isUpdate: Boolean = false, + val isTitleBlankError: Boolean = false, + val isUnknownError: Boolean = false, + val onMessageShow: () -> Unit = {}, +) { + val hasMessage = isAdd || isDelete || isUpdate || isTitleBlankError || isUnknownError +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailViewModel.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailViewModel.kt new file mode 100644 index 00000000..913e78c6 --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailViewModel.kt @@ -0,0 +1,37 @@ +package io.github.taetae98coding.diary.feature.tag.detail + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.domain.tag.usecase.FindTagUseCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import org.koin.android.annotation.KoinViewModel + +@OptIn(ExperimentalCoroutinesApi::class) +@KoinViewModel +internal class TagDetailViewModel( + private val savedStateHandle: SavedStateHandle, + private val findTagUseCase: FindTagUseCase, +) : ViewModel() { + private val tagId = savedStateHandle.getStateFlow(TAG_ID, null) + + val tag = tagId.flatMapLatest { findTagUseCase(it) } + .mapLatest { it.getOrNull() } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) + + fun fetch(tagId: String?) { + savedStateHandle[TAG_ID] = tagId + } + + companion object { + private const val TAG_ID = "TAG_ID" + } +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListFloatingButton.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListFloatingButton.kt new file mode 100644 index 00000000..413e5b5b --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListFloatingButton.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.feature.tag.list + +internal sealed class TagListFloatingButton { + data object None : TagListFloatingButton() + data class Add(val onAdd: () -> Unit) : TagListFloatingButton() +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreen.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreen.kt new file mode 100644 index 00000000..dac60938 --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreen.kt @@ -0,0 +1,132 @@ +package io.github.taetae98coding.diary.feature.tag.list + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.core.model.tag.Tag +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.icon.AddIcon +import io.github.taetae98coding.diary.core.resources.tag +import org.jetbrains.compose.resources.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun TagListScreen( + floatingButtonProvider: () -> TagListFloatingButton, + listProvider: () -> List?, + onTag: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(text = stringResource(Res.string.tag)) }, + ) + }, + floatingActionButton = { + when (val button = floatingButtonProvider()) { + is TagListFloatingButton.Add -> { + FloatingActionButton(onClick = button.onAdd) { + AddIcon() + } + } + + is TagListFloatingButton.None -> Unit + } + }, + ) { + Content( + listProvider = listProvider, + onTag = onTag, + modifier = Modifier.fillMaxSize() + .padding(it), + ) + } +} + +@Composable +private fun Content( + listProvider: () -> List?, + onTag: (String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + contentPadding = DiaryTheme.dimen.screenPaddingValues, + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + val list = listProvider() + + if (list == null) { + items( + count = 5, + contentType = { "Tag" }, + ) { + TagItem( + tag = null, + onClick = {}, + ) + } + } else { + if (list.isEmpty()) { + item { + Box( + modifier = Modifier.fillParentMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "태그가 없습니다 🐶", + style = DiaryTheme.typography.headlineMedium, + ) + } + } + } else { + items( + items = list, + key = { it.id }, + contentType = { "Tag" }, + ) { + TagItem( + onClick = { onTag(it.id) }, + tag = it, + ) + } + } + } + } +} + +@Composable +private fun TagItem( + tag: Tag?, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Card( + onClick = onClick, + modifier = modifier, + ) { + Box( + modifier = Modifier.fillMaxSize() + .padding(12.dp), + contentAlignment = Alignment.Center, + ) { + Text(text = tag?.detail?.title.orEmpty()) + } + } +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListViewModel.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListViewModel.kt new file mode 100644 index 00000000..78f0b307 --- /dev/null +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListViewModel.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.feature.tag.list + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.domain.tag.usecase.PageTagUseCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import org.koin.android.annotation.KoinViewModel + +@OptIn(ExperimentalCoroutinesApi::class) +@KoinViewModel +internal class TagListViewModel( + pageTagUseCase: PageTagUseCase, +) : ViewModel() { + val list = pageTagUseCase().mapLatest { it.getOrNull() } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) +} diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt index 6662c3bc..f19bb7db 100644 --- a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt @@ -9,7 +9,7 @@ import androidx.activity.result.contract.ActivityResultContracts import io.github.taetae98coding.diary.app.App public class DiaryActivity : ComponentActivity() { - private val notificationPermissionLauncer = registerForActivityResult( + private val notificationPermissionLauncher = registerForActivityResult( contract = ActivityResultContracts.RequestPermission(), callback = {} ) @@ -21,6 +21,6 @@ public class DiaryActivity : ComponentActivity() { App() } - notificationPermissionLauncer.launch(Manifest.permission.POST_NOTIFICATIONS) + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } } diff --git a/app/platform/common/build.gradle.kts b/app/platform/common/build.gradle.kts index 209b0741..461d51ee 100644 --- a/app/platform/common/build.gradle.kts +++ b/app/platform/common/build.gradle.kts @@ -7,6 +7,7 @@ kotlin { commonMain { dependencies { implementation(project(":app:data:memo")) + implementation(project(":app:data:tag")) implementation(project(":app:data:account")) implementation(project(":app:data:credential")) implementation(project(":app:data:holiday")) @@ -15,6 +16,7 @@ kotlin { implementation(project(":app:data:fcm")) implementation(project(":app:domain:memo")) + implementation(project(":app:domain:tag")) implementation(project(":app:domain:account")) implementation(project(":app:domain:credential")) implementation(project(":app:domain:holiday")) @@ -27,6 +29,7 @@ kotlin { implementation(project(":app:core:holiday-service")) implementation(project(":app:feature:memo")) + implementation(project(":app:feature:tag")) implementation(project(":app:feature:calendar")) implementation(project(":app:feature:more")) implementation(project(":app:feature:account")) diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/App.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/App.kt index efdf640b..e2cb32db 100644 --- a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/App.kt +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/App.kt @@ -21,9 +21,11 @@ import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme import io.github.taetae98coding.diary.core.navigation.calendar.CalendarDestination import io.github.taetae98coding.diary.core.navigation.memo.MemoDestination import io.github.taetae98coding.diary.core.navigation.more.MoreDestination +import io.github.taetae98coding.diary.core.navigation.tag.TagDestination import io.github.taetae98coding.diary.core.resources.icon.CalendarIcon import io.github.taetae98coding.diary.core.resources.icon.MemoIcon import io.github.taetae98coding.diary.core.resources.icon.MoreIcon +import io.github.taetae98coding.diary.core.resources.icon.TagIcon import org.jetbrains.compose.resources.stringResource @Composable @@ -45,6 +47,7 @@ private fun AppScaffold( val isNavigationVisible by remember { derivedStateOf { val visibleDestination = listOf( + TagDestination::class, MemoDestination::class, CalendarDestination::class, MoreDestination::class, @@ -62,6 +65,7 @@ private fun AppScaffold( navigationSuiteItems = { listOf( // AppNavigation.Memo, + AppNavigation.Tag, AppNavigation.Calendar, AppNavigation.More, ).forEach { navigation -> @@ -99,5 +103,6 @@ private fun AppNavigationIcon( AppNavigation.Memo -> MemoIcon(modifier = modifier) AppNavigation.Calendar -> CalendarIcon(modifier = modifier) AppNavigation.More -> MoreIcon(modifier = modifier) + AppNavigation.Tag -> TagIcon(modifier = modifier) } } diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt index bb6b99bd..9bad7996 100644 --- a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt @@ -10,6 +10,7 @@ import io.github.taetae98coding.diary.data.fcm.FCMDataModule import io.github.taetae98coding.diary.data.fetch.FetchDataModule import io.github.taetae98coding.diary.data.holiday.HolidayDataModule import io.github.taetae98coding.diary.data.memo.MemoDataModule +import io.github.taetae98coding.diary.data.tag.TagDataModule import io.github.taetae98coding.diary.domain.account.AccountDomainModule import io.github.taetae98coding.diary.domain.backup.BackupDomainModule import io.github.taetae98coding.diary.domain.credential.CredentialDomainModule @@ -17,10 +18,12 @@ import io.github.taetae98coding.diary.domain.fcm.FCMDomainModule import io.github.taetae98coding.diary.domain.fetch.FetchDomainModule import io.github.taetae98coding.diary.domain.holiday.HolidayDomainModule import io.github.taetae98coding.diary.domain.memo.MemoDomainModule +import io.github.taetae98coding.diary.domain.tag.TagDomainModule import io.github.taetae98coding.diary.feature.account.AccountFeatureModule import io.github.taetae98coding.diary.feature.calendar.CalendarFeatureModule import io.github.taetae98coding.diary.feature.memo.MemoFeatureModule import io.github.taetae98coding.diary.feature.more.MoreFeatureModule +import io.github.taetae98coding.diary.feature.tag.TagFeatureModule import io.github.taetae98coding.diary.library.firebase.KFirebase import io.github.taetae98coding.diary.library.firebase.messaging.KFirebaseMessaging import io.github.taetae98coding.diary.library.firebase.messaging.messaging @@ -35,6 +38,7 @@ import org.koin.core.annotation.Singleton DiaryServiceModule::class, HolidayServiceModule::class, MemoDataModule::class, + TagDataModule::class, AccountDataModule::class, HolidayDataModule::class, BackupDataModule::class, @@ -42,6 +46,7 @@ import org.koin.core.annotation.Singleton FCMDataModule::class, CredentialDataModule::class, MemoDomainModule::class, + TagDomainModule::class, AccountDomainModule::class, HolidayDomainModule::class, BackupDomainModule::class, @@ -49,6 +54,7 @@ import org.koin.core.annotation.Singleton FCMDomainModule::class, CredentialDomainModule::class, MemoFeatureModule::class, + TagFeatureModule::class, CalendarFeatureModule::class, MoreFeatureModule::class, AccountFeatureModule::class, diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppNavHost.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppNavHost.kt index 5699e549..7e11c8e6 100644 --- a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppNavHost.kt +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppNavHost.kt @@ -9,6 +9,7 @@ import io.github.taetae98coding.diary.feature.account.accountNavigation import io.github.taetae98coding.diary.feature.calendar.calendarNavigation import io.github.taetae98coding.diary.feature.memo.memoNavigation import io.github.taetae98coding.diary.feature.more.moreNavigation +import io.github.taetae98coding.diary.feature.tag.tagNavigation @Composable internal fun AppNavHost( @@ -21,6 +22,7 @@ internal fun AppNavHost( modifier = modifier, ) { memoNavigation(navController = state.navController) + tagNavigation(navController = state.navController) calendarNavigation(navController = state.navController) moreNavigation(navController = state.navController) accountNavigation(navController = state.navController) diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/navigation/AppNavigation.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/navigation/AppNavigation.kt index a159440e..a41d5638 100644 --- a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/navigation/AppNavigation.kt +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/navigation/AppNavigation.kt @@ -3,10 +3,12 @@ package io.github.taetae98coding.diary.app.navigation import io.github.taetae98coding.diary.core.navigation.calendar.CalendarDestination import io.github.taetae98coding.diary.core.navigation.memo.MemoDestination import io.github.taetae98coding.diary.core.navigation.more.MoreDestination +import io.github.taetae98coding.diary.core.navigation.tag.TagDestination import io.github.taetae98coding.diary.core.resources.Res import io.github.taetae98coding.diary.core.resources.calendar import io.github.taetae98coding.diary.core.resources.memo import io.github.taetae98coding.diary.core.resources.more +import io.github.taetae98coding.diary.core.resources.tag import org.jetbrains.compose.resources.StringResource internal enum class AppNavigation( @@ -18,6 +20,11 @@ internal enum class AppNavigation( route = MemoDestination, ), + Tag( + title = Res.string.tag, + route = TagDestination, + ), + Calendar( title = Res.string.calendar, route = CalendarDestination, diff --git a/build-logic/src/main/kotlin/plugin/convention/AppFeaturePlugin.kt b/build-logic/src/main/kotlin/plugin/convention/AppFeaturePlugin.kt index 6aaade2f..50b43c6b 100644 --- a/build-logic/src/main/kotlin/plugin/convention/AppFeaturePlugin.kt +++ b/build-logic/src/main/kotlin/plugin/convention/AppFeaturePlugin.kt @@ -41,6 +41,7 @@ internal class AppFeaturePlugin : Plugin { implementation(project(":app:core:resources")) implementation(project(":library:color")) + implementation(project(":library:compose")) implementation(project(":library:kotlin")) implementation(project(":library:navigation")) implementation(project(":library:coroutines")) diff --git a/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/tag/TagTitleBlankException.kt b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/tag/TagTitleBlankException.kt new file mode 100644 index 00000000..3a626379 --- /dev/null +++ b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/tag/TagTitleBlankException.kt @@ -0,0 +1,3 @@ +package io.github.taetae98coding.diary.common.exception.tag + +public class TagTitleBlankException : Exception() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 291ce091..0678f8a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ androidx-lifecycle = "2.8.5" compose-markdown = "0.27.0" # https://github.com/mikepenz/multiplatform-markdown-renderer/releases koin = "4.0.0" # https://github.com/InsertKoinIO/koin/releases -koin-annotations = "2.0.0-Beta1" # https://github.com/InsertKoinIO/koin-annotations/releases +koin-annotations = "2.0.0-Beta2" # https://github.com/InsertKoinIO/koin-annotations/releases datastore = "1.1.1" # https://developer.android.com/jetpack/androidx/releases/datastore?hl=en room = "2.7.0-alpha11" # https://developer.android.com/jetpack/androidx/releases/room?hl=en sqlite = "2.5.0-alpha11" # https://developer.android.com/jetpack/androidx/releases/sqlite?hl=en diff --git a/library/compose/build.gradle.kts b/library/compose/build.gradle.kts new file mode 100644 index 00000000..0b60b0e6 --- /dev/null +++ b/library/compose/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + id("diary.compose") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(compose.material3) + implementation(libs.compose.material3.adaptive.navigation) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.library.compose" +} diff --git a/library/compose/src/commonMain/kotlin/io/github/taetae98coding/library/compose/ThreePaneScaffoldNavigatorExt.kt b/library/compose/src/commonMain/kotlin/io/github/taetae98coding/library/compose/ThreePaneScaffoldNavigatorExt.kt new file mode 100644 index 00000000..d1ed6d5e --- /dev/null +++ b/library/compose/src/commonMain/kotlin/io/github/taetae98coding/library/compose/ThreePaneScaffoldNavigatorExt.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.library.compose + +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +public fun ThreePaneScaffoldNavigator<*>.isListVisible(): Boolean { + return scaffoldValue.secondary == PaneAdaptedValue.Expanded +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +public fun ThreePaneScaffoldNavigator<*>.isDetailVisible(): Boolean { + return scaffoldValue.primary == PaneAdaptedValue.Expanded +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0031627f..885b1247 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -61,6 +61,7 @@ include(":app:core:navigation") include(":app:core:resources") include(":app:data:memo") +include(":app:data:tag") include(":app:data:account") include(":app:data:holiday") include(":app:data:backup") @@ -69,6 +70,7 @@ include(":app:data:fcm") include(":app:data:credential") include(":app:domain:memo") +include(":app:domain:tag") include(":app:domain:account") include(":app:domain:holiday") include(":app:domain:backup") @@ -77,6 +79,7 @@ include(":app:domain:fcm") include(":app:domain:credential") include(":app:feature:memo") +include(":app:feature:tag") include(":app:feature:calendar") include(":app:feature:more") include(":app:feature:account") @@ -102,6 +105,7 @@ include(":common:exception") include(":common:model") include(":library:color") +include(":library:compose") include(":library:coroutines") include(":library:datetime") include(":library:koin-datastore")