From f6c950d192c1d0bccf91c56eb6c6ea61bd948bdc Mon Sep 17 00:00:00 2001 From: taetae98coding Date: Mon, 9 Dec 2024 14:28:48 +0900 Subject: [PATCH] 1.3.1 feat: app - ListScreen Animation(TagList, TagMemoList) feat: app - buddy calendar feat: server - buddy calendar --- .../calendar/compose/topbar/CalendarTopBar.kt | 4 +- app/core/compose/build.gradle.kts | 6 + .../calendar/CalendarScaffoldPreview.kt | 22 + .../core/compose/back/KBackHandler.apple.kt} | 0 ...TransparentScrimDialogProperties.apple.kt} | 0 .../core/compose/calendar/CalendarScaffold.kt | 124 +++++ .../compose/calendar/CalendarScaffoldState.kt | 11 + .../calendar/RememberCalendarScaffoldState.kt | 19 + .../memo/MemoListDetailPaneScaffold.kt | 84 ++++ .../add/RememberMemoDetailScaffoldAddState.kt | 34 ++ .../compose/memo/detail/MemoDetailScaffold.kt | 217 +++++++++ .../memo/detail/MemoDetailScaffoldState.kt | 68 +++ .../memo/detail/MemoDetailScaffoldUiState.kt | 11 + .../RememberMemoDetailScaffoldDetailState.kt | 46 ++ .../core/compose/memo/tag/MemoTagScaffold.kt | 104 +++++ .../ReverseModalNavigationDrawer.kt | 55 +++ .../navigation/ReverserModalDrawerSheet.kt | 35 ++ .../core/compose/tag/PrimaryTagCardItem.kt | 43 ++ .../diary/core/compose/tag/TagCardFlow.kt | 70 +++ .../diary/core/compose/tag/TagCardItem.kt | 48 ++ .../core/compose/tag/TagCardItemUiState.kt | 12 + .../diary/core/compose/topbar/TopBarTitle.kt | 18 + .../core/compose/back/KBackHandler.wasmJs.kt | 6 - app/core/coroutines/build.gradle.kts | 1 + app/core/design-system/build.gradle.kts | 1 + .../system/shape/CornerBasedShapeExt.kt | 13 + .../core/design/system/shape/DiaryShape.kt | 7 + .../system/theme/CompositionLocalExt.kt | 2 + .../core/design/system/theme/DiaryTheme.kt | 8 + .../diary/database/room/dao/MemoRoomDao.kt | 106 ++--- .../diary/database/room/mapper/MemoMapper.kt | 62 +-- .../core/diary/service/buddy/BuddyService.kt | 116 +++-- .../core/diary/service/mapper/MemoMapper.kt | 64 +++ .../core/diary/service/memo/MemoService.kt | 78 +--- .../buddy/BuddyGroupCalendarDestination.kt | 14 + .../buddy/BuddyGroupMemoAddDestination.kt | 21 + .../buddy/BuddyGroupMemoDetailDestination.kt | 14 + .../navigation/memo/MemoAddDestination.kt | 6 +- .../buddy/repository/BuddyRepositoryImpl.kt | 44 +- .../repository/MemoFetchRepositoryImpl.kt | 2 +- .../buddy/repository/BuddyRepository.kt | 6 + .../buddy/usecase/AddBuddyGroupMemoUseCase.kt | 38 ++ .../domain/buddy/usecase/FindBuddyUseCase.kt | 27 +- .../usecase/PageBuddyGroupCalendarMemo.kt | 34 ++ app/feature/buddy/build.gradle.kts | 3 + .../diary/feature/buddy/BuddyNavigation.kt | 55 ++- .../add/RememberBuddyDetailScreenAddState.kt | 22 +- .../BuddyGroupCalendarHolidayViewModel.kt | 53 +++ .../BuddyGroupCalendarMemoViewModel.kt | 70 +++ .../buddy/calendar/BuddyGroupCalendarRoute.kt | 74 +++ .../diary/feature/buddy/calendar/MemoKey.kt | 8 + .../feature/buddy/detail/BuddyDetailScreen.kt | 294 +++++++----- .../buddy/detail/BuddyDetailScreenState.kt | 87 ++-- .../RememberBuddyDetailScreenDetailState.kt | 29 ++ .../feature/buddy/home/BuddyHomeRoute.kt | 176 +++++--- .../feature/buddy/list/BuddyListScreen.kt | 135 ++++-- .../buddy/memo/add/BuddyGroupMemoAddRoute.kt | 48 ++ .../memo/add/BuddyGroupMemoAddViewModel.kt | 141 ++++++ .../detail/BuddyGroupMemoDetailViewModel.kt | 15 + .../memo/detail/BuddyGroupmemoDetailRoute.kt | 33 ++ .../calendar/CalendarHomeScreenPreview.kt | 23 - .../calendar/home/CalendarHomeRoute.kt | 76 ++-- .../calendar/home/CalendarHomeScreen.kt | 124 ----- .../calendar/home/CalendarHomeScreenState.kt | 7 - .../home/RememberCalendarScreenState.kt | 16 - .../memo/detail/MemoDetailScreenPreview.kt | 66 --- .../diary/feature/memo/tag/MemoTagPreview.kt | 84 ---- .../feature/memo/tag/MemoTagScreenPreview.kt | 68 --- .../diary/feature/memo/tag/TagFlowPreview.kt | 65 --- .../diary/feature/memo/add/MemoAddRoute.kt | 120 ++--- .../feature/memo/add/MemoAddViewModel.kt | 208 ++++----- .../add/RememberMemoDetailScreenAddState.kt | 34 -- .../memo/detail/MemoDetailActionButton.kt | 11 - .../memo/detail/MemoDetailFloatingButton.kt | 9 - .../memo/detail/MemoDetailNavigationButton.kt | 9 - .../feature/memo/detail/MemoDetailRoute.kt | 114 ++--- .../feature/memo/detail/MemoDetailScreen.kt | 274 ----------- .../memo/detail/MemoDetailScreenState.kt | 68 --- .../memo/detail/MemoDetailScreenUiState.kt | 13 - .../memo/detail/MemoDetailTagViewModel.kt | 8 +- .../memo/detail/MemoDetailViewModel.kt | 22 +- .../RememberMemoDetailScreenDetailState.kt | 48 -- .../diary/feature/memo/tag/MemoTag.kt | 86 ---- .../memo/tag/MemoTagNavigationButton.kt | 9 - .../diary/feature/memo/tag/MemoTagScreen.kt | 110 ----- .../diary/feature/memo/tag/TagFlow.kt | 70 --- .../diary/feature/memo/tag/TagUiState.kt | 12 - .../diary/feature/tag/home/TagHomeRoute.kt | 425 +++++++++--------- .../diary/feature/tag/list/TagListScreen.kt | 289 ++++++------ .../diary/feature/tag/memo/TagMemoScreen.kt | 299 ++++++------ app/platform/macos/build.gradle.kts | 66 +++ .../github/taetae98coding/diary/MacosApp.kt | 15 + .../src/main/kotlin/ext/DependencyExt.kt | 9 + .../plugin/datastore/DataStorePlugin.kt | 3 + .../kotlin/KotlinMultiplatformAllPlugin.kt | 4 +- .../kotlin/KotlinMultiplatformCommonPlugin.kt | 3 + .../src/main/kotlin/plugin/room/RoomPlugin.kt | 3 + .../ext/NetworkExceptionExt.apple.kt | 5 + .../exception/ext/NetworkExceptionExt.ios.kt | 3 - .../common/model/memo/LegacyMemoEntity.kt | 34 ++ .../diary/common/model/memo/MemoEntity.kt | 44 +- .../common/model/response/DiaryResponse.kt | 3 +- gradle.properties | 4 + gradle/libs.versions.toml | 2 +- library/firebase-messaging/build.gradle.kts | 3 +- .../messaging/KFirebaseMessagingExt.apple.kt} | 2 +- .../messaging/KFirebaseMessagingImpl.kt | 6 +- .../koin/datastore/DataStoreExt.apple.kt} | 22 +- .../diary/library/koin/room/RoomExt.apple.kt} | 24 +- .../diary/plugin/DatabasePlugin.kt | 4 + .../diary/core/database/AccountTable.kt | 12 +- .../database/BuddyGroupAccountRelation.kt | 64 +-- .../diary/core/database/BuddyGroupTable.kt | 11 +- .../diary/core/database/FCMTokenTable.kt | 52 +-- .../diary/core/database/MemoAccountTable.kt | 29 ++ .../core/database/MemoBuddyGroupTable.kt | 29 ++ .../diary/core/database/MemoQuery.kt | 13 + .../diary/core/database/MemoTable.kt | 138 +++--- .../diary/core/database/MemoTagTable.kt | 54 ++- .../diary/core/database/TagTable.kt | 107 ++--- .../taetae98coding/diary/core/model/Memo.kt | 1 - .../diary/core/model/MemoAndTagIds.kt | 4 +- .../buddy/repository/BuddyRepositoryImpl.kt | 84 ++-- .../memo/repository/MemoRepositoryImpl.kt | 13 +- .../buddy/repository/BuddyRepository.kt | 9 +- .../buddy/usecase/FindBuddyGroupMemoByDate.kt | 24 + .../domain/buddy/usecase/FindBuddyUseCase.kt | 2 +- .../usecase/UpsertBuddyGroupMemoUseCase.kt | 29 ++ .../domain/memo/repository/MemoRepository.kt | 2 +- .../domain/memo/usecase/UpsertMemoUseCase.kt | 5 +- .../diary/feature/buddy/BuddyRouting.kt | 238 ++++++---- .../diary/feature/memo/MemoRouting.kt | 135 +++--- settings.gradle.kts | 3 + 133 files changed, 4051 insertions(+), 2977 deletions(-) create mode 100644 app/core/compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffoldPreview.kt rename app/core/compose/src/{iosMain/kotlin/io/github/taetae98coding/diary/core/compose/back/KBackHandler.ios.kt => appleMain/kotlin/io/github/taetae98coding/diary/core/compose/back/KBackHandler.apple.kt} (100%) rename app/core/compose/src/{iosMain/kotlin/io/github/taetae98coding/diary/core/compose/dialog/TransparentScrimDialogProperties.ios.kt => appleMain/kotlin/io/github/taetae98coding/diary/core/compose/dialog/TransparentScrimDialogProperties.apple.kt} (100%) create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffold.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffoldState.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/RememberCalendarScaffoldState.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/MemoListDetailPaneScaffold.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/add/RememberMemoDetailScaffoldAddState.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffold.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffoldState.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffoldUiState.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/RememberMemoDetailScaffoldDetailState.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/tag/MemoTagScaffold.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/navigation/ReverseModalNavigationDrawer.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/navigation/ReverserModalDrawerSheet.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/PrimaryTagCardItem.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardFlow.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardItem.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardItemUiState.kt create mode 100644 app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/topbar/TopBarTitle.kt delete mode 100644 app/core/compose/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/compose/back/KBackHandler.wasmJs.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/shape/CornerBasedShapeExt.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/shape/DiaryShape.kt create mode 100644 app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/mapper/MemoMapper.kt create mode 100644 app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupCalendarDestination.kt create mode 100644 app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupMemoAddDestination.kt create mode 100644 app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupMemoDetailDestination.kt create mode 100644 app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/AddBuddyGroupMemoUseCase.kt create mode 100644 app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/PageBuddyGroupCalendarMemo.kt create mode 100644 app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarHolidayViewModel.kt create mode 100644 app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarMemoViewModel.kt create mode 100644 app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarRoute.kt create mode 100644 app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/MemoKey.kt create mode 100644 app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/RememberBuddyDetailScreenDetailState.kt create mode 100644 app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/add/BuddyGroupMemoAddRoute.kt create mode 100644 app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/add/BuddyGroupMemoAddViewModel.kt create mode 100644 app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/detail/BuddyGroupMemoDetailViewModel.kt create mode 100644 app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/detail/BuddyGroupmemoDetailRoute.kt delete mode 100644 app/feature/calendar/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarHomeScreenPreview.kt delete mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeScreen.kt delete mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeScreenState.kt delete mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/RememberCalendarScreenState.kt delete mode 100644 app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenPreview.kt delete mode 100644 app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagPreview.kt delete mode 100644 app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagScreenPreview.kt delete mode 100644 app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagFlowPreview.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/RememberMemoDetailScreenAddState.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailActionButton.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailFloatingButton.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailNavigationButton.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreen.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenState.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenUiState.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/RememberMemoDetailScreenDetailState.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTag.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagNavigationButton.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagScreen.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagFlow.kt delete mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagUiState.kt create mode 100644 app/platform/macos/build.gradle.kts create mode 100644 app/platform/macos/src/macosMain/kotlin/io/github/taetae98coding/diary/MacosApp.kt create mode 100644 common/exception/src/appleMain/kotlin/io/github/taetae98coding/diary/common/exception/ext/NetworkExceptionExt.apple.kt delete mode 100644 common/exception/src/iosMain/kotlin/io/github/taetae98coding/diary/common/exception/ext/NetworkExceptionExt.ios.kt create mode 100644 common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/LegacyMemoEntity.kt rename library/firebase-messaging/src/{iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.ios.kt => appleMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.apple.kt} (83%) rename library/firebase-messaging/src/{iosMain => appleMain}/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt (90%) rename library/koin-datastore/src/{iosMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.ios.kt => appleMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.apple.kt} (56%) rename library/koin-room/src/{iosMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.ios.kt => appleMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.apple.kt} (57%) create mode 100644 server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoAccountTable.kt create mode 100644 server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoBuddyGroupTable.kt create mode 100644 server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoQuery.kt create mode 100644 server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyGroupMemoByDate.kt create mode 100644 server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/UpsertBuddyGroupMemoUseCase.kt diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/topbar/CalendarTopBar.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/topbar/CalendarTopBar.kt index 499ec995..973e32c9 100644 --- a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/topbar/CalendarTopBar.kt +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/topbar/CalendarTopBar.kt @@ -30,8 +30,9 @@ import kotlinx.coroutines.launch @Composable public fun CalendarTopBar( state: CalendarState, - actions: @Composable RowScope.() -> Unit = {}, modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, ) { CenterAlignedTopAppBar( title = { @@ -60,6 +61,7 @@ public fun CalendarTopBar( } }, modifier = modifier, + navigationIcon = navigationIcon, actions = actions, ) } diff --git a/app/core/compose/build.gradle.kts b/app/core/compose/build.gradle.kts index 6c145cfc..360ac686 100644 --- a/app/core/compose/build.gradle.kts +++ b/app/core/compose/build.gradle.kts @@ -8,10 +8,16 @@ kotlin { sourceSets { commonMain { dependencies { + implementation(project(":app:core:calendar-compose")) implementation(project(":app:core:design-system")) + implementation(project(":app:core:model")) + + implementation(project(":library:color")) + implementation(project(":library:datetime")) implementation(compose.material3) implementation(libs.compose.material3.adaptive.navigation) + implementation(libs.lifecycle.compose) } } diff --git a/app/core/compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffoldPreview.kt b/app/core/compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffoldPreview.kt new file mode 100644 index 00000000..305ec6a9 --- /dev/null +++ b/app/core/compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffoldPreview.kt @@ -0,0 +1,22 @@ +package io.github.taetae98coding.diary.core.compose.calendar + +import androidx.compose.runtime.Composable +import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +@DiaryPreview +@Composable +private fun CalendarHomeScreenPreview() { + DiaryTheme { + CalendarScaffold( + state = rememberCalendarScaffoldState( + onFilter = {}, + ), + onSelectDate = {}, + hasFilterProvider = { false }, + textItemListProvider = { emptyList() }, + holidayListProvider = { emptyList() }, + onCalendarItemClick = {}, + ) + } +} diff --git a/app/core/compose/src/iosMain/kotlin/io/github/taetae98coding/diary/core/compose/back/KBackHandler.ios.kt b/app/core/compose/src/appleMain/kotlin/io/github/taetae98coding/diary/core/compose/back/KBackHandler.apple.kt similarity index 100% rename from app/core/compose/src/iosMain/kotlin/io/github/taetae98coding/diary/core/compose/back/KBackHandler.ios.kt rename to app/core/compose/src/appleMain/kotlin/io/github/taetae98coding/diary/core/compose/back/KBackHandler.apple.kt diff --git a/app/core/compose/src/iosMain/kotlin/io/github/taetae98coding/diary/core/compose/dialog/TransparentScrimDialogProperties.ios.kt b/app/core/compose/src/appleMain/kotlin/io/github/taetae98coding/diary/core/compose/dialog/TransparentScrimDialogProperties.apple.kt similarity index 100% rename from app/core/compose/src/iosMain/kotlin/io/github/taetae98coding/diary/core/compose/dialog/TransparentScrimDialogProperties.ios.kt rename to app/core/compose/src/appleMain/kotlin/io/github/taetae98coding/diary/core/compose/dialog/TransparentScrimDialogProperties.apple.kt diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffold.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffold.kt new file mode 100644 index 00000000..b04a842c --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffold.kt @@ -0,0 +1,124 @@ +package io.github.taetae98coding.diary.core.compose.calendar + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.lifecycle.compose.LifecycleResumeEffect +import io.github.taetae98coding.diary.core.calendar.compose.Calendar +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.core.calendar.compose.modifier.calendarDateRangeSelectable +import io.github.taetae98coding.diary.core.calendar.compose.topbar.CalendarTopBar +import io.github.taetae98coding.diary.core.calendar.compose.topbar.TodayIcon +import io.github.taetae98coding.diary.core.design.system.icon.FilterIcon +import io.github.taetae98coding.diary.core.design.system.icon.FilterOffIcon +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.library.datetime.todayIn +import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDate + +@Composable +public fun CalendarScaffold( + state: CalendarScaffoldState, + onSelectDate: (ClosedRange) -> Unit, + hasFilterProvider: () -> Boolean, + textItemListProvider: () -> List, + holidayListProvider: () -> List, + onCalendarItemClick: (Any) -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, +) { + val coroutineScope = rememberCoroutineScope() + + Scaffold( + modifier = modifier + .onPreviewKeyEvent { + when { + !state.calendarState.isScrollInProgress && it.key == Key.DirectionRight -> { + coroutineScope.launch { state.calendarState.animateScrollToForward() } + true + } + + !state.calendarState.isScrollInProgress && it.key == Key.DirectionLeft -> { + coroutineScope.launch { state.calendarState.animateScrollToBackward() } + true + } + + it.key == Key.F1 -> { + state.onFilter() + true + } + + !state.calendarState.isScrollInProgress && it.key == Key.F2 -> { + coroutineScope.launch { state.calendarState.animateScrollToToday() } + true + } + + else -> false + } + } + .focusRequester(state.focusRequester) + .focusable(), + topBar = { + CalendarTopBar( + state = state.calendarState, + actions = { + IconButton(onClick = state.onFilter) { + Crossfade(hasFilterProvider()) { hasFilter -> + if (hasFilter) { + FilterIcon(tint = DiaryTheme.color.primary) + } else { + FilterOffIcon() + } + } + } + + IconButton(onClick = { coroutineScope.launch { state.calendarState.animateScrollToToday() } }) { + TodayIcon() + } + }, + navigationIcon = navigationIcon, + ) + }, + ) { + var today by remember { mutableStateOf(LocalDate.todayIn()) } + + Calendar( + state = state.calendarState, + primaryDateListProvider = { listOf(today) }, + textItemListProvider = textItemListProvider, + holidayListProvider = holidayListProvider, + onCalendarItemClick = onCalendarItemClick, + modifier = Modifier + .fillMaxSize() + .padding(it) + .calendarDateRangeSelectable( + state = state.calendarState, + onSelectDate = onSelectDate, + ), + ) + + LifecycleResumeEffect(Unit) { + today = LocalDate.todayIn() + onPauseOrDispose { } + } + } + + LifecycleResumeEffect(state.focusRequester) { + state.focusRequester.requestFocus() + onPauseOrDispose { } + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffoldState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffoldState.kt new file mode 100644 index 00000000..ed0ad537 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/CalendarScaffoldState.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.core.compose.calendar + +import androidx.compose.ui.focus.FocusRequester +import io.github.taetae98coding.diary.core.calendar.compose.state.CalendarState + +public class CalendarScaffoldState( + public val onFilter: () -> Unit, + public val calendarState: CalendarState, +) { + public val focusRequester: FocusRequester = FocusRequester() +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/RememberCalendarScaffoldState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/RememberCalendarScaffoldState.kt new file mode 100644 index 00000000..86b712ba --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/calendar/RememberCalendarScaffoldState.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.core.compose.calendar + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import io.github.taetae98coding.diary.core.calendar.compose.state.rememberCalendarState + +@Composable +public fun rememberCalendarScaffoldState( + onFilter: () -> Unit, +): CalendarScaffoldState { + val calendarState = rememberCalendarState() + + return remember { + CalendarScaffoldState( + onFilter = onFilter, + calendarState = calendarState, + ) + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/MemoListDetailPaneScaffold.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/MemoListDetailPaneScaffold.kt new file mode 100644 index 00000000..fbbb7b11 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/MemoListDetailPaneScaffold.kt @@ -0,0 +1,84 @@ +package io.github.taetae98coding.diary.core.compose.memo + +import androidx.compose.material3.IconButton +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +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.layout.calculatePaneScaffoldDirective +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.compose.adaptive.isListVisible +import io.github.taetae98coding.diary.core.compose.back.KBackHandler +import io.github.taetae98coding.diary.core.compose.memo.detail.MemoDetailScaffold +import io.github.taetae98coding.diary.core.compose.memo.detail.MemoDetailScaffoldState +import io.github.taetae98coding.diary.core.compose.memo.detail.MemoDetailScaffoldUiState +import io.github.taetae98coding.diary.core.compose.memo.tag.MemoTagScaffold +import io.github.taetae98coding.diary.core.compose.tag.TagCardItemUiState +import io.github.taetae98coding.diary.core.design.system.icon.NavigateUpIcon + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +public fun MemoListDetailPaneScaffold( + onNavigateUp: () -> Unit, + onDetailTag: (String) -> Unit, + detailScaffoldStateProvider: () -> MemoDetailScaffoldState, + detailUiStateProvider: () -> MemoDetailScaffoldUiState, + detailTagListProvider: () -> List?, + detailTitle: @Composable () -> Unit, + onTagAdd: () -> Unit, + tagListProvider: () -> List?, + modifier: Modifier = Modifier, + detailFloatingActionButton: @Composable () -> Unit = {}, +) { + val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val navigator = rememberListDetailPaneScaffoldNavigator(scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)) + + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + MemoDetailScaffold( + onTagTitle = { navigator.navigateTo(ThreePaneScaffoldRole.Primary) }, + onTag = onDetailTag, + state = detailScaffoldStateProvider(), + uiStateProvider = detailUiStateProvider, + tagListProvider = detailTagListProvider, + title = detailTitle, + navigationIcon = { + IconButton(onClick = onNavigateUp) { + NavigateUpIcon() + } + }, + floatingActionButton = detailFloatingActionButton, + ) + } + }, + detailPane = { + AnimatedPane { + MemoTagScaffold( + onTagAdd = onTagAdd, + navigationIcon = { + if (!navigator.isListVisible()) { + IconButton(onClick = navigator::navigateBack) { + NavigateUpIcon() + } + } + }, + primaryTagListProvider = detailTagListProvider, + tagListProvider = tagListProvider, + ) + } + }, + modifier = modifier, + ) + + KBackHandler( + isEnabled = navigator.canNavigateBack(), + onBack = navigator::navigateBack, + ) +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/add/RememberMemoDetailScaffoldAddState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/add/RememberMemoDetailScaffoldAddState.kt new file mode 100644 index 00000000..120d77c9 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/add/RememberMemoDetailScaffoldAddState.kt @@ -0,0 +1,34 @@ +package io.github.taetae98coding.diary.core.compose.memo.add + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.github.taetae98coding.diary.core.compose.memo.detail.MemoDetailScaffoldState +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.design.system.diary.date.rememberDiaryDateState +import kotlinx.datetime.LocalDate + +@Composable +public fun rememberMemoDetailScaffoldAddState( + initialStart: LocalDate?, + initialEndInclusive: LocalDate?, +): MemoDetailScaffoldState.Add { + val coroutineScope = rememberCoroutineScope() + val componentState = rememberDiaryComponentState() + val dateState = + rememberDiaryDateState( + initialStart = initialStart, + initialEndInclusive = initialEndInclusive, + ) + val colorState = rememberDiaryColorState() + + return remember { + MemoDetailScaffoldState.Add( + coroutineScope = coroutineScope, + componentState = componentState, + dateState = dateState, + colorState = colorState, + ) + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffold.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffold.kt new file mode 100644 index 00000000..920b55f5 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffold.kt @@ -0,0 +1,217 @@ +package io.github.taetae98coding.diary.core.compose.memo.detail + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +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.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.compose.tag.PrimaryTagCardItem +import io.github.taetae98coding.diary.core.compose.tag.TagCardFlow +import io.github.taetae98coding.diary.core.compose.tag.TagCardItemUiState +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.diary.date.DiaryDate +import io.github.taetae98coding.diary.core.design.system.emoji.Emoji +import io.github.taetae98coding.diary.core.design.system.icon.ChevronRightIcon +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +public fun MemoDetailScaffold( + onTagTitle: () -> Unit, + onTag: (String) -> Unit, + state: MemoDetailScaffoldState, + uiStateProvider: () -> MemoDetailScaffoldUiState, + tagListProvider: () -> List?, + modifier: Modifier = Modifier, + title: @Composable () -> Unit = {}, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = title, + navigationIcon = navigationIcon, + actions = actions, + ) + }, + snackbarHost = { SnackbarHost(hostState = state.hostState) }, + floatingActionButton = floatingActionButton, + ) { + Content( + onTagTitle = onTagTitle, + onTag = onTag, + state = state, + tagListProvider = tagListProvider, + modifier = Modifier + .fillMaxSize() + .padding(it) + .padding(DiaryTheme.dimen.screenPaddingValues), + ) + } + + Message( + state = state, + uiStateProvider = uiStateProvider, + ) + + LaunchedFocus(state = state) +} + +@Composable +private fun Message( + state: MemoDetailScaffoldState, + uiStateProvider: () -> MemoDetailScaffoldUiState, +) { + val uiState = uiStateProvider() + + LaunchedEffect( + uiState.isAdd, + uiState.isDelete, + uiState.isUpdate, + uiState.isTitleBlankError, + uiState.isUnknownError, + ) { + when { + uiState.isAdd -> { + state.showMessage("메모 추가 ${Emoji.congratulate.random()}") + state.clearInput() + state.requestTitleFocus() + } + + uiState.isDelete -> { +// if (state is MemoDetailScreenState.Detail) { +// state.onDelete() +// } + } + + uiState.isUpdate -> { +// if (state is MemoDetailScreenState.Detail) { +// state.onUpdate() +// } + } + + uiState.isTitleBlankError -> { + state.showMessage("제목을 입력해 주세요 ${Emoji.check.random()}") + state.titleError() + } + + uiState.isUnknownError -> state.showMessage("알 수 없는 에러가 발생했어요 잠시 후 다시 시도해 주세요 ${Emoji.error.random()}") + } + + uiState.onMessageShow() + } +} + +@Composable +private fun LaunchedFocus( + state: MemoDetailScaffoldState, +) { + LaunchedEffect(state) { + if (state is MemoDetailScaffoldState.Add) { + state.requestTitleFocus() + } + } +} + +@Composable +private fun Content( + onTagTitle: () -> Unit, + onTag: (String) -> Unit, + state: MemoDetailScaffoldState, + tagListProvider: () -> List?, + modifier: Modifier = Modifier, +) { + Column( + modifier = Modifier + .verticalScroll(state = rememberScrollState()) + .then(modifier), + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + DiaryComponent(state = state.componentState) + DiaryDate(state = state.dateState) + InternalDiaryTag( + onTagTitle = onTagTitle, + onTag = onTag, + state = state, + listProvider = tagListProvider, + ) + InternalDiaryColor(state = state) + } +} + +@Composable +private fun InternalDiaryTag( + onTagTitle: () -> Unit, + onTag: (String) -> Unit, + state: MemoDetailScaffoldState, + listProvider: () -> List?, + modifier: Modifier = Modifier, +) { + TagCardFlow( + title = { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onTagTitle) + .minimumInteractiveComponentSize() + .padding(horizontal = DiaryTheme.dimen.diaryHorizontalPadding), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = "태그") + ChevronRightIcon() + } + }, + listProvider = listProvider, + empty = { Text(text = "태그가 없어요 🐻‍❄️") }, + tag = { + PrimaryTagCardItem( + uiState = it, + onClick = { onTag(it.id) }, + ) + }, + modifier = modifier + .fillMaxWidth() + .heightIn(min = 150.dp, max = 200.dp), + ) +} + +@Composable +private fun InternalDiaryColor( + state: MemoDetailScaffoldState, + modifier: Modifier = Modifier, +) { + Row(modifier = modifier) { + DiaryColor( + state = state.colorState, + modifier = Modifier + .weight(1F) + .height(100.dp), + ) + + Spacer(modifier = Modifier.weight(1F)) + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffoldState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffoldState.kt new file mode 100644 index 00000000..e3410775 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffoldState.kt @@ -0,0 +1,68 @@ +package io.github.taetae98coding.diary.core.compose.memo.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.design.system.diary.date.DiaryDateState +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +public sealed class MemoDetailScaffoldState { + protected abstract val coroutineScope: CoroutineScope + + internal abstract val componentState: DiaryComponentState + internal abstract val dateState: DiaryDateState + internal abstract val colorState: DiaryColorState + + private var messageJob: Job? = null + + internal val hostState: SnackbarHostState = SnackbarHostState() + + public data class Add( + override val coroutineScope: CoroutineScope, + override val componentState: DiaryComponentState, + override val dateState: DiaryDateState, + override val colorState: DiaryColorState, + ) : MemoDetailScaffoldState() + + public data class Detail( + val onDelete: () -> Unit, + val onUpdate: () -> Unit, + override val coroutineScope: CoroutineScope, + override val componentState: DiaryComponentState, + override val dateState: DiaryDateState, + override val colorState: DiaryColorState, + ) : MemoDetailScaffoldState() + + public val detail: MemoDetail + get() { + return MemoDetail( + title = componentState.title, + description = componentState.description, + start = dateState.start.takeIf { dateState.hasDate }, + endInclusive = dateState.endInclusive.takeIf { dateState.hasDate }, + color = colorState.color.toArgb(), + ) + } + + internal fun requestTitleFocus() { + componentState.requestTitleFocus() + } + + internal fun titleError() { + requestTitleFocus() + componentState.titleError() + } + + internal fun showMessage(message: String) { + messageJob?.cancel() + messageJob = coroutineScope.launch { hostState.showSnackbar(message) } + } + + internal fun clearInput() { + componentState.clearInput() + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffoldUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffoldUiState.kt new file mode 100644 index 00000000..1a24cce7 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/MemoDetailScaffoldUiState.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.core.compose.memo.detail + +public data class MemoDetailScaffoldUiState( + 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 = {}, +) diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/RememberMemoDetailScaffoldDetailState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/RememberMemoDetailScaffoldDetailState.kt new file mode 100644 index 00000000..7168af55 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/detail/RememberMemoDetailScaffoldDetailState.kt @@ -0,0 +1,46 @@ +package io.github.taetae98coding.diary.core.compose.memo.detail + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.graphics.Color +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.design.system.diary.date.rememberDiaryDateState +import io.github.taetae98coding.diary.core.model.memo.MemoDetail + +@Composable +public fun rememberMemoDetailScaffoldDetailState( + onDelete: () -> Unit, + onUpdate: () -> Unit, + detailProvider: () -> MemoDetail?, +): MemoDetailScaffoldState.Detail { + val detail = detailProvider() + + val coroutineScope = rememberCoroutineScope() + val componentState = rememberDiaryComponentState( + inputs = arrayOf(detail?.title, detail?.description), + initialTitle = detail?.title.orEmpty(), + initialDescription = detail?.description.orEmpty(), + ) + val dateState = rememberDiaryDateState( + inputs = arrayOf(detail?.start, detail?.endInclusive), + initialStart = detail?.start, + initialEndInclusive = detail?.endInclusive, + ) + val colorState = rememberDiaryColorState( + inputs = arrayOf(detail?.color), + initialColor = detail?.color?.let { Color(it) } ?: Color.Unspecified, + ) + + return remember(coroutineScope, componentState, dateState, colorState) { + MemoDetailScaffoldState.Detail( + onDelete = onDelete, + onUpdate = onUpdate, + coroutineScope = coroutineScope, + componentState = componentState, + dateState = dateState, + colorState = colorState, + ) + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/tag/MemoTagScaffold.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/tag/MemoTagScaffold.kt new file mode 100644 index 00000000..5e6789b3 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/tag/MemoTagScaffold.kt @@ -0,0 +1,104 @@ +package io.github.taetae98coding.diary.core.compose.memo.tag + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.core.compose.tag.PrimaryTagCardItem +import io.github.taetae98coding.diary.core.compose.tag.TagCardFlow +import io.github.taetae98coding.diary.core.compose.tag.TagCardItem +import io.github.taetae98coding.diary.core.compose.tag.TagCardItemUiState +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +public fun MemoTagScaffold( + onTagAdd: () -> Unit, + navigationIcon: @Composable () -> Unit, + primaryTagListProvider: () -> List?, + tagListProvider: () -> List?, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(text = "메모 태그") }, + navigationIcon = navigationIcon, + ) + }, + ) { + Content( + onTagAdd = onTagAdd, + primaryTagListProvider = primaryTagListProvider, + tagListProvider = tagListProvider, + modifier = Modifier.fillMaxSize() + .padding(it) + .padding(DiaryTheme.dimen.screenPaddingValues) + ) + } +} + +@Composable +private fun Content( + onTagAdd: () -> Unit, + primaryTagListProvider: () -> List?, + tagListProvider: () -> List?, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + TagCardFlow( + modifier = Modifier + .fillMaxWidth() + .weight(3F), + listProvider = primaryTagListProvider, + title = { + Text( + text = "캘린더 태그", + modifier = Modifier + .fillMaxWidth() + .minimumInteractiveComponentSize() + .padding(horizontal = DiaryTheme.dimen.diaryHorizontalPadding), + ) + }, + empty = { Text(text = "태그가 없어요 🐻") }, + tag = { + PrimaryTagCardItem(uiState = it) + }, + ) + + TagCardFlow( + modifier = Modifier + .fillMaxWidth() + .weight(7F), + listProvider = tagListProvider, + title = { + Text( + text = "태그", + modifier = Modifier + .fillMaxWidth() + .minimumInteractiveComponentSize() + .padding(horizontal = DiaryTheme.dimen.diaryHorizontalPadding), + ) + }, + empty = { + Button(onClick = onTagAdd) { + Text(text = "태그 추가하러 가기") + } + }, + tag = { TagCardItem(uiState = it) }, + ) + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/navigation/ReverseModalNavigationDrawer.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/navigation/ReverseModalNavigationDrawer.kt new file mode 100644 index 00000000..1ea7008e --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/navigation/ReverseModalNavigationDrawer.kt @@ -0,0 +1,55 @@ +package io.github.taetae98coding.diary.core.compose.navigation + +import androidx.compose.material3.DismissibleNavigationDrawer +import androidx.compose.material3.DrawerDefaults +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection + +@Composable +public fun ReverseModalNavigationDrawer( + drawerContent: @Composable () -> Unit, + modifier: Modifier = Modifier, + drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed), + gesturesEnabled: Boolean = true, + scrimColor: Color = DrawerDefaults.scrimColor, + content: @Composable () -> Unit, +) { + val originLayoutDirection = LocalLayoutDirection.current + val reverseLayoutDirection = when (originLayoutDirection) { + LayoutDirection.Ltr -> LayoutDirection.Rtl + LayoutDirection.Rtl -> LayoutDirection.Ltr + } + + CompositionLocalProvider( + LocalLayoutDirection provides reverseLayoutDirection, + ) { + ModalNavigationDrawer( + drawerContent = { + CompositionLocalProvider( + LocalLayoutDirection provides originLayoutDirection, + ) { + drawerContent() + } + }, + modifier = modifier, + drawerState = drawerState, + gesturesEnabled = gesturesEnabled, + scrimColor = scrimColor, + content = { + CompositionLocalProvider( + LocalLayoutDirection provides originLayoutDirection, + ) { + content() + } + }, + ) + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/navigation/ReverserModalDrawerSheet.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/navigation/ReverserModalDrawerSheet.kt new file mode 100644 index 00000000..8390e9f8 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/navigation/ReverserModalDrawerSheet.kt @@ -0,0 +1,35 @@ +package io.github.taetae98coding.diary.core.compose.navigation + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.DrawerDefaults +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import io.github.taetae98coding.diary.core.design.system.shape.start +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +@Composable +public fun ReverseModalDrawerSheet( + modifier: Modifier = Modifier, + drawerShape: Shape = DiaryTheme.shape.large.start(), + drawerContainerColor: Color = DrawerDefaults.modalContainerColor, + drawerContentColor: Color = contentColorFor(drawerContainerColor), + drawerTonalElevation: Dp = DrawerDefaults.ModalDrawerElevation, + windowInsets: WindowInsets = DrawerDefaults.windowInsets, + content: @Composable ColumnScope.() -> Unit +) { + ModalDrawerSheet( + modifier = modifier, + drawerShape = drawerShape, + drawerContainerColor = drawerContainerColor, + drawerContentColor = drawerContentColor, + drawerTonalElevation = drawerTonalElevation, + windowInsets = windowInsets, + content = content + ) +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/PrimaryTagCardItem.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/PrimaryTagCardItem.kt new file mode 100644 index 00000000..04b36980 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/PrimaryTagCardItem.kt @@ -0,0 +1,43 @@ +package io.github.taetae98coding.diary.core.compose.tag + +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import io.github.taetae98coding.diary.library.color.toContrastColor + +@Composable +public fun PrimaryTagCardItem( + uiState: TagCardItemUiState, + modifier: Modifier = Modifier, +) { + PrimaryTagCardItem( + uiState = uiState, + onClick = { + if (uiState.isSelected) { + uiState.unselect.value() + } else { + uiState.select.value() + } + }, + modifier = modifier, + ) +} + +@Composable +public fun PrimaryTagCardItem( + uiState: TagCardItemUiState, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TagCardItem( + uiState = uiState, + onClick = onClick, + modifier = modifier, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = Color(uiState.color), + selectedLabelColor = Color(uiState.color).toContrastColor(), + selectedLeadingIconColor = Color(uiState.color).toContrastColor(), + ), + ) +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardFlow.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardFlow.kt new file mode 100644 index 00000000..b1d298e9 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardFlow.kt @@ -0,0 +1,70 @@ +package io.github.taetae98coding.diary.core.compose.tag + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize +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.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +@OptIn(ExperimentalLayoutApi::class) +@Composable +public fun TagCardFlow( + title: @Composable () -> Unit, + listProvider: () -> List?, + empty: @Composable () -> Unit, + tag: @Composable (TagCardItemUiState) -> Unit, + modifier: Modifier = Modifier, +) { + Card(modifier = modifier.height(IntrinsicSize.Min)) { + val isLoading by remember { derivedStateOf { listProvider() == null } } + val isEmpty by remember { derivedStateOf { !isLoading && listProvider().isNullOrEmpty() } } + + title() + + if (isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } else if (isEmpty) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + empty() + } + } else { + FlowRow( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(DiaryTheme.dimen.itemSpace), + horizontalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace, Alignment.CenterHorizontally), + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace, Alignment.CenterVertically), + ) { + listProvider()?.forEach { + key(it.id) { + tag(it) + } + } + } + } + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardItem.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardItem.kt new file mode 100644 index 00000000..c0296126 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardItem.kt @@ -0,0 +1,48 @@ +package io.github.taetae98coding.diary.core.compose.tag + +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.SelectableChipColors +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.core.design.system.chip.DiaryFilterChip +import io.github.taetae98coding.diary.core.design.system.icon.TagIcon + +@Composable +public fun TagCardItem( + uiState: TagCardItemUiState, + modifier: Modifier = Modifier, + colors: SelectableChipColors = FilterChipDefaults.filterChipColors(), +) { + TagCardItem( + uiState = uiState, + onClick = { + if (uiState.isSelected) { + uiState.unselect.value() + } else { + uiState.select.value() + } + }, + modifier = modifier, + colors = colors, + ) +} + +@Composable +public fun TagCardItem( + uiState: TagCardItemUiState, + onClick: () -> Unit, + modifier: Modifier = Modifier, + colors: SelectableChipColors = FilterChipDefaults.filterChipColors(), +) { + DiaryFilterChip( + selected = uiState.isSelected, + onClick = onClick, + label = { Text(text = uiState.title) }, + modifier = modifier, + leadingIcon = { TagIcon() }, + shape = CircleShape, + colors = colors, + ) +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardItemUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardItemUiState.kt new file mode 100644 index 00000000..c91acb84 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardItemUiState.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.core.compose.tag + +import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty + +public data class TagCardItemUiState( + val id: String, + val title: String, + val isSelected: Boolean, + val color: Int, + val select: SkipProperty<() -> Unit>, + val unselect: SkipProperty<() -> Unit>, +) diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/topbar/TopBarTitle.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/topbar/TopBarTitle.kt new file mode 100644 index 00000000..e3c50b99 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/topbar/TopBarTitle.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.compose.topbar + +import androidx.compose.foundation.basicMarquee +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun TopBarTitle( + text: String, + modifier: Modifier = Modifier, +) { + Text( + text = text, + modifier = modifier.basicMarquee(iterations = Int.MAX_VALUE), + maxLines = 1, + ) +} diff --git a/app/core/compose/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/compose/back/KBackHandler.wasmJs.kt b/app/core/compose/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/compose/back/KBackHandler.wasmJs.kt deleted file mode 100644 index 61078799..00000000 --- a/app/core/compose/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/core/compose/back/KBackHandler.wasmJs.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.taetae98coding.diary.core.compose.back - -import androidx.compose.runtime.Composable - -@Composable -public actual fun KBackHandler(isEnabled: Boolean, onBack: () -> Unit): Unit = Unit diff --git a/app/core/coroutines/build.gradle.kts b/app/core/coroutines/build.gradle.kts index 2154a7f0..80c2fc79 100644 --- a/app/core/coroutines/build.gradle.kts +++ b/app/core/coroutines/build.gradle.kts @@ -28,6 +28,7 @@ kotlin { nonAndroidMain.dependsOn(commonMain.get()) jvmMain.get().dependsOn(nonAndroidMain) iosMain.get().dependsOn(nonAndroidMain) + macosMain.get().dependsOn(nonAndroidMain) } } diff --git a/app/core/design-system/build.gradle.kts b/app/core/design-system/build.gradle.kts index 4d612de7..39481dfe 100644 --- a/app/core/design-system/build.gradle.kts +++ b/app/core/design-system/build.gradle.kts @@ -44,6 +44,7 @@ kotlin { nonAndroidMain.dependsOn(commonMain.get()) jvmMain.get().dependsOn(nonAndroidMain) iosMain.get().dependsOn(nonAndroidMain) + macosMain.get().dependsOn(nonAndroidMain) } } diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/shape/CornerBasedShapeExt.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/shape/CornerBasedShapeExt.kt new file mode 100644 index 00000000..fb1431b4 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/shape/CornerBasedShapeExt.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.core.design.system.shape + +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.ui.unit.dp + +public fun CornerBasedShape.start(): CornerBasedShape { + return copy(topEnd = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)) +} + +public fun CornerBasedShape.end(): CornerBasedShape { + return copy(topStart = CornerSize(0.dp), bottomStart = CornerSize(0.dp)) +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/shape/DiaryShape.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/shape/DiaryShape.kt new file mode 100644 index 00000000..0d206576 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/shape/DiaryShape.kt @@ -0,0 +1,7 @@ +package io.github.taetae98coding.diary.core.design.system.shape + +import androidx.compose.foundation.shape.CornerBasedShape + +public data class DiaryShape( + val large: CornerBasedShape, +) diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/CompositionLocalExt.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/CompositionLocalExt.kt index 13d6d66d..57e11967 100644 --- a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/CompositionLocalExt.kt +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/CompositionLocalExt.kt @@ -3,10 +3,12 @@ package io.github.taetae98coding.diary.core.design.system.theme import androidx.compose.runtime.staticCompositionLocalOf import io.github.taetae98coding.diary.core.design.system.color.DiaryColor import io.github.taetae98coding.diary.core.design.system.dimen.DiaryDimen +import io.github.taetae98coding.diary.core.design.system.shape.DiaryShape import io.github.taetae98coding.diary.core.design.system.typography.DiaryTypography private const val MESSAGE = "DiaryTheme not found." internal val LocalDiaryColor = staticCompositionLocalOf { error(MESSAGE) } +internal val LocalDiaryShape = staticCompositionLocalOf { error(MESSAGE) } internal val LocalDiaryTypography = staticCompositionLocalOf { error(MESSAGE) } internal val LocalDiaryDimen = staticCompositionLocalOf { error(MESSAGE) } 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 34c8e6f9..586cc9da 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 @@ -8,6 +8,7 @@ import io.github.taetae98coding.diary.core.design.system.color.DiaryColor import io.github.taetae98coding.diary.core.design.system.color.platformDarkColorScheme import io.github.taetae98coding.diary.core.design.system.color.platformLightColorScheme import io.github.taetae98coding.diary.core.design.system.dimen.DiaryDimen +import io.github.taetae98coding.diary.core.design.system.shape.DiaryShape import io.github.taetae98coding.diary.core.design.system.typography.DiaryTypography public data object DiaryTheme { @@ -15,6 +16,10 @@ public data object DiaryTheme { @Composable get() = LocalDiaryColor.current + val shape: DiaryShape + @Composable + get() = LocalDiaryShape.current + val typography: DiaryTypography @Composable get() = LocalDiaryTypography.current @@ -42,6 +47,9 @@ public fun DiaryTheme( background = colorScheme.background, onSurface = colorScheme.onSurface, ), + LocalDiaryShape provides DiaryShape( + large = MaterialTheme.shapes.large, + ), LocalDiaryTypography provides DiaryTypography( headlineMedium = MaterialTheme.typography.headlineMedium, titleLarge = MaterialTheme.typography.titleLarge, diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoRoomDao.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoRoomDao.kt index 48eece69..14b4997f 100644 --- a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoRoomDao.kt +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoRoomDao.kt @@ -19,67 +19,67 @@ import org.koin.core.annotation.Factory @OptIn(ExperimentalCoroutinesApi::class) @Factory internal class MemoRoomDao( - private val clock: Clock, - private val database: DiaryDatabase, + private val clock: Clock, + private val database: DiaryDatabase, ) : MemoDao { - override suspend fun upsert(dto: MemoAndTagIds) { - database.memo().upsertMemoAndTagIds(dto) - } + override suspend fun upsert(dto: MemoAndTagIds) { + database.memo().upsertMemoAndTagIds(dto) + } - override suspend fun update(memoId: String, detail: MemoDetail) { - database - .memo() - .update( - memoId = memoId, - title = detail.title, - description = detail.description, - start = detail.start, - endInclusive = detail.endInclusive, - color = detail.color, - updateAt = clock.now(), - ) - } + override suspend fun update(memoId: String, detail: MemoDetail) { + database + .memo() + .update( + memoId = memoId, + title = detail.title, + description = detail.description, + start = detail.start, + endInclusive = detail.endInclusive, + color = detail.color, + updateAt = clock.now(), + ) + } - override suspend fun updatePrimaryTag(memoId: String, tagId: String?) { - database.memo().updatePrimaryTag(memoId, tagId, clock.now()) - } + override suspend fun updatePrimaryTag(memoId: String, tagId: String?) { + database.memo().updatePrimaryTag(memoId, tagId, clock.now()) + } - override suspend fun updateFinish(memoId: String, isFinish: Boolean) { - database.memo().updateFinish(memoId, isFinish, clock.now()) - } + override suspend fun updateFinish(memoId: String, isFinish: Boolean) { + database.memo().updateFinish(memoId, isFinish, clock.now()) + } - override suspend fun updateDelete(memoId: String, isDelete: Boolean) { - database.memo().updateDelete(memoId, isDelete, clock.now()) - } + override suspend fun updateDelete(memoId: String, isDelete: Boolean) { + database.memo().updateDelete(memoId, isDelete, clock.now()) + } - override fun getById(memoId: String): Flow = - database - .memo() - .getById(memoId) - .mapLatest { it?.toDto() } + override fun getById(memoId: String): Flow = + database + .memo() + .getById(memoId) + .mapLatest { it?.toDto() } - override fun getMemoAndTagIdsByIds(memoIds: Set): Flow> = - database - .memo() - .getMemoAndTagIdsByIds(memoIds) - .mapLatest { map -> - map.map { entry -> - MemoAndTagIds( - memo = entry.key.toDto(), - tagIds = entry.value.map { it.tagId }.toSet(), - ) - } - } + override fun getMemoAndTagIdsByIds(memoIds: Set): Flow> = + database + .memo() + .getMemoAndTagIdsByIds(memoIds) + .mapLatest { map -> + map.map { entry -> + MemoAndTagIds( + memo = entry.key.toDto(), + tagIds = entry.value.map { it.tagId }.toSet(), + ) + } + } - override fun findByDateRange(owner: String?, dateRange: ClosedRange, tagFilter: Set): Flow> = - database - .memo() - .findByDateRange(owner, dateRange.start, dateRange.endInclusive, tagFilter.isNotEmpty(), tagFilter) - .mapCollectionLatest(MemoEntity::toDto) + override fun findByDateRange(owner: String?, dateRange: ClosedRange, tagFilter: Set): Flow> = + database + .memo() + .findByDateRange(owner, dateRange.start, dateRange.endInclusive, tagFilter.isNotEmpty(), tagFilter) + .mapCollectionLatest(MemoEntity::toDto) - override suspend fun upsert(memoList: List) { - database.memo().upsertMemoAndTagIds(memoList) - } + override suspend fun upsert(memoList: List) { + database.memo().upsertMemoAndTagIds(memoList) + } - override fun getLastServerUpdateAt(owner: String?): Flow = database.memo().getLastUpdateAt(owner) + override fun getLastServerUpdateAt(owner: String?): Flow = database.memo().getLastUpdateAt(owner) } diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/mapper/MemoMapper.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/mapper/MemoMapper.kt index a8972059..b16374ca 100644 --- a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/mapper/MemoMapper.kt +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/mapper/MemoMapper.kt @@ -5,36 +5,36 @@ import io.github.taetae98coding.diary.core.model.memo.MemoDetail import io.github.taetae98coding.diary.core.model.memo.MemoDto internal fun MemoDto.toEntity(): MemoEntity = - MemoEntity( - id = id, - title = detail.title, - description = detail.description, - start = detail.start, - endInclusive = detail.endInclusive, - color = detail.color, - isFinish = isFinish, - isDelete = isDelete, - owner = owner, - primaryTag = primaryTag, - updateAt = updateAt, - serverUpdateAt = serverUpdateAt, - ) + MemoEntity( + id = id, + title = detail.title, + description = detail.description, + start = detail.start, + endInclusive = detail.endInclusive, + color = detail.color, + isFinish = isFinish, + isDelete = isDelete, + owner = owner, + primaryTag = primaryTag, + updateAt = updateAt, + serverUpdateAt = serverUpdateAt, + ) internal fun MemoEntity.toDto(): MemoDto = - MemoDto( - id = id, - detail = - MemoDetail( - title = title, - description = description, - start = start, - endInclusive = endInclusive, - color = color, - ), - owner = owner, - primaryTag = primaryTag, - isFinish = isFinish, - isDelete = isDelete, - updateAt = updateAt, - serverUpdateAt = serverUpdateAt, - ) + MemoDto( + id = id, + detail = + MemoDetail( + title = title, + description = description, + start = start, + endInclusive = endInclusive, + color = color, + ), + owner = owner, + primaryTag = primaryTag, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + serverUpdateAt = serverUpdateAt, + ) diff --git a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/buddy/BuddyService.kt b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/buddy/BuddyService.kt index 11a057bc..e8dccb2c 100644 --- a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/buddy/BuddyService.kt +++ b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/buddy/BuddyService.kt @@ -2,12 +2,17 @@ package io.github.taetae98coding.diary.core.diary.service.buddy import io.github.taetae98coding.diary.common.model.buddy.BuddyEntity import io.github.taetae98coding.diary.common.model.buddy.BuddyGroupEntity +import io.github.taetae98coding.diary.common.model.memo.MemoEntity import io.github.taetae98coding.diary.common.model.request.buddy.UpsertBuddyGroupRequest import io.github.taetae98coding.diary.core.diary.service.DiaryServiceModule import io.github.taetae98coding.diary.core.diary.service.ext.getOrThrow +import io.github.taetae98coding.diary.core.diary.service.mapper.toDto +import io.github.taetae98coding.diary.core.diary.service.mapper.toEntity import io.github.taetae98coding.diary.core.model.buddy.Buddy import io.github.taetae98coding.diary.core.model.buddy.BuddyGroup import io.github.taetae98coding.diary.core.model.buddy.BuddyGroupDetail +import io.github.taetae98coding.diary.core.model.memo.MemoAndTagIds +import io.github.taetae98coding.diary.core.model.memo.MemoDto import io.ktor.client.HttpClient import io.ktor.client.request.get import io.ktor.client.request.parameter @@ -15,59 +20,82 @@ import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.ContentType import io.ktor.http.contentType +import kotlinx.datetime.LocalDate import org.koin.core.annotation.Factory import org.koin.core.annotation.Named @Factory public class BuddyService internal constructor( - @Named(DiaryServiceModule.DIARY_CLIENT) - private val client: HttpClient, + @Named(DiaryServiceModule.DIARY_CLIENT) + private val client: HttpClient, ) { - public suspend fun upsert( - buddyGroup: BuddyGroup, - buddyIds: Set, - ) { - client - .post("/buddy/group/upsert") { - val body = UpsertBuddyGroupRequest( - buddyGroup = BuddyGroupEntity( - id = buddyGroup.id, - title = buddyGroup.detail.title, - description = buddyGroup.detail.description, - ), - buddyIds = buddyIds, - ) + public suspend fun upsert( + buddyGroup: BuddyGroup, + buddyIds: Set, + ) { + client + .post("/buddy/group/upsert") { + val body = UpsertBuddyGroupRequest( + buddyGroup = BuddyGroupEntity( + id = buddyGroup.id, + title = buddyGroup.detail.title, + description = buddyGroup.detail.description, + ), + buddyIds = buddyIds, + ) - setBody(body) - contentType(ContentType.Application.Json) - }.getOrThrow() - } + setBody(body) + contentType(ContentType.Application.Json) + }.getOrThrow() + } - public suspend fun findBuddyGroup(): List { - val response = client.get("/buddy/group/find").getOrThrow>() + public suspend fun upsert( + groupId: String, + memoAndTagIds: MemoAndTagIds, + ) { + client.post("/buddy/group/$groupId/upsertMemo") { + setBody(memoAndTagIds.toEntity()) + contentType(ContentType.Application.Json) + }.getOrThrow() + } - return response.map { - BuddyGroup( - id = it.id, - detail = BuddyGroupDetail( - title = it.title, - description = it.description, - ), - ) - } - } + public suspend fun findBuddyGroup(): List { + val response = client.get("/buddy/group/find").getOrThrow>() - public suspend fun findBuddyByEmail(email: String): List { - val response = client - .get("/buddy/find") { - parameter("email", email) - }.getOrThrow>() + return response.map { + BuddyGroup( + id = it.id, + detail = BuddyGroupDetail( + title = it.title, + description = it.description, + ), + ) + } + } - return response.map { - Buddy( - uid = it.uid, - email = it.email, - ) - } - } + public suspend fun findBuddyByEmail(email: String): List { + val response = client + .get("/buddy/find") { + parameter("email", email) + }.getOrThrow>() + + return response.map { + Buddy( + uid = it.uid, + email = it.email, + ) + } + } + + public suspend fun findMemoByDateRange( + groupId: String, + dateRange: ClosedRange, + ): List { + val response = client.get("/buddy/group/$groupId/findMemoByDateRange") { + parameter("start", dateRange.start) + parameter("endInclusive", dateRange.endInclusive) + }.getOrThrow>() + + return response.map(MemoEntity::toDto) + } } diff --git a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/mapper/MemoMapper.kt b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/mapper/MemoMapper.kt new file mode 100644 index 00000000..65a203e5 --- /dev/null +++ b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/mapper/MemoMapper.kt @@ -0,0 +1,64 @@ +package io.github.taetae98coding.diary.core.diary.service.mapper + +import io.github.taetae98coding.diary.common.model.memo.LegacyMemoEntity +import io.github.taetae98coding.diary.common.model.memo.MemoEntity +import io.github.taetae98coding.diary.core.model.memo.MemoAndTagIds +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.core.model.memo.MemoDto + +internal fun MemoAndTagIds.toEntity(): LegacyMemoEntity { + return LegacyMemoEntity( + id = memo.id, + title = memo.detail.title, + description = memo.detail.description, + start = memo.detail.start, + endInclusive = memo.detail.endInclusive, + color = memo.detail.color, + owner = requireNotNull(memo.owner), + primaryTag = memo.primaryTag, + tagIds = tagIds, + isFinish = memo.isFinish, + isDelete = memo.isDelete, + updateAt = memo.updateAt, + ) +} + +internal fun LegacyMemoEntity.toDto(): MemoDto { + return MemoDto( + id = id, + detail = + MemoDetail( + title = title, + description = description, + start = start, + endInclusive = endInclusive, + color = color, + ), + owner = owner, + primaryTag = primaryTag, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + serverUpdateAt = updateAt, + ) +} + +internal fun MemoEntity.toDto(): MemoDto { + return MemoDto( + id = id, + detail = + MemoDetail( + title = title, + description = description, + start = start, + endInclusive = endInclusive, + color = color, + ), + owner = null, + primaryTag = primaryTag, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + serverUpdateAt = updateAt, + ) +} diff --git a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/memo/MemoService.kt b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/memo/MemoService.kt index 35d63a3d..823a1ac9 100644 --- a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/memo/MemoService.kt +++ b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/memo/MemoService.kt @@ -1,11 +1,11 @@ package io.github.taetae98coding.diary.core.diary.service.memo -import io.github.taetae98coding.diary.common.model.memo.MemoEntity +import io.github.taetae98coding.diary.common.model.memo.LegacyMemoEntity import io.github.taetae98coding.diary.core.diary.service.DiaryServiceModule import io.github.taetae98coding.diary.core.diary.service.ext.getOrThrow +import io.github.taetae98coding.diary.core.diary.service.mapper.toDto +import io.github.taetae98coding.diary.core.diary.service.mapper.toEntity import io.github.taetae98coding.diary.core.model.memo.MemoAndTagIds -import io.github.taetae98coding.diary.core.model.memo.MemoDetail -import io.github.taetae98coding.diary.core.model.memo.MemoDto import io.ktor.client.HttpClient import io.ktor.client.request.get import io.ktor.client.request.parameter @@ -19,63 +19,23 @@ import org.koin.core.annotation.Named @Factory public class MemoService internal constructor( - @Named(DiaryServiceModule.DIARY_CLIENT) - private val client: HttpClient, + @Named(DiaryServiceModule.DIARY_CLIENT) + private val client: HttpClient, ) { - public suspend fun upsert(list: List) { - client - .post("/memo/upsert") { - val body = - list.map { - MemoEntity( - id = it.memo.id, - title = it.memo.detail.title, - description = it.memo.detail.description, - start = it.memo.detail.start, - endInclusive = it.memo.detail.endInclusive, - color = it.memo.detail.color, - owner = requireNotNull(it.memo.owner), - primaryTag = it.memo.primaryTag, - tagIds = it.tagIds, - isFinish = it.memo.isFinish, - isDelete = it.memo.isDelete, - updateAt = it.memo.updateAt, - ) - } + public suspend fun upsert(list: List) { + client.post("/memo/upsert") { + contentType(ContentType.Application.Json) + setBody(list.map(MemoAndTagIds::toEntity)) + }.getOrThrow() + } - contentType(ContentType.Application.Json) - setBody(body) - }.getOrThrow() - } + public suspend fun fetch(updateAt: Instant): List { + val response = client.get("/memo/fetch") { + parameter("updateAt", updateAt) + }.getOrThrow>() - public suspend fun fetch(updateAt: Instant): List { - val response = - client - .get("/memo/fetch") { - parameter("updateAt", updateAt) - }.getOrThrow>() - - return response.map { - val dto = - MemoDto( - id = it.id, - detail = - MemoDetail( - title = it.title, - description = it.description, - start = it.start, - endInclusive = it.endInclusive, - color = it.color, - ), - owner = it.owner, - primaryTag = it.primaryTag, - isFinish = it.isFinish, - isDelete = it.isDelete, - updateAt = it.updateAt, - serverUpdateAt = it.updateAt, - ) - - MemoAndTagIds(dto, it.tagIds) - } - } + return response.map { + MemoAndTagIds(it.toDto(), it.tagIds) + } + } } diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupCalendarDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupCalendarDestination.kt new file mode 100644 index 00000000..743b74c4 --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupCalendarDestination.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.core.navigation.buddy + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class BuddyGroupCalendarDestination( + @SerialName(GROUP_ID) + val groupId: String, +) { + public companion object { + public const val GROUP_ID: String = "GROUP_ID" + } +} diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupMemoAddDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupMemoAddDestination.kt new file mode 100644 index 00000000..7df0a5d2 --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupMemoAddDestination.kt @@ -0,0 +1,21 @@ +package io.github.taetae98coding.diary.core.navigation.buddy + +import kotlinx.datetime.LocalDate +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class BuddyGroupMemoAddDestination( + @SerialName(GROUP_ID) + val groupId: String, + @SerialName(START) + val start: LocalDate? = null, + @SerialName(END_INCLUSIVE) + val endInclusive: LocalDate? = null, +) { + public companion object { + public const val GROUP_ID: String = "groupId" + public const val START: String = "start" + public const val END_INCLUSIVE: String = "endInclusive" + } +} diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupMemoDetailDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupMemoDetailDestination.kt new file mode 100644 index 00000000..09637add --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyGroupMemoDetailDestination.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.core.navigation.buddy + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class BuddyGroupMemoDetailDestination( + @SerialName(MEMO_ID) + val memoId: String +) { + public companion object { + public const val MEMO_ID: String = "memoId" + } +} diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoAddDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoAddDestination.kt index 4c3930f3..d637f10d 100644 --- a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoAddDestination.kt +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoAddDestination.kt @@ -6,14 +6,16 @@ import kotlinx.serialization.Serializable @Serializable public data class MemoAddDestination( - @SerialName("start") + @SerialName(START) val start: LocalDate? = null, - @SerialName("endInclusive") + @SerialName(END_INCLUSIVE) val endInclusive: LocalDate? = null, @SerialName(SELECTED_TAG) val selectedTag: String? = null, ) { public companion object { + public const val START: String = "start" + public const val END_INCLUSIVE: String = "endInclusive" public const val SELECTED_TAG: String = "selectedTag" } } diff --git a/app/data/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/data/buddy/repository/BuddyRepositoryImpl.kt b/app/data/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/data/buddy/repository/BuddyRepositoryImpl.kt index f8c93493..4ef57fb3 100644 --- a/app/data/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/data/buddy/repository/BuddyRepositoryImpl.kt +++ b/app/data/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/data/buddy/repository/BuddyRepositoryImpl.kt @@ -3,25 +3,51 @@ package io.github.taetae98coding.diary.data.buddy.repository import io.github.taetae98coding.diary.core.diary.service.buddy.BuddyService import io.github.taetae98coding.diary.core.model.buddy.Buddy import io.github.taetae98coding.diary.core.model.buddy.BuddyGroup +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.MemoAndTagIds +import io.github.taetae98coding.diary.core.model.memo.MemoDto import io.github.taetae98coding.diary.domain.buddy.repository.BuddyRepository +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.datetime.LocalDate import org.koin.core.annotation.Factory -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid @OptIn(ExperimentalUuidApi::class) @Factory internal class BuddyRepositoryImpl( - private val remoteDataSource: BuddyService, + private val remoteDataSource: BuddyService, ) : BuddyRepository { - override suspend fun upsert(buddyGroup: BuddyGroup, buddyIds: Set) { - remoteDataSource.upsert(buddyGroup, buddyIds) - } + override suspend fun upsert(buddyGroup: BuddyGroup, buddyIds: Set) { + remoteDataSource.upsert(buddyGroup, buddyIds) + } + + override suspend fun upsert( + groupId: String, + memo: Memo, + tagIds: Set, + ) { + remoteDataSource.upsert(groupId, MemoAndTagIds(memo.toDto(), tagIds)) + } + + override fun findMemoByDateRange(groupId: String, dateRange: ClosedRange): Flow> { + return flow { + remoteDataSource.findMemoByDateRange(groupId, dateRange) + .map(MemoDto::toMemo) + .also { emit(it) } + } + } + + override fun findBuddyGroup(): Flow> = flow { emit(remoteDataSource.findBuddyGroup()) } - override fun findBuddyGroup(): Flow> = flow { emit(remoteDataSource.findBuddyGroup()) } + override fun findBuddyByEmail(email: String): Flow> = flow { emit(remoteDataSource.findBuddyByEmail(email)) } - override fun findBuddyByEmail(email: String): Flow> = flow { emit(remoteDataSource.findBuddyByEmail(email)) } + override suspend fun getNextBuddyGroupId(): String = Uuid.random().toString() - override suspend fun getNextBuddyGroupId(): String = Uuid.random().toString() + override suspend fun getNextMemoId(): String { + return Uuid.random().toString() + } } diff --git a/app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/repository/MemoFetchRepositoryImpl.kt b/app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/repository/MemoFetchRepositoryImpl.kt index 00594185..7e2b9113 100644 --- a/app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/repository/MemoFetchRepositoryImpl.kt +++ b/app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/repository/MemoFetchRepositoryImpl.kt @@ -20,7 +20,7 @@ internal class MemoFetchRepositoryImpl( val updateAt = localDataSource.getLastServerUpdateAt(uid).first() ?: Instant.fromEpochMilliseconds(0L) val list = remoteDataSource.fetch(updateAt) - if (list.isEmpty()) { + if (list.isEmpty()) { break } diff --git a/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/repository/BuddyRepository.kt b/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/repository/BuddyRepository.kt index 5150a5dc..d0e502d2 100644 --- a/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/repository/BuddyRepository.kt +++ b/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/repository/BuddyRepository.kt @@ -2,14 +2,20 @@ package io.github.taetae98coding.diary.domain.buddy.repository import io.github.taetae98coding.diary.core.model.buddy.Buddy import io.github.taetae98coding.diary.core.model.buddy.BuddyGroup +import io.github.taetae98coding.diary.core.model.memo.Memo import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.LocalDate public interface BuddyRepository { public suspend fun upsert(buddyGroup: BuddyGroup, buddyIds: Set) + public suspend fun upsert(groupId: String, memo: Memo, tagIds: Set) + + public fun findMemoByDateRange(groupId: String, dateRange: ClosedRange): Flow> public fun findBuddyGroup(): Flow> public fun findBuddyByEmail(email: String): Flow> public suspend fun getNextBuddyGroupId(): String + public suspend fun getNextMemoId(): String } diff --git a/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/AddBuddyGroupMemoUseCase.kt b/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/AddBuddyGroupMemoUseCase.kt new file mode 100644 index 00000000..74c406fd --- /dev/null +++ b/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/AddBuddyGroupMemoUseCase.kt @@ -0,0 +1,38 @@ +package io.github.taetae98coding.diary.domain.buddy.usecase + +import io.github.taetae98coding.diary.common.exception.memo.MemoTitleBlankException +import io.github.taetae98coding.diary.core.model.memo.Memo +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.domain.buddy.repository.BuddyRepository +import kotlinx.datetime.Clock +import org.koin.core.annotation.Factory + +@Factory +public class AddBuddyGroupMemoUseCase internal constructor( + private val clock: Clock, + private val repository: BuddyRepository, +) { + public suspend operator fun invoke( + groupId: String?, + detail: MemoDetail, + primaryTag: String?, + tagIds: Set, + ): Result { + return runCatching { + if (groupId.isNullOrBlank()) return@runCatching + if (detail.title.isBlank()) throw MemoTitleBlankException() + + val memo = Memo( + id = repository.getNextMemoId(), + detail = detail, + primaryTag = primaryTag, + owner = groupId, + isFinish = false, + isDelete = false, + updateAt = clock.now(), + ) + + repository.upsert(groupId, memo, tagIds) + } + } +} diff --git a/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyUseCase.kt b/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyUseCase.kt index 9cd14373..da8516e5 100644 --- a/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyUseCase.kt +++ b/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyUseCase.kt @@ -1,12 +1,13 @@ package io.github.taetae98coding.diary.domain.buddy.usecase +import io.github.taetae98coding.diary.core.model.account.Account.Guest import io.github.taetae98coding.diary.core.model.buddy.Buddy +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase import io.github.taetae98coding.diary.domain.buddy.repository.BuddyRepository 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.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest import org.koin.core.annotation.Factory @@ -14,13 +15,21 @@ import org.koin.core.annotation.Factory @OptIn(ExperimentalCoroutinesApi::class) @Factory public class FindBuddyUseCase internal constructor( - private val repository: BuddyRepository, + private val getAccountUseCase: GetAccountUseCase, + private val repository: BuddyRepository, ) { - public operator fun invoke(email: String?): Flow>> { - if (email.isNullOrBlank()) return flowOf(Result.success(emptyList())) + public operator fun invoke(email: String?): Flow>> { + if (email.isNullOrBlank()) return flowOf(Result.success(emptyList())) - return flow { emitAll(repository.findBuddyByEmail(email)) } - .mapLatest { Result.success(it) } - .catch { emit(Result.failure(it)) } - } + return getAccountUseCase().mapLatest { it.getOrThrow() } + .flatMapLatest { account -> + if (account is Guest) { + flowOf(emptyList()) + } else { + repository.findBuddyByEmail(email) + } + } + .mapLatest { Result.success(it) } + .catch { emit(Result.failure(it)) } + } } diff --git a/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/PageBuddyGroupCalendarMemo.kt b/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/PageBuddyGroupCalendarMemo.kt new file mode 100644 index 00000000..14f52920 --- /dev/null +++ b/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/PageBuddyGroupCalendarMemo.kt @@ -0,0 +1,34 @@ +package io.github.taetae98coding.diary.domain.buddy.usecase + +import io.github.taetae98coding.diary.core.model.account.Account +import io.github.taetae98coding.diary.core.model.memo.Memo +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.buddy.repository.BuddyRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest +import kotlinx.datetime.LocalDate +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class PageBuddyGroupCalendarMemo internal constructor( + private val getAccountUseCase: GetAccountUseCase, + private val repository: BuddyRepository, +) { + public operator fun invoke(groupId: String, dateRange: ClosedRange): Flow>> { + return getAccountUseCase().mapLatest { it.getOrThrow() } + .flatMapLatest { account -> + if (account is Account.Guest) { + flowOf(emptyList()) + } else { + repository.findMemoByDateRange(groupId, dateRange) + } + } + .mapLatest { Result.success(it) } + .catch { emit(Result.failure(it)) } + } +} diff --git a/app/feature/buddy/build.gradle.kts b/app/feature/buddy/build.gradle.kts index 873d379f..a87512ea 100644 --- a/app/feature/buddy/build.gradle.kts +++ b/app/feature/buddy/build.gradle.kts @@ -6,8 +6,11 @@ kotlin { sourceSets { commonMain { dependencies { + implementation(project(":app:core:calendar-compose")) + implementation(project(":app:domain:account")) implementation(project(":app:domain:buddy")) + implementation(project(":app:domain:holiday")) } } } diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/BuddyNavigation.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/BuddyNavigation.kt index 545f9ef6..812d8e20 100644 --- a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/BuddyNavigation.kt +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/BuddyNavigation.kt @@ -5,22 +5,57 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.compose.navigation +import androidx.navigation.toRoute import io.github.taetae98coding.diary.core.compose.adaptive.isListVisible import io.github.taetae98coding.diary.core.navigation.buddy.BuddyDestination +import io.github.taetae98coding.diary.core.navigation.buddy.BuddyGroupCalendarDestination +import io.github.taetae98coding.diary.core.navigation.buddy.BuddyGroupMemoAddDestination +import io.github.taetae98coding.diary.core.navigation.buddy.BuddyGroupMemoDetailDestination import io.github.taetae98coding.diary.core.navigation.buddy.BuddyHomeDestination +import io.github.taetae98coding.diary.feature.buddy.calendar.BuddyGroupCalendarRoute import io.github.taetae98coding.diary.feature.buddy.home.BuddyHomeRoute +import io.github.taetae98coding.diary.feature.buddy.memo.add.BuddyGroupMemoAddRoute +import io.github.taetae98coding.diary.feature.buddy.memo.detail.BuddyGroupMemoDetailRoute +import io.github.taetae98coding.diary.library.navigation.LocalDateNavType +import kotlin.reflect.typeOf +import kotlinx.datetime.LocalDate @OptIn(ExperimentalMaterial3AdaptiveApi::class) public fun NavGraphBuilder.buddyNavigation( - navController: NavController, + navController: NavController, ) { - navigation( - startDestination = BuddyHomeDestination, - ) { - composable { backStackEntry -> - BuddyHomeRoute( - onScaffoldValueChange = { backStackEntry.savedStateHandle["app_navigation_visible"] = it.isListVisible() }, - ) - } - } + navigation( + startDestination = BuddyHomeDestination, + ) { + composable { backStackEntry -> + BuddyHomeRoute( + navigateToBuddyCalendar = { navController.navigate(BuddyGroupCalendarDestination(it)) }, + onScaffoldValueChange = { backStackEntry.savedStateHandle["app_navigation_visible"] = it.isListVisible() }, + ) + } + + composable { + val route = it.toRoute() + + BuddyGroupCalendarRoute( + navigateUp = navController::popBackStack, + navigateToBuddyGroupMemoAdd = { navController.navigate(BuddyGroupMemoAddDestination(route.groupId, it.start, it.endInclusive)) }, + navigateToBuddyGroupMemoDetail = {navController.navigate(BuddyGroupMemoDetailDestination(it))}, + ) + } + + composable( + typeMap = mapOf(typeOf() to LocalDateNavType), + ) { + BuddyGroupMemoAddRoute( + navigateUp = navController::popBackStack, + ) + } + + composable { + BuddyGroupMemoDetailRoute( + navigateUp = navController::popBackStack, + ) + } + } } diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/add/RememberBuddyDetailScreenAddState.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/add/RememberBuddyDetailScreenAddState.kt index 13892bbb..75b9e3d4 100644 --- a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/add/RememberBuddyDetailScreenAddState.kt +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/add/RememberBuddyDetailScreenAddState.kt @@ -1,5 +1,7 @@ package io.github.taetae98coding.diary.feature.buddy.add +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -9,15 +11,15 @@ import io.github.taetae98coding.diary.feature.buddy.detail.BuddyDetailScreenStat @Composable internal fun rememberBuddyDetailScreenAddState(): BuddyDetailScreenState.Add { - val coroutineScope = rememberCoroutineScope() - val componentState = rememberDiaryComponentState() - val buddyBottomSheetState = rememberBuddyBottomSheetState() + val coroutineScope = rememberCoroutineScope() + val componentState = rememberDiaryComponentState() + val buddyBottomSheetState = rememberBuddyBottomSheetState() - return remember { - BuddyDetailScreenState.Add( - coroutineScope = coroutineScope, - componentState = componentState, - buddyBottomSheetState = buddyBottomSheetState, - ) - } + return remember { + BuddyDetailScreenState.Add( + coroutineScope = coroutineScope, + componentState = componentState, + buddyBottomSheetState = buddyBottomSheetState, + ) + } } diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarHolidayViewModel.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarHolidayViewModel.kt new file mode 100644 index 00000000..b0420b71 --- /dev/null +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarHolidayViewModel.kt @@ -0,0 +1,53 @@ +package io.github.taetae98coding.diary.feature.buddy.calendar + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.domain.holiday.usecase.FindHolidayUseCase +import io.github.taetae98coding.diary.library.coroutines.combine +import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlinx.datetime.plus +import org.koin.android.annotation.KoinViewModel + +@OptIn(ExperimentalCoroutinesApi::class) +@KoinViewModel +internal class BuddyGroupCalendarHolidayViewModel( + findHolidayUseCase: FindHolidayUseCase, +) : ViewModel() { + private val yearAndMonth = MutableStateFlow?>(null) + + val holidayList = yearAndMonth + .filterNotNull() + .mapLatest { (year, month) -> LocalDate(year, month, 1) } + .mapLatest { localDate -> IntRange(-2, 2).map { localDate.plus(it, DateTimeUnit.MONTH) } } + .mapLatest { list -> list.map { findHolidayUseCase(it.year, it.month) } } + .flatMapLatest { list -> list.combine { array -> array.flatMap { it.getOrNull().orEmpty() } } } + .mapCollectionLatest { + CalendarItemUiState.Holiday( + text = it.name, + start = it.start, + endInclusive = it.endInclusive, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList(), + ) + + fun fetchHoliday(year: Int, month: Month) { + viewModelScope.launch { + yearAndMonth.emit(year to month) + } + } +} diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarMemoViewModel.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarMemoViewModel.kt new file mode 100644 index 00000000..eb2e32b3 --- /dev/null +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarMemoViewModel.kt @@ -0,0 +1,70 @@ +package io.github.taetae98coding.diary.feature.buddy.calendar + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.core.navigation.buddy.BuddyGroupCalendarDestination +import io.github.taetae98coding.diary.domain.buddy.usecase.PageBuddyGroupCalendarMemo +import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlinx.datetime.minus +import kotlinx.datetime.plus +import org.koin.android.annotation.KoinViewModel + +@OptIn(ExperimentalCoroutinesApi::class) +@KoinViewModel +internal class BuddyGroupCalendarMemoViewModel( + savedStateHandle: SavedStateHandle, + private val pageBuddyGroupCalendarMemo: PageBuddyGroupCalendarMemo, +) : ViewModel() { + private val route = savedStateHandle.toRoute() + private val yearAndMonth = MutableStateFlow?>(null) + + private val refreshCount = MutableStateFlow(0) + + val memoList = yearAndMonth + .filterNotNull() + .mapLatest { (year, month) -> LocalDate(year, month, 1) } + .mapLatest { it.minus(3, DateTimeUnit.MONTH)..it.plus(3, DateTimeUnit.MONTH) } + .flatMapLatest { dateRange -> + refreshCount.flatMapLatest { + pageBuddyGroupCalendarMemo(route.groupId, dateRange) + } + } + .mapLatest { it.getOrNull().orEmpty() } + .mapCollectionLatest { + CalendarItemUiState.Text( + key = MemoKey(it.id), + text = it.detail.title, + color = it.detail.color, + start = requireNotNull(it.detail.start), + endInclusive = requireNotNull(it.detail.endInclusive), + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList(), + ) + + fun fetchMemo(year: Int, month: Month) { + viewModelScope.launch { + yearAndMonth.emit(year to month) + } + } + + fun refresh() { + refreshCount.value++ + } +} diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarRoute.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarRoute.kt new file mode 100644 index 00000000..9a98f6ac --- /dev/null +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/BuddyGroupCalendarRoute.kt @@ -0,0 +1,74 @@ +package io.github.taetae98coding.diary.feature.buddy.calendar + +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.LifecycleStartEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.taetae98coding.diary.core.compose.calendar.CalendarScaffold +import io.github.taetae98coding.diary.core.compose.calendar.CalendarScaffoldState +import io.github.taetae98coding.diary.core.compose.calendar.rememberCalendarScaffoldState +import io.github.taetae98coding.diary.core.design.system.icon.NavigateUpIcon +import kotlinx.datetime.LocalDate +import org.koin.compose.viewmodel.koinViewModel + +@Composable +internal fun BuddyGroupCalendarRoute( + navigateUp: () -> Unit, + navigateToBuddyGroupMemoAdd: (ClosedRange) -> Unit, + navigateToBuddyGroupMemoDetail: (String) -> Unit, + modifier: Modifier = Modifier, + memoViewModel: BuddyGroupCalendarMemoViewModel = koinViewModel(), + holidayViewModel: BuddyGroupCalendarHolidayViewModel = koinViewModel(), +) { + val state = rememberCalendarScaffoldState( + onFilter = {}, + ) + val memoList by memoViewModel.memoList.collectAsStateWithLifecycle() + val holidayList by holidayViewModel.holidayList.collectAsStateWithLifecycle() + + CalendarScaffold( + state = state, + onSelectDate = navigateToBuddyGroupMemoAdd, + hasFilterProvider = { false }, + textItemListProvider = { memoList }, + holidayListProvider = { holidayList }, + onCalendarItemClick = { + when (it) { + is MemoKey -> navigateToBuddyGroupMemoDetail(it.id) + } + }, + navigationIcon = { + IconButton(onClick = navigateUp) { + NavigateUpIcon() + } + }, + ) + + Fetch( + state = state, + memoViewModel = memoViewModel, + holidayViewModel = holidayViewModel, + ) + + LifecycleStartEffect(memoViewModel) { + memoViewModel.refresh() + onStopOrDispose { + + } + } +} + +@Composable +private fun Fetch( + state: CalendarScaffoldState, + memoViewModel: BuddyGroupCalendarMemoViewModel, + holidayViewModel: BuddyGroupCalendarHolidayViewModel, +) { + LaunchedEffect(state.calendarState.year, state.calendarState.month) { + memoViewModel.fetchMemo(state.calendarState.year, state.calendarState.month) + holidayViewModel.fetchHoliday(state.calendarState.year, state.calendarState.month) + } +} diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/MemoKey.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/MemoKey.kt new file mode 100644 index 00000000..51306c79 --- /dev/null +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/calendar/MemoKey.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.feature.buddy.calendar + +import kotlin.jvm.JvmInline + +@JvmInline +internal value class MemoKey( + val id: String, +) diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/BuddyDetailScreen.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/BuddyDetailScreen.kt index 260541b5..70e71e10 100644 --- a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/BuddyDetailScreen.kt +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/BuddyDetailScreen.kt @@ -7,151 +7,231 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Menu import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.NavigationDrawerItem 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.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import io.github.taetae98coding.diary.core.compose.button.FloatingAddButton +import io.github.taetae98coding.diary.core.compose.back.KBackHandler +import io.github.taetae98coding.diary.core.compose.navigation.ReverseModalDrawerSheet +import io.github.taetae98coding.diary.core.compose.navigation.ReverseModalNavigationDrawer import io.github.taetae98coding.diary.core.design.system.diary.component.DiaryComponent import io.github.taetae98coding.diary.core.design.system.emoji.Emoji -import io.github.taetae98coding.diary.core.design.system.icon.NavigateUpIcon +import io.github.taetae98coding.diary.core.design.system.icon.CalendarIcon +import io.github.taetae98coding.diary.core.design.system.icon.MemoIcon +import io.github.taetae98coding.diary.core.design.system.icon.TagIcon import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme import io.github.taetae98coding.diary.core.model.buddy.Buddy import io.github.taetae98coding.diary.feature.buddy.common.BuddyBottomSheetUiState +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun BuddyDetailScreen( - onNavigateUp: () -> Unit, - onAdd: () -> Unit, - state: BuddyDetailScreenState, - uiStateProvider: () -> BuddyDetailScreenUiState, - buddyUiStateProvider: () -> List?, - buddyBottomSheetUiStateProvider: () -> BuddyBottomSheetUiState, - modifier: Modifier = Modifier, + state: BuddyDetailScreenState, + uiStateProvider: () -> BuddyDetailScreenUiState, + buddyUiStateProvider: () -> List?, + buddyBottomSheetUiStateProvider: () -> BuddyBottomSheetUiState, + modifier: Modifier = Modifier, + topBarTitle: @Composable () -> Unit = {}, + navigationIcon: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, ) { - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = { Text("그룹 추가") }, - navigationIcon = { - IconButton(onClick = onNavigateUp) { - NavigateUpIcon() - } - }, - ) - }, - snackbarHost = { SnackbarHost(hostState = state.hostState) }, - floatingActionButton = { - val isProgress by remember { derivedStateOf { uiStateProvider().isProgress } } - - FloatingAddButton( - onClick = onAdd, - progressProvider = { isProgress }, - ) - }, - ) { - Content( - state = state, - buddyUiStateProvider = buddyUiStateProvider, - buddyBottomSheetUiStateProvider = buddyBottomSheetUiStateProvider, - modifier = Modifier - .fillMaxSize() - .padding(it) - .padding(DiaryTheme.dimen.screenPaddingValues), - ) - } - - Message( - state = state, - uiStateProvider = uiStateProvider, - ) + if (state is BuddyDetailScreenState.Detail) { + val coroutineScope = rememberCoroutineScope() + + ReverseModalNavigationDrawer( + modifier = modifier, + drawerContent = { + ReverseModalDrawerSheet { + NavigationDrawerItem( + label = { Text("메모") }, + selected = false, + onClick = {}, + icon = { MemoIcon() }, + ) + NavigationDrawerItem( + label = { Text("태그") }, + selected = false, + onClick = {}, + icon = { TagIcon() }, + ) + NavigationDrawerItem( + label = { Text("캘린더") }, + selected = false, + onClick = state.onCalendar, + icon = { CalendarIcon() }, + ) + } + }, + drawerState = state.drawerState, + ) { + ScreenScaffold( + state = state, + uiStateProvider = uiStateProvider, + buddyUiStateProvider = buddyUiStateProvider, + buddyBottomSheetUiStateProvider = buddyBottomSheetUiStateProvider, + topBarTitle = topBarTitle, + navigationIcon = navigationIcon, + floatingActionButton = floatingActionButton, + ) + } + + KBackHandler(isEnabled = state.drawerState.isOpen) { + coroutineScope.launch { state.drawerState.close() } + } + } else { + ScreenScaffold( + state = state, + uiStateProvider = uiStateProvider, + buddyUiStateProvider = buddyUiStateProvider, + buddyBottomSheetUiStateProvider = buddyBottomSheetUiStateProvider, + modifier = modifier, + topBarTitle = topBarTitle, + navigationIcon = navigationIcon, + floatingActionButton = floatingActionButton, + ) + } + + Message( + state = state, + uiStateProvider = uiStateProvider, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ScreenScaffold( + state: BuddyDetailScreenState, + uiStateProvider: () -> BuddyDetailScreenUiState, + buddyUiStateProvider: () -> List?, + buddyBottomSheetUiStateProvider: () -> BuddyBottomSheetUiState, + modifier: Modifier = Modifier, + topBarTitle: @Composable () -> Unit = {}, + navigationIcon: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, +) { + Scaffold( + topBar = { + val coroutineScope = rememberCoroutineScope() + + TopAppBar( + title = topBarTitle, + navigationIcon = navigationIcon, + actions = { + if (state is BuddyDetailScreenState.Detail) { + IconButton( + onClick = { coroutineScope.launch { state.drawerState.open() } }, + ) { + Icon( + imageVector = Icons.Rounded.Menu, + contentDescription = null, + ) + } + } + }, + ) + }, + snackbarHost = { SnackbarHost(hostState = state.hostState) }, + floatingActionButton = floatingActionButton, + ) { + Content( + state = state, + buddyUiStateProvider = buddyUiStateProvider, + buddyBottomSheetUiStateProvider = buddyBottomSheetUiStateProvider, + modifier = Modifier + .fillMaxSize() + .padding(it) + .padding(DiaryTheme.dimen.screenPaddingValues), + ) + } } @Composable private fun Message( - state: BuddyDetailScreenState, - uiStateProvider: () -> BuddyDetailScreenUiState, + state: BuddyDetailScreenState, + uiStateProvider: () -> BuddyDetailScreenUiState, ) { - val uiState = uiStateProvider() - - LaunchedEffect( - uiState.isAdd, - uiState.isExit, - uiState.isUpdate, - uiState.isTitleBlankError, - uiState.isNetworkError, - uiState.isUnknownError, - ) { - if (!uiState.hasMessage) return@LaunchedEffect - - when { - uiState.isAdd -> { - state.showMessage("그룹 추가 ${Emoji.congratulate.random()}") - state.clearInput() - state.requestTitleFocus() - } - - uiState.isExit -> { + val uiState = uiStateProvider() + + LaunchedEffect( + uiState.isAdd, + uiState.isExit, + uiState.isUpdate, + uiState.isTitleBlankError, + uiState.isNetworkError, + uiState.isUnknownError, + ) { + if (!uiState.hasMessage) return@LaunchedEffect + + when { + uiState.isAdd -> { + state.showMessage("그룹 추가 ${Emoji.congratulate.random()}") + state.clearInput() + state.requestTitleFocus() + } + + uiState.isExit -> { // if (state is TagDetailScreenState.Detail) { // state.onDelete() // } - } + } - uiState.isUpdate -> { + uiState.isUpdate -> { // if (state is TagDetailScreenState.Detail) { // state.onUpdate() // } - } + } - uiState.isTitleBlankError -> { - state.showMessage("제목을 입력해 주세요 ${Emoji.check.random()}") - state.requestTitleFocus() - state.titleError() - } + uiState.isTitleBlankError -> { + state.showMessage("제목을 입력해 주세요 ${Emoji.check.random()}") + state.requestTitleFocus() + state.titleError() + } - uiState.isNetworkError -> { - state.showMessage("네트워크 상태를 확인해 주세요 ${Emoji.error.random()}") - } + uiState.isNetworkError -> { + state.showMessage("네트워크 상태를 확인해 주세요 ${Emoji.error.random()}") + } - uiState.isUnknownError -> state.showMessage("알 수 없는 에러가 발생했어요 잠시 후 다시 시도해 주세요 ${Emoji.error.random()}") - } + uiState.isUnknownError -> state.showMessage("알 수 없는 에러가 발생했어요 잠시 후 다시 시도해 주세요 ${Emoji.error.random()}") + } - uiState.onMessageShow() - } + uiState.onMessageShow() + } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun Content( - state: BuddyDetailScreenState, - buddyUiStateProvider: () -> List?, - buddyBottomSheetUiStateProvider: () -> BuddyBottomSheetUiState, - modifier: Modifier = Modifier, + state: BuddyDetailScreenState, + buddyUiStateProvider: () -> List?, + buddyBottomSheetUiStateProvider: () -> BuddyBottomSheetUiState, + modifier: Modifier = Modifier, ) { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .then(modifier), - verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), - ) { - DiaryComponent( - state = state.componentState, - ) - - BuddyGroup( - state = state.buddyBottomSheetState, - buddyUiStateProvider = buddyUiStateProvider, - bottomSheetUiState = buddyBottomSheetUiStateProvider, - modifier = Modifier.fillMaxWidth(), - ) - } + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .then(modifier), + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + DiaryComponent( + state = state.componentState, + ) + + BuddyGroup( + state = state.buddyBottomSheetState, + buddyUiStateProvider = buddyUiStateProvider, + bottomSheetUiState = buddyBottomSheetUiStateProvider, + modifier = Modifier.fillMaxWidth(), + ) + } } diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/BuddyDetailScreenState.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/BuddyDetailScreenState.kt index cfb971b7..9237d561 100644 --- a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/BuddyDetailScreenState.kt +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/BuddyDetailScreenState.kt @@ -1,5 +1,6 @@ package io.github.taetae98coding.diary.feature.buddy.detail +import androidx.compose.material3.DrawerState import androidx.compose.material3.SnackbarHostState import io.github.taetae98coding.diary.core.design.system.diary.component.DiaryComponentState import io.github.taetae98coding.diary.core.model.buddy.BuddyGroupDetail @@ -9,43 +10,51 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch internal sealed class BuddyDetailScreenState { - protected abstract val coroutineScope: CoroutineScope - - abstract val componentState: DiaryComponentState - abstract val buddyBottomSheetState: BuddyBottomSheetState - - private var messageJob: Job? = null - - val hostState: SnackbarHostState = SnackbarHostState() - - data class Add( - override val coroutineScope: CoroutineScope, - override val componentState: DiaryComponentState, - override val buddyBottomSheetState: BuddyBottomSheetState, - ) : BuddyDetailScreenState() - - val detail: BuddyGroupDetail - get() { - return BuddyGroupDetail( - title = componentState.title, - description = componentState.description, - ) - } - - fun requestTitleFocus() { - componentState.requestTitleFocus() - } - - fun clearInput() { - componentState.clearInput() - } - - fun titleError() { - componentState.titleError() - } - - fun showMessage(message: String) { - messageJob?.cancel() - messageJob = coroutineScope.launch { hostState.showSnackbar(message) } - } + protected abstract val coroutineScope: CoroutineScope + + abstract val componentState: DiaryComponentState + abstract val buddyBottomSheetState: BuddyBottomSheetState + + private var messageJob: Job? = null + + val hostState: SnackbarHostState = SnackbarHostState() + + data class Add( + override val coroutineScope: CoroutineScope, + override val componentState: DiaryComponentState, + override val buddyBottomSheetState: BuddyBottomSheetState, + ) : BuddyDetailScreenState() + + data class Detail( + val onCalendar: () -> Unit, + override val coroutineScope: CoroutineScope, + val drawerState: DrawerState, + override val componentState: DiaryComponentState, + override val buddyBottomSheetState: BuddyBottomSheetState, + ) : BuddyDetailScreenState() + + val detail: BuddyGroupDetail + get() { + return BuddyGroupDetail( + title = componentState.title, + description = componentState.description, + ) + } + + 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/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/RememberBuddyDetailScreenDetailState.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/RememberBuddyDetailScreenDetailState.kt new file mode 100644 index 00000000..2e6d980c --- /dev/null +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/detail/RememberBuddyDetailScreenDetailState.kt @@ -0,0 +1,29 @@ +package io.github.taetae98coding.diary.feature.buddy.detail + +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.github.taetae98coding.diary.core.design.system.diary.component.rememberDiaryComponentState +import io.github.taetae98coding.diary.feature.buddy.common.rememberBuddyBottomSheetState + +@Composable +internal fun rememberBuddyDetailScreenDetailState( + onCalendar: () -> Unit, +): BuddyDetailScreenState.Detail { + val coroutineScope = rememberCoroutineScope() + val drawerState = rememberDrawerState(DrawerValue.Closed) + val componentState = rememberDiaryComponentState() + val buddyBottomSheetState = rememberBuddyBottomSheetState() + + return remember { + BuddyDetailScreenState.Detail( + onCalendar = onCalendar, + coroutineScope = coroutineScope, + drawerState = drawerState, + componentState = componentState, + buddyBottomSheetState = buddyBottomSheetState, + ) + } +} diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/home/BuddyHomeRoute.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/home/BuddyHomeRoute.kt index c1f62333..aa9f5881 100644 --- a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/home/BuddyHomeRoute.kt +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/home/BuddyHomeRoute.kt @@ -1,22 +1,34 @@ package io.github.taetae98coding.diary.feature.buddy.home +import androidx.compose.material3.IconButton import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo 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.layout.ThreePaneScaffoldValue +import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective 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.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.window.core.layout.WindowWidthSizeClass +import io.github.taetae98coding.diary.core.compose.adaptive.isDetailVisible +import io.github.taetae98coding.diary.core.compose.adaptive.isListVisible +import io.github.taetae98coding.diary.core.compose.button.FloatingAddButton +import io.github.taetae98coding.diary.core.compose.topbar.TopBarTitle +import io.github.taetae98coding.diary.core.design.system.icon.NavigateUpIcon import io.github.taetae98coding.diary.feature.buddy.add.BuddyAddViewModel import io.github.taetae98coding.diary.feature.buddy.add.rememberBuddyDetailScreenAddState import io.github.taetae98coding.diary.feature.buddy.detail.BuddyDetailScreen import io.github.taetae98coding.diary.feature.buddy.detail.BuddyDetailScreenState +import io.github.taetae98coding.diary.feature.buddy.detail.rememberBuddyDetailScreenDetailState import io.github.taetae98coding.diary.feature.buddy.list.BuddyListScreen import io.github.taetae98coding.diary.feature.buddy.list.BuddyListViewModel import org.koin.compose.viewmodel.koinViewModel @@ -24,74 +36,132 @@ import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun BuddyHomeRoute( - onScaffoldValueChange: (ThreePaneScaffoldValue) -> Unit, - modifier: Modifier = Modifier, - listViewModel: BuddyListViewModel = koinViewModel(), - addViewModel: BuddyAddViewModel = koinViewModel(), + navigateToBuddyCalendar: (String) -> Unit, + onScaffoldValueChange: (ThreePaneScaffoldValue) -> Unit, + modifier: Modifier = Modifier, + listViewModel: BuddyListViewModel = koinViewModel(), + addViewModel: BuddyAddViewModel = koinViewModel(), ) { - val navigator = rememberListDetailPaneScaffoldNavigator() + val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val navigator = rememberListDetailPaneScaffoldNavigator(scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)) - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), - value = navigator.scaffoldValue, - listPane = { - AnimatedPane { - val groupList by listViewModel.groupList.collectAsStateWithLifecycle() + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + val isFloatingVisible by remember { + derivedStateOf { + if (navigator.isDetailVisible()) { + navigator.currentDestination?.content != null + } else { + true + } + } + } + val groupList by listViewModel.groupList.collectAsStateWithLifecycle() - BuddyListScreen( - groupListProvider = { groupList }, - onAdd = { navigator.navigateTo(ThreePaneScaffoldRole.Primary) }, - ) - } - }, - detailPane = { - AnimatedPane { - val state = rememberBuddyDetailScreenAddState() - val uiState by addViewModel.uiState.collectAsStateWithLifecycle() - val buddyUiState by addViewModel.buddyUiState.collectAsStateWithLifecycle() - val buddyBottomSheetState by addViewModel.buddyBottomSheetUiState.collectAsStateWithLifecycle() + BuddyListScreen( + listProvider = { groupList }, + onGroup = { navigator.navigateTo(ThreePaneScaffoldRole.Primary, it) }, + floatingActionButton = { + if (isFloatingVisible) { + FloatingAddButton( + onClick = { navigator.navigateTo(ThreePaneScaffoldRole.Primary, null) }, + ) + } + }, + ) + } + }, + detailPane = { + AnimatedPane { + val isAdd by remember { derivedStateOf { navigator.currentDestination?.content == null } } + val isNavigateUpVisible by remember(windowAdaptiveInfo) { + derivedStateOf { + if (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { + !navigator.isListVisible() + } else { + true + } + } + } + val state = if (isAdd) { + rememberBuddyDetailScreenAddState() + } else { + rememberBuddyDetailScreenDetailState( + onCalendar = { + navigator.currentDestination?.content?.let(navigateToBuddyCalendar) + }, + ) + } + val uiState by addViewModel.uiState.collectAsStateWithLifecycle() + val buddyUiState by addViewModel.buddyUiState.collectAsStateWithLifecycle() + val buddyBottomSheetState by addViewModel.buddyBottomSheetUiState.collectAsStateWithLifecycle() - BuddyDetailScreen( - onNavigateUp = navigator::navigateBack, - onAdd = { addViewModel.add(state.detail) }, - state = state, - uiStateProvider = { uiState }, - buddyUiStateProvider = { buddyUiState }, - buddyBottomSheetUiStateProvider = { buddyBottomSheetState }, - ) + BuddyDetailScreen( + state = state, + uiStateProvider = { uiState }, + buddyUiStateProvider = { buddyUiState }, + buddyBottomSheetUiStateProvider = { buddyBottomSheetState }, + topBarTitle = { + if (isAdd) { + TopBarTitle(text = "그룹 추가") + } else { + TopBarTitle(text = "그룹") + } + }, + navigationIcon = { + if (isNavigateUpVisible) { + IconButton(onClick = navigator::navigateBack) { + NavigateUpIcon() + } + } + }, + floatingActionButton = { + if (isAdd) { + val isProgress by remember { derivedStateOf { uiState.isProgress } } - FetchAccount( - addViewModel = addViewModel, - state = state, - ) - } - }, - modifier = modifier, - ) + FloatingAddButton( + onClick = { addViewModel.add(state.detail) }, + progressProvider = { isProgress }, + ) + } + } + ) - LaunchedScaffoldValue( - navigator = navigator, - onScaffoldValueChange = onScaffoldValueChange, - ) + FetchAccount( + addViewModel = addViewModel, + state = state, + ) + } + }, + modifier = modifier, + ) + + LaunchedScaffoldValue( + navigator = navigator, + onScaffoldValueChange = onScaffoldValueChange, + ) } @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable private fun LaunchedScaffoldValue( - navigator: ThreePaneScaffoldNavigator, - onScaffoldValueChange: (ThreePaneScaffoldValue) -> Unit, + navigator: ThreePaneScaffoldNavigator, + onScaffoldValueChange: (ThreePaneScaffoldValue) -> Unit, ) { - LaunchedEffect(navigator.scaffoldValue) { - onScaffoldValueChange(navigator.scaffoldValue) - } + LaunchedEffect(navigator.scaffoldValue) { + onScaffoldValueChange(navigator.scaffoldValue) + } } @Composable private fun FetchAccount( - addViewModel: BuddyAddViewModel, - state: BuddyDetailScreenState, + addViewModel: BuddyAddViewModel, + state: BuddyDetailScreenState, ) { - LaunchedEffect(state.buddyBottomSheetState.email) { - addViewModel.fetch(state.buddyBottomSheetState.email) - } + LaunchedEffect(state.buddyBottomSheetState.email) { + addViewModel.fetch(state.buddyBottomSheetState.email) + } } diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/list/BuddyListScreen.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/list/BuddyListScreen.kt index ad554e78..140964e5 100644 --- a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/list/BuddyListScreen.kt +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/list/BuddyListScreen.kt @@ -1,11 +1,12 @@ package io.github.taetae98coding.diary.feature.buddy.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.CircularProgressIndicator +import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -13,46 +14,108 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import io.github.taetae98coding.diary.core.compose.button.FloatingAddButton +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme import io.github.taetae98coding.diary.core.model.buddy.BuddyGroup @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun BuddyListScreen( - onAdd: () -> Unit, - groupListProvider: () -> List?, - modifier: Modifier = Modifier, + listProvider: () -> List?, + onGroup: (String) -> Unit, + modifier: Modifier = Modifier, + floatingActionButton: @Composable () -> Unit = {}, ) { - Scaffold( - modifier = modifier, - topBar = { TopAppBar(title = { Text(text = "버디(개발중 🚧)") }) }, - floatingActionButton = { FloatingAddButton(onClick = onAdd) }, - ) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(it), - ) { - val groupList = groupListProvider() + Scaffold( + modifier = modifier, + topBar = { TopAppBar(title = { Text(text = "버디") }) }, + floatingActionButton = floatingActionButton, + ) { + Content( + listProvider = listProvider, + onGroup = onGroup, + modifier = Modifier.fillMaxSize() + .padding(it), + ) + } +} + +@Composable +private fun Content( + listProvider: () -> List?, + onGroup: (String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + contentPadding = DiaryTheme.dimen.screenPaddingValues, + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + val list = listProvider() - if (groupList == null) { - item { - Box( - modifier = Modifier.fillParentMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } - } - } else { - items( - items = groupList, - key = { it.id }, - contentType = { "BuddyGroup" }, - ) { - Text(text = it.detail.title) - } - } - } - } + if (list == null) { + items( + count = 5, + contentType = { "BuddyGroup" }, + ) { + BuddyListItem( + group = null, + onClick = {}, + modifier = Modifier.animateItem(), + ) + } + } else if (list.isEmpty()) { + item( + key = "Loading", + contentType = "Loading", + ) { + Box( + modifier = Modifier.fillParentMaxSize() + .animateItem(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "그룹이 없어요 🐭", + style = DiaryTheme.typography.headlineMedium, + ) + } + } + } else { + items( + items = list, + key = { it.id }, + contentType = { "BuddyGroup" }, + ) { + BuddyListItem( + group = it, + onClick = { onGroup(it.id) }, + modifier = Modifier.animateItem(), + ) + } + } + } +} + +@Composable +private fun BuddyListItem( + group: BuddyGroup?, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Card( + onClick = onClick, + modifier = modifier, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = group?.detail?.title.orEmpty(), + style = DiaryTheme.typography.titleLarge, + ) + } + } } diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/add/BuddyGroupMemoAddRoute.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/add/BuddyGroupMemoAddRoute.kt new file mode 100644 index 00000000..b5a0b6eb --- /dev/null +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/add/BuddyGroupMemoAddRoute.kt @@ -0,0 +1,48 @@ +package io.github.taetae98coding.diary.feature.buddy.memo.add + +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.taetae98coding.diary.core.compose.button.FloatingAddButton +import io.github.taetae98coding.diary.core.compose.memo.MemoListDetailPaneScaffold +import io.github.taetae98coding.diary.core.compose.memo.add.rememberMemoDetailScaffoldAddState +import io.github.taetae98coding.diary.core.compose.topbar.TopBarTitle +import org.koin.compose.viewmodel.koinViewModel + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +internal fun BuddyGroupMemoAddRoute( + navigateUp: () -> Unit, + modifier: Modifier = Modifier, + addViewModel: BuddyGroupMemoAddViewModel = koinViewModel(), +) { + val detailScaffoldState = rememberMemoDetailScaffoldAddState( + initialStart = addViewModel.route.start, + initialEndInclusive = addViewModel.route.endInclusive, + ) + val detailUiState by addViewModel.uiState.collectAsStateWithLifecycle() + + MemoListDetailPaneScaffold( + onNavigateUp = navigateUp, + onDetailTag = { }, + detailScaffoldStateProvider = { detailScaffoldState }, + detailUiStateProvider = { detailUiState }, + detailTagListProvider = { null }, + detailTitle = { TopBarTitle(text = "그룹 메모 추가") }, + onTagAdd = {}, + tagListProvider = { null }, + modifier = modifier, + detailFloatingActionButton = { + val isProgress by remember { derivedStateOf { detailUiState.isProgress } } + + FloatingAddButton( + onClick = { addViewModel.add(detailScaffoldState.detail) }, + progressProvider = { isProgress }, + ) + }, + ) +} diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/add/BuddyGroupMemoAddViewModel.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/add/BuddyGroupMemoAddViewModel.kt new file mode 100644 index 00000000..832d1af3 --- /dev/null +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/add/BuddyGroupMemoAddViewModel.kt @@ -0,0 +1,141 @@ +package io.github.taetae98coding.diary.feature.buddy.memo.add + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import io.github.taetae98coding.diary.common.exception.memo.MemoTitleBlankException +import io.github.taetae98coding.diary.core.compose.memo.detail.MemoDetailScaffoldUiState +import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty +import io.github.taetae98coding.diary.core.compose.tag.TagCardItemUiState +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.core.model.tag.Tag +import io.github.taetae98coding.diary.core.navigation.buddy.BuddyGroupMemoAddDestination +import io.github.taetae98coding.diary.core.navigation.memo.MemoAddDestination +import io.github.taetae98coding.diary.domain.buddy.usecase.AddBuddyGroupMemoUseCase +import io.github.taetae98coding.diary.library.navigation.LocalDateNavType +import kotlin.reflect.typeOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDate +import org.koin.android.annotation.KoinViewModel + +@KoinViewModel +internal class BuddyGroupMemoAddViewModel( + savedStateHandle: SavedStateHandle, + private val addBuddyGroupMemoUseCase: AddBuddyGroupMemoUseCase, +) : ViewModel() { + val route = savedStateHandle.toRoute( + typeMap = mapOf(typeOf() to LocalDateNavType), + ) + + private val _uiState = MutableStateFlow(MemoDetailScaffoldUiState(onMessageShow = ::clearMessage)) + val uiState = _uiState.asStateFlow() + + private val selectedTag = MutableStateFlow(setOfNotNull(savedStateHandle.get(MemoAddDestination.SELECTED_TAG))) + private val primaryTag = MutableStateFlow(null) + + private val tagPageList = MutableStateFlow(emptyList()) + + val primaryTagList = + combine(tagPageList, selectedTag, primaryTag) { list, selected, primary -> + list + ?.filter { selected.contains(it.id) } + ?.map { + TagCardItemUiState( + id = it.id, + title = it.detail.title, + isSelected = it.id == primary, + color = it.detail.color, + select = SkipProperty { primaryTag(it.id) }, + unselect = SkipProperty { deletePrimaryTag() }, + ) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) + + val tagList = + combine(tagPageList, selectedTag) { list, selected -> + list?.map { + TagCardItemUiState( + id = it.id, + title = it.detail.title, + isSelected = selected.contains(it.id), + color = it.detail.color, + select = SkipProperty { selectTag(it.id) }, + unselect = SkipProperty { unselectTag(it.id) }, + ) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) + + fun add(detail: MemoDetail) { + if (uiState.value.isProgress) return + + viewModelScope.launch { + _uiState.update { it.copy(isProgress = true) } + addBuddyGroupMemoUseCase( + groupId = route.groupId, + detail = detail, + primaryTag = primaryTag.value, + tagIds = selectedTag.value, + ).onSuccess { + _uiState.update { it.copy(isProgress = false, isAdd = true) } + }.onFailure(::handleThrowable) + } + } + + private fun handleThrowable(throwable: Throwable) { + when (throwable) { + is MemoTitleBlankException -> _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, + ) + } + } + + private fun selectTag(tagId: String) { + selectedTag.update { + buildSet { + addAll(it) + add(tagId) + } + } + } + + private fun unselectTag(tagId: String) { + selectedTag.update { + buildSet { + addAll(it) + remove(tagId) + } + } + } + + private fun primaryTag(tagId: String) { + primaryTag.update { tagId } + } + + private fun deletePrimaryTag() { + primaryTag.update { null } + } +} diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/detail/BuddyGroupMemoDetailViewModel.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/detail/BuddyGroupMemoDetailViewModel.kt new file mode 100644 index 00000000..1dc88813 --- /dev/null +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/detail/BuddyGroupMemoDetailViewModel.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.feature.buddy.memo.detail + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.navigation.toRoute +import io.github.taetae98coding.diary.core.navigation.buddy.BuddyGroupMemoDetailDestination +import org.koin.android.annotation.KoinViewModel + +@KoinViewModel +internal class BuddyGroupMemoDetailViewModel( + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val route = savedStateHandle.toRoute() + +} diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/detail/BuddyGroupmemoDetailRoute.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/detail/BuddyGroupmemoDetailRoute.kt new file mode 100644 index 00000000..b23cef96 --- /dev/null +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/memo/detail/BuddyGroupmemoDetailRoute.kt @@ -0,0 +1,33 @@ +package io.github.taetae98coding.diary.feature.buddy.memo.detail + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.core.compose.memo.MemoListDetailPaneScaffold +import io.github.taetae98coding.diary.core.compose.memo.detail.MemoDetailScaffoldUiState +import io.github.taetae98coding.diary.core.compose.memo.detail.rememberMemoDetailScaffoldDetailState +import io.github.taetae98coding.diary.core.compose.topbar.TopBarTitle + +@Composable +internal fun BuddyGroupMemoDetailRoute( + navigateUp: () -> Unit, + modifier: Modifier = Modifier, + detailViewModel: BuddyGroupMemoDetailViewModel, +) { + val state = rememberMemoDetailScaffoldDetailState( + onDelete = { }, + onUpdate = { }, + detailProvider = { null }, + ) + + MemoListDetailPaneScaffold( + onNavigateUp = navigateUp, + onDetailTag = { }, + detailScaffoldStateProvider = { state }, + detailUiStateProvider = { MemoDetailScaffoldUiState() }, + detailTagListProvider = { null }, + detailTitle = { TopBarTitle(text = "") }, + onTagAdd = {}, + tagListProvider = { null }, + modifier = modifier, + ) +} diff --git a/app/feature/calendar/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarHomeScreenPreview.kt b/app/feature/calendar/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarHomeScreenPreview.kt deleted file mode 100644 index ec71f27c..00000000 --- a/app/feature/calendar/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarHomeScreenPreview.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.taetae98coding.diary.feature.calendar - -import androidx.compose.runtime.Composable -import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview -import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme -import io.github.taetae98coding.diary.feature.calendar.home.CalendarHomeScreen -import io.github.taetae98coding.diary.feature.calendar.home.rememberCalendarHomeScreenState - -@DiaryPreview -@Composable -private fun CalendarHomeScreenPreview() { - DiaryTheme { - CalendarHomeScreen( - state = rememberCalendarHomeScreenState(), - onSelectDate = {}, - onFilter = {}, - hasFilterProvider = { false }, - textItemListProvider = { emptyList() }, - holidayListProvider = { emptyList() }, - onCalendarItemClick = {}, - ) - } -} diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeRoute.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeRoute.kt index 3d0a91f6..3431169e 100644 --- a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeRoute.kt +++ b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeRoute.kt @@ -5,53 +5,57 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.taetae98coding.diary.core.compose.calendar.CalendarScaffold +import io.github.taetae98coding.diary.core.compose.calendar.CalendarScaffoldState +import io.github.taetae98coding.diary.core.compose.calendar.rememberCalendarScaffoldState import kotlinx.datetime.LocalDate import org.koin.compose.viewmodel.koinViewModel @Composable internal fun CalendarHomeRoute( - navigateToCalendarFilter: () -> Unit, - navigateToMemoAdd: (ClosedRange) -> Unit, - navigateToMemoDetail: (String) -> Unit, - modifier: Modifier = Modifier, - viewModel: CalendarHomeViewModel = koinViewModel(), - holidayViewModel: CalendarHomeHolidayViewModel = koinViewModel(), + navigateToCalendarFilter: () -> Unit, + navigateToMemoAdd: (ClosedRange) -> Unit, + navigateToMemoDetail: (String) -> Unit, + modifier: Modifier = Modifier, + viewModel: CalendarHomeViewModel = koinViewModel(), + holidayViewModel: CalendarHomeHolidayViewModel = koinViewModel(), ) { - val state = rememberCalendarHomeScreenState() - val hasFilter by viewModel.hasFilter.collectAsStateWithLifecycle() - val textItemList by viewModel.textItemList.collectAsStateWithLifecycle() - val holidayList by holidayViewModel.holidayList.collectAsStateWithLifecycle() + val state = rememberCalendarScaffoldState( + onFilter = navigateToCalendarFilter, + ) + val hasFilter by viewModel.hasFilter.collectAsStateWithLifecycle() + val textItemList by viewModel.textItemList.collectAsStateWithLifecycle() + val holidayList by holidayViewModel.holidayList.collectAsStateWithLifecycle() - CalendarHomeScreen( - state = state, - onSelectDate = navigateToMemoAdd, - onFilter = navigateToCalendarFilter, - hasFilterProvider = { hasFilter }, - textItemListProvider = { textItemList }, - holidayListProvider = { holidayList }, - onCalendarItemClick = { key -> - when (key) { - is MemoKey -> navigateToMemoDetail(key.id) - } - }, - modifier = modifier, - ) + CalendarScaffold( + state = state, + onSelectDate = navigateToMemoAdd, + hasFilterProvider = { hasFilter }, + textItemListProvider = { textItemList }, + holidayListProvider = { holidayList }, + onCalendarItemClick = { key -> + when (key) { + is MemoKey -> navigateToMemoDetail(key.id) + } + }, + modifier = modifier, + ) - Fetch( - state = state, - memoViewModel = viewModel, - holidayViewModel = holidayViewModel, - ) + Fetch( + state = state, + memoViewModel = viewModel, + holidayViewModel = holidayViewModel, + ) } @Composable private fun Fetch( - state: CalendarHomeScreenState, - memoViewModel: CalendarHomeViewModel, - holidayViewModel: CalendarHomeHolidayViewModel, + state: CalendarScaffoldState, + memoViewModel: CalendarHomeViewModel, + holidayViewModel: CalendarHomeHolidayViewModel, ) { - LaunchedEffect(state.calendarState.year, state.calendarState.month) { - memoViewModel.fetchMemo(state.calendarState.year, state.calendarState.month) - holidayViewModel.fetchHoliday(state.calendarState.year, state.calendarState.month) - } + LaunchedEffect(state.calendarState.year, state.calendarState.month) { + memoViewModel.fetchMemo(state.calendarState.year, state.calendarState.month) + holidayViewModel.fetchHoliday(state.calendarState.year, state.calendarState.month) + } } diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeScreen.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeScreen.kt deleted file mode 100644 index 6391e995..00000000 --- a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeScreen.kt +++ /dev/null @@ -1,124 +0,0 @@ -package io.github.taetae98coding.diary.feature.calendar.home - -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.lifecycle.compose.LifecycleResumeEffect -import io.github.taetae98coding.diary.core.calendar.compose.Calendar -import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState -import io.github.taetae98coding.diary.core.calendar.compose.modifier.calendarDateRangeSelectable -import io.github.taetae98coding.diary.core.calendar.compose.topbar.CalendarTopBar -import io.github.taetae98coding.diary.core.calendar.compose.topbar.TodayIcon -import io.github.taetae98coding.diary.core.design.system.icon.FilterIcon -import io.github.taetae98coding.diary.core.design.system.icon.FilterOffIcon -import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme -import io.github.taetae98coding.diary.library.datetime.todayIn -import kotlinx.coroutines.launch -import kotlinx.datetime.LocalDate - -@Composable -internal fun CalendarHomeScreen( - state: CalendarHomeScreenState, - onSelectDate: (ClosedRange) -> Unit, - onFilter: () -> Unit, - hasFilterProvider: () -> Boolean, - textItemListProvider: () -> List, - holidayListProvider: () -> List, - onCalendarItemClick: (Any) -> Unit, - modifier: Modifier = Modifier, -) { - val coroutineScope = rememberCoroutineScope() - val focusRequester = remember { FocusRequester() } - - Scaffold( - modifier = modifier - .onPreviewKeyEvent { - when { - !state.calendarState.isScrollInProgress && it.key == Key.DirectionRight -> { - coroutineScope.launch { state.calendarState.animateScrollToForward() } - true - } - - !state.calendarState.isScrollInProgress && it.key == Key.DirectionLeft -> { - coroutineScope.launch { state.calendarState.animateScrollToBackward() } - true - } - - it.key == Key.F1 -> { - onFilter() - true - } - - !state.calendarState.isScrollInProgress && it.key == Key.F2 -> { - coroutineScope.launch { state.calendarState.animateScrollToToday() } - true - } - - else -> false - } - }.focusRequester(focusRequester) - .focusable(), - topBar = { - CalendarTopBar( - state = state.calendarState, - actions = { - IconButton(onClick = onFilter) { - Crossfade(hasFilterProvider()) { hasFilter -> - if (hasFilter) { - FilterIcon(tint = DiaryTheme.color.primary) - } else { - FilterOffIcon() - } - } - } - - IconButton(onClick = { coroutineScope.launch { state.calendarState.animateScrollToToday() } }) { - TodayIcon() - } - }, - ) - }, - ) { - var today by remember { mutableStateOf(LocalDate.todayIn()) } - - Calendar( - state = state.calendarState, - primaryDateListProvider = { listOf(today) }, - textItemListProvider = textItemListProvider, - holidayListProvider = holidayListProvider, - onCalendarItemClick = onCalendarItemClick, - modifier = Modifier - .fillMaxSize() - .padding(it) - .calendarDateRangeSelectable( - state = state.calendarState, - onSelectDate = onSelectDate, - ), - ) - - LifecycleResumeEffect(Unit) { - today = LocalDate.todayIn() - onPauseOrDispose { } - } - } - - LifecycleResumeEffect(focusRequester) { - focusRequester.requestFocus() - onPauseOrDispose { } - } -} diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeScreenState.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeScreenState.kt deleted file mode 100644 index a7d393b4..00000000 --- a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeScreenState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.taetae98coding.diary.feature.calendar.home - -import io.github.taetae98coding.diary.core.calendar.compose.state.CalendarState - -internal class CalendarHomeScreenState( - val calendarState: CalendarState, -) diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/RememberCalendarScreenState.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/RememberCalendarScreenState.kt deleted file mode 100644 index 0cdd584d..00000000 --- a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/RememberCalendarScreenState.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.github.taetae98coding.diary.feature.calendar.home - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import io.github.taetae98coding.diary.core.calendar.compose.state.rememberCalendarState - -@Composable -internal fun rememberCalendarHomeScreenState(): CalendarHomeScreenState { - val calendarState = rememberCalendarState() - - return remember { - CalendarHomeScreenState( - calendarState = calendarState, - ) - } -} diff --git a/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenPreview.kt b/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenPreview.kt deleted file mode 100644 index dacad92c..00000000 --- a/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenPreview.kt +++ /dev/null @@ -1,66 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.detail - -import androidx.compose.runtime.Composable -import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview -import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme -import io.github.taetae98coding.diary.core.model.memo.MemoDetail -import io.github.taetae98coding.diary.feature.memo.add.rememberMemoDetailScreenAddState -import io.github.taetae98coding.diary.library.color.randomArgb -import kotlinx.datetime.LocalDate - -@DiaryPreview -@Composable -private fun AddPreview() { - DiaryTheme { - MemoDetailScreen( - state = rememberMemoDetailScreenAddState( - initialStart = null, - initialEndInclusive = null, - ), - titleProvider = { "Title" }, - navigateButtonProvider = { MemoDetailNavigationButton.NavigateUp(onNavigateUp = {}) }, - actionButtonProvider = { MemoDetailActionButton.None }, - floatingButtonProvider = { MemoDetailFloatingButton.Add(onAdd = {}) }, - uiStateProvider = { MemoDetailScreenUiState() }, - onTagTitle = {}, - onTag = {}, - tagListProvider = { null }, - ) - } -} - -@DiaryPreview -@Composable -private fun DetailPreview() { - DiaryTheme { - MemoDetailScreen( - state = rememberMemoDetailScreenDetailState( - onDelete = {}, - onUpdate = {}, - detailProvider = { - MemoDetail( - title = "Title", - description = "Description", - start = LocalDate(2024, 11, 1), - endInclusive = LocalDate(2024, 11, 1), - color = randomArgb(), - ) - }, - ), - titleProvider = { "Title" }, - navigateButtonProvider = { MemoDetailNavigationButton.NavigateUp(onNavigateUp = {}) }, - actionButtonProvider = { - MemoDetailActionButton.FinishAndDetail( - isFinish = false, - onFinishChange = {}, - delete = {}, - ) - }, - floatingButtonProvider = { MemoDetailFloatingButton.None }, - uiStateProvider = { MemoDetailScreenUiState() }, - onTagTitle = {}, - onTag = {}, - tagListProvider = { null }, - ) - } -} diff --git a/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagPreview.kt b/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagPreview.kt deleted file mode 100644 index 8f110548..00000000 --- a/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagPreview.kt +++ /dev/null @@ -1,84 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.tag - -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty -import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview -import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme -import io.github.taetae98coding.diary.library.color.randomArgb - -@DiaryPreview -@Composable -private fun PrimaryMemoTagPreview() { - DiaryTheme { - Surface { - PrimaryMemoTag( - uiState = TagUiState( - id = "id", - title = "Title", - isSelected = false, - color = randomArgb(), - select = SkipProperty {}, - unselect = SkipProperty {}, - ), - ) - } - } -} - -@DiaryPreview -@Composable -private fun SelectedPrimaryMemoTagPreview() { - DiaryTheme { - Surface { - PrimaryMemoTag( - uiState = TagUiState( - id = "id", - title = "Title", - isSelected = true, - color = randomArgb(), - select = SkipProperty {}, - unselect = SkipProperty {}, - ), - ) - } - } -} - -@DiaryPreview -@Composable -private fun MemoTagPreview() { - DiaryTheme { - Surface { - MemoTag( - uiState = TagUiState( - id = "id", - title = "Title", - isSelected = false, - color = randomArgb(), - select = SkipProperty {}, - unselect = SkipProperty {}, - ), - ) - } - } -} - -@DiaryPreview -@Composable -private fun SelectedMemoTagPreview() { - DiaryTheme { - Surface { - MemoTag( - uiState = TagUiState( - id = "id", - title = "Title", - isSelected = true, - color = randomArgb(), - select = SkipProperty {}, - unselect = SkipProperty {}, - ), - ) - } - } -} diff --git a/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagScreenPreview.kt b/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagScreenPreview.kt deleted file mode 100644 index fd550246..00000000 --- a/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagScreenPreview.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.tag - -import androidx.compose.runtime.Composable -import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty -import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview -import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme -import io.github.taetae98coding.diary.library.color.randomArgb - -@DiaryPreview -@Composable -private fun LoadingMemoTagScreenPreview() { - DiaryTheme { - MemoTagScreen( - navigateButtonProvider = { MemoTagNavigationButton.NavigateUp(onNavigateUp = {}) }, - onTagAdd = {}, - memoTagListProvider = { null }, - tagListProvider = { null }, - ) - } -} - -@DiaryPreview -@Composable -private fun EmptyMemoTagScreenPreview() { - DiaryTheme { - MemoTagScreen( - navigateButtonProvider = { MemoTagNavigationButton.NavigateUp(onNavigateUp = {}) }, - onTagAdd = {}, - memoTagListProvider = { emptyList() }, - tagListProvider = { emptyList() }, - ) - } -} - -@DiaryPreview -@Composable -private fun MemoTagScreenPreview() { - DiaryTheme { - MemoTagScreen( - navigateButtonProvider = { MemoTagNavigationButton.NavigateUp(onNavigateUp = {}) }, - onTagAdd = {}, - memoTagListProvider = { - List(5) { - TagUiState( - id = it.toString(), - title = "Title $it", - isSelected = it == 0, - color = randomArgb(), - select = SkipProperty {}, - unselect = SkipProperty {}, - ) - } - }, - tagListProvider = { - List(15) { - TagUiState( - id = it.toString(), - title = "Title $it", - isSelected = it == 0, - color = randomArgb(), - select = SkipProperty {}, - unselect = SkipProperty {}, - ) - } - }, - ) - } -} diff --git a/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagFlowPreview.kt b/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagFlowPreview.kt deleted file mode 100644 index 78b56f85..00000000 --- a/app/feature/memo/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagFlowPreview.kt +++ /dev/null @@ -1,65 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.tag - -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty -import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview -import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme -import io.github.taetae98coding.diary.library.color.randomArgb - -@DiaryPreview -@Composable -private fun LoadingTagFlowPreview() { - DiaryTheme { - Surface { - TagFlow( - title = { Text(text = "Title") }, - listProvider = { null }, - empty = { Text(text = "Empty") }, - tag = { MemoTag(uiState = it) }, - ) - } - } -} - -@DiaryPreview -@Composable -private fun EmptyTagFlowPreview() { - DiaryTheme { - Surface { - TagFlow( - title = { Text(text = "Title") }, - listProvider = { emptyList() }, - empty = { Text(text = "Empty") }, - tag = { MemoTag(uiState = it) }, - ) - } - } -} - -@DiaryPreview -@Composable -private fun TagFlowPreview() { - DiaryTheme { - Surface { - TagFlow( - title = { Text(text = "Title") }, - listProvider = { - List(10) { - TagUiState( - id = it.toString(), - title = "Title $it", - isSelected = it == 0, - color = randomArgb(), - select = SkipProperty {}, - unselect = SkipProperty {}, - ) - } - }, - empty = { Text(text = "Empty") }, - tag = { MemoTag(uiState = it) }, - ) - } - } -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddRoute.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddRoute.kt index 664ff0f2..1eac9076 100644 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddRoute.kt +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddRoute.kt @@ -1,99 +1,51 @@ package io.github.taetae98coding.diary.feature.memo.add import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -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.layout.calculatePaneScaffoldDirective -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.window.core.layout.WindowWidthSizeClass -import io.github.taetae98coding.diary.core.compose.adaptive.isListVisible -import io.github.taetae98coding.diary.core.compose.back.KBackHandler -import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailActionButton -import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailFloatingButton -import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailNavigationButton -import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailScreen -import io.github.taetae98coding.diary.feature.memo.tag.MemoTagNavigationButton -import io.github.taetae98coding.diary.feature.memo.tag.MemoTagScreen +import io.github.taetae98coding.diary.core.compose.button.FloatingAddButton +import io.github.taetae98coding.diary.core.compose.memo.MemoListDetailPaneScaffold +import io.github.taetae98coding.diary.core.compose.memo.add.rememberMemoDetailScaffoldAddState +import io.github.taetae98coding.diary.core.compose.topbar.TopBarTitle import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun MemoAddRoute( - navigateUp: () -> Unit, - navigateToTagAdd: () -> Unit, - navigateToTagDetail: (String) -> Unit, - modifier: Modifier = Modifier, - addViewModel: MemoAddViewModel = koinViewModel(), + navigateUp: () -> Unit, + navigateToTagAdd: () -> Unit, + navigateToTagDetail: (String) -> Unit, + modifier: Modifier = Modifier, + addViewModel: MemoAddViewModel = koinViewModel(), ) { - val windowAdaptiveInfo = currentWindowAdaptiveInfo() - val navigator = rememberListDetailPaneScaffoldNavigator(scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)) - - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), - value = navigator.scaffoldValue, - listPane = { - val state = rememberMemoDetailScreenAddState( - initialStart = addViewModel.route.start, - initialEndInclusive = addViewModel.route.endInclusive, - ) - - AnimatedPane { - val uiState by addViewModel.uiState.collectAsStateWithLifecycle() - val tagList by addViewModel.memoTagList.collectAsStateWithLifecycle() - - MemoDetailScreen( - state = state, - titleProvider = { "메모 추가" }, - navigateButtonProvider = { MemoDetailNavigationButton.NavigateUp(onNavigateUp = navigateUp) }, - actionButtonProvider = { MemoDetailActionButton.None }, - floatingButtonProvider = { MemoDetailFloatingButton.Add { addViewModel.add(state.memoDetail) } }, - uiStateProvider = { uiState }, - onTagTitle = { navigator.navigateTo(ThreePaneScaffoldRole.Primary) }, - onTag = navigateToTagDetail, - tagListProvider = { tagList }, - ) - } - }, - detailPane = { - AnimatedPane { - val isNavigateUpVisible = remember(windowAdaptiveInfo) { - if (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { - !navigator.isListVisible() - } else { - true - } - } - - val memoTagList by addViewModel.memoTagList.collectAsStateWithLifecycle() - val tagList by addViewModel.tagList.collectAsStateWithLifecycle() - - MemoTagScreen( - navigateButtonProvider = { - if (isNavigateUpVisible) { - MemoTagNavigationButton.NavigateUp(onNavigateUp = navigator::navigateBack) - } else { - MemoTagNavigationButton.None - } - }, - onTagAdd = navigateToTagAdd, - memoTagListProvider = { memoTagList }, - tagListProvider = { tagList }, - ) - } - }, - modifier = modifier, - ) - - KBackHandler( - isEnabled = navigator.canNavigateBack(), - onBack = navigator::navigateBack, - ) + val detailScaffoldState = rememberMemoDetailScaffoldAddState( + initialStart = addViewModel.route.start, + initialEndInclusive = addViewModel.route.endInclusive, + ) + val detailUiState by addViewModel.uiState.collectAsStateWithLifecycle() + val primaryTagList by addViewModel.primaryTagList.collectAsStateWithLifecycle() + val tagList by addViewModel.tagList.collectAsStateWithLifecycle() + + MemoListDetailPaneScaffold( + onNavigateUp = navigateUp, + onDetailTag = navigateToTagDetail, + detailScaffoldStateProvider = { detailScaffoldState }, + detailUiStateProvider = { detailUiState }, + detailTagListProvider = { primaryTagList }, + detailTitle = { TopBarTitle(text = "메모 추가") }, + onTagAdd = navigateToTagAdd, + tagListProvider = { tagList }, + detailFloatingActionButton = { + val isProgress by remember { derivedStateOf { detailUiState.isProgress } } + + FloatingAddButton( + onClick = { addViewModel.add(detailScaffoldState.detail) }, + progressProvider = { isProgress }, + ) + }, + ) } diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddViewModel.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddViewModel.kt index f903bff3..89c950f7 100644 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddViewModel.kt +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddViewModel.kt @@ -5,14 +5,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import io.github.taetae98coding.diary.common.exception.memo.MemoTitleBlankException +import io.github.taetae98coding.diary.core.compose.memo.detail.MemoDetailScaffoldUiState import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty +import io.github.taetae98coding.diary.core.compose.tag.TagCardItemUiState import io.github.taetae98coding.diary.core.model.memo.MemoDetail import io.github.taetae98coding.diary.core.navigation.memo.MemoAddDestination import io.github.taetae98coding.diary.domain.memo.usecase.AddMemoUseCase import io.github.taetae98coding.diary.domain.tag.usecase.PageTagUseCase -import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailScreenUiState -import io.github.taetae98coding.diary.feature.memo.tag.TagUiState import io.github.taetae98coding.diary.library.navigation.LocalDateNavType +import kotlin.reflect.typeOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -24,123 +25,122 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate import org.koin.android.annotation.KoinViewModel -import kotlin.reflect.typeOf @OptIn(ExperimentalCoroutinesApi::class) @KoinViewModel internal class MemoAddViewModel( - savedStateHandle: SavedStateHandle, - private val addMemoUseCase: AddMemoUseCase, - pageTagUseCase: PageTagUseCase, + savedStateHandle: SavedStateHandle, + private val addMemoUseCase: AddMemoUseCase, + pageTagUseCase: PageTagUseCase, ) : ViewModel() { - val route = - savedStateHandle.toRoute( - typeMap = mapOf(typeOf() to LocalDateNavType), - ) + val route = + savedStateHandle.toRoute( + typeMap = mapOf(typeOf() to LocalDateNavType), + ) - private val _uiState = MutableStateFlow(MemoDetailScreenUiState(onMessageShow = ::clearMessage)) - val uiState = _uiState.asStateFlow() + private val _uiState = MutableStateFlow(MemoDetailScaffoldUiState(onMessageShow = ::clearMessage)) + val uiState = _uiState.asStateFlow() - private val tagPageList = - pageTagUseCase() - .mapLatest { it.getOrNull() } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, - ) - private val selectedTag = MutableStateFlow(setOfNotNull(savedStateHandle.get(MemoAddDestination.SELECTED_TAG))) - private val primaryTag = MutableStateFlow(null) + private val tagPageList = + pageTagUseCase() + .mapLatest { it.getOrNull() } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) + private val selectedTag = MutableStateFlow(setOfNotNull(savedStateHandle.get(MemoAddDestination.SELECTED_TAG))) + private val primaryTag = MutableStateFlow(null) - val memoTagList = - combine(tagPageList, selectedTag, primaryTag) { list, selected, primary -> - list - ?.filter { selected.contains(it.id) } - ?.map { - TagUiState( - id = it.id, - title = it.detail.title, - isSelected = it.id == primary, - color = it.detail.color, - select = SkipProperty { primaryTag(it.id) }, - unselect = SkipProperty { deletePrimaryTag() }, - ) - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, - ) + val primaryTagList = + combine(tagPageList, selectedTag, primaryTag) { list, selected, primary -> + list + ?.filter { selected.contains(it.id) } + ?.map { + TagCardItemUiState( + id = it.id, + title = it.detail.title, + isSelected = it.id == primary, + color = it.detail.color, + select = SkipProperty { primaryTag(it.id) }, + unselect = SkipProperty { deletePrimaryTag() }, + ) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) - val tagList = - combine(tagPageList, selectedTag) { list, selected -> - list?.map { - TagUiState( - id = it.id, - title = it.detail.title, - isSelected = selected.contains(it.id), - color = it.detail.color, - select = SkipProperty { selectTag(it.id) }, - unselect = SkipProperty { unselectTag(it.id) }, - ) - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, - ) + val tagList = + combine(tagPageList, selectedTag) { list, selected -> + list?.map { + TagCardItemUiState( + id = it.id, + title = it.detail.title, + isSelected = selected.contains(it.id), + color = it.detail.color, + select = SkipProperty { selectTag(it.id) }, + unselect = SkipProperty { unselectTag(it.id) }, + ) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) - private fun clearMessage() { - _uiState.update { - it.copy( - isAdd = false, - isTitleBlankError = false, - isUnknownError = false, - ) - } - } + private fun clearMessage() { + _uiState.update { + it.copy( + isAdd = false, + isTitleBlankError = false, + isUnknownError = false, + ) + } + } - fun add(detail: MemoDetail) { - if (uiState.value.isProgress) return + fun add(detail: MemoDetail) { + if (uiState.value.isProgress) return - viewModelScope.launch { - _uiState.update { it.copy(isProgress = true) } - addMemoUseCase(detail = detail, primaryTag = primaryTag.value, tagIds = selectedTag.value) - .onSuccess { _uiState.update { it.copy(isProgress = false, isAdd = true) } } - .onFailure(::handleThrowable) - } - } + viewModelScope.launch { + _uiState.update { it.copy(isProgress = true) } + addMemoUseCase(detail = detail, primaryTag = primaryTag.value, tagIds = selectedTag.value) + .onSuccess { _uiState.update { it.copy(isProgress = false, isAdd = true) } } + .onFailure(::handleThrowable) + } + } - private fun handleThrowable(throwable: Throwable) { - when (throwable) { - is MemoTitleBlankException -> _uiState.update { it.copy(isProgress = false, isTitleBlankError = true) } - else -> _uiState.update { it.copy(isProgress = false, isUnknownError = true) } - } - } + private fun handleThrowable(throwable: Throwable) { + when (throwable) { + is MemoTitleBlankException -> _uiState.update { it.copy(isProgress = false, isTitleBlankError = true) } + else -> _uiState.update { it.copy(isProgress = false, isUnknownError = true) } + } + } - private fun selectTag(tagId: String) { - selectedTag.update { - buildSet { - addAll(it) - add(tagId) - } - } - } + private fun selectTag(tagId: String) { + selectedTag.update { + buildSet { + addAll(it) + add(tagId) + } + } + } - private fun unselectTag(tagId: String) { - selectedTag.update { - buildSet { - addAll(it) - remove(tagId) - } - } - } + private fun unselectTag(tagId: String) { + selectedTag.update { + buildSet { + addAll(it) + remove(tagId) + } + } + } - private fun primaryTag(tagId: String) { - primaryTag.update { tagId } - } + private fun primaryTag(tagId: String) { + primaryTag.update { tagId } + } - private fun deletePrimaryTag() { - primaryTag.update { null } - } + private fun deletePrimaryTag() { + primaryTag.update { null } + } } diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/RememberMemoDetailScreenAddState.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/RememberMemoDetailScreenAddState.kt deleted file mode 100644 index b36f22e3..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/RememberMemoDetailScreenAddState.kt +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.add - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -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.design.system.diary.date.rememberDiaryDateState -import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailScreenState -import kotlinx.datetime.LocalDate - -@Composable -internal fun rememberMemoDetailScreenAddState( - initialStart: LocalDate?, - initialEndInclusive: LocalDate?, -): MemoDetailScreenState.Add { - val coroutineScope = rememberCoroutineScope() - val componentState = rememberDiaryComponentState() - val dateState = - rememberDiaryDateState( - initialStart = initialStart, - initialEndInclusive = initialEndInclusive, - ) - val colorState = rememberDiaryColorState() - - return remember { - MemoDetailScreenState.Add( - coroutineScope = coroutineScope, - componentState = componentState, - dateState = dateState, - colorState = colorState, - ) - } -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailActionButton.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailActionButton.kt deleted file mode 100644 index 498c4f2a..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailActionButton.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.detail - -internal sealed class MemoDetailActionButton { - data object None : MemoDetailActionButton() - - data class FinishAndDetail( - val isFinish: Boolean, - val onFinishChange: (Boolean) -> Unit, - val delete: () -> Unit, - ) : MemoDetailActionButton() -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailFloatingButton.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailFloatingButton.kt deleted file mode 100644 index 7ba4d129..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailFloatingButton.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.detail - -internal sealed class MemoDetailFloatingButton { - data object None : MemoDetailFloatingButton() - - data class Add( - val onAdd: () -> Unit, - ) : MemoDetailFloatingButton() -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailNavigationButton.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailNavigationButton.kt deleted file mode 100644 index 37a6963a..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailNavigationButton.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.detail - -internal sealed class MemoDetailNavigationButton { - data object None : MemoDetailNavigationButton() - - data class NavigateUp( - val onNavigateUp: () -> Unit, - ) : MemoDetailNavigationButton() -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailRoute.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailRoute.kt index a9930158..97df92a1 100644 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailRoute.kt +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailRoute.kt @@ -1,99 +1,43 @@ package io.github.taetae98coding.diary.feature.memo.detail +import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -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.layout.calculatePaneScaffoldDirective -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.window.core.layout.WindowWidthSizeClass -import io.github.taetae98coding.diary.core.compose.adaptive.isListVisible -import io.github.taetae98coding.diary.core.compose.back.KBackHandler -import io.github.taetae98coding.diary.feature.memo.tag.MemoTagNavigationButton -import io.github.taetae98coding.diary.feature.memo.tag.MemoTagScreen +import io.github.taetae98coding.diary.core.compose.memo.MemoListDetailPaneScaffold +import io.github.taetae98coding.diary.core.compose.memo.detail.rememberMemoDetailScaffoldDetailState import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun MemoDetailRoute( - navigateUp: () -> Unit, - navigateToTagAdd: () -> Unit, - navigateToTagDetail: (String) -> Unit, - modifier: Modifier = Modifier, - detailViewModel: MemoDetailViewModel = koinViewModel(), - detailTagViewModel: MemoDetailTagViewModel = koinViewModel(), + navigateUp: () -> Unit, + navigateToTagAdd: () -> Unit, + navigateToTagDetail: (String) -> Unit, + modifier: Modifier = Modifier, + detailViewModel: MemoDetailViewModel = koinViewModel(), + detailTagViewModel: MemoDetailTagViewModel = koinViewModel(), ) { - val windowAdaptiveInfo = currentWindowAdaptiveInfo() - val navigator = rememberListDetailPaneScaffoldNavigator(scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)) - - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), - value = navigator.scaffoldValue, - listPane = { - val detail by detailViewModel.detail.collectAsStateWithLifecycle() - val state = rememberMemoDetailScreenDetailState( - onDelete = navigateUp, - onUpdate = navigateUp, - detailProvider = { detail }, - ) - - AnimatedPane { - val actionButton by detailViewModel.actionButton.collectAsStateWithLifecycle() - val uiState by detailViewModel.uiState.collectAsStateWithLifecycle() - val tagList by detailTagViewModel.memoTagUiStateList.collectAsStateWithLifecycle() - - MemoDetailScreen( - state = state, - titleProvider = { detail?.title }, - navigateButtonProvider = { MemoDetailNavigationButton.NavigateUp(onNavigateUp = { detailViewModel.update(state.memoDetail) }) }, - actionButtonProvider = { actionButton }, - floatingButtonProvider = { MemoDetailFloatingButton.None }, - uiStateProvider = { uiState }, - onTagTitle = { navigator.navigateTo(ThreePaneScaffoldRole.Primary) }, - onTag = navigateToTagDetail, - tagListProvider = { tagList }, - ) - } - }, - detailPane = { - val isNavigateUpVisible = remember(windowAdaptiveInfo) { - if (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { - !navigator.isListVisible() - } else { - true - } - } - - AnimatedPane { - val memoTagList by detailTagViewModel.memoTagUiStateList.collectAsStateWithLifecycle() - val tagList by detailTagViewModel.tagList.collectAsStateWithLifecycle() - - MemoTagScreen( - navigateButtonProvider = { - if (isNavigateUpVisible) { - MemoTagNavigationButton.NavigateUp(onNavigateUp = navigator::navigateBack) - } else { - MemoTagNavigationButton.None - } - }, - onTagAdd = navigateToTagAdd, - memoTagListProvider = { memoTagList }, - tagListProvider = { tagList }, - ) - } - }, - modifier = modifier, - ) - - KBackHandler( - isEnabled = navigator.canNavigateBack(), - onBack = navigator::navigateBack, - ) + val detail by detailViewModel.detail.collectAsStateWithLifecycle() + val state = rememberMemoDetailScaffoldDetailState( + onDelete = navigateUp, + onUpdate = navigateUp, + detailProvider = { detail }, + ) + val uiState by detailViewModel.uiState.collectAsStateWithLifecycle() + val primaryTagList by detailTagViewModel.primaryTagList.collectAsStateWithLifecycle() + val tagList by detailTagViewModel.tagList.collectAsStateWithLifecycle() + + MemoListDetailPaneScaffold( + onNavigateUp = navigateUp, + onDetailTag = navigateToTagDetail, + detailScaffoldStateProvider = { state }, + detailUiStateProvider = { uiState }, + detailTagListProvider = { primaryTagList }, + detailTitle = { Text(text = detail?.title.orEmpty()) }, + onTagAdd = navigateToTagAdd, + tagListProvider = { tagList }, + ) } 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 deleted file mode 100644 index 296f67ca..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreen.kt +++ /dev/null @@ -1,274 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.detail - -import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.clickable -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.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -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.IconButton -import androidx.compose.material3.IconToggleButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.minimumInteractiveComponentSize -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.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import io.github.taetae98coding.diary.core.compose.button.FloatingAddButton -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.diary.date.DiaryDate -import io.github.taetae98coding.diary.core.design.system.emoji.Emoji -import io.github.taetae98coding.diary.core.design.system.icon.ChevronRightIcon -import io.github.taetae98coding.diary.core.design.system.icon.DeleteIcon -import io.github.taetae98coding.diary.core.design.system.icon.FinishIcon -import io.github.taetae98coding.diary.core.design.system.icon.NavigateUpIcon -import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme -import io.github.taetae98coding.diary.feature.memo.tag.PrimaryMemoTag -import io.github.taetae98coding.diary.feature.memo.tag.TagFlow -import io.github.taetae98coding.diary.feature.memo.tag.TagUiState - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -internal fun MemoDetailScreen( - state: MemoDetailScreenState, - titleProvider: () -> String?, - navigateButtonProvider: () -> MemoDetailNavigationButton, - actionButtonProvider: () -> MemoDetailActionButton, - floatingButtonProvider: () -> MemoDetailFloatingButton, - uiStateProvider: () -> MemoDetailScreenUiState, - onTagTitle: () -> Unit, - onTag: (String) -> Unit, - tagListProvider: () -> List?, - modifier: Modifier = Modifier, -) { - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = { - titleProvider()?.let { - Text( - text = it, - modifier = Modifier.basicMarquee(iterations = Int.MAX_VALUE), - maxLines = 1, - ) - } - }, - navigationIcon = { - when (val button = navigateButtonProvider()) { - is MemoDetailNavigationButton.NavigateUp -> { - IconButton(onClick = button.onNavigateUp) { - NavigateUpIcon() - } - } - - is MemoDetailNavigationButton.None -> Unit - } - }, - actions = { - when (val button = actionButtonProvider()) { - is MemoDetailActionButton.FinishAndDetail -> { - IconToggleButton( - checked = button.isFinish, - onCheckedChange = button.onFinishChange, - ) { - FinishIcon() - } - - IconButton(onClick = button.delete) { - DeleteIcon() - } - } - - else -> Unit - } - }, - ) - }, - snackbarHost = { SnackbarHost(hostState = state.hostState) }, - floatingActionButton = { - when (val button = floatingButtonProvider()) { - is MemoDetailFloatingButton.Add -> { - val isProgress by remember { derivedStateOf { uiStateProvider().isProgress } } - - FloatingAddButton( - onClick = button.onAdd, - progressProvider = { isProgress }, - ) - } - - is MemoDetailFloatingButton.None -> Unit - } - }, - ) { - Content( - state = state, - onTagTitle = onTagTitle, - onTag = onTag, - tagListProvider = tagListProvider, - modifier = Modifier - .fillMaxSize() - .padding(it) - .padding(DiaryTheme.dimen.screenPaddingValues), - ) - } - - Message( - state = state, - uiStateProvider = uiStateProvider, - ) - - LaunchedFocus(state = state) -} - -@Composable -private fun Message( - state: MemoDetailScreenState, - uiStateProvider: () -> MemoDetailScreenUiState, -) { - val uiState = uiStateProvider() - - LaunchedEffect( - uiState.isAdd, - uiState.isDelete, - uiState.isUpdate, - uiState.isTitleBlankError, - uiState.isUnknownError, - ) { - if (!uiState.hasMessage) return@LaunchedEffect - - when { - uiState.isAdd -> { - state.showMessage("메모 추가 ${Emoji.congratulate.random()}") - state.clearInput() - state.requestTitleFocus() - } - - uiState.isDelete -> { - if (state is MemoDetailScreenState.Detail) { - state.onDelete() - } - } - - uiState.isUpdate -> { - if (state is MemoDetailScreenState.Detail) { - state.onUpdate() - } - } - - uiState.isTitleBlankError -> { - state.showMessage("제목을 입력해 주세요 ${Emoji.check.random()}") - state.titleError() - } - - uiState.isUnknownError -> state.showMessage("알 수 없는 에러가 발생했어요 잠시 후 다시 시도해 주세요 ${Emoji.error.random()}") - } - - uiState.onMessageShow() - } -} - -@Composable -private fun LaunchedFocus( - state: MemoDetailScreenState, -) { - LaunchedEffect(state) { - if (state is MemoDetailScreenState.Add) { - state.requestTitleFocus() - } - } -} - -@Composable -private fun Content( - state: MemoDetailScreenState, - onTagTitle: () -> Unit, - onTag: (String) -> Unit, - tagListProvider: () -> List?, - modifier: Modifier = Modifier, -) { - Column( - modifier = Modifier - .verticalScroll(state = rememberScrollState()) - .then(modifier), - verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), - ) { - DiaryComponent(state = state.componentState) - DiaryDate(state = state.dateState) - InternalDiaryTag( - onTitle = onTagTitle, - onTag = onTag, - listProvider = tagListProvider, - ) - InternalDiaryColor(state = state) - } -} - -@Composable -private fun InternalDiaryTag( - onTitle: () -> Unit, - onTag: (String) -> Unit, - listProvider: () -> List?, - modifier: Modifier = Modifier, -) { - TagFlow( - title = { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onTitle) - .minimumInteractiveComponentSize() - .padding(horizontal = DiaryTheme.dimen.diaryHorizontalPadding), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text(text = "태그") - ChevronRightIcon() - } - }, - listProvider = listProvider, - empty = { Text(text = "태그가 없어요 🐻‍❄️") }, - tag = { - PrimaryMemoTag( - uiState = it, - onClick = { onTag(it.id) }, - ) - }, - modifier = modifier - .fillMaxWidth() - .heightIn(min = 150.dp, max = 200.dp), - ) -} - -@Composable -private fun InternalDiaryColor( - state: MemoDetailScreenState, - modifier: Modifier = Modifier, -) { - Row(modifier = modifier) { - DiaryColor( - state = state.colorState, - modifier = Modifier - .weight(1F) - .height(100.dp), - ) - - Spacer(modifier = Modifier.weight(1F)) - } -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenState.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenState.kt deleted file mode 100644 index 59759996..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenState.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.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.design.system.diary.date.DiaryDateState -import io.github.taetae98coding.diary.core.model.memo.MemoDetail -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch - -internal sealed class MemoDetailScreenState { - protected abstract val coroutineScope: CoroutineScope - - abstract val componentState: DiaryComponentState - abstract val dateState: DiaryDateState - 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 dateState: DiaryDateState, - override val colorState: DiaryColorState, - ) : MemoDetailScreenState() - - data class Detail( - val onDelete: () -> Unit, - val onUpdate: () -> Unit, - override val coroutineScope: CoroutineScope, - override val componentState: DiaryComponentState, - override val dateState: DiaryDateState, - override val colorState: DiaryColorState, - ) : MemoDetailScreenState() - - val memoDetail: MemoDetail - get() { - return MemoDetail( - title = componentState.title, - description = componentState.description, - start = dateState.start.takeIf { dateState.hasDate }, - endInclusive = dateState.endInclusive.takeIf { dateState.hasDate }, - color = colorState.color.toArgb(), - ) - } - - fun requestTitleFocus() { - componentState.requestTitleFocus() - } - - fun titleError() { - requestTitleFocus() - componentState.titleError() - } - - fun showMessage(message: String) { - messageJob?.cancel() - messageJob = coroutineScope.launch { hostState.showSnackbar(message) } - } - - fun clearInput() { - componentState.clearInput() - } -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenUiState.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenUiState.kt deleted file mode 100644 index b5ff6468..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenUiState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.detail - -internal data class MemoDetailScreenUiState( - 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/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailTagViewModel.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailTagViewModel.kt index 981e3943..f4f7d79c 100644 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailTagViewModel.kt +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailTagViewModel.kt @@ -4,13 +4,13 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty +import io.github.taetae98coding.diary.core.compose.tag.TagCardItemUiState import io.github.taetae98coding.diary.core.navigation.memo.MemoDetailDestination import io.github.taetae98coding.diary.domain.memo.usecase.DeleteMemoPrimaryTagUseCase import io.github.taetae98coding.diary.domain.memo.usecase.FindMemoTagUseCase import io.github.taetae98coding.diary.domain.memo.usecase.SelectMemoTagUseCase import io.github.taetae98coding.diary.domain.memo.usecase.UnselectMemoTagUseCase import io.github.taetae98coding.diary.domain.memo.usecase.UpdateMemoPrimaryTagUseCase -import io.github.taetae98coding.diary.feature.memo.tag.TagUiState import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted @@ -42,11 +42,11 @@ internal class MemoDetailTagViewModel( initialValue = null, ) - val memoTagUiStateList = + val primaryTagList = memoTagList .mapLatest { list -> list?.filter { it.isSelected } } .mapCollectionLatest { - TagUiState( + TagCardItemUiState( id = it.tag.id, title = it.tag.detail.title, isSelected = it.isPrimary, @@ -63,7 +63,7 @@ internal class MemoDetailTagViewModel( val tagList = memoTagList .mapCollectionLatest { - TagUiState( + TagCardItemUiState( id = it.tag.id, title = it.tag.detail.title, isSelected = it.isSelected, diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailViewModel.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailViewModel.kt index 691b08f1..6c44521a 100644 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailViewModel.kt +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import io.github.taetae98coding.diary.core.compose.memo.detail.MemoDetailScaffoldUiState import io.github.taetae98coding.diary.core.model.memo.MemoDetail import io.github.taetae98coding.diary.core.navigation.memo.MemoDetailDestination import io.github.taetae98coding.diary.domain.memo.usecase.DeleteMemoUseCase @@ -34,7 +35,7 @@ internal class MemoDetailViewModel( ) : ViewModel() { private val route = savedStateHandle.toRoute() - private val _uiState = MutableStateFlow(MemoDetailScreenUiState(onMessageShow = ::clearMessage)) + private val _uiState = MutableStateFlow(MemoDetailScaffoldUiState(onMessageShow = ::clearMessage)) val uiState = _uiState.asStateFlow() private val memo = @@ -56,25 +57,6 @@ internal class MemoDetailViewModel( initialValue = null, ) - val actionButton = - memo - .mapLatest { - MemoDetailActionButton.FinishAndDetail( - isFinish = it?.isFinish ?: false, - onFinishChange = ::onFinishChange, - delete = ::delete, - ) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = - MemoDetailActionButton.FinishAndDetail( - isFinish = false, - onFinishChange = ::onFinishChange, - delete = ::delete, - ), - ) - private fun onFinishChange(isFinish: Boolean) { viewModelScope.launch { if (isFinish) { diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/RememberMemoDetailScreenDetailState.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/RememberMemoDetailScreenDetailState.kt deleted file mode 100644 index 66644f6d..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/RememberMemoDetailScreenDetailState.kt +++ /dev/null @@ -1,48 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.detail - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.graphics.Color -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.design.system.diary.date.rememberDiaryDateState -import io.github.taetae98coding.diary.core.model.memo.MemoDetail - -@Composable -internal fun rememberMemoDetailScreenDetailState( - onDelete: () -> Unit, - onUpdate: () -> Unit, - detailProvider: () -> MemoDetail?, -): MemoDetailScreenState.Detail { - val detail = detailProvider() - val coroutineScope = rememberCoroutineScope() - val componentState = - rememberDiaryComponentState( - inputs = arrayOf(detail?.title, detail?.description), - initialTitle = detail?.title.orEmpty(), - initialDescription = detail?.description.orEmpty(), - ) - val dateState = - rememberDiaryDateState( - inputs = arrayOf(detail?.start, detail?.endInclusive), - initialStart = detail?.start, - initialEndInclusive = detail?.endInclusive, - ) - val colorState = - rememberDiaryColorState( - inputs = arrayOf(detail?.color), - initialColor = detail?.color?.let { Color(it) } ?: Color.Unspecified, - ) - - return remember(componentState, dateState, colorState) { - MemoDetailScreenState.Detail( - onDelete = onDelete, - onUpdate = onUpdate, - coroutineScope = coroutineScope, - componentState = componentState, - dateState = dateState, - colorState = colorState, - ) - } -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTag.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTag.kt deleted file mode 100644 index eecac686..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTag.kt +++ /dev/null @@ -1,86 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.tag - -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.FilterChipDefaults -import androidx.compose.material3.SelectableChipColors -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import io.github.taetae98coding.diary.core.design.system.chip.DiaryFilterChip -import io.github.taetae98coding.diary.core.design.system.icon.TagIcon -import io.github.taetae98coding.diary.library.color.toContrastColor - -@Composable -internal fun PrimaryMemoTag( - uiState: TagUiState, - modifier: Modifier = Modifier, -) { - PrimaryMemoTag( - uiState = uiState, - onClick = { - if (uiState.isSelected) { - uiState.unselect.value() - } else { - uiState.select.value() - } - }, - modifier = modifier, - ) -} - -@Composable -internal fun PrimaryMemoTag( - uiState: TagUiState, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - MemoTag( - uiState = uiState, - onClick = onClick, - modifier = modifier, - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = Color(uiState.color), - selectedLabelColor = Color(uiState.color).toContrastColor(), - selectedLeadingIconColor = Color(uiState.color).toContrastColor(), - ), - ) -} - -@Composable -internal fun MemoTag( - uiState: TagUiState, - modifier: Modifier = Modifier, - colors: SelectableChipColors = FilterChipDefaults.filterChipColors(), -) { - MemoTag( - uiState = uiState, - onClick = { - if (uiState.isSelected) { - uiState.unselect.value() - } else { - uiState.select.value() - } - }, - modifier = modifier, - colors = colors, - ) -} - -@Composable -internal fun MemoTag( - uiState: TagUiState, - onClick: () -> Unit, - modifier: Modifier = Modifier, - colors: SelectableChipColors = FilterChipDefaults.filterChipColors(), -) { - DiaryFilterChip( - selected = uiState.isSelected, - onClick = onClick, - label = { Text(text = uiState.title) }, - modifier = modifier, - leadingIcon = { TagIcon() }, - shape = CircleShape, - colors = colors, - ) -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagNavigationButton.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagNavigationButton.kt deleted file mode 100644 index 11579461..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagNavigationButton.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.tag - -internal sealed class MemoTagNavigationButton { - data object None : MemoTagNavigationButton() - - data class NavigateUp( - val onNavigateUp: () -> Unit, - ) : MemoTagNavigationButton() -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagScreen.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagScreen.kt deleted file mode 100644 index e2174fd0..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/MemoTagScreen.kt +++ /dev/null @@ -1,110 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.tag - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.minimumInteractiveComponentSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import io.github.taetae98coding.diary.core.design.system.icon.NavigateUpIcon -import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -internal fun MemoTagScreen( - navigateButtonProvider: () -> MemoTagNavigationButton, - onTagAdd: () -> Unit, - memoTagListProvider: () -> List?, - tagListProvider: () -> List?, - modifier: Modifier = Modifier, -) { - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = { Text(text = "메모 태그") }, - navigationIcon = { - when (val button = navigateButtonProvider()) { - is MemoTagNavigationButton.NavigateUp -> { - IconButton(onClick = button.onNavigateUp) { - NavigateUpIcon() - } - } - - is MemoTagNavigationButton.None -> Unit - } - }, - ) - }, - ) { - Content( - onTagAdd = onTagAdd, - memoTagListProvider = memoTagListProvider, - tagListProvider = tagListProvider, - modifier = Modifier - .fillMaxWidth() - .padding(it) - .padding(DiaryTheme.dimen.diaryPaddingValues), - ) - } -} - -@Composable -private fun Content( - onTagAdd: () -> Unit, - memoTagListProvider: () -> List?, - tagListProvider: () -> List?, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), - ) { - TagFlow( - modifier = Modifier - .fillMaxWidth() - .weight(3F), - listProvider = memoTagListProvider, - title = { - Text( - text = "캘린더 태그", - modifier = Modifier - .fillMaxWidth() - .minimumInteractiveComponentSize() - .padding(horizontal = DiaryTheme.dimen.diaryHorizontalPadding), - ) - }, - empty = { Text(text = "태그가 없어요 🐻") }, - tag = { PrimaryMemoTag(uiState = it) }, - ) - - TagFlow( - modifier = Modifier - .fillMaxWidth() - .weight(7F), - listProvider = tagListProvider, - title = { - Text( - text = "태그", - modifier = Modifier - .fillMaxWidth() - .minimumInteractiveComponentSize() - .padding(horizontal = DiaryTheme.dimen.diaryHorizontalPadding), - ) - }, - empty = { - Button(onClick = onTagAdd) { - Text(text = "태그 추가하러 가기") - } - }, - tag = { MemoTag(uiState = it) }, - ) - } -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagFlow.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagFlow.kt deleted file mode 100644 index a847418f..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagFlow.kt +++ /dev/null @@ -1,70 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.tag - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.IntrinsicSize -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.Card -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme - -@OptIn(ExperimentalLayoutApi::class) -@Composable -internal fun TagFlow( - title: @Composable () -> Unit, - listProvider: () -> List?, - empty: @Composable () -> Unit, - tag: @Composable (TagUiState) -> Unit, - modifier: Modifier = Modifier, -) { - Card(modifier = modifier.height(IntrinsicSize.Min)) { - val isLoading by remember { derivedStateOf { listProvider() == null } } - val isEmpty by remember { derivedStateOf { !isLoading && listProvider().isNullOrEmpty() } } - - title() - - if (isLoading) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } - } else if (isEmpty) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - empty() - } - } else { - FlowRow( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(DiaryTheme.dimen.itemSpace), - horizontalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace, Alignment.CenterHorizontally), - verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace, Alignment.CenterVertically), - ) { - listProvider()?.forEach { - key(it.id) { - tag(it) - } - } - } - } - } -} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagUiState.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagUiState.kt deleted file mode 100644 index 834626d6..00000000 --- a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/tag/TagUiState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.taetae98coding.diary.feature.memo.tag - -import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty - -public data class TagUiState( - val id: String, - val title: String, - val isSelected: Boolean, - val color: Int, - val select: SkipProperty<() -> Unit>, - val unselect: SkipProperty<() -> Unit>, -) diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/home/TagHomeRoute.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/home/TagHomeRoute.kt index c5759aa3..8fadc532 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/home/TagHomeRoute.kt +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/home/TagHomeRoute.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -45,246 +44,242 @@ import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun TagHomeRoute( - navigateToMemoAdd: (String) -> Unit, - navigateToMemoDetail: (String) -> Unit, - onScaffoldValueChange: (ThreePaneScaffoldValue) -> Unit, - modifier: Modifier = Modifier, - listViewModel: TagListViewModel = koinViewModel(), - addViewModel: TagAddViewModel = koinViewModel(), - detailViewModel: TagDetailViewModel = koinViewModel(), - memoViewModel: TagMemoViewModel = koinViewModel(), + navigateToMemoAdd: (String) -> Unit, + navigateToMemoDetail: (String) -> Unit, + onScaffoldValueChange: (ThreePaneScaffoldValue) -> Unit, + modifier: Modifier = Modifier, + listViewModel: TagListViewModel = koinViewModel(), + addViewModel: TagAddViewModel = koinViewModel(), + detailViewModel: TagDetailViewModel = koinViewModel(), + memoViewModel: TagMemoViewModel = koinViewModel(), ) { - val windowAdaptiveInfo = currentWindowAdaptiveInfo() - val navigator = rememberListDetailPaneScaffoldNavigator(scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)) + val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val navigator = rememberListDetailPaneScaffoldNavigator(scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)) - var tagHomeNavigate by remember { mutableStateOf(TagHomeNavigate.None) } - var detailPaneRefreshCount by remember { mutableIntStateOf(0) } + var tagHomeNavigate by remember { mutableStateOf(TagHomeNavigate.None) } - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), - value = navigator.scaffoldValue, - listPane = { - AnimatedPane { - val isFloatingVisible by remember { - derivedStateOf { - if (navigator.isDetailVisible()) { - navigator.currentDestination?.content != null - } else { - true - } - } - } + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + val isFloatingVisible by remember { + derivedStateOf { + if (navigator.isDetailVisible()) { + navigator.currentDestination?.content != null + } else { + true + } + } + } - val list by listViewModel.list.collectAsStateWithLifecycle() - val uiState by listViewModel.uiState.collectAsStateWithLifecycle() + val list by listViewModel.list.collectAsStateWithLifecycle() + val uiState by listViewModel.uiState.collectAsStateWithLifecycle() - TagListScreen( - state = rememberTagListScreenState(), - floatingButtonProvider = { - if (isFloatingVisible) { - TagListFloatingButton.Add( - onAdd = { - if (navigator.isDetailVisible()) { - tagHomeNavigate = TagHomeNavigate.Add - } else { - navigator.navigateTo(ThreePaneScaffoldRole.Primary) - } - }, - ) - } else { - TagListFloatingButton.None - } - }, - listProvider = { list }, - onTag = { - if (navigator.isDetailVisible() && navigator.currentDestination?.content != null) { - tagHomeNavigate = TagHomeNavigate.Tag(it) - } else { - navigator.navigateTo(ThreePaneScaffoldRole.Primary, it) - detailPaneRefreshCount++ - } - }, - uiStateProvider = { uiState }, - ) - } - }, - detailPane = { - AnimatedPane { - val tagDetail by detailViewModel.tagDetail.collectAsStateWithLifecycle() - val isAdd = remember(detailPaneRefreshCount) { navigator.currentDestination?.content == null } - val state = if (isAdd) { - rememberTagDetailScreenAddState() - } else { - rememberTagDetailScreenDetailState( - onUpdate = { - when (val navigate = tagHomeNavigate) { - is TagHomeNavigate.Add -> { - navigator.navigateTo(ThreePaneScaffoldRole.Primary) - detailPaneRefreshCount++ - tagHomeNavigate = TagHomeNavigate.None - } + TagListScreen( + state = rememberTagListScreenState(), + floatingButtonProvider = { + if (isFloatingVisible) { + TagListFloatingButton.Add( + onAdd = { + if (navigator.isDetailVisible()) { + tagHomeNavigate = TagHomeNavigate.Add + } else { + navigator.navigateTo(ThreePaneScaffoldRole.Primary) + } + }, + ) + } else { + TagListFloatingButton.None + } + }, + listProvider = { list }, + onTag = { + if (navigator.isDetailVisible() && navigator.currentDestination?.content != null) { + tagHomeNavigate = TagHomeNavigate.Tag(it) + } else { + navigator.navigateTo(ThreePaneScaffoldRole.Primary, it) + } + }, + uiStateProvider = { uiState }, + ) + } + }, + detailPane = { + AnimatedPane { + val tagDetail by detailViewModel.tagDetail.collectAsStateWithLifecycle() + val isAdd by remember { + derivedStateOf { + navigator.currentDestination?.content == null + } + } + val state = if (isAdd) { + rememberTagDetailScreenAddState() + } else { + rememberTagDetailScreenDetailState( + onUpdate = { + when (val navigate = tagHomeNavigate) { + is TagHomeNavigate.Add -> { + navigator.navigateTo(ThreePaneScaffoldRole.Primary) + tagHomeNavigate = TagHomeNavigate.None + } - is TagHomeNavigate.Tag -> { - navigator.navigateTo(ThreePaneScaffoldRole.Primary, navigate.tagId) - tagHomeNavigate = TagHomeNavigate.None - } + is TagHomeNavigate.Tag -> { + navigator.navigateTo(ThreePaneScaffoldRole.Primary, navigate.tagId) + tagHomeNavigate = TagHomeNavigate.None + } - is TagHomeNavigate.None -> { - navigator.navigateBack() - } - } - }, - onDelete = { - navigator.navigateBack() - if (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { - detailPaneRefreshCount++ - } - }, - onMemo = { navigator.currentDestination?.content?.let { navigator.navigateTo(ThreePaneScaffoldRole.Tertiary, it) } }, - detailProvider = { tagDetail }, - ) - } - val isNavigateUpVisible by remember(windowAdaptiveInfo) { - derivedStateOf { - if (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { - !navigator.isListVisible() - } else { - true - } - } - } - val detailActionButton by detailViewModel.actionButton.collectAsStateWithLifecycle() - val uiState = if (isAdd) { - addViewModel.uiState.collectAsStateWithLifecycle() - } else { - detailViewModel.uiState.collectAsStateWithLifecycle() - } + is TagHomeNavigate.None -> { + navigator.navigateBack() + } + } + }, + onDelete = { navigator.navigateBack() }, + onMemo = { navigator.currentDestination?.content?.let { navigator.navigateTo(ThreePaneScaffoldRole.Tertiary, it) } }, + detailProvider = { tagDetail }, + ) + } + val isNavigateUpVisible by remember(windowAdaptiveInfo) { + derivedStateOf { + if (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { + !navigator.isListVisible() + } else { + true + } + } + } + val detailActionButton by detailViewModel.actionButton.collectAsStateWithLifecycle() + val uiState = if (isAdd) { + addViewModel.uiState.collectAsStateWithLifecycle() + } else { + detailViewModel.uiState.collectAsStateWithLifecycle() + } - TagDetailScreen( - state = state, - titleProvider = { - if (isAdd) { - "태그 추가" - } else { - tagDetail?.title - } - }, - navigateButtonProvider = { - if (isNavigateUpVisible) { - TagDetailNavigationButton.NavigateUp( - onNavigateUp = { - if (isAdd) { - navigator.navigateBack() - } else { - detailViewModel.update(state.tagDetail) - } - }, - ) - } else { - TagDetailNavigationButton.None - } - }, - actionButtonProvider = { - if (isAdd) { - TagDetailActionButton.None - } else { - detailActionButton - } - }, - floatingButtonProvider = { - if (isAdd) { - TagDetailFloatingButton.Add(onAdd = { addViewModel.add(state.tagDetail) }) - } else { - TagDetailFloatingButton.None - } - }, - uiStateProvider = { uiState.value }, - ) + TagDetailScreen( + state = state, + titleProvider = { + if (isAdd) { + "태그 추가" + } else { + tagDetail?.title + } + }, + navigateButtonProvider = { + if (isNavigateUpVisible) { + TagDetailNavigationButton.NavigateUp( + onNavigateUp = { + if (isAdd) { + navigator.navigateBack() + } else { + detailViewModel.update(state.tagDetail) + } + }, + ) + } else { + TagDetailNavigationButton.None + } + }, + actionButtonProvider = { + if (isAdd) { + TagDetailActionButton.None + } else { + detailActionButton + } + }, + floatingButtonProvider = { + if (isAdd) { + TagDetailFloatingButton.Add(onAdd = { addViewModel.add(state.tagDetail) }) + } else { + TagDetailFloatingButton.None + } + }, + uiStateProvider = { uiState.value }, + ) - LaunchedEffect(tagHomeNavigate) { - when (tagHomeNavigate) { - is TagHomeNavigate.Add -> { - detailViewModel.update(state.tagDetail) - } + LaunchedEffect(tagHomeNavigate) { + when (tagHomeNavigate) { + is TagHomeNavigate.Add -> { + detailViewModel.update(state.tagDetail) + } - is TagHomeNavigate.Tag -> { - detailViewModel.update(state.tagDetail) - } + is TagHomeNavigate.Tag -> { + detailViewModel.update(state.tagDetail) + } - is TagHomeNavigate.None -> Unit - } - } - } - }, - modifier = modifier, - extraPane = { - AnimatedPane { - val uiState by memoViewModel.uiState.collectAsStateWithLifecycle() - val list by memoViewModel.memoList.collectAsStateWithLifecycle() + is TagHomeNavigate.None -> Unit + } + } + } + }, + modifier = modifier, + extraPane = { + AnimatedPane { + val uiState by memoViewModel.uiState.collectAsStateWithLifecycle() + val list by memoViewModel.memoList.collectAsStateWithLifecycle() - val isNavigateUpVisible = remember(windowAdaptiveInfo) { - if (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { - !navigator.isDetailVisible() - } else { - true - } - } + val isNavigateUpVisible = remember(windowAdaptiveInfo) { + if (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { + !navigator.isDetailVisible() + } else { + true + } + } - TagMemoScreen( - state = rememberTagMemoScreenState(), - navigateButtonProvider = { - if (isNavigateUpVisible) { - TagMemoNavigateButton.NavigateUp(onNavigateUp = navigator::navigateBack) - } else { - TagMemoNavigateButton.None - } - }, - uiStateProvider = { uiState }, - onAdd = { navigator.currentDestination?.content?.let(navigateToMemoAdd) }, - listProvider = { list }, - onMemo = navigateToMemoDetail, - ) - } - }, - ) + TagMemoScreen( + state = rememberTagMemoScreenState(), + navigateButtonProvider = { + if (isNavigateUpVisible) { + TagMemoNavigateButton.NavigateUp(onNavigateUp = navigator::navigateBack) + } else { + TagMemoNavigateButton.None + } + }, + uiStateProvider = { uiState }, + onAdd = { navigator.currentDestination?.content?.let(navigateToMemoAdd) }, + listProvider = { list }, + onMemo = navigateToMemoDetail, + ) + } + }, + ) - LaunchedScaffoldValue( - navigator = navigator, - onScaffoldValueChange = onScaffoldValueChange, - ) + LaunchedScaffoldValue( + navigator = navigator, + onScaffoldValueChange = onScaffoldValueChange, + ) - LaunchedFetch( - navigator = navigator, - detailViewModel = detailViewModel, - memoViewModel = memoViewModel, - ) + LaunchedFetch( + navigator = navigator, + detailViewModel = detailViewModel, + memoViewModel = memoViewModel, + ) - KBackHandler( - isEnabled = navigator.canNavigateBack(), - onBack = navigator::navigateBack, - ) + KBackHandler( + isEnabled = navigator.canNavigateBack(), + onBack = navigator::navigateBack, + ) } @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable private fun LaunchedFetch( - navigator: ThreePaneScaffoldNavigator, - detailViewModel: TagDetailViewModel, - memoViewModel: TagMemoViewModel, + navigator: ThreePaneScaffoldNavigator, + detailViewModel: TagDetailViewModel, + memoViewModel: TagMemoViewModel, ) { - LaunchedEffect(navigator.currentDestination?.content, detailViewModel) { - detailViewModel.fetch(navigator.currentDestination?.content) - memoViewModel.fetch(navigator.currentDestination?.content) - } + LaunchedEffect(navigator.currentDestination?.content, detailViewModel) { + detailViewModel.fetch(navigator.currentDestination?.content) + memoViewModel.fetch(navigator.currentDestination?.content) + } } @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable private fun LaunchedScaffoldValue( - navigator: ThreePaneScaffoldNavigator, - onScaffoldValueChange: (ThreePaneScaffoldValue) -> Unit, + navigator: ThreePaneScaffoldNavigator, + onScaffoldValueChange: (ThreePaneScaffoldValue) -> Unit, ) { - LaunchedEffect(navigator.scaffoldValue) { - onScaffoldValueChange(navigator.scaffoldValue) - } + LaunchedEffect(navigator.scaffoldValue) { + onScaffoldValueChange(navigator.scaffoldValue) + } } 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 index 37e019f5..a818b9c9 100644 --- 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 @@ -17,9 +17,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar 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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -31,169 +28,173 @@ import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun TagListScreen( - state: TagListScreenState, - floatingButtonProvider: () -> TagListFloatingButton, - listProvider: () -> List?, - onTag: (String) -> Unit, - uiStateProvider: () -> TagListScreenUiState, - modifier: Modifier = Modifier, + state: TagListScreenState, + floatingButtonProvider: () -> TagListFloatingButton, + listProvider: () -> List?, + onTag: (String) -> Unit, + uiStateProvider: () -> TagListScreenUiState, + modifier: Modifier = Modifier, ) { - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = { Text(text = "태그") }, - ) - }, - snackbarHost = { SnackbarHost(hostState = state.hostState) }, - floatingActionButton = { - val button = floatingButtonProvider() + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(text = "태그") }, + ) + }, + snackbarHost = { SnackbarHost(hostState = state.hostState) }, + floatingActionButton = { + val button = floatingButtonProvider() - AnimatedVisibility( - visible = button is TagListFloatingButton.Add, - enter = scaleIn(), - exit = scaleOut(), - ) { - FloatingAddButton( - onClick = { - if (button is TagListFloatingButton.Add) { - button.onAdd() - } - }, - ) - } - }, - ) { - Content( - listProvider = listProvider, - onTag = onTag, - modifier = Modifier - .fillMaxSize() - .padding(it), - ) - } + AnimatedVisibility( + visible = button is TagListFloatingButton.Add, + enter = scaleIn(), + exit = scaleOut(), + ) { + FloatingAddButton( + onClick = { + if (button is TagListFloatingButton.Add) { + button.onAdd() + } + }, + ) + } + }, + ) { + Content( + listProvider = listProvider, + onTag = onTag, + modifier = Modifier + .fillMaxSize() + .padding(it), + ) + } - Message( - state = state, - uiStateProvider = uiStateProvider, - ) + Message( + state = state, + uiStateProvider = uiStateProvider, + ) } @Composable private fun Message( - state: TagListScreenState, - uiStateProvider: () -> TagListScreenUiState, + state: TagListScreenState, + uiStateProvider: () -> TagListScreenUiState, ) { - val uiState = uiStateProvider() + val uiState = uiStateProvider() - LaunchedEffect( - uiState.finishTagId, - uiState.deleteTagId, - uiState.isUnknownError, - ) { - if (!uiState.hasMessage) return@LaunchedEffect + LaunchedEffect( + uiState.finishTagId, + uiState.deleteTagId, + uiState.isUnknownError, + ) { + if (!uiState.hasMessage) return@LaunchedEffect - when { - !uiState.finishTagId.isNullOrBlank() -> { - state.showMessage( - message = "태그 완료 ${Emoji.congratulate.random()}", - actionLabel = "취소", - ) { - uiState.restartTag(uiState.finishTagId) - } - } + when { + !uiState.finishTagId.isNullOrBlank() -> { + state.showMessage( + message = "태그 완료 ${Emoji.congratulate.random()}", + actionLabel = "취소", + ) { + uiState.restartTag(uiState.finishTagId) + } + } - !uiState.deleteTagId.isNullOrBlank() -> { - state.showMessage( - message = "태그 삭제 ${Emoji.congratulate.random()}", - actionLabel = "취소", - ) { - uiState.restoreTag(uiState.deleteTagId) - } - } + !uiState.deleteTagId.isNullOrBlank() -> { + state.showMessage( + message = "태그 삭제 ${Emoji.congratulate.random()}", + actionLabel = "취소", + ) { + uiState.restoreTag(uiState.deleteTagId) + } + } - uiState.isUnknownError -> state.showMessage("알 수 없는 에러가 발생했어요 잠시 후 다시 시도해 주세요 ${Emoji.error.random()}") - } + uiState.isUnknownError -> state.showMessage("알 수 없는 에러가 발생했어요 잠시 후 다시 시도해 주세요 ${Emoji.error.random()}") + } - uiState.onMessageShow() - } + uiState.onMessageShow() + } } @Composable private fun Content( - listProvider: () -> List?, - onTag: (String) -> Unit, - modifier: Modifier = Modifier, + listProvider: () -> List?, + onTag: (String) -> Unit, + modifier: Modifier = Modifier, ) { - val isLoading by remember { derivedStateOf { listProvider() == null } } - val isEmpty by remember { derivedStateOf { !isLoading && listProvider().isNullOrEmpty() } } + LazyColumn( + modifier = modifier, + contentPadding = DiaryTheme.dimen.screenPaddingValues, + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + val list = listProvider() - if (isEmpty) { - Box( - modifier = modifier, - contentAlignment = Alignment.Center, - ) { - Text( - text = "태그가 없어요 🐼", - style = DiaryTheme.typography.headlineMedium, - ) - } - } else { - LazyColumn( - modifier = modifier, - contentPadding = DiaryTheme.dimen.screenPaddingValues, - verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), - ) { - if (isLoading) { - items( - count = 5, - contentType = { "Tag" }, - ) { - TagItem( - uiState = null, - onClick = {}, - ) - } - } else { - items( - items = listProvider().orEmpty(), - key = { it.id }, - contentType = { "Tag" }, - ) { - TagItem( - uiState = it, - onClick = { onTag(it.id) }, - modifier = Modifier.animateItem(), - ) - } - } - } - } + if (list == null) { + items( + count = 5, + contentType = { "Tag" }, + ) { + TagItem( + uiState = null, + onClick = { }, + modifier = Modifier.animateItem(), + ) + } + } else if (list.isEmpty()) { + item( + key = "Loading", + contentType = "Loading", + ) { + Box( + modifier = Modifier.fillParentMaxSize() + .animateItem(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "태그가 없어요 🐼", + style = DiaryTheme.typography.headlineMedium, + ) + } + } + } else { + items( + items = listProvider().orEmpty(), + key = { it.id }, + contentType = { "Tag" }, + ) { + TagItem( + uiState = it, + onClick = { onTag(it.id) }, + modifier = Modifier.animateItem(), + ) + } + } + } } @Composable private fun TagItem( - uiState: TagListItemUiState?, - onClick: () -> Unit, - modifier: Modifier = Modifier, + uiState: TagListItemUiState?, + onClick: () -> Unit, + modifier: Modifier = Modifier, ) { - FinishAndDeleteSwipeBox( - modifier = modifier, - onFinish = { uiState?.finish?.value?.invoke() }, - onDelete = { uiState?.delete?.value?.invoke() }, - ) { - Card(onClick = onClick) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = uiState?.title.orEmpty(), - style = DiaryTheme.typography.titleLarge, - ) - } - } - } + FinishAndDeleteSwipeBox( + modifier = modifier, + onFinish = { uiState?.finish?.value?.invoke() }, + onDelete = { uiState?.delete?.value?.invoke() }, + ) { + Card(onClick = onClick) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = uiState?.title.orEmpty(), + style = DiaryTheme.typography.titleLarge, + ) + } + } + } } diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreen.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreen.kt index 7af4ea67..601dd5ea 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreen.kt +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreen.kt @@ -18,9 +18,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar 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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -34,174 +31,178 @@ import io.github.taetae98coding.diary.library.color.multiplyAlpha @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun TagMemoScreen( - state: TagMemoScreenState, - navigateButtonProvider: () -> TagMemoNavigateButton, - uiStateProvider: () -> TagMemoScreenUiState, - onAdd: () -> Unit, - listProvider: () -> List?, - onMemo: (String) -> Unit, - modifier: Modifier = Modifier, + state: TagMemoScreenState, + navigateButtonProvider: () -> TagMemoNavigateButton, + uiStateProvider: () -> TagMemoScreenUiState, + onAdd: () -> Unit, + listProvider: () -> List?, + onMemo: (String) -> Unit, + modifier: Modifier = Modifier, ) { - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = { }, - navigationIcon = { - when (val button = navigateButtonProvider()) { - is TagMemoNavigateButton.NavigateUp -> { - IconButton(onClick = button.onNavigateUp) { - NavigateUpIcon() - } - } + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { }, + navigationIcon = { + when (val button = navigateButtonProvider()) { + is TagMemoNavigateButton.NavigateUp -> { + IconButton(onClick = button.onNavigateUp) { + NavigateUpIcon() + } + } - is TagMemoNavigateButton.None -> Unit - } - }, - ) - }, - snackbarHost = { SnackbarHost(hostState = state.hostState) }, - floatingActionButton = { FloatingAddButton(onClick = onAdd) }, - ) { - Content( - listProvider = listProvider, - onMemo = onMemo, - modifier = Modifier - .fillMaxSize() - .padding(it), - ) - } + is TagMemoNavigateButton.None -> Unit + } + }, + ) + }, + snackbarHost = { SnackbarHost(hostState = state.hostState) }, + floatingActionButton = { FloatingAddButton(onClick = onAdd) }, + ) { + Content( + listProvider = listProvider, + onMemo = onMemo, + modifier = Modifier + .fillMaxSize() + .padding(it), + ) + } - Message( - state = state, - uiStateProvider = uiStateProvider, - ) + Message( + state = state, + uiStateProvider = uiStateProvider, + ) } @Composable private fun Message( - state: TagMemoScreenState, - uiStateProvider: () -> TagMemoScreenUiState, + state: TagMemoScreenState, + uiStateProvider: () -> TagMemoScreenUiState, ) { - val uiState = uiStateProvider() + val uiState = uiStateProvider() - LaunchedEffect( - uiState.finishTagId, - uiState.deleteTagId, - uiState.isUnknownError, - ) { - if (!uiState.hasMessage) return@LaunchedEffect + LaunchedEffect( + uiState.finishTagId, + uiState.deleteTagId, + uiState.isUnknownError, + ) { + if (!uiState.hasMessage) return@LaunchedEffect - when { - !uiState.finishTagId.isNullOrBlank() -> { - state.showMessage( - message = "메모 완료 ${Emoji.congratulate.random()}", - actionLabel = "취소", - ) { - uiState.restartTag(uiState.finishTagId) - } - } + when { + !uiState.finishTagId.isNullOrBlank() -> { + state.showMessage( + message = "메모 완료 ${Emoji.congratulate.random()}", + actionLabel = "취소", + ) { + uiState.restartTag(uiState.finishTagId) + } + } - !uiState.deleteTagId.isNullOrBlank() -> { - state.showMessage( - message = "메모 삭제 ${Emoji.congratulate.random()}", - actionLabel = "취소", - ) { - uiState.restoreTag(uiState.deleteTagId) - } - } + !uiState.deleteTagId.isNullOrBlank() -> { + state.showMessage( + message = "메모 삭제 ${Emoji.congratulate.random()}", + actionLabel = "취소", + ) { + uiState.restoreTag(uiState.deleteTagId) + } + } - uiState.isUnknownError -> state.showMessage("알 수 없는 에러가 발생했어요 잠시 후 다시 시도해 주세요 ${Emoji.error.random()}") - } + uiState.isUnknownError -> state.showMessage("알 수 없는 에러가 발생했어요 잠시 후 다시 시도해 주세요 ${Emoji.error.random()}") + } - uiState.onMessageShow() - } + uiState.onMessageShow() + } } @Composable private fun Content( - listProvider: () -> List?, - onMemo: (String) -> Unit, - modifier: Modifier = Modifier, + listProvider: () -> List?, + onMemo: (String) -> Unit, + modifier: Modifier = Modifier, ) { - val isLoading by remember { derivedStateOf { listProvider() == null } } - val isEmpty by remember { derivedStateOf { !isLoading && listProvider().isNullOrEmpty() } } + LazyColumn( + modifier = modifier, + contentPadding = DiaryTheme.dimen.screenPaddingValues, + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + val list = listProvider() - if (isEmpty) { - Box( - modifier = modifier, - contentAlignment = Alignment.Center, - ) { - Text( - text = "메모가 없어요 🐰", - style = DiaryTheme.typography.headlineMedium, - ) - } - } else { - LazyColumn( - modifier = modifier, - contentPadding = DiaryTheme.dimen.screenPaddingValues, - verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), - ) { - if (isLoading) { - items( - count = 5, - contentType = { "Tag" }, - ) { - MemoItem( - uiState = null, - onClick = {}, - ) - } - } else { - items( - items = listProvider().orEmpty(), - key = { it.id }, - contentType = { "Tag" }, - ) { - MemoItem( - uiState = it, - onClick = { onMemo(it.id) }, - modifier = Modifier.animateItem(), - ) - } - } - } - } + if (list == null) { + items( + count = 5, + contentType = { "Memo" }, + ) { + MemoItem( + uiState = null, + onClick = {}, + modifier = Modifier.animateItem(), + ) + } + } else if (list.isEmpty()) { + item( + key = "Loading", + contentType = "Loading", + ) { + Box( + modifier = Modifier.fillParentMaxSize() + .animateItem(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "메모가 없어요 🐰", + style = DiaryTheme.typography.headlineMedium, + ) + } + } + } else { + items( + items = listProvider().orEmpty(), + key = { it.id }, + contentType = { "Memo" }, + ) { + MemoItem( + uiState = it, + onClick = { onMemo(it.id) }, + modifier = Modifier.animateItem(), + ) + } + } + } } @Composable private fun MemoItem( - uiState: MemoListItemUiState?, - onClick: () -> Unit, - modifier: Modifier = Modifier, + uiState: MemoListItemUiState?, + onClick: () -> Unit, + modifier: Modifier = Modifier, ) { - FinishAndDeleteSwipeBox( - modifier = modifier, - onFinish = { uiState?.finish?.value?.invoke() }, - onDelete = { uiState?.delete?.value?.invoke() }, - ) { - Card(onClick = onClick) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - ) { - MaterialTheme.typography.bodySmall - Text( - text = uiState?.title.orEmpty(), - style = DiaryTheme.typography.titleLarge, - ) - if (uiState?.dateRange != null) { - Text( - text = listOf(uiState.dateRange.start, uiState.dateRange.endInclusive) - .distinct() - .joinToString(separator = " ~ "), - color = LocalContentColor.current.multiplyAlpha(0.5F), - style = DiaryTheme.typography.labelSmall, - ) - } - } - } - } + FinishAndDeleteSwipeBox( + modifier = modifier, + onFinish = { uiState?.finish?.value?.invoke() }, + onDelete = { uiState?.delete?.value?.invoke() }, + ) { + Card(onClick = onClick) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + ) { + MaterialTheme.typography.bodySmall + Text( + text = uiState?.title.orEmpty(), + style = DiaryTheme.typography.titleLarge, + ) + if (uiState?.dateRange != null) { + Text( + text = listOf(uiState.dateRange.start, uiState.dateRange.endInclusive) + .distinct() + .joinToString(separator = " ~ "), + color = LocalContentColor.current.multiplyAlpha(0.5F), + style = DiaryTheme.typography.labelSmall, + ) + } + } + } + } } diff --git a/app/platform/macos/build.gradle.kts b/app/platform/macos/build.gradle.kts new file mode 100644 index 00000000..48f09e7d --- /dev/null +++ b/app/platform/macos/build.gradle.kts @@ -0,0 +1,66 @@ + +import com.codingfeline.buildkonfig.compiler.FieldSpec +import ext.getLocalProperty + +private val localProperties = requireNotNull(project.getLocalProperty()) + +plugins { + id("diary.kotlin.multiplatform") + id("diary.compose") + alias(libs.plugins.buildkonfig) +} + +kotlin { + listOf( + macosX64(), + macosArm64() + ).forEach { + it.binaries { + executable { + entryPoint = "io.github.taetae98coding.diary.main" + } + } + } + + applyDefaultHierarchyTemplate() + + sourceSets { + commonMain { + dependencies { + implementation(project(":app:platform:common")) +// implementation(project(":app:core:coroutines")) +// implementation(project(":app:core:diary-service")) +// implementation(project(":app:core:holiday-service")) +// + implementation(compose.ui) +// implementation(libs.lifecycle.compose) +// +// implementation(project.dependencies.platform(libs.koin.bom)) +// implementation(libs.koin.core) +// +// implementation(libs.ktor.client.darwin) + } + } + } +} + +buildkonfig { + packageName = Build.NAMESPACE + + defaultConfigs {} + + defaultConfigs("dev") { + buildConfigField(FieldSpec.Type.STRING, "FLAVOR", "dev") + buildConfigField(FieldSpec.Type.STRING, "DIARY_API_URL", localProperties.getProperty("diary.dev.api.base.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_URL", localProperties.getProperty("holiday.dev.api.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_KEY", localProperties.getProperty("holiday.dev.api.key")) + } + + defaultConfigs("real") { + buildConfigField(FieldSpec.Type.STRING, "FLAVOR", "real") + buildConfigField(FieldSpec.Type.STRING, "DIARY_API_URL", localProperties.getProperty("diary.real.api.base.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_URL", localProperties.getProperty("holiday.real.api.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_KEY", localProperties.getProperty("holiday.real.api.key")) + } +} + diff --git a/app/platform/macos/src/macosMain/kotlin/io/github/taetae98coding/diary/MacosApp.kt b/app/platform/macos/src/macosMain/kotlin/io/github/taetae98coding/diary/MacosApp.kt new file mode 100644 index 00000000..747e1907 --- /dev/null +++ b/app/platform/macos/src/macosMain/kotlin/io/github/taetae98coding/diary/MacosApp.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary + +import androidx.compose.ui.window.Window +import io.github.taetae98coding.diary.app.App +import platform.AppKit.NSApplication + +public fun main() { + val app = NSApplication.sharedApplication() + + Window("Diary") { + App() + } + + app.run() +} diff --git a/build-logic/src/main/kotlin/ext/DependencyExt.kt b/build-logic/src/main/kotlin/ext/DependencyExt.kt index 8e70f068..1850a42a 100644 --- a/build-logic/src/main/kotlin/ext/DependencyExt.kt +++ b/build-logic/src/main/kotlin/ext/DependencyExt.kt @@ -43,11 +43,19 @@ internal fun DependencyHandler.kspIos( add("kspIosSimulatorArm64", dependencyNotation) } +internal fun DependencyHandler.kspMacos( + dependencyNotation: Provider, +) { + add("kspMacosX64", dependencyNotation) + add("kspMacosArm64", dependencyNotation) +} + public fun DependencyHandler.kspCommon( dependencyNotation: Provider, ) { kspJvm(dependencyNotation) kspIos(dependencyNotation) + kspMacos(dependencyNotation) } public fun DependencyHandler.kspAll( @@ -56,4 +64,5 @@ public fun DependencyHandler.kspAll( kspJvm(dependencyNotation) kspAndroid(dependencyNotation) kspIos(dependencyNotation) + kspMacos(dependencyNotation) } diff --git a/build-logic/src/main/kotlin/plugin/datastore/DataStorePlugin.kt b/build-logic/src/main/kotlin/plugin/datastore/DataStorePlugin.kt index c6f34ab9..841cbc00 100644 --- a/build-logic/src/main/kotlin/plugin/datastore/DataStorePlugin.kt +++ b/build-logic/src/main/kotlin/plugin/datastore/DataStorePlugin.kt @@ -28,6 +28,9 @@ internal class DataStorePlugin : Plugin { iosArm64() iosSimulatorArm64() + macosX64() + macosArm64() + applyDefaultHierarchyTemplate() sourceSets { diff --git a/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformAllPlugin.kt b/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformAllPlugin.kt index 513204ce..0e224bc3 100644 --- a/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformAllPlugin.kt +++ b/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformAllPlugin.kt @@ -3,7 +3,6 @@ package plugin.kotlin import ext.withKotlinMultiplatform import org.gradle.api.Plugin import org.gradle.api.Project -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl internal class KotlinMultiplatformAllPlugin : Plugin{ private val kotlinMultiplatformPlugin = KotlinMultiplatformPlugin() @@ -20,6 +19,9 @@ internal class KotlinMultiplatformAllPlugin : Plugin{ iosArm64() iosSimulatorArm64() + macosX64() + macosArm64() + applyDefaultHierarchyTemplate() } } diff --git a/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformCommonPlugin.kt b/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformCommonPlugin.kt index d093ffac..c52ee882 100644 --- a/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformCommonPlugin.kt +++ b/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformCommonPlugin.kt @@ -17,6 +17,9 @@ internal class KotlinMultiplatformCommonPlugin : Plugin{ iosArm64() iosSimulatorArm64() + macosX64() + macosArm64() + applyDefaultHierarchyTemplate() } } diff --git a/build-logic/src/main/kotlin/plugin/room/RoomPlugin.kt b/build-logic/src/main/kotlin/plugin/room/RoomPlugin.kt index c68d1942..91a555ce 100644 --- a/build-logic/src/main/kotlin/plugin/room/RoomPlugin.kt +++ b/build-logic/src/main/kotlin/plugin/room/RoomPlugin.kt @@ -38,6 +38,9 @@ internal class RoomPlugin : Plugin { iosArm64() iosSimulatorArm64() + macosX64() + macosArm64() + applyDefaultHierarchyTemplate() sourceSets { diff --git a/common/exception/src/appleMain/kotlin/io/github/taetae98coding/diary/common/exception/ext/NetworkExceptionExt.apple.kt b/common/exception/src/appleMain/kotlin/io/github/taetae98coding/diary/common/exception/ext/NetworkExceptionExt.apple.kt new file mode 100644 index 00000000..5d9aa037 --- /dev/null +++ b/common/exception/src/appleMain/kotlin/io/github/taetae98coding/diary/common/exception/ext/NetworkExceptionExt.apple.kt @@ -0,0 +1,5 @@ +package io.github.taetae98coding.diary.common.exception.ext + +public actual fun Throwable.isNetworkException(): Boolean { + return false +} diff --git a/common/exception/src/iosMain/kotlin/io/github/taetae98coding/diary/common/exception/ext/NetworkExceptionExt.ios.kt b/common/exception/src/iosMain/kotlin/io/github/taetae98coding/diary/common/exception/ext/NetworkExceptionExt.ios.kt deleted file mode 100644 index 2f45153c..00000000 --- a/common/exception/src/iosMain/kotlin/io/github/taetae98coding/diary/common/exception/ext/NetworkExceptionExt.ios.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.taetae98coding.diary.common.exception.ext - -public actual fun Throwable.isNetworkException(): Boolean = false diff --git a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/LegacyMemoEntity.kt b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/LegacyMemoEntity.kt new file mode 100644 index 00000000..54ec9433 --- /dev/null +++ b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/LegacyMemoEntity.kt @@ -0,0 +1,34 @@ +package io.github.taetae98coding.diary.common.model.memo + +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class LegacyMemoEntity( + @SerialName("id") + val id: String = "", + @SerialName("title") + val title: String = "", + @SerialName("description") + val description: String = "", + @SerialName("start") + val start: LocalDate? = null, + @SerialName("endInclusive") + val endInclusive: LocalDate? = null, + @SerialName("color") + val color: Int = -16777216, + @SerialName("owner") + val owner: String, + @SerialName("primaryTag") + val primaryTag: String? = null, + @SerialName("tagIds") + val tagIds: Set = emptySet(), + @SerialName("isFinish") + val isFinish: Boolean = false, + @SerialName("isDelete") + val isDelete: Boolean = false, + @SerialName("updateAt") + val updateAt: Instant = Instant.fromEpochMilliseconds(0L), +) diff --git a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/MemoEntity.kt b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/MemoEntity.kt index 0144325a..2cf1ec38 100644 --- a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/MemoEntity.kt +++ b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/MemoEntity.kt @@ -7,28 +7,24 @@ import kotlinx.serialization.Serializable @Serializable public data class MemoEntity( - @SerialName("id") - val id: String = "", - @SerialName("title") - val title: String = "", - @SerialName("description") - val description: String = "", - @SerialName("start") - val start: LocalDate? = null, - @SerialName("endInclusive") - val endInclusive: LocalDate? = null, - @SerialName("color") - val color: Int = -16777216, - @SerialName("owner") - val owner: String = "", - @SerialName("primaryTag") - val primaryTag: String? = null, - @SerialName("tagIds") - val tagIds: Set = emptySet(), - @SerialName("isFinish") - val isFinish: Boolean = false, - @SerialName("isDelete") - val isDelete: Boolean = false, - @SerialName("updateAt") - val updateAt: Instant = Instant.fromEpochMilliseconds(0L), + @SerialName("id") + val id: String = "", + @SerialName("title") + val title: String = "", + @SerialName("description") + val description: String = "", + @SerialName("start") + val start: LocalDate? = null, + @SerialName("endInclusive") + val endInclusive: LocalDate? = null, + @SerialName("color") + val color: Int = -16777216, + @SerialName("primaryTag") + val primaryTag: String? = null, + @SerialName("isFinish") + val isFinish: Boolean = false, + @SerialName("isDelete") + val isDelete: Boolean = false, + @SerialName("updateAt") + val updateAt: Instant = Instant.fromEpochMilliseconds(0L), ) diff --git a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/DiaryResponse.kt b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/DiaryResponse.kt index 3b9eeeda..ce7b1066 100644 --- a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/DiaryResponse.kt +++ b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/DiaryResponse.kt @@ -11,7 +11,8 @@ public data class DiaryResponse( public companion object { public val Success: DiaryResponse = DiaryResponse(200, "SUCCESS", Unit) public val Created: DiaryResponse = DiaryResponse(201, "CREATED", Unit) - public val Unauthorized: DiaryResponse = DiaryResponse(401, "Unauthorized", Unit) + public val BadRequest: DiaryResponse = DiaryResponse(400, "BadRequest", Unit) + public val Unauthorized: DiaryResponse = DiaryResponse(401, "Unauthorized", Unit) public val InternalServerError: DiaryResponse = DiaryResponse(500, "InternalServerError", Unit) public val AlreadyExistEmail: DiaryResponse = DiaryResponse(1000, "AlreadyExistEmail", Unit) diff --git a/gradle.properties b/gradle.properties index a1d8f715..ecf1746c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,10 @@ kotlin.code.style=official kotlin.native.ignoreDisabledTargets=true kotlin.experimental.swift-export.enabled=true +## todo remove +kotlin.native.cacheKind.macosArm64=none +org.jetbrains.compose.experimental.macos.enabled=true + firebase.cocoapods.version=11.5.0 # Android diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e444f1e6..a5df9af0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ kotlinx-coroutines = "1.9.0" # https://github.com/Kotlin/kotlinx. kotlinx-datetime = "0.6.1" # https://github.com/Kotlin/kotlinx-datetime/releases ### multiplatform -compose = "1.7.1" # https://github.com/JetBrains/compose-multiplatform/releases + compose = "1.7.1" # https://github.com/JetBrains/compose-multiplatform/releases compose-material3-adaptive = "1.0.1" navigation = "2.8.0-alpha10" lifecycle = "2.8.4" diff --git a/library/firebase-messaging/build.gradle.kts b/library/firebase-messaging/build.gradle.kts index 3eead0b9..8fcc6911 100644 --- a/library/firebase-messaging/build.gradle.kts +++ b/library/firebase-messaging/build.gradle.kts @@ -7,6 +7,7 @@ plugins { kotlin { cocoapods { ios.deploymentTarget = "18.0" + osx.deploymentTarget = "15.1.1" noPodspec() pod( @@ -29,7 +30,7 @@ kotlin { } } - iosMain { + appleMain { dependencies { implementation(libs.kotlinx.coroutines.core) } diff --git a/library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.ios.kt b/library/firebase-messaging/src/appleMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.apple.kt similarity index 83% rename from library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.ios.kt rename to library/firebase-messaging/src/appleMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.apple.kt index aab58298..27942c24 100644 --- a/library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.ios.kt +++ b/library/firebase-messaging/src/appleMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.apple.kt @@ -3,4 +3,4 @@ package io.github.taetae98coding.diary.library.firebase.messaging import io.github.taetae98coding.diary.library.firebase.KFirebase public actual val KFirebase.messaging: KFirebaseMessaging - get() = KFirebaseMessagingImpl() + get() = KFirebaseMessagingImpl() diff --git a/library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt b/library/firebase-messaging/src/appleMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt similarity index 90% rename from library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt rename to library/firebase-messaging/src/appleMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt index 619d28da..2e1bb3ad 100644 --- a/library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt +++ b/library/firebase-messaging/src/appleMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt @@ -1,16 +1,16 @@ package io.github.taetae98coding.diary.library.firebase.messaging import cocoapods.FirebaseMessaging.FIRMessaging -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.coroutines.suspendCancellableCoroutine @OptIn(ExperimentalForeignApi::class) internal class KFirebaseMessagingImpl : KFirebaseMessaging { override suspend fun getToken(): String = suspendCancellableCoroutine { continuation -> - FIRMessaging.messaging().tokenWithCompletion { token, error -> + FIRMessaging.Companion.messaging().tokenWithCompletion { token, error -> if (error != null) { continuation.resumeWithException(Exception(error.toString())) } else if (token.isNullOrBlank()) { diff --git a/library/koin-datastore/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.ios.kt b/library/koin-datastore/src/appleMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.apple.kt similarity index 56% rename from library/koin-datastore/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.ios.kt rename to library/koin-datastore/src/appleMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.apple.kt index 73b3f0bd..c5db579d 100644 --- a/library/koin-datastore/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.ios.kt +++ b/library/koin-datastore/src/appleMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.apple.kt @@ -9,17 +9,17 @@ import platform.Foundation.NSUserDomainMask @OptIn(ExperimentalForeignApi::class) internal actual fun KoinComponent.getDataStoreAbsolutePath(name: String): String { - val documentDirectory: NSURL? = - NSFileManager.defaultManager - .URLForDirectory( - directory = NSDocumentDirectory, - inDomain = NSUserDomainMask, - appropriateForURL = null, - create = false, - error = null, - )?.also { + val documentDirectory: NSURL? = + NSFileManager.defaultManager + .URLForDirectory( + directory = NSDocumentDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = false, + error = null, + )?.also { // NSFileManager.defaultManager.removeItemAtURL(it, null) - } + } - return requireNotNull(documentDirectory).path + "/$name" + return requireNotNull(documentDirectory).path + "/$name" } diff --git a/library/koin-room/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.ios.kt b/library/koin-room/src/appleMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.apple.kt similarity index 57% rename from library/koin-room/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.ios.kt rename to library/koin-room/src/appleMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.apple.kt index 33c64a73..75a23134 100644 --- a/library/koin-room/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.ios.kt +++ b/library/koin-room/src/appleMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.apple.kt @@ -10,19 +10,19 @@ import platform.Foundation.NSUserDomainMask @OptIn(ExperimentalForeignApi::class) public actual inline fun KoinComponent.platformDatabaseBuilder( - name: String, + name: String ): RoomDatabase.Builder { - val documentDirectory = - NSFileManager.defaultManager - .URLForDirectory( - directory = NSDocumentDirectory, - inDomain = NSUserDomainMask, - appropriateForURL = null, - create = false, - error = null, - )?.also { + val documentDirectory = + NSFileManager.defaultManager + .URLForDirectory( + directory = NSDocumentDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = false, + error = null, + )?.also { // NSFileManager.defaultManager.removeItemAtURL(it, null) - } + } - return Room.databaseBuilder(name = "${documentDirectory?.path}/$name") + return Room.databaseBuilder(name = "${documentDirectory?.path}/$name") } diff --git a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt index b65c1527..adf8913f 100644 --- a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt +++ b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt @@ -4,6 +4,8 @@ import io.github.taetae98coding.diary.core.database.AccountTable import io.github.taetae98coding.diary.core.database.BuddyGroupAccountRelation import io.github.taetae98coding.diary.core.database.BuddyGroupTable import io.github.taetae98coding.diary.core.database.FCMTokenTable +import io.github.taetae98coding.diary.core.database.MemoAccountTable +import io.github.taetae98coding.diary.core.database.MemoBuddyGroupTable import io.github.taetae98coding.diary.core.database.MemoTable import io.github.taetae98coding.diary.core.database.MemoTagTable import io.github.taetae98coding.diary.core.database.TagTable @@ -26,10 +28,12 @@ internal fun Application.installDatabase() { AccountTable, TagTable, MemoTable, + MemoAccountTable, MemoTagTable, FCMTokenTable, BuddyGroupTable, BuddyGroupAccountRelation, + MemoBuddyGroupTable, ) } } diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/AccountTable.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/AccountTable.kt index 0371db2e..67b8e442 100644 --- a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/AccountTable.kt +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/AccountTable.kt @@ -1,21 +1,23 @@ package io.github.taetae98coding.diary.core.database import io.github.taetae98coding.diary.core.model.Account +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IdTable +import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.selectAll -public data object AccountTable : Table(name = "Account") { +public data object AccountTable : IdTable(name = "Account") { + private val UID = varchar("uid", 255).default("").uniqueIndex() private val EMAIL = varchar("email", 255).default("") private val PASSWORD = varchar("password", 255).default("") - internal val UID = varchar("uid", 255).default("").uniqueIndex() - + override val id: Column> = UID.entityId() override val primaryKey: PrimaryKey = PrimaryKey(EMAIL) - public fun contains(email: String): Boolean = + public fun contains(email: String): Boolean = selectAll() .where { EMAIL eq email } .any() diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/BuddyGroupAccountRelation.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/BuddyGroupAccountRelation.kt index a72836d6..335f2bc4 100644 --- a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/BuddyGroupAccountRelation.kt +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/BuddyGroupAccountRelation.kt @@ -8,34 +8,38 @@ import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.upsert public data object BuddyGroupAccountRelation : Table("BuddyGroupAccount") { - private val BUDDY_GROUP_ID = reference( - name = "buddyGroupId", - refColumn = BuddyGroupTable.ID, - onDelete = ReferenceOption.CASCADE, - onUpdate = ReferenceOption.CASCADE, - ) - - private val BUDDY_ID = reference( - name = "buddyId", - refColumn = AccountTable.UID, - onDelete = ReferenceOption.CASCADE, - onUpdate = ReferenceOption.CASCADE, - ) - - override val primaryKey: PrimaryKey = PrimaryKey(BUDDY_GROUP_ID, BUDDY_ID) - - public fun findByUid(uid: String): List = selectAll() - .where { BUDDY_ID eq uid } - .map { it[BUDDY_GROUP_ID] } - - public fun upsert(buddyGroupId: String, buddyId: String) { - upsert { - it[BUDDY_GROUP_ID] = buddyGroupId - it[BUDDY_ID] = buddyId - } - } - - public fun deleteByBuddyGroupId(buddyGroupId: String) { - deleteWhere { BUDDY_GROUP_ID eq buddyGroupId } - } + private val BUDDY_GROUP_ID = reference( + name = "buddyGroupId", + foreign = BuddyGroupTable, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ) + + private val BUDDY_ID = reference( + name = "buddyId", + foreign = AccountTable, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ) + + override val primaryKey: PrimaryKey = PrimaryKey(BUDDY_GROUP_ID, BUDDY_ID) + + public fun findByUid(uid: String): List = selectAll() + .where { BUDDY_ID eq uid } + .map { it[BUDDY_GROUP_ID].value } + + public fun findBuddyIdByGroupId(groupId: String): List = selectAll() + .where { BUDDY_GROUP_ID eq groupId } + .map { it[BUDDY_ID].value } + + public fun upsert(buddyGroupId: String, buddyId: String) { + upsert { + it[BUDDY_GROUP_ID] = buddyGroupId + it[BUDDY_ID] = buddyId + } + } + + public fun deleteByBuddyGroupId(buddyGroupId: String) { + deleteWhere { BUDDY_GROUP_ID eq buddyGroupId } + } } diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/BuddyGroupTable.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/BuddyGroupTable.kt index 22b1dce9..10f85670 100644 --- a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/BuddyGroupTable.kt +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/BuddyGroupTable.kt @@ -1,16 +1,19 @@ package io.github.taetae98coding.diary.core.database import io.github.taetae98coding.diary.core.model.BuddyGroup -import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IdTable +import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.upsert -public data object BuddyGroupTable : Table("BuddyGroup") { - internal val ID = varchar("id", 255).default("") +public data object BuddyGroupTable : IdTable("BuddyGroup") { + private val ID = varchar("id", 255).default("") private val TITLE = varchar("title", 255).default("") private val DESCRIPTION = text("description") - override val primaryKey: PrimaryKey = PrimaryKey(ID) + override val id: Column> = ID.entityId() + override val primaryKey: PrimaryKey = PrimaryKey(id) public fun findByIds(ids: Set): List = selectAll() .where { ID inList ids } diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/FCMTokenTable.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/FCMTokenTable.kt index b561f301..296ff24d 100644 --- a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/FCMTokenTable.kt +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/FCMTokenTable.kt @@ -1,41 +1,43 @@ package io.github.taetae98coding.diary.core.database import kotlinx.datetime.Clock +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IdTable +import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.ReferenceOption import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.kotlin.datetime.timestamp import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.upsert -public data object FCMTokenTable : Table(name = "FCMToken") { - private val TOKEN = varchar("token", 255).default("") - private val OWNER = - reference( - name = "owner", - refColumn = AccountTable.UID, - onDelete = ReferenceOption.CASCADE, - onUpdate = ReferenceOption.CASCADE, - ).nullable() +public data object FCMTokenTable : IdTable(name = "FCMToken") { + private val TOKEN = varchar("token", 255).default("") + private val OWNER = reference( + name = "owner", + foreign = AccountTable, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ) - private val UPDATE_AT = timestamp("updateAt").default(Clock.System.now()) + private val UPDATE_AT = timestamp("updateAt").default(Clock.System.now()) - override val primaryKey: PrimaryKey = PrimaryKey(TOKEN) + override val id: Column> = TOKEN.entityId() + override val primaryKey: PrimaryKey = PrimaryKey(id) - public fun upsert(token: String, owner: String) { - upsert { - it[TOKEN] = token - it[OWNER] = owner - it[UPDATE_AT] = Clock.System.now() - } - } + public fun upsert(token: String, owner: String) { + upsert { + it[TOKEN] = token + it[OWNER] = owner + it[UPDATE_AT] = Clock.System.now() + } + } - public fun delete(token: String) { - deleteWhere { TOKEN eq token } - } + public fun delete(token: String) { + deleteWhere { TOKEN eq token } + } - public fun findByOwner(owner: String): List = selectAll() - .where { OWNER eq owner } - .map { row -> row[TOKEN] } + public fun findByOwner(owner: String): List = selectAll() + .where { OWNER eq owner } + .map { row -> row[TOKEN] } } diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoAccountTable.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoAccountTable.kt new file mode 100644 index 00000000..23e4b77c --- /dev/null +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoAccountTable.kt @@ -0,0 +1,29 @@ +package io.github.taetae98coding.diary.core.database + +import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.upsert + +public data object MemoAccountTable : Table("MemoAccount") { + internal val MEMO_ID = reference( + name = "memoId", + foreign = MemoTable, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ) + internal val OWNER = reference( + name = "owner", + foreign = AccountTable, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ) + + public override val primaryKey: PrimaryKey = PrimaryKey(MEMO_ID, OWNER) + + public fun upsert(memoId: String, owner: String) { + upsert { + it[MEMO_ID] = memoId + it[OWNER] = owner + } + } +} diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoBuddyGroupTable.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoBuddyGroupTable.kt new file mode 100644 index 00000000..00229c52 --- /dev/null +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoBuddyGroupTable.kt @@ -0,0 +1,29 @@ +package io.github.taetae98coding.diary.core.database + +import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.upsert + +public data object MemoBuddyGroupTable : Table("MemoBuddyGroup") { + internal val MEMO_ID = reference( + name = "memoId", + foreign = MemoTable, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ) + internal val BUDDY_GROUP = reference( + name = "buddyGroup", + foreign = BuddyGroupTable, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ) + + public override val primaryKey: PrimaryKey = PrimaryKey(MEMO_ID, BUDDY_GROUP) + + public fun upsert(memoId: String, buddyGroup: String) { + upsert { + it[MEMO_ID] = memoId + it[BUDDY_GROUP] = buddyGroup + } + } +} diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoQuery.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoQuery.kt new file mode 100644 index 00000000..bd73d45d --- /dev/null +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoQuery.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.core.database + +import io.github.taetae98coding.diary.core.model.Memo + +public data object MemoQuery { + public fun upsertMemo(memo: Memo, tagIds: Set) { + MemoTable.upsert(memo) + MemoTagTable.deleteByMemoId(memo.id) + tagIds.forEach { + MemoTagTable.upsert(memo.id, it) + } + } +} diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTable.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTable.kt index 07bef0b1..506e1a8f 100644 --- a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTable.kt +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTable.kt @@ -3,82 +3,90 @@ package io.github.taetae98coding.diary.core.database import io.github.taetae98coding.diary.core.model.Memo import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IdTable +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.JoinType import org.jetbrains.exposed.sql.ReferenceOption import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.kotlin.datetime.date import org.jetbrains.exposed.sql.kotlin.datetime.timestamp import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.upsert -public data object MemoTable : Table(name = "Memo") { - internal val ID = varchar("id", 255).default("") - private val TITLE = varchar("title", 255).default("") - private val DESCRIPTION = text("description") - private val START = date("start").nullable().default(null) - private val END_INCLUSIVE = date("endInclusive").nullable().default(null) - private val COLOR = integer("color").default(0xFFFFFFFF.toInt()) - private val OWNER = - reference( - name = "owner", - refColumn = AccountTable.UID, - onDelete = ReferenceOption.CASCADE, - onUpdate = ReferenceOption.CASCADE, - ) - private val PRIMARY_TAG = - reference( - name = "primaryTag", - refColumn = TagTable.ID, - onDelete = ReferenceOption.CASCADE, - onUpdate = ReferenceOption.CASCADE, - ).nullable() - private val IS_FINISH = bool("isFinish").default(false) - private val IS_DELETE = bool("isDelete").default(false) - private val UPDATE_AT = timestamp("updateAt").default(Clock.System.now()) +public data object MemoTable : IdTable(name = "Memo") { + private val ID = varchar("id", 255).default("") + private val TITLE = varchar("title", 255).default("") + private val DESCRIPTION = text("description") + private val START = date("start").nullable().default(null) + private val END_INCLUSIVE = date("endInclusive").nullable().default(null) + private val COLOR = integer("color").default(0xFFFFFFFF.toInt()) + private val PRIMARY_TAG = reference( + name = "primaryTag", + foreign = TagTable, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ).nullable() + private val IS_FINISH = bool("isFinish").default(false) + private val IS_DELETE = bool("isDelete").default(false) + private val UPDATE_AT = timestamp("updateAt").default(Clock.System.now()) - override val primaryKey: PrimaryKey = PrimaryKey(ID) + override val id: Column> = ID.entityId() + override val primaryKey: PrimaryKey = PrimaryKey(id) - public fun upsert(memo: Memo) { - upsert { - it[ID] = memo.id - it[TITLE] = memo.title - it[DESCRIPTION] = memo.description - it[START] = memo.start - it[END_INCLUSIVE] = memo.endInclusive - it[COLOR] = memo.color - it[OWNER] = memo.owner - it[PRIMARY_TAG] = memo.primaryTag - it[IS_FINISH] = memo.isFinish - it[IS_DELETE] = memo.isDelete - it[UPDATE_AT] = memo.updateAt - } - } + internal fun upsert(memo: Memo) { + upsert { + it[ID] = memo.id + it[TITLE] = memo.title + it[DESCRIPTION] = memo.description + it[START] = memo.start + it[END_INCLUSIVE] = memo.endInclusive + it[COLOR] = memo.color + it[PRIMARY_TAG] = memo.primaryTag + it[IS_FINISH] = memo.isFinish + it[IS_DELETE] = memo.isDelete + it[UPDATE_AT] = memo.updateAt + } + } - public fun findByIds(ids: Set): List = - selectAll() - .where { ID.inList(ids) } - .map { it.toMemo() } + public fun findByIds(ids: Set): List = + selectAll() + .where { ID.inList(ids) } + .map { it.toMemo() } - public fun findByUpdateAt(uid: String, updateAt: Instant): List = - selectAll() - .where { (OWNER eq uid) and (UPDATE_AT greater updateAt) } - .orderBy(UPDATE_AT) - .limit(50) - .map { it.toMemo() } + public fun findByUpdateAtOwner(owner: String, updateAt: Instant): List { + return join(otherTable = MemoAccountTable, joinType = JoinType.INNER) { ID eq MemoAccountTable.MEMO_ID } + .selectAll() + .where { (MemoAccountTable.OWNER eq owner) and (UPDATE_AT greater updateAt) } + .orderBy(UPDATE_AT) + .limit(50) + .map { it.toMemo() } + } - private fun ResultRow.toMemo(): Memo = - Memo( - id = get(ID), - title = get(TITLE), - description = get(DESCRIPTION), - start = get(START), - endInclusive = get(END_INCLUSIVE), - color = get(COLOR), - owner = get(OWNER), - primaryTag = get(PRIMARY_TAG), - isFinish = get(IS_FINISH), - isDelete = get(IS_DELETE), - updateAt = get(UPDATE_AT), - ) + public fun findGroupMemoByDateRange(groupId: String, dateRange: ClosedRange): List { + return join(otherTable = MemoBuddyGroupTable, joinType = JoinType.INNER) { ID eq MemoBuddyGroupTable.MEMO_ID } + .selectAll() + .where { + (MemoBuddyGroupTable.BUDDY_GROUP eq groupId) and + (START.isNotNull() and (START lessEq dateRange.endInclusive)) and + (END_INCLUSIVE.isNotNull() and (END_INCLUSIVE greaterEq dateRange.start)) + } + .map { it.toMemo() } + } + + private fun ResultRow.toMemo(): Memo = + Memo( + id = get(ID), + title = get(TITLE), + description = get(DESCRIPTION), + start = get(START), + endInclusive = get(END_INCLUSIVE), + color = get(COLOR), + primaryTag = get(PRIMARY_TAG)?.value, + isFinish = get(IS_FINISH), + isDelete = get(IS_DELETE), + updateAt = get(UPDATE_AT), + ) } diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTagTable.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTagTable.kt index 21e71885..6f9fcf91 100644 --- a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTagTable.kt +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTagTable.kt @@ -8,37 +8,35 @@ import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.upsert public data object MemoTagTable : Table(name = "MemoTag") { - private val MEMO_ID = - reference( - name = "memoId", - refColumn = MemoTable.ID, - onDelete = ReferenceOption.CASCADE, - onUpdate = ReferenceOption.CASCADE, - ) + private val MEMO_ID = reference( + name = "memoId", + foreign = MemoTable, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ) - private val TAG_ID = - reference( - name = "tagId", - refColumn = TagTable.ID, - onDelete = ReferenceOption.CASCADE, - onUpdate = ReferenceOption.CASCADE, - ) + private val TAG_ID = reference( + name = "tagId", + foreign = TagTable, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ) - override val primaryKey: PrimaryKey = PrimaryKey(MEMO_ID, TAG_ID) + override val primaryKey: PrimaryKey = PrimaryKey(MEMO_ID, TAG_ID) - public fun upsert(memoId: String, tagId: String) { - upsert { - it[MEMO_ID] = memoId - it[TAG_ID] = tagId - } - } + internal fun upsert(memoId: String, tagId: String) { + upsert { + it[MEMO_ID] = memoId + it[TAG_ID] = tagId + } + } - public fun deleteByMemoId(memoId: String) { - deleteWhere { MEMO_ID eq memoId } - } + internal fun deleteByMemoId(memoId: String) { + deleteWhere { MEMO_ID eq memoId } + } - public fun findTagIdsByMemoId(memoId: String): List = - selectAll() - .where { MEMO_ID eq memoId } - .map { it[TAG_ID] } + public fun findTagIdsByMemoId(memoId: String): List = + selectAll() + .where { MEMO_ID eq memoId } + .map { it[TAG_ID].value } } diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/TagTable.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/TagTable.kt index 9692ccf3..46397b4b 100644 --- a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/TagTable.kt +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/TagTable.kt @@ -3,69 +3,70 @@ package io.github.taetae98coding.diary.core.database import io.github.taetae98coding.diary.core.model.Tag import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IdTable +import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.ReferenceOption import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.kotlin.datetime.timestamp import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.upsert -public data object TagTable : Table(name = "Tag") { - internal val ID = varchar("id", 255).default("") +public data object TagTable : IdTable(name = "Tag") { + private val ID = varchar("id", 255).default("") + private val TITLE = varchar("title", 255).default("") + private val DESCRIPTION = text("description") + private val COLOR = integer("color").default(0xFFFFFFFF.toInt()) + private val OWNER = reference( + name = "owner", + foreign = AccountTable, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ) + private val IS_FINISH = bool("isFinish").default(false) + private val IS_DELETE = bool("isDelete").default(false) + private val UPDATE_AT = timestamp("updateAt").default(Clock.System.now()) - private val TITLE = varchar("title", 255).default("") - private val DESCRIPTION = text("description") - private val COLOR = integer("color").default(0xFFFFFFFF.toInt()) - private val OWNER = - reference( - name = "owner", - refColumn = AccountTable.UID, - onDelete = ReferenceOption.CASCADE, - onUpdate = ReferenceOption.CASCADE, - ) - private val IS_FINISH = bool("isFinish").default(false) - private val IS_DELETE = bool("isDelete").default(false) - private val UPDATE_AT = timestamp("updateAt").default(Clock.System.now()) + override val id: Column> = ID.entityId() + override val primaryKey: PrimaryKey = PrimaryKey(id) - override val primaryKey: PrimaryKey = PrimaryKey(ID) + public fun upsert(list: List) { + list.forEach { tag -> + upsert { + it[ID] = tag.id + it[TITLE] = tag.title + it[DESCRIPTION] = tag.description + it[COLOR] = tag.color + it[OWNER] = tag.owner + it[IS_FINISH] = tag.isFinish + it[IS_DELETE] = tag.isDelete + it[UPDATE_AT] = tag.updateAt + } + } + } - public fun upsert(list: List) { - list.forEach { tag -> - upsert { - it[ID] = tag.id - it[TITLE] = tag.title - it[DESCRIPTION] = tag.description - it[COLOR] = tag.color - it[OWNER] = tag.owner - it[IS_FINISH] = tag.isFinish - it[IS_DELETE] = tag.isDelete - it[UPDATE_AT] = tag.updateAt - } - } - } + public fun findByIds(ids: Set): List = + selectAll() + .where { ID.inList(ids) } + .map { it.toTag() } - public fun findByIds(ids: Set): List = - selectAll() - .where { ID.inList(ids) } - .map { it.toTag() } + public fun findByUpdateAt(uid: String, updateAt: Instant): List = + selectAll() + .where { (OWNER eq uid) and (UPDATE_AT greater updateAt) } + .orderBy(UPDATE_AT) + .limit(50) + .map { it.toTag() } - public fun findByUpdateAt(uid: String, updateAt: Instant): List = - selectAll() - .where { (OWNER eq uid) and (UPDATE_AT greater updateAt) } - .orderBy(UPDATE_AT) - .limit(50) - .map { it.toTag() } - - private fun ResultRow.toTag(): Tag = - Tag( - id = get(ID), - title = get(TITLE), - description = get(DESCRIPTION), - color = get(COLOR), - owner = get(OWNER), - isFinish = get(IS_FINISH), - isDelete = get(IS_DELETE), - updateAt = get(UPDATE_AT), - ) + private fun ResultRow.toTag(): Tag = + Tag( + id = get(ID), + title = get(TITLE), + description = get(DESCRIPTION), + color = get(COLOR), + owner = get(OWNER).value, + isFinish = get(IS_FINISH), + isDelete = get(IS_DELETE), + updateAt = get(UPDATE_AT), + ) } diff --git a/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Memo.kt b/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Memo.kt index 5b934687..26ebec34 100644 --- a/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Memo.kt +++ b/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Memo.kt @@ -10,7 +10,6 @@ public data class Memo( val start: LocalDate?, val endInclusive: LocalDate?, val color: Int, - val owner: String, val primaryTag: String?, val isFinish: Boolean, val isDelete: Boolean, diff --git a/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/MemoAndTagIds.kt b/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/MemoAndTagIds.kt index 3a96067a..6fabe841 100644 --- a/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/MemoAndTagIds.kt +++ b/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/MemoAndTagIds.kt @@ -1,6 +1,6 @@ package io.github.taetae98coding.diary.core.model public data class MemoAndTagIds( - val memo: Memo, - val tagIds: Set, + val memo: Memo, + val tagIds: Set, ) diff --git a/server/data/buddy/src/main/kotlin/io/github/taetae98coding/diary/data/buddy/repository/BuddyRepositoryImpl.kt b/server/data/buddy/src/main/kotlin/io/github/taetae98coding/diary/data/buddy/repository/BuddyRepositoryImpl.kt index 66166ee3..25a0e8cf 100644 --- a/server/data/buddy/src/main/kotlin/io/github/taetae98coding/diary/data/buddy/repository/BuddyRepositoryImpl.kt +++ b/server/data/buddy/src/main/kotlin/io/github/taetae98coding/diary/data/buddy/repository/BuddyRepositoryImpl.kt @@ -3,44 +3,70 @@ package io.github.taetae98coding.diary.data.buddy.repository import io.github.taetae98coding.diary.core.database.AccountTable import io.github.taetae98coding.diary.core.database.BuddyGroupAccountRelation import io.github.taetae98coding.diary.core.database.BuddyGroupTable +import io.github.taetae98coding.diary.core.database.MemoBuddyGroupTable +import io.github.taetae98coding.diary.core.database.MemoQuery +import io.github.taetae98coding.diary.core.database.MemoTable import io.github.taetae98coding.diary.core.model.Buddy import io.github.taetae98coding.diary.core.model.BuddyGroup import io.github.taetae98coding.diary.core.model.BuddyGroupAndBuddyIds +import io.github.taetae98coding.diary.core.model.Memo import io.github.taetae98coding.diary.domain.buddy.repository.BuddyRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.datetime.LocalDate import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.koin.core.annotation.Factory @Factory internal class BuddyRepositoryImpl : BuddyRepository { - override suspend fun upsert(buddyGroupAndBuddyIds: BuddyGroupAndBuddyIds) { - newSuspendedTransaction { - BuddyGroupTable.upsert(buddyGroupAndBuddyIds.buddyGroup) - BuddyGroupAccountRelation.deleteByBuddyGroupId(buddyGroupAndBuddyIds.buddyGroup.id) - buddyGroupAndBuddyIds.buddyIds.forEach { - BuddyGroupAccountRelation.upsert(buddyGroupAndBuddyIds.buddyGroup.id, it) - } - } - } - - override fun findGroupByUid(uid: String): Flow> = flow { - newSuspendedTransaction { - val groupIds = BuddyGroupAccountRelation.findByUid(uid).toSet() - BuddyGroupTable.findByIds(groupIds) - }.also { - emit(it) - } - } - - override fun findGroupById(id: String): Flow = flow { - newSuspendedTransaction { BuddyGroupTable.findById(id) } - .also { emit(it) } - } - - override fun findByEmail(email: String, uid: String?): Flow> = flow { - newSuspendedTransaction { AccountTable.findByEmail(email, uid) } - .map { Buddy(uid = it.uid, email = it.email) } - .also { emit(it) } - } + override suspend fun upsert(buddyGroupAndBuddyIds: BuddyGroupAndBuddyIds) { + newSuspendedTransaction { + BuddyGroupTable.upsert(buddyGroupAndBuddyIds.buddyGroup) + BuddyGroupAccountRelation.deleteByBuddyGroupId(buddyGroupAndBuddyIds.buddyGroup.id) + buddyGroupAndBuddyIds.buddyIds.forEach { + BuddyGroupAccountRelation.upsert(buddyGroupAndBuddyIds.buddyGroup.id, it) + } + } + } + + override suspend fun upsert(groupId: String, memo: Memo, tagIds: Set) { + newSuspendedTransaction { + MemoQuery.upsertMemo(memo, tagIds) + MemoBuddyGroupTable.upsert(memo.id, groupId) + } + } + + override fun findGroupByUid(uid: String): Flow> = flow { + newSuspendedTransaction { + val groupIds = BuddyGroupAccountRelation.findByUid(uid).toSet() + BuddyGroupTable.findByIds(groupIds) + }.also { + emit(it) + } + } + + override fun findGroupById(id: String): Flow = flow { + newSuspendedTransaction { BuddyGroupTable.findById(id) } + .also { emit(it) } + } + + override fun findBuddyIdByGroupId(groupId: String): Flow> { + return flow { + newSuspendedTransaction { BuddyGroupAccountRelation.findBuddyIdByGroupId(groupId) } + .also { emit(it) } + } + } + + override fun findMemoByDate(groupId: String, dateRange: ClosedRange): Flow> { + return flow { + newSuspendedTransaction { MemoTable.findGroupMemoByDateRange(groupId, dateRange) } + .also { emit(it) } + } + } + + override fun findBuddyByEmail(email: String, uid: String?): Flow> = flow { + newSuspendedTransaction { AccountTable.findByEmail(email, uid) } + .map { Buddy(uid = it.uid, email = it.email) } + .also { emit(it) } + } } diff --git a/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt b/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt index c07ac2fc..234aafad 100644 --- a/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt +++ b/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt @@ -1,5 +1,7 @@ package io.github.taetae98coding.diary.data.memo.repository +import io.github.taetae98coding.diary.core.database.MemoAccountTable +import io.github.taetae98coding.diary.core.database.MemoQuery import io.github.taetae98coding.diary.core.database.MemoTable import io.github.taetae98coding.diary.core.database.MemoTagTable import io.github.taetae98coding.diary.core.model.Memo @@ -13,14 +15,11 @@ import org.koin.core.annotation.Factory @Factory internal class MemoRepositoryImpl : MemoRepository { - override suspend fun upsert(list: List) { + override suspend fun upsert(list: List, owner: String) { newSuspendedTransaction { list.forEach { memoAndTagIds -> - MemoTable.upsert(memoAndTagIds.memo) - MemoTagTable.deleteByMemoId(memoAndTagIds.memo.id) - memoAndTagIds.tagIds.forEach { - MemoTagTable.upsert(memoAndTagIds.memo.id, it) - } + MemoQuery.upsertMemo(memoAndTagIds.memo, memoAndTagIds.tagIds) + MemoAccountTable.upsert(memoAndTagIds.memo.id, owner) } } } @@ -31,7 +30,7 @@ internal class MemoRepositoryImpl : MemoRepository { flow { newSuspendedTransaction { MemoTable - .findByUpdateAt(uid, updateAt) + .findByUpdateAtOwner(uid, updateAt) .map { memo -> MemoAndTagIds( memo = memo, diff --git a/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/repository/BuddyRepository.kt b/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/repository/BuddyRepository.kt index fbcffbfa..745b64b8 100644 --- a/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/repository/BuddyRepository.kt +++ b/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/repository/BuddyRepository.kt @@ -3,14 +3,21 @@ package io.github.taetae98coding.diary.domain.buddy.repository import io.github.taetae98coding.diary.core.model.Buddy import io.github.taetae98coding.diary.core.model.BuddyGroup import io.github.taetae98coding.diary.core.model.BuddyGroupAndBuddyIds +import io.github.taetae98coding.diary.core.model.Memo import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.LocalDate public interface BuddyRepository { public suspend fun upsert(buddyGroup: BuddyGroupAndBuddyIds) + public suspend fun upsert(groupId: String, memo: Memo, tagIds: Set) public fun findGroupById(id: String): Flow public fun findGroupByUid(uid: String): Flow> - public fun findByEmail(email: String, uid: String?): Flow> + public fun findBuddyIdByGroupId(groupId: String): Flow> + public fun findBuddyByEmail(email: String, uid: String?): Flow> + + public fun findMemoByDate(groupId: String, dateRange: ClosedRange): Flow> + } diff --git a/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyGroupMemoByDate.kt b/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyGroupMemoByDate.kt new file mode 100644 index 00000000..83a0cc1e --- /dev/null +++ b/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyGroupMemoByDate.kt @@ -0,0 +1,24 @@ +package io.github.taetae98coding.diary.domain.buddy.usecase + +import io.github.taetae98coding.diary.core.model.Memo +import io.github.taetae98coding.diary.domain.buddy.repository.BuddyRepository +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.mapLatest +import kotlinx.datetime.LocalDate +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class FindBuddyGroupMemoByDate internal constructor( + private val repository: BuddyRepository, +) { + public operator fun invoke(groupId: String, dateRange: ClosedRange): Flow>> { + return flow { emitAll(repository.findMemoByDate(groupId, dateRange)) } + .mapLatest { Result.success(it) } + .catch { emit(Result.failure(it)) } + } +} diff --git a/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyUseCase.kt b/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyUseCase.kt index 058c2e66..8bfaba26 100644 --- a/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyUseCase.kt +++ b/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyUseCase.kt @@ -19,7 +19,7 @@ public class FindBuddyUseCase internal constructor( public operator fun invoke(email: String?, uid: String?): Flow>> { if (email.isNullOrBlank()) return flowOf(Result.success(emptyList())) - return flow { emitAll(repository.findByEmail(email, uid)) } + return flow { emitAll(repository.findBuddyByEmail(email, uid)) } .mapLatest { Result.success(it) } .catch { emit(Result.failure(it)) } } diff --git a/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/UpsertBuddyGroupMemoUseCase.kt b/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/UpsertBuddyGroupMemoUseCase.kt new file mode 100644 index 00000000..f209765f --- /dev/null +++ b/server/domain/buddy/src/main/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/UpsertBuddyGroupMemoUseCase.kt @@ -0,0 +1,29 @@ +package io.github.taetae98coding.diary.domain.buddy.usecase + +import io.github.taetae98coding.diary.core.model.Memo +import io.github.taetae98coding.diary.domain.buddy.repository.BuddyRepository +import io.github.taetae98coding.diary.domain.fcm.repository.FCMRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class UpsertBuddyGroupMemoUseCase internal constructor( + private val buddyRepository: BuddyRepository, + private val fcmRepository: FCMRepository, +) { + public suspend operator fun invoke( + groupId: String, + memo: Memo, + tagIds: Set, + ): Result { + return runCatching { + // TODO Permission Check + + buddyRepository.upsert(groupId, memo, tagIds) + buddyRepository.findBuddyIdByGroupId(groupId).first() + .forEach { + fcmRepository.send(it, "그룹 메모", "${memo.title}가 추가되었습니다.") + } + } + } +} diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt index 2d8709a3..4dfbb0e4 100644 --- a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant public interface MemoRepository { - public suspend fun upsert(list: List) + public suspend fun upsert(list: List, owner: String) public fun findByIds(ids: Set): Flow> diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpsertMemoUseCase.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpsertMemoUseCase.kt index 6f713f1a..6f392197 100644 --- a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpsertMemoUseCase.kt +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpsertMemoUseCase.kt @@ -10,9 +10,8 @@ import org.koin.core.annotation.Factory public class UpsertMemoUseCase internal constructor( private val repository: MemoRepository, ) { - public suspend operator fun invoke(list: List): Result { + public suspend operator fun invoke(list: List, owner: String): Result { return runCatching { - // TODO Permission Check val ids = list.map { it.memo.id }.toSet() val originMap = repository @@ -38,7 +37,7 @@ public class UpsertMemoUseCase internal constructor( ) } - repository.upsert(validList) + repository.upsert(validList, owner) } } } diff --git a/server/feature/buddy/src/main/kotlin/io/github/taetae98coding/diary/feature/buddy/BuddyRouting.kt b/server/feature/buddy/src/main/kotlin/io/github/taetae98coding/diary/feature/buddy/BuddyRouting.kt index c7aec819..707b3102 100644 --- a/server/feature/buddy/src/main/kotlin/io/github/taetae98coding/diary/feature/buddy/BuddyRouting.kt +++ b/server/feature/buddy/src/main/kotlin/io/github/taetae98coding/diary/feature/buddy/BuddyRouting.kt @@ -2,12 +2,17 @@ package io.github.taetae98coding.diary.feature.buddy import io.github.taetae98coding.diary.common.model.buddy.BuddyEntity import io.github.taetae98coding.diary.common.model.buddy.BuddyGroupEntity +import io.github.taetae98coding.diary.common.model.memo.LegacyMemoEntity +import io.github.taetae98coding.diary.common.model.memo.MemoEntity import io.github.taetae98coding.diary.common.model.request.buddy.UpsertBuddyGroupRequest import io.github.taetae98coding.diary.common.model.response.DiaryResponse import io.github.taetae98coding.diary.core.model.BuddyGroup import io.github.taetae98coding.diary.core.model.BuddyGroupAndBuddyIds +import io.github.taetae98coding.diary.core.model.Memo +import io.github.taetae98coding.diary.domain.buddy.usecase.FindBuddyGroupMemoByDate import io.github.taetae98coding.diary.domain.buddy.usecase.FindBuddyGroupUseCase import io.github.taetae98coding.diary.domain.buddy.usecase.FindBuddyUseCase +import io.github.taetae98coding.diary.domain.buddy.usecase.UpsertBuddyGroupMemoUseCase import io.github.taetae98coding.diary.domain.buddy.usecase.UpsertBuddyGroupUseCase import io.ktor.http.HttpStatusCode import io.ktor.server.auth.authenticate @@ -19,87 +24,160 @@ import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.route import kotlinx.coroutines.flow.first +import kotlinx.datetime.LocalDate import org.koin.ktor.plugin.scope public fun Route.buddyRouting() { - route("/buddy") { - authenticate("account") { - get("/find") { - val email = call.parameters["email"] - val uid = call - .principal() - ?.payload - ?.getClaim("uid") - ?.toString() - - val useCase = call.scope.get() - - useCase(email, uid) - .first() - .onSuccess { list -> - list - .map { - BuddyEntity( - uid = it.uid, - email = it.email, - ) - }.also { - call.respond(DiaryResponse.success(it)) - } - }.onFailure { call.respond(DiaryResponse.InternalServerError) } - } - - route("/group") { - get("/find") { - val principal = call.principal() - if (principal == null) { - call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) - return@get - } - - val uid = principal.payload.getClaim("uid").asString() - val useCase = call.scope.get() - - useCase(uid) - .first() - .onSuccess { list -> - list - .map { - BuddyGroupEntity( - id = it.id, - title = it.title, - description = it.description, - ) - }.also { - call.respond(DiaryResponse.success(it)) - } - }.onFailure { call.respond(DiaryResponse.InternalServerError) } - } - - post("/upsert") { request -> - val principal = call.principal() - if (principal == null) { - call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) - return@post - } - - val uid = principal.payload.getClaim("uid").asString() - val buddyGroupAndBuddyIds = BuddyGroupAndBuddyIds( - buddyGroup = BuddyGroup( - id = request.buddyGroup.id, - title = request.buddyGroup.title, - description = request.buddyGroup.description, - ), - buddyIds = request.buddyIds, - ) - - val useCase = call.scope.get() - - useCase(buddyGroupAndBuddyIds, uid) - .onSuccess { call.respond(DiaryResponse.Success) } - .onFailure { call.respond(DiaryResponse.InternalServerError) } - } - } - } - } + route("/buddy") { + authenticate("account") { + get("/find") { + val email = call.parameters["email"] + val uid = call + .principal() + ?.payload + ?.getClaim("uid") + ?.toString() + + val useCase = call.scope.get() + + useCase(email, uid) + .first() + .onSuccess { list -> + list + .map { + BuddyEntity( + uid = it.uid, + email = it.email, + ) + }.also { + call.respond(DiaryResponse.success(it)) + } + }.onFailure { call.respond(DiaryResponse.InternalServerError) } + } + + route("/group") { + get("/find") { + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@get + } + + val uid = principal.payload.getClaim("uid").asString() + val useCase = call.scope.get() + + useCase(uid) + .first() + .onSuccess { list -> + list + .map { + BuddyGroupEntity( + id = it.id, + title = it.title, + description = it.description, + ) + }.also { + call.respond(DiaryResponse.success(it)) + } + }.onFailure { call.respond(DiaryResponse.InternalServerError) } + } + + post("/upsert") { request -> + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@post + } + + val uid = principal.payload.getClaim("uid").asString() + val buddyGroupAndBuddyIds = BuddyGroupAndBuddyIds( + buddyGroup = BuddyGroup( + id = request.buddyGroup.id, + title = request.buddyGroup.title, + description = request.buddyGroup.description, + ), + buddyIds = request.buddyIds, + ) + + val useCase = call.scope.get() + + useCase(buddyGroupAndBuddyIds, uid) + .onSuccess { call.respond(DiaryResponse.Success) } + .onFailure { call.respond(DiaryResponse.InternalServerError) } + } + + post("/{groupId}/upsertMemo") { request -> + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@post + } + + val groupId = call.parameters["groupId"] ?: run { + call.respond(HttpStatusCode.BadRequest, DiaryResponse.BadRequest) + return@post + } + + val useCase = call.scope.get() + + useCase(groupId, request.toMemo(), request.tagIds) + .onSuccess { call.respond(DiaryResponse.Success) } + .onFailure { call.respond(DiaryResponse.InternalServerError) } + } + + get("/{groupId}/findMemoByDateRange") { + val groupId = call.parameters["groupId"] ?: run { + call.respond(HttpStatusCode.BadRequest, DiaryResponse.BadRequest) + return@get + } + val start = call.parameters["start"]?.let(LocalDate::parse) ?: run { + call.respond(HttpStatusCode.BadRequest, DiaryResponse.BadRequest) + return@get + } + val endInclusive = call.parameters["endInclusive"]?.let(LocalDate::parse) ?: run { + call.respond(HttpStatusCode.BadRequest, DiaryResponse.BadRequest) + return@get + } + val useCase = call.scope.get() + + useCase(groupId, start..endInclusive) + .first() + .onSuccess { list -> + val body = list.map { + MemoEntity( + id = it.id, + title = it.title, + description = it.description, + start = it.start, + endInclusive = it.endInclusive, + color = it.color, + primaryTag = it.primaryTag, + isFinish = it.isFinish, + isDelete = it.isDelete, + updateAt = it.updateAt, + ) + } + + call.respond(DiaryResponse.success(body)) + }.onFailure { + call.respond(DiaryResponse.InternalServerError) + } + } + } + } + } } + +private fun LegacyMemoEntity.toMemo(): Memo = + Memo( + id = id, + title = title, + description = description, + start = start, + endInclusive = endInclusive, + color = color, + primaryTag = primaryTag, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + ) diff --git a/server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt b/server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt index 5b34040e..27a925b2 100644 --- a/server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt +++ b/server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt @@ -1,9 +1,9 @@ package io.github.taetae98coding.diary.feature.memo -import io.github.taetae98coding.diary.common.model.memo.MemoEntity +import io.github.taetae98coding.diary.common.model.memo.LegacyMemoEntity import io.github.taetae98coding.diary.common.model.response.DiaryResponse -import io.github.taetae98coding.diary.core.model.Memo import io.github.taetae98coding.diary.core.model.MemoAndTagIds +import io.github.taetae98coding.diary.core.model.Memo import io.github.taetae98coding.diary.domain.memo.usecase.FetchMemoUseCase import io.github.taetae98coding.diary.domain.memo.usecase.UpsertMemoUseCase import io.ktor.http.HttpStatusCode @@ -20,76 +20,77 @@ import kotlinx.datetime.Instant import org.koin.ktor.plugin.scope public fun Route.memoRouting() { - route("/memo") { - authenticate("account") { - post>("/upsert") { request -> - val principal = call.principal() - if (principal == null) { - call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) - return@post - } + route("/memo") { + authenticate("account") { + post>("/upsert") { request -> + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@post + } - val useCase = call.scope.get() - val memoList = - request.map { - MemoAndTagIds( - memo = it.toMemo(), - tagIds = it.tagIds, - ) - } + val useCase = call.scope.get() + val memoList = + request.map { + MemoAndTagIds( + memo = it.toMemo(), + tagIds = it.tagIds, + ) + } - useCase(memoList) - .onSuccess { call.respond(DiaryResponse.Success) } - .onFailure { call.respond(DiaryResponse.InternalServerError) } - } + useCase(memoList, principal.payload.getClaim("uid").asString()) + .onSuccess { call.respond(DiaryResponse.Success) } + .onFailure { call.respond(DiaryResponse.InternalServerError) } + } - get("/fetch") { - val principal = call.principal() - if (principal == null) { - call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) - return@get - } + get("/fetch") { + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@get + } - val uid = principal.payload.getClaim("uid").asString() - val updateAt = call.parameters["updateAt"]?.let { Instant.parse(it) } ?: return@get - val useCase = call.scope.get() + val uid = principal.payload.getClaim("uid").asString() + val updateAt = call.parameters["updateAt"]?.let { Instant.parse(it) } ?: return@get + val useCase = call.scope.get() - useCase(uid, updateAt) - .first() - .onSuccess { call.respond(DiaryResponse.success(it.map(MemoAndTagIds::toEntity))) } - .onFailure { call.respond(DiaryResponse.InternalServerError) } - } - } - } + useCase(uid, updateAt) + .first() + .onSuccess { list -> + call.respond(DiaryResponse.success(list.map { it.toEntity(uid) })) + } + .onFailure { call.respond(DiaryResponse.InternalServerError) } + } + } + } } -private fun MemoEntity.toMemo(): Memo = - Memo( - id = id, - title = title, - description = description, - start = start, - endInclusive = endInclusive, - color = color, - owner = owner, - primaryTag = primaryTag, - isFinish = isFinish, - isDelete = isDelete, - updateAt = updateAt, - ) +private fun LegacyMemoEntity.toMemo(): Memo = + Memo( + id = id, + title = title, + description = description, + start = start, + endInclusive = endInclusive, + color = color, + primaryTag = primaryTag, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + ) -private fun MemoAndTagIds.toEntity(): MemoEntity = - MemoEntity( - id = memo.id, - title = memo.title, - description = memo.description, - start = memo.start, - endInclusive = memo.endInclusive, - color = memo.color, - owner = memo.owner, - primaryTag = memo.primaryTag, - tagIds = tagIds, - isFinish = memo.isFinish, - isDelete = memo.isDelete, - updateAt = memo.updateAt, - ) +private fun MemoAndTagIds.toEntity(owner: String): LegacyMemoEntity = + LegacyMemoEntity( + id = memo.id, + title = memo.title, + description = memo.description, + start = memo.start, + endInclusive = memo.endInclusive, + color = memo.color, + owner = owner, + primaryTag = memo.primaryTag, + tagIds = tagIds, + isFinish = memo.isFinish, + isDelete = memo.isDelete, + updateAt = memo.updateAt, + ) diff --git a/settings.gradle.kts b/settings.gradle.kts index af8352bf..9dcd17f2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,7 @@ pluginManagement { } mavenCentral() gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } includeBuild("build-logic") @@ -24,6 +25,7 @@ dependencyResolutionManagement { } } mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } @@ -32,6 +34,7 @@ rootProject.name = "Diary" include(":app:platform:jvm") include(":app:platform:android") include(":app:platform:ios") +include(":app:platform:macos") include(":app:platform:common") include(":app:core:diary-service")