diff --git a/CHANGELOG.md b/CHANGELOG.md index b7abb5db..95d7cf1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. +## [1.3.2] - 2024-12-18 + +### ๐Ÿš€ Features + +- App - BuddyGroupMemo Detail update, finish, delete +- Android - multi instance + +### ๐Ÿ› Bug Fixes + +- App - MemoDetailRoute Finish, Delete Button +- App - MemoDetail Update +- Server - filter not fcm requester + ## [1.3.1] - 2024-12-10 ### ๐Ÿš€ Features diff --git a/Diary/Diary.xcodeproj/project.pbxproj b/Diary/Diary.xcodeproj/project.pbxproj index b5e4777b..4b128c75 100644 --- a/Diary/Diary.xcodeproj/project.pbxproj +++ b/Diary/Diary.xcodeproj/project.pbxproj @@ -186,7 +186,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd \"$SRCROOT/..\"\n\n./gradlew buildCocoapods\n\ncase \"${CONFIGURATION}\" in\n \"DevDebug\" )\n./gradlew :app:platform:ios:embedAndSignAppleFrameworkForXcode -Pbuildkonfig.flavor=dev ;;\n \"DevRelease\" )\n./gradlew :app:platform:ios:embedAndSignAppleFrameworkForXcode -Pbuildkonfig.flavor=dev ;;\n \"RealDebug\" )\n./gradlew :app:platform:ios:embedAndSignAppleFrameworkForXcode -Pbuildkonfig.flavor=real ;;\n \"RealRelease\" )\n./gradlew :app:platform:ios:embedAndSignAppleFrameworkForXcode -Pbuildkonfig.flavor=real ;;\n*)\n;;\nesac\n"; + shellScript = "cd \"$SRCROOT/..\"\n\ncase \"${CONFIGURATION}\" in\n \"DevDebug\" )\n./gradlew :app:platform:ios:embedAndSignAppleFrameworkForXcode -Pbuildkonfig.flavor=dev ;;\n \"DevRelease\" )\n./gradlew :app:platform:ios:embedAndSignAppleFrameworkForXcode -Pbuildkonfig.flavor=dev ;;\n \"RealDebug\" )\n./gradlew :app:platform:ios:embedAndSignAppleFrameworkForXcode -Pbuildkonfig.flavor=real ;;\n \"RealRelease\" )\n./gradlew :app:platform:ios:embedAndSignAppleFrameworkForXcode -Pbuildkonfig.flavor=real ;;\n*)\n;;\nesac\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -272,10 +272,10 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Diary/Diary.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Diary/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 4TV6L66XZ8; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -292,7 +292,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.3.2; PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary.debug; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -388,7 +388,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.3.2; PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -490,7 +490,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.3.2; PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary.dev.debug; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -564,10 +564,10 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Diary/Diary.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Diary/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 4TV6L66XZ8; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -584,7 +584,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.3.2; PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary.dev; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; 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 973e32c9..2cb863ab 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 @@ -31,7 +31,7 @@ import kotlinx.coroutines.launch public fun CalendarTopBar( state: CalendarState, modifier: Modifier = Modifier, - navigationIcon: @Composable () -> Unit = {}, + navigationIcon: @Composable () -> Unit = {}, actions: @Composable RowScope.() -> Unit = {}, ) { CenterAlignedTopAppBar( @@ -61,7 +61,7 @@ public fun CalendarTopBar( } }, modifier = modifier, - navigationIcon = navigationIcon, + navigationIcon = navigationIcon, actions = actions, ) } 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 index 305ec6a9..5cb8b756 100644 --- 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 @@ -7,16 +7,16 @@ 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 = {}, - ) - } + DiaryTheme { + CalendarScaffold( + state = rememberCalendarScaffoldState( + onFilter = {}, + ), + onSelectDate = {}, + hasFilterProvider = { false }, + textItemListProvider = { emptyList() }, + holidayListProvider = { emptyList() }, + onCalendarItemClick = {}, + ) + } } 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 index b04a842c..038b2f8c 100644 --- 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 @@ -32,93 +32,92 @@ 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 = {}, + state: CalendarScaffoldState, + onSelectDate: (ClosedRange) -> Unit, + hasFilterProvider: () -> Boolean, + textItemListProvider: () -> List, + holidayListProvider: () -> List, + onCalendarItemClick: (Any) -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, ) { - val coroutineScope = rememberCoroutineScope() + val coroutineScope = rememberCoroutineScope() - Scaffold( - modifier = modifier - .onPreviewKeyEvent { - when { - !state.calendarState.isScrollInProgress && it.key == Key.DirectionRight -> { - coroutineScope.launch { state.calendarState.animateScrollToForward() } - true - } + 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 - } + !state.calendarState.isScrollInProgress && it.key == Key.DirectionLeft -> { + coroutineScope.launch { state.calendarState.animateScrollToBackward() } + true + } - it.key == Key.F1 -> { - state.onFilter() - true - } + it.key == Key.F1 -> { + state.onFilter() + true + } - !state.calendarState.isScrollInProgress && it.key == Key.F2 -> { - coroutineScope.launch { state.calendarState.animateScrollToToday() } - 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() - } - } - } + 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()) } + 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, - ), - ) + 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(Unit) { + today = LocalDate.todayIn() + onPauseOrDispose { } + } + } - LifecycleResumeEffect(state.focusRequester) { - state.focusRequester.requestFocus() - 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 index ed0ad537..03987b4c 100644 --- 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 @@ -4,8 +4,8 @@ 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 onFilter: () -> Unit, + public val calendarState: CalendarState, ) { - public val focusRequester: FocusRequester = FocusRequester() + 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 index 86b712ba..48cd35e6 100644 --- 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 @@ -6,14 +6,14 @@ import io.github.taetae98coding.diary.core.calendar.compose.state.rememberCalend @Composable public fun rememberCalendarScaffoldState( - onFilter: () -> Unit, + onFilter: () -> Unit, ): CalendarScaffoldState { - val calendarState = rememberCalendarState() + val calendarState = rememberCalendarState() - return remember { - CalendarScaffoldState( - onFilter = onFilter, - calendarState = calendarState, - ) - } + return remember { + CalendarScaffoldState( + onFilter = onFilter, + calendarState = calendarState, + ) + } } diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/error/NetworkError.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/error/NetworkError.kt new file mode 100644 index 00000000..891a9d2c --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/error/NetworkError.kt @@ -0,0 +1,35 @@ +package io.github.taetae98coding.diary.core.compose.error + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.core.design.system.emoji.Emoji +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +@Composable +public fun NetworkError( + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + val emoji = remember { Emoji.check.random() } + + Text( + text = "๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š” $emoji", + style = DiaryTheme.typography.headlineMedium, + ) + TextButton(onClick = onRetry) { + Text(text = "์ƒˆ๋กœ๊ณ ์นจ") + } + } + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/error/UnknownError.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/error/UnknownError.kt new file mode 100644 index 00000000..ef0d9daf --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/error/UnknownError.kt @@ -0,0 +1,35 @@ +package io.github.taetae98coding.diary.core.compose.error + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.core.design.system.emoji.Emoji +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +@Composable +public fun UnknownError( + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + val emoji = remember { Emoji.check.random() } + + Text( + text = "์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” $emoji", + style = DiaryTheme.typography.headlineMedium, + ) + TextButton(onClick = onRetry) { + Text(text = "์ƒˆ๋กœ๊ณ ์นจ") + } + } + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/MemoDetailMultiPaneScaffold.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/MemoDetailMultiPaneScaffold.kt new file mode 100644 index 00000000..86aff5ee --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/MemoDetailMultiPaneScaffold.kt @@ -0,0 +1,96 @@ +package io.github.taetae98coding.diary.core.compose.memo + +import androidx.compose.foundation.layout.RowScope +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.ThreePaneScaffoldNavigator +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.card.TagCardItemUiState +import io.github.taetae98coding.diary.core.design.system.icon.NavigateUpIcon + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +public fun MemoDetailMultiPaneScaffold( + onNavigateUp: () -> Unit, + onDetailTag: (String) -> Unit, + detailScaffoldStateProvider: () -> MemoDetailScaffoldState, + detailUiStateProvider: () -> MemoDetailScaffoldUiState, + detailTagListProvider: () -> List?, + detailTitle: @Composable () -> Unit, + onTagAdd: () -> Unit, + tagListProvider: () -> List?, + modifier: Modifier = Modifier, + detailActions: @Composable RowScope.() -> Unit = {}, + 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() + } + }, + actions = detailActions, + floatingActionButton = detailFloatingActionButton, + ) + } + }, + detailPane = { + AnimatedPane { + MemoTagScaffold( + onTagAdd = onTagAdd, + navigationIcon = { + if (!navigator.isListVisible()) { + IconButton(onClick = navigator::navigateBack) { + NavigateUpIcon() + } + } + }, + primaryTagListProvider = detailTagListProvider, + tagListProvider = tagListProvider, + ) + } + }, + modifier = modifier, + ) + + NavigateUp(navigator = navigator) +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +private fun NavigateUp( + navigator: ThreePaneScaffoldNavigator<*>, +) { + KBackHandler( + isEnabled = navigator.canNavigateBack(), + onBack = navigator::navigateBack, + ) +} 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 deleted file mode 100644 index fbbb7b11..00000000 --- a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/MemoListDetailPaneScaffold.kt +++ /dev/null @@ -1,84 +0,0 @@ -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 index 120d77c9..14243514 100644 --- 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 @@ -11,24 +11,24 @@ import kotlinx.datetime.LocalDate @Composable public fun rememberMemoDetailScaffoldAddState( - initialStart: LocalDate?, - initialEndInclusive: LocalDate?, + initialStart: LocalDate?, + initialEndInclusive: LocalDate?, ): MemoDetailScaffoldState.Add { - val coroutineScope = rememberCoroutineScope() - val componentState = rememberDiaryComponentState() - val dateState = - rememberDiaryDateState( - initialStart = initialStart, - initialEndInclusive = initialEndInclusive, - ) - val colorState = rememberDiaryColorState() + 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, - ) - } + 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 index 920b55f5..b4e039d6 100644 --- 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 @@ -24,9 +24,9 @@ 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.compose.tag.card.PrimaryTagCardItem +import io.github.taetae98coding.diary.core.compose.tag.card.TagCardFlow +import io.github.taetae98coding.diary.core.compose.tag.card.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 @@ -37,181 +37,186 @@ 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 = {}, + 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) + 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, + 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() - } + val uiState = uiStateProvider() + + LaunchedEffect( + uiState.isAdd, + uiState.isDelete, + uiState.isUpdate, + uiState.isTitleBlankError, + uiState.isNetworkError, + uiState.isUnknownError, + ) { + when { + uiState.isAdd -> { + state.showMessage("๋ฉ”๋ชจ ์ถ”๊ฐ€ ${Emoji.congratulate.random()}") + state.clearInput() + state.requestTitleFocus() + } + + uiState.isDelete -> { + if (state is MemoDetailScaffoldState.Detail) { + state.onDelete() + } + } + + uiState.isUpdate -> { + if (state is MemoDetailScaffoldState.Detail) { + state.onUpdate() + } + } + + uiState.isTitleBlankError -> { + state.showMessage("์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ${Emoji.check.random()}") + state.titleError() + } + + uiState.isNetworkError -> { + state.showMessage("๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š” ${Emoji.error.random()}") + } + + uiState.isUnknownError -> state.showMessage("์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š” ${Emoji.error.random()}") + } + + uiState.onMessageShow() + } } @Composable private fun LaunchedFocus( - state: MemoDetailScaffoldState, + state: MemoDetailScaffoldState, ) { - LaunchedEffect(state) { - if (state is MemoDetailScaffoldState.Add) { - state.requestTitleFocus() - } - } + 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, + 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) - } + 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, + 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), - ) + 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, + state: MemoDetailScaffoldState, + modifier: Modifier = Modifier, ) { - Row(modifier = modifier) { - DiaryColor( - state = state.colorState, - modifier = Modifier - .weight(1F) - .height(100.dp), - ) - - Spacer(modifier = Modifier.weight(1F)) - } + 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 index e3410775..b9290e4d 100644 --- 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 @@ -11,58 +11,58 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch public sealed class MemoDetailScaffoldState { - protected abstract val coroutineScope: CoroutineScope + protected abstract val coroutineScope: CoroutineScope - internal abstract val componentState: DiaryComponentState - internal abstract val dateState: DiaryDateState - internal abstract val colorState: DiaryColorState + internal abstract val componentState: DiaryComponentState + internal abstract val dateState: DiaryDateState + internal abstract val colorState: DiaryColorState - private var messageJob: Job? = null + private var messageJob: Job? = null - internal val hostState: SnackbarHostState = SnackbarHostState() + 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 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 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(), - ) - } + 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 requestTitleFocus() { + componentState.requestTitleFocus() + } - internal fun titleError() { - requestTitleFocus() - componentState.titleError() - } + internal fun titleError() { + requestTitleFocus() + componentState.titleError() + } - internal fun showMessage(message: String) { - messageJob?.cancel() - messageJob = coroutineScope.launch { hostState.showSnackbar(message) } - } + internal fun showMessage(message: String) { + messageJob?.cancel() + messageJob = coroutineScope.launch { hostState.showSnackbar(message) } + } - internal fun clearInput() { - componentState.clearInput() - } + 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 index 1a24cce7..ae6f645b 100644 --- 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 @@ -1,11 +1,12 @@ 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 = {}, + val isProgress: Boolean = false, + val isAdd: Boolean = false, + val isDelete: Boolean = false, + val isUpdate: Boolean = false, + val isTitleBlankError: Boolean = false, + val isNetworkError: 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 index 7168af55..33836b62 100644 --- 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 @@ -11,36 +11,36 @@ import io.github.taetae98coding.diary.core.model.memo.MemoDetail @Composable public fun rememberMemoDetailScaffoldDetailState( - onDelete: () -> Unit, - onUpdate: () -> Unit, - detailProvider: () -> MemoDetail?, + onDelete: () -> Unit, + onUpdate: () -> Unit, + detailProvider: () -> MemoDetail?, ): MemoDetailScaffoldState.Detail { - val detail = detailProvider() + 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, - ) + 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, - ) - } + 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/list/MemoListItem.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/list/MemoListItem.kt new file mode 100644 index 00000000..9d4f02bb --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/list/MemoListItem.kt @@ -0,0 +1,51 @@ +package io.github.taetae98coding.diary.core.compose.memo.list + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.compose.swipe.FinishAndDeleteSwipeBox +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.library.color.multiplyAlpha + +@Composable +public fun MemoListItem( + 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, + ) + } + } + } + } +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/MemoListItemUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/list/MemoListItemUiState.kt similarity index 72% rename from app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/MemoListItemUiState.kt rename to app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/list/MemoListItemUiState.kt index 49db7deb..4b07a215 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/MemoListItemUiState.kt +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/memo/list/MemoListItemUiState.kt @@ -1,9 +1,9 @@ -package io.github.taetae98coding.diary.feature.tag.memo +package io.github.taetae98coding.diary.core.compose.memo.list import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty import kotlinx.datetime.LocalDate -internal data class MemoListItemUiState( +public data class MemoListItemUiState( val id: String, val title: String, val dateRange: ClosedRange?, 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 index 5e6789b3..cfd337e2 100644 --- 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 @@ -13,92 +13,93 @@ 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.compose.tag.card.PrimaryTagCardItem +import io.github.taetae98coding.diary.core.compose.tag.card.TagCardFlow +import io.github.taetae98coding.diary.core.compose.tag.card.TagCardItem +import io.github.taetae98coding.diary.core.compose.tag.card.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, + 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) - ) - } + 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, + 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) - }, - ) + 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) }, - ) - } + 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 index 1ea7008e..541b4b2c 100644 --- 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 @@ -1,6 +1,5 @@ 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 @@ -15,41 +14,41 @@ 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, + 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 - } + 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() - } - }, - ) - } + 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 index 8390e9f8..4f0b49a8 100644 --- 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 @@ -15,21 +15,21 @@ 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 + 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 - ) + 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 deleted file mode 100644 index 04b36980..00000000 --- a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/PrimaryTagCardItem.kt +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index b1d298e9..00000000 --- a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardFlow.kt +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index c0296126..00000000 --- a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardItem.kt +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index c91acb84..00000000 --- a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagCardItemUiState.kt +++ /dev/null @@ -1,12 +0,0 @@ -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/tag/TagListDetailNavigate.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagListDetailNavigate.kt new file mode 100644 index 00000000..308d0b56 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagListDetailNavigate.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.core.compose.tag + +internal sealed class TagListDetailNavigate { + data object None : TagListDetailNavigate() + + data object Add : TagListDetailNavigate() + + data class Tag( + val tagId: String, + ) : TagListDetailNavigate() +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagListMultiPaneScaffold.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagListMultiPaneScaffold.kt new file mode 100644 index 00000000..9308a415 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/TagListMultiPaneScaffold.kt @@ -0,0 +1,242 @@ +package io.github.taetae98coding.diary.core.compose.tag + +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +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.back.KBackHandler +import io.github.taetae98coding.diary.core.compose.tag.add.rememberTagDetailScaffoldAddState +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffold +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldActions +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldActionsUiState +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldFloatingButton +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldNavigationIcon +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldState +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldUiState +import io.github.taetae98coding.diary.core.compose.tag.detail.rememberTagDetailScaffoldDetailState +import io.github.taetae98coding.diary.core.compose.tag.list.TagListScaffold +import io.github.taetae98coding.diary.core.compose.tag.list.TagListScaffoldFloatingButton +import io.github.taetae98coding.diary.core.compose.tag.list.TagListScaffoldUiState +import io.github.taetae98coding.diary.core.compose.tag.list.TagListUiState +import io.github.taetae98coding.diary.core.compose.tag.list.rememberTagListScaffoldState +import io.github.taetae98coding.diary.core.compose.tag.memo.TagMemoListUiState +import io.github.taetae98coding.diary.core.compose.tag.memo.TagMemoNavigateIcon +import io.github.taetae98coding.diary.core.compose.tag.memo.TagMemoScaffold +import io.github.taetae98coding.diary.core.compose.tag.memo.TagMemoScaffoldUiState +import io.github.taetae98coding.diary.core.compose.tag.memo.rememberTagMemoScaffoldState +import io.github.taetae98coding.diary.core.model.tag.TagDetail + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +public fun TagListMultiPaneScaffold( + navigateToMemoAdd: (String) -> Unit, + navigateToMemoDetail: (String) -> Unit, + navigator: ThreePaneScaffoldNavigator, + listScaffoldUiStateProvider: () -> TagListScaffoldUiState, + listUiStateProvider: () -> TagListUiState, + addUiStateProvider: () -> TagDetailScaffoldUiState.Add, + detailProvider: () -> TagDetail?, + detailUiStateProvider: () -> TagDetailScaffoldUiState.Detail, + detailActionsUiStateProvider: () -> TagDetailScaffoldActionsUiState, + tagMemoListUiStateProvider: () -> TagMemoListUiState, + tagMemoUiStateProvider: () -> TagMemoScaffoldUiState, + modifier: Modifier = Modifier, +) { + var tagListDetailNavigate by remember { mutableStateOf(TagListDetailNavigate.None) } + + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + TagListScaffold( + state = rememberTagListScaffoldState( + navigateToTag = { + if (navigator.isDetailVisible() && navigator.currentDestination?.content != null) { + tagListDetailNavigate = TagListDetailNavigate.Tag(it) + } else { + navigator.navigateTo(ThreePaneScaffoldRole.Primary, it) + } + }, + ), + uiStateProvider = listScaffoldUiStateProvider, + listUiStateProvider = listUiStateProvider, + floatingButtonProvider = { + val isVisible = if (navigator.isDetailVisible()) { + navigator.currentDestination?.content != null + } else { + true + } + + if (isVisible) { + TagListScaffoldFloatingButton.Add { + if (navigator.isDetailVisible() && navigator.currentDestination?.content != null) { + tagListDetailNavigate = TagListDetailNavigate.Add + } else { + navigator.navigateTo(ThreePaneScaffoldRole.Primary) + } + } + } else { + TagListScaffoldFloatingButton.None + } + }, + ) + } + }, + detailPane = { + AnimatedPane { + val state = if (navigator.currentDestination?.content == null) { + rememberTagDetailScaffoldAddState() + } else { + rememberTagDetailScaffoldDetailState( + onUpdate = { + when (val navigate = tagListDetailNavigate) { + is TagListDetailNavigate.Add -> { + navigator.navigateTo(ThreePaneScaffoldRole.Primary) + tagListDetailNavigate = TagListDetailNavigate.None + } + + is TagListDetailNavigate.Tag -> { + navigator.navigateTo(ThreePaneScaffoldRole.Primary, navigate.tagId) + tagListDetailNavigate = TagListDetailNavigate.None + } + + is TagListDetailNavigate.None -> { + navigator.navigateBack() + } + } + }, + onDelete = navigator::navigateBack, + navigateToMemo = { navigator.currentDestination?.content?.let { navigator.navigateTo(ThreePaneScaffoldRole.Tertiary, it) } }, + detailProvider = detailProvider, + ) + } + + TagDetailScaffold( + state = state, + uiStateProvider = { + if (navigator.currentDestination?.content == null) { + addUiStateProvider() + } else { + detailUiStateProvider() + } + }, + titleProvider = { + if (navigator.currentDestination?.content == null) { + "ํƒœ๊ทธ ์ถ”๊ฐ€" + } else { + detailProvider()?.title + } + }, + navigationIconProvider = { + if (!navigator.isListVisible()) { + TagDetailScaffoldNavigationIcon.NavigateUp { + if (navigator.currentDestination?.content == null) { + navigator.navigateBack() + } else { + detailUiStateProvider().update(state.tagDetail) + } + } + } else { + TagDetailScaffoldNavigationIcon.None + } + }, + actionsProvider = { + if (navigator.currentDestination?.content == null) { + TagDetailScaffoldActions.None + } else { + val actionsUiState = detailActionsUiStateProvider() + + TagDetailScaffoldActions.FinishAndDelete( + isFinish = actionsUiState.isFinish, + finish = actionsUiState.finish, + restart = actionsUiState.restart, + delete = actionsUiState.delete, + ) + } + }, + floatingButtonProvider = { + if (navigator.currentDestination?.content == null) { + val uiState = addUiStateProvider() + + TagDetailScaffoldFloatingButton.Add( + add = { uiState.add(state.tagDetail) }, + isInProgress = uiState.isAddInProgress, + ) + } else { + TagDetailScaffoldFloatingButton.None + } + }, + ) + + HandleNavigate( + state = state, + navigateProvider = { tagListDetailNavigate }, + detailUiStateProvider = detailUiStateProvider, + ) + } + }, + modifier = modifier, + extraPane = { + AnimatedPane { + TagMemoScaffold( + state = rememberTagMemoScaffoldState(), + uiStateProvider = tagMemoUiStateProvider, + listUiStateProvider = tagMemoListUiStateProvider, + onAdd = { navigator.currentDestination?.content?.let(navigateToMemoAdd) }, + onMemo = navigateToMemoDetail, + navigateIconProvider = { + if (navigator.isDetailVisible()) { + TagMemoNavigateIcon.None + } else { + TagMemoNavigateIcon.NavigateUp(navigator::navigateBack) + } + }, + ) + } + }, + ) + + NavigateUp(navigator = navigator) +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +private fun NavigateUp( + navigator: ThreePaneScaffoldNavigator<*>, +) { + KBackHandler( + isEnabled = navigator.canNavigateBack(), + onBack = navigator::navigateBack, + ) +} + +@Composable +private fun HandleNavigate( + state: TagDetailScaffoldState, + navigateProvider: () -> TagListDetailNavigate, + detailUiStateProvider: () -> TagDetailScaffoldUiState.Detail, +) { + val navigate = navigateProvider() + + LaunchedEffect(navigate) { + when (navigate) { + is TagListDetailNavigate.Add, is TagListDetailNavigate.Tag -> { + detailUiStateProvider().update(state.tagDetail) + } + + is TagListDetailNavigate.None -> Unit + } + } +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/RememberTagDetailScreenAddState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/add/RememberTagDetailScaffoldAddState.kt similarity index 66% rename from app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/RememberTagDetailScreenAddState.kt rename to app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/add/RememberTagDetailScaffoldAddState.kt index 4e9213bb..4cbefc1a 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/RememberTagDetailScreenAddState.kt +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/add/RememberTagDetailScaffoldAddState.kt @@ -1,20 +1,20 @@ -package io.github.taetae98coding.diary.feature.tag.add +package io.github.taetae98coding.diary.core.compose.tag.add import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldState 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.feature.tag.detail.TagDetailScreenState @Composable -internal fun rememberTagDetailScreenAddState(): TagDetailScreenState.Add { +public fun rememberTagDetailScaffoldAddState(): TagDetailScaffoldState.Add { val coroutineScope = rememberCoroutineScope() val componentState = rememberDiaryComponentState() val colorState = rememberDiaryColorState() - return remember { - TagDetailScreenState.Add( + return remember(componentState, colorState) { + TagDetailScaffoldState.Add( coroutineScope = coroutineScope, componentState = componentState, colorState = colorState, diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/card/PrimaryTagCardItem.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/card/PrimaryTagCardItem.kt new file mode 100644 index 00000000..858afe6b --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/card/PrimaryTagCardItem.kt @@ -0,0 +1,43 @@ +package io.github.taetae98coding.diary.core.compose.tag.card + +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/card/TagCardFlow.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/card/TagCardFlow.kt new file mode 100644 index 00000000..9d7c74df --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/card/TagCardFlow.kt @@ -0,0 +1,70 @@ +package io.github.taetae98coding.diary.core.compose.tag.card + +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/card/TagCardItem.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/card/TagCardItem.kt new file mode 100644 index 00000000..2c9140b2 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/card/TagCardItem.kt @@ -0,0 +1,48 @@ +package io.github.taetae98coding.diary.core.compose.tag.card + +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/card/TagCardItemUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/card/TagCardItemUiState.kt new file mode 100644 index 00000000..e5a37e63 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/card/TagCardItemUiState.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.core.compose.tag.card + +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/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/RememberTagDetailScreenDetailState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/RememberTagDetailScaffoldState.kt similarity index 82% rename from app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/RememberTagDetailScreenDetailState.kt rename to app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/RememberTagDetailScaffoldState.kt index 2b124ee1..19396bbe 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/RememberTagDetailScreenDetailState.kt +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/RememberTagDetailScaffoldState.kt @@ -1,4 +1,4 @@ -package io.github.taetae98coding.diary.feature.tag.detail +package io.github.taetae98coding.diary.core.compose.tag.detail import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -9,12 +9,12 @@ import io.github.taetae98coding.diary.core.design.system.diary.component.remembe import io.github.taetae98coding.diary.core.model.tag.TagDetail @Composable -internal fun rememberTagDetailScreenDetailState( +public fun rememberTagDetailScaffoldDetailState( onUpdate: () -> Unit, onDelete: () -> Unit, - onMemo: () -> Unit, + navigateToMemo: () -> Unit, detailProvider: () -> TagDetail?, -): TagDetailScreenState.Detail { +): TagDetailScaffoldState.Detail { val detail = detailProvider() val coroutineScope = rememberCoroutineScope() val componentState = @@ -30,10 +30,10 @@ internal fun rememberTagDetailScreenDetailState( ) return remember(componentState, colorState) { - TagDetailScreenState.Detail( + TagDetailScaffoldState.Detail( onUpdate = onUpdate, onDelete = onDelete, - onMemo = onMemo, + navigateToMemo = navigateToMemo, coroutineScope = coroutineScope, componentState = componentState, colorState = colorState, diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailActionsUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailActionsUiState.kt new file mode 100644 index 00000000..1ef196c7 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailActionsUiState.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.core.compose.tag.detail + +public data class TagDetailActionsUiState( + val isFinish: Boolean, + val finish: () -> Unit, + val restart: () -> Unit, + val delete: () -> Unit, +) diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreen.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffold.kt similarity index 50% rename from app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreen.kt rename to app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffold.kt index eb18f41b..3d35182f 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreen.kt +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffold.kt @@ -1,14 +1,10 @@ -package io.github.taetae98coding.diary.feature.tag.detail +package io.github.taetae98coding.diary.core.compose.tag.detail -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -24,9 +20,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 @@ -41,14 +34,14 @@ import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun TagDetailScreen( - state: TagDetailScreenState, - titleProvider: () -> String?, - navigateButtonProvider: () -> TagDetailNavigationButton, - actionButtonProvider: () -> TagDetailActionButton, - floatingButtonProvider: () -> TagDetailFloatingButton, - uiStateProvider: () -> TagDetailScreenUiState, +public fun TagDetailScaffold( + state: TagDetailScaffoldState, + uiStateProvider: () -> TagDetailScaffoldUiState, modifier: Modifier = Modifier, + titleProvider: () -> String? = { null }, + navigationIconProvider: () -> TagDetailScaffoldNavigationIcon = { TagDetailScaffoldNavigationIcon.None }, + actionsProvider: () -> TagDetailScaffoldActions = { TagDetailScaffoldActions.None }, + floatingButtonProvider: () -> TagDetailScaffoldFloatingButton = { TagDetailScaffoldFloatingButton.None }, ) { Scaffold( modifier = modifier, @@ -56,79 +49,53 @@ internal fun TagDetailScreen( TopAppBar( title = { titleProvider()?.let { Text(text = it) } }, navigationIcon = { - when (val button = navigateButtonProvider()) { - is TagDetailNavigationButton.NavigateUp -> { - IconButton(onClick = button.onNavigateUp) { + when (val navigationIcon = navigationIconProvider()) { + is TagDetailScaffoldNavigationIcon.NavigateUp -> { + IconButton(onClick = navigationIcon.navigateUp) { NavigateUpIcon() } } - is TagDetailNavigationButton.None -> Unit + is TagDetailScaffoldNavigationIcon.None -> Unit } }, actions = { - val button = actionButtonProvider() + when (val actions = actionsProvider()) { + is TagDetailScaffoldActions.FinishAndDelete -> { + IconToggleButton( + checked = actions.isFinish, + onCheckedChange = { isFinish -> + if (isFinish) { + actions.finish() + } else { + actions.restart() + } + }, + ) { + FinishIcon() + } - AnimatedVisibility( - visible = button is TagDetailActionButton.FinishAndDetail, - enter = fadeIn(), - exit = fadeOut(), - ) { - val isFinish = if (button is TagDetailActionButton.FinishAndDetail) { - button.isFinish - } else { - false + IconButton(onClick = actions.delete) { + DeleteIcon() + } } - IconToggleButton( - checked = isFinish, - onCheckedChange = { - if (button is TagDetailActionButton.FinishAndDetail) { - button.onFinishChange(it) - } - }, - ) { - FinishIcon() - } - } - - AnimatedVisibility( - visible = button is TagDetailActionButton.FinishAndDetail, - enter = fadeIn(), - exit = fadeOut(), - ) { - IconButton( - onClick = { - if (button is TagDetailActionButton.FinishAndDetail) { - button.delete() - } - }, - ) { - DeleteIcon() - } + is TagDetailScaffoldActions.None -> Unit } }, ) }, snackbarHost = { SnackbarHost(hostState = state.hostState) }, floatingActionButton = { - val button = floatingButtonProvider() - - AnimatedVisibility( - visible = button is TagDetailFloatingButton.Add, - enter = scaleIn(), - exit = scaleOut(), - ) { - val isProgress by remember { derivedStateOf { uiStateProvider().isProgress } } + when (val floatingButton = floatingButtonProvider()) { + is TagDetailScaffoldFloatingButton.Add -> { + FloatingAddButton( + onClick = floatingButton.add, + progressProvider = { floatingButton.isInProgress }, + ) + } - FloatingAddButton( - onClick = { - if (button is TagDetailFloatingButton.Add) { - button.onAdd() - } - }, - progressProvider = { isProgress }, - ) + is TagDetailScaffoldFloatingButton.None -> Unit } }, ) { @@ -141,76 +108,80 @@ internal fun TagDetailScreen( ) } - Message( + HandleUiState( state = state, uiStateProvider = uiStateProvider, ) - - LaunchedFocus(state = state) } @Composable -private fun Message( - state: TagDetailScreenState, - uiStateProvider: () -> TagDetailScreenUiState, +private fun HandleUiState( + state: TagDetailScaffoldState, + uiStateProvider: () -> TagDetailScaffoldUiState, ) { 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 TagDetailScreenState.Detail) { - state.onDelete() - } - } - - uiState.isUpdate -> { - if (state is TagDetailScreenState.Detail) { - state.onUpdate() - } - } - uiState.isTitleBlankError -> { state.showMessage("์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š” ${Emoji.check.random()}") state.requestTitleFocus() state.titleError() + uiState.clearState() } - uiState.isUnknownError -> state.showMessage("์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š” ${Emoji.error.random()}") + uiState.isUnknownError -> { + state.showMessage("์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š” ${Emoji.error.random()}") + uiState.clearState() + } } + } - uiState.onMessageShow() + if (uiState is TagDetailScaffoldUiState.Add) { + LaunchedEffect( + uiState.isAddFinish, + ) { + when { + uiState.isAddFinish -> { + state.showMessage("ํƒœ๊ทธ ์ถ”๊ฐ€ ${Emoji.congratulate.random()}") + state.clearInput() + state.requestTitleFocus() + uiState.clearState() + } + } + } } -} -@Composable -private fun LaunchedFocus( - state: TagDetailScreenState, -) { - LaunchedEffect(state) { - if (state is TagDetailScreenState.Add) { - state.requestTitleFocus() + if (uiState is TagDetailScaffoldUiState.Detail) { + LaunchedEffect( + uiState.isUpdateFinish, + uiState.isDeleteFinish, + ) { + when { + uiState.isUpdateFinish -> { + if (state is TagDetailScaffoldState.Detail) { + state.onUpdate() + } + uiState.clearState() + } + + uiState.isDeleteFinish -> { + if (state is TagDetailScaffoldState.Detail) { + state.onDelete() + } + uiState.clearState() + } + } } } } @Composable private fun Content( - state: TagDetailScreenState, + state: TagDetailScaffoldState, modifier: Modifier = Modifier, ) { Column( @@ -228,9 +199,9 @@ private fun Content( .height(100.dp), ) - if (state is TagDetailScreenState.Detail) { + if (state is TagDetailScaffoldState.Detail) { Card( - onClick = state.onMemo, + onClick = state.navigateToMemo, modifier = Modifier .weight(1F) .height(100.dp), @@ -242,6 +213,8 @@ private fun Content( Text(text = "๋ฉ”๋ชจ") } } + } else { + Spacer(modifier = Modifier.weight(1F)) } } } diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldActions.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldActions.kt new file mode 100644 index 00000000..a989c025 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldActions.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.core.compose.tag.detail + +public sealed class TagDetailScaffoldActions { + public data object None : TagDetailScaffoldActions() + + public data class FinishAndDelete( + val isFinish: Boolean = false, + val finish: () -> Unit = {}, + val restart: () -> Unit = {}, + val delete: () -> Unit = {}, + ) : TagDetailScaffoldActions() +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldActionsUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldActionsUiState.kt new file mode 100644 index 00000000..a6bc52c9 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldActionsUiState.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.core.compose.tag.detail + +public data class TagDetailScaffoldActionsUiState( + val isFinish: Boolean = false, + val finish: () -> Unit = {}, + val restart: () -> Unit = {}, + val delete: () -> Unit = {}, +) diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldFloatingButton.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldFloatingButton.kt new file mode 100644 index 00000000..a14233cc --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldFloatingButton.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.core.compose.tag.detail + +public sealed class TagDetailScaffoldFloatingButton { + public data object None : TagDetailScaffoldFloatingButton() + + public data class Add( + val isInProgress: Boolean, + val add: () -> Unit, + ) : TagDetailScaffoldFloatingButton() +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldNavigationIcon.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldNavigationIcon.kt new file mode 100644 index 00000000..f2a3be5b --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldNavigationIcon.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.core.compose.tag.detail + +public sealed class TagDetailScaffoldNavigationIcon { + public data object None : TagDetailScaffoldNavigationIcon() + + public data class NavigateUp( + val navigateUp: () -> Unit, + ) : TagDetailScaffoldNavigationIcon() +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldState.kt similarity index 67% rename from app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenState.kt rename to app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldState.kt index fcc62c27..2ada61de 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenState.kt +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldState.kt @@ -1,4 +1,4 @@ -package io.github.taetae98coding.diary.feature.tag.detail +package io.github.taetae98coding.diary.core.compose.tag.detail import androidx.compose.material3.SnackbarHostState import androidx.compose.ui.graphics.toArgb @@ -9,32 +9,32 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -internal sealed class TagDetailScreenState { +public sealed class TagDetailScaffoldState { protected abstract val coroutineScope: CoroutineScope - abstract val componentState: DiaryComponentState - abstract val colorState: DiaryColorState + internal abstract val componentState: DiaryComponentState + internal abstract val colorState: DiaryColorState private var messageJob: Job? = null - val hostState: SnackbarHostState = SnackbarHostState() + internal val hostState: SnackbarHostState = SnackbarHostState() - data class Add( + public data class Add( override val coroutineScope: CoroutineScope, override val componentState: DiaryComponentState, override val colorState: DiaryColorState, - ) : TagDetailScreenState() + ) : TagDetailScaffoldState() - data class Detail( + public data class Detail( val onUpdate: () -> Unit, val onDelete: () -> Unit, - val onMemo: () -> Unit, + val navigateToMemo: () -> Unit, override val coroutineScope: CoroutineScope, override val componentState: DiaryComponentState, override val colorState: DiaryColorState, - ) : TagDetailScreenState() + ) : TagDetailScaffoldState() - val tagDetail: TagDetail + public val tagDetail: TagDetail get() { return TagDetail( title = componentState.title, @@ -43,19 +43,19 @@ internal sealed class TagDetailScreenState { ) } - fun requestTitleFocus() { + internal fun requestTitleFocus() { componentState.requestTitleFocus() } - fun clearInput() { + internal fun clearInput() { componentState.clearInput() } - fun titleError() { + internal fun titleError() { componentState.titleError() } - fun showMessage(message: String) { + internal fun showMessage(message: String) { messageJob?.cancel() messageJob = coroutineScope.launch { hostState.showSnackbar(message) } } diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldUiState.kt new file mode 100644 index 00000000..c1e0149b --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/detail/TagDetailScaffoldUiState.kt @@ -0,0 +1,27 @@ +package io.github.taetae98coding.diary.core.compose.tag.detail + +import io.github.taetae98coding.diary.core.model.tag.TagDetail + +public sealed class TagDetailScaffoldUiState { + public abstract val isTitleBlankError: Boolean + public abstract val isUnknownError: Boolean + public abstract val clearState: () -> Unit + + public data class Add( + val isAddInProgress: Boolean = false, + val isAddFinish: Boolean = false, + override val isTitleBlankError: Boolean = false, + override val isUnknownError: Boolean = false, + val add: (TagDetail) -> Unit = {}, + override val clearState: () -> Unit, + ) : TagDetailScaffoldUiState() + + public data class Detail( + val isUpdateFinish: Boolean = false, + val isDeleteFinish: Boolean = false, + override val isTitleBlankError: Boolean = false, + override val isUnknownError: Boolean = false, + val update: (TagDetail) -> Unit = {}, + override val clearState: () -> Unit, + ) : TagDetailScaffoldUiState() +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/RememberTagListScaffoldState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/RememberTagListScaffoldState.kt new file mode 100644 index 00000000..b5155f5c --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/RememberTagListScaffoldState.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.core.compose.tag.list + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope + +@Composable +internal fun rememberTagListScaffoldState( + navigateToTag: (String) -> Unit, +): TagListScaffoldState { + val coroutineScope = rememberCoroutineScope() + + return remember { + TagListScaffoldState( + coroutineScope = coroutineScope, + navigateToTag = navigateToTag, + ) + } +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListItemUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListItemUiState.kt similarity index 65% rename from app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListItemUiState.kt rename to app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListItemUiState.kt index 37d79eea..6b181d84 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListItemUiState.kt +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListItemUiState.kt @@ -1,8 +1,8 @@ -package io.github.taetae98coding.diary.feature.tag.list +package io.github.taetae98coding.diary.core.compose.tag.list import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty -internal data class TagListItemUiState( +public data class TagListItemUiState( val id: String, val title: String, val finish: SkipProperty<() -> Unit>, diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffold.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffold.kt new file mode 100644 index 00000000..625e51da --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffold.kt @@ -0,0 +1,223 @@ +package io.github.taetae98coding.diary.core.compose.tag.list + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.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.compose.error.NetworkError +import io.github.taetae98coding.diary.core.compose.error.UnknownError +import io.github.taetae98coding.diary.core.compose.swipe.FinishAndDeleteSwipeBox +import io.github.taetae98coding.diary.core.design.system.emoji.Emoji +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +public fun TagListScaffold( + state: TagListScaffoldState, + uiStateProvider: () -> TagListScaffoldUiState, + listUiStateProvider: () -> TagListUiState, + modifier: Modifier = Modifier, + floatingButtonProvider: () -> TagListScaffoldFloatingButton = { TagListScaffoldFloatingButton.None }, +) { + Scaffold( + modifier = modifier, + topBar = { TopAppBar(title = { Text(text = "ํƒœ๊ทธ") }) }, + snackbarHost = { SnackbarHost(hostState = state.hostState) }, + floatingActionButton = { + when (val floatingButton = floatingButtonProvider()) { + is TagListScaffoldFloatingButton.Add -> { + FloatingAddButton(onClick = floatingButton.add) + } + + is TagListScaffoldFloatingButton.None -> Unit + } + }, + ) { + Content( + state = state, + listUiStateProvider = listUiStateProvider, + modifier = Modifier + .fillMaxSize() + .padding(it), + ) + } + + HandleUiState( + state = state, + uiStateProvider = uiStateProvider, + ) +} + +@Composable +private fun HandleUiState( + state: TagListScaffoldState, + uiStateProvider: () -> TagListScaffoldUiState, +) { + val uiState = uiStateProvider() + + LaunchedEffect( + uiState.isTagFinish, + uiState.isTagDelete, + uiState.isNetworkError, + uiState.isUnknownError, + ) { + when { + uiState.isTagFinish -> { + state.showMessage( + message = "ํƒœ๊ทธ ์™„๋ฃŒ ${Emoji.congratulate.random()}", + actionLabel = "์ทจ์†Œ", + ) { + uiState.restart.value() + } + uiState.clearState() + } + + uiState.isTagDelete -> { + state.showMessage( + message = "ํƒœ๊ทธ ์‚ญ์ œ ${Emoji.congratulate.random()}", + actionLabel = "์ทจ์†Œ", + ) { + uiState.restore.value() + } + uiState.clearState() + } + + uiState.isNetworkError -> state.showMessage("๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š” ${Emoji.check.random()}") + + uiState.isUnknownError -> { + state.showMessage("์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š” ${Emoji.error.random()}") + uiState.clearState() + } + } + } +} + +@Composable +private fun Content( + state: TagListScaffoldState, + listUiStateProvider: () -> TagListUiState, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + contentPadding = DiaryTheme.dimen.screenPaddingValues, + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + when (val uiState = listUiStateProvider()) { + is TagListUiState.Loading -> { + items( + count = 5, + contentType = { "Tag" }, + ) { + TagItem( + uiState = null, + onClick = { }, + modifier = Modifier.animateItem(), + ) + } + } + + is TagListUiState.NetworkError -> { + item( + key = "NetworkError", + contentType = "NetworkError", + ) { + NetworkError( + onRetry = uiState.retry, + modifier = Modifier + .fillParentMaxSize() + .animateItem(), + ) + } + } + + is TagListUiState.UnknownError -> { + item( + key = "UnknownError", + contentType = "UnknownError", + ) { + UnknownError( + onRetry = uiState.retry, + modifier = Modifier + .fillParentMaxSize() + .animateItem(), + ) + } + } + + is TagListUiState.State -> { + if (uiState.list.isEmpty()) { + item( + key = "Empty", + contentType = "Empty", + ) { + Box( + modifier = Modifier + .fillParentMaxSize() + .animateItem(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "ํƒœ๊ทธ๊ฐ€ ์—†์–ด์š” ๐Ÿผ", + style = DiaryTheme.typography.headlineMedium, + ) + } + } + } else { + items( + items = uiState.list, + key = { it.id }, + contentType = { "Tag" }, + ) { + TagItem( + uiState = it, + onClick = { state.navigateToTag(it.id) }, + modifier = Modifier.animateItem(), + ) + } + } + } + } + } +} + +@Composable +private fun TagItem( + 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, + ) + } + } + } +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffoldFloatingButton.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffoldFloatingButton.kt new file mode 100644 index 00000000..b697373a --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffoldFloatingButton.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.core.compose.tag.list + +public sealed class TagListScaffoldFloatingButton { + public data object None : TagListScaffoldFloatingButton() + + public data class Add( + public val add: () -> Unit, + ) : TagListScaffoldFloatingButton() +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreenState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffoldState.kt similarity index 76% rename from app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreenState.kt rename to app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffoldState.kt index 3ee581a7..8b26f420 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreenState.kt +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffoldState.kt @@ -1,4 +1,4 @@ -package io.github.taetae98coding.diary.feature.tag.memo +package io.github.taetae98coding.diary.core.compose.tag.list import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState @@ -8,14 +8,15 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -internal class TagMemoScreenState( +public class TagListScaffoldState( private val coroutineScope: CoroutineScope, + internal val navigateToTag: (String) -> Unit, ) { private var messageJob: Job? = null - val hostState: SnackbarHostState = SnackbarHostState() + internal val hostState: SnackbarHostState = SnackbarHostState() - fun showMessage( + internal fun showMessage( message: String, actionLabel: String, onResult: (SnackbarResult) -> Unit, @@ -36,7 +37,7 @@ internal class TagMemoScreenState( } } - fun showMessage( + internal fun showMessage( message: String, ) { messageJob?.cancel() diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffoldUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffoldUiState.kt new file mode 100644 index 00000000..597cdd92 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListScaffoldUiState.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.core.compose.tag.list + +import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty + +public data class TagListScaffoldUiState( + val isTagFinish: Boolean = false, + val isTagDelete: Boolean = false, + val isNetworkError: Boolean = false, + val isUnknownError: Boolean = false, + val restart: SkipProperty<() -> Unit> = SkipProperty {}, + val restore: SkipProperty<() -> Unit> = SkipProperty {}, + val clearState: () -> Unit = {}, +) diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListUiState.kt new file mode 100644 index 00000000..1dbdb2a4 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/list/TagListUiState.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary.core.compose.tag.list + +public sealed class TagListUiState { + public data object Loading : TagListUiState() + + public data class NetworkError( + val retry: () -> Unit, + ) : TagListUiState() + + public data class UnknownError( + val retry: () -> Unit, + ) : TagListUiState() + + public data class State( + public val list: List, + ) : TagListUiState() +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/RememberTagListScreenState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/RememberTagMemoScaffoldState.kt similarity index 55% rename from app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/RememberTagListScreenState.kt rename to app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/RememberTagMemoScaffoldState.kt index 07040bd2..4aad4e35 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/RememberTagListScreenState.kt +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/RememberTagMemoScaffoldState.kt @@ -1,14 +1,14 @@ -package io.github.taetae98coding.diary.feature.tag.list +package io.github.taetae98coding.diary.core.compose.tag.memo import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @Composable -internal fun rememberTagListScreenState(): TagListScreenState { +public fun rememberTagMemoScaffoldState(): TagMemoScaffoldState { val coroutineScope = rememberCoroutineScope() return remember { - TagListScreenState(coroutineScope = coroutineScope) + TagMemoScaffoldState(coroutineScope = coroutineScope) } } diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoListUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoListUiState.kt new file mode 100644 index 00000000..db9b3a35 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoListUiState.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.core.compose.tag.memo + +import io.github.taetae98coding.diary.core.compose.memo.list.MemoListItemUiState + +public sealed class TagMemoListUiState { + public data object Loading : TagMemoListUiState() + + public data class State( + val list: List, + ) : TagMemoListUiState() + + public data class NetworkError( + val retry: () -> Unit, + ) : TagMemoListUiState() + + public data class UnknownError( + val retry: () -> Unit, + ) : TagMemoListUiState() +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoNavigateIcon.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoNavigateIcon.kt new file mode 100644 index 00000000..29e9e366 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoNavigateIcon.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.core.compose.tag.memo + +public sealed class TagMemoNavigateIcon { + public data object None : TagMemoNavigateIcon() + + public data class NavigateUp( + val navigateUp: () -> Unit, + ) : TagMemoNavigateIcon() +} diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoScaffold.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoScaffold.kt new file mode 100644 index 00000000..ac1d8ad0 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoScaffold.kt @@ -0,0 +1,196 @@ +package io.github.taetae98coding.diary.core.compose.tag.memo + +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.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.core.compose.button.FloatingAddButton +import io.github.taetae98coding.diary.core.compose.error.NetworkError +import io.github.taetae98coding.diary.core.compose.error.UnknownError +import io.github.taetae98coding.diary.core.compose.memo.list.MemoListItem +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.theme.DiaryTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +public fun TagMemoScaffold( + state: TagMemoScaffoldState, + uiStateProvider: () -> TagMemoScaffoldUiState, + listUiStateProvider: () -> TagMemoListUiState, + onAdd: () -> Unit, + onMemo: (String) -> Unit, + modifier: Modifier = Modifier, + navigateIconProvider: () -> TagMemoNavigateIcon = { TagMemoNavigateIcon.None }, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = {}, + navigationIcon = { + when (val navigateIcon = navigateIconProvider()) { + is TagMemoNavigateIcon.NavigateUp -> { + IconButton(onClick = navigateIcon.navigateUp) { + NavigateUpIcon() + } + } + + is TagMemoNavigateIcon.None -> Unit + } + }, + ) + }, + snackbarHost = { SnackbarHost(hostState = state.hostState) }, + floatingActionButton = { FloatingAddButton(onClick = onAdd) }, + ) { + Content( + listUiStateProvider = listUiStateProvider, + onMemo = onMemo, + modifier = Modifier + .fillMaxSize() + .padding(it), + ) + } + + HandleUiState( + state = state, + uiStateProvider = uiStateProvider, + ) +} + +@Composable +private fun HandleUiState( + state: TagMemoScaffoldState, + uiStateProvider: () -> TagMemoScaffoldUiState, +) { + val uiState = uiStateProvider() + + LaunchedEffect( + uiState.isFinish, + uiState.isDelete, + uiState.isUnknownError, + ) { + when { + uiState.isFinish -> { + state.showMessage( + message = "๋ฉ”๋ชจ ์™„๋ฃŒ ${Emoji.congratulate.random()}", + actionLabel = "์ทจ์†Œ", + ) { + uiState.restartTag() + } + uiState.clearState() + } + + uiState.isDelete -> { + state.showMessage( + message = "๋ฉ”๋ชจ ์‚ญ์ œ ${Emoji.congratulate.random()}", + actionLabel = "์ทจ์†Œ", + ) { + uiState.restoreTag() + } + uiState.clearState() + } + + uiState.isUnknownError -> { + state.showMessage("์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š” ${Emoji.error.random()}") + uiState.clearState() + } + } + } +} + +@Composable +private fun Content( + listUiStateProvider: () -> TagMemoListUiState, + onMemo: (String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + contentPadding = DiaryTheme.dimen.screenPaddingValues, + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + val listUiState = listUiStateProvider() + + if (listUiState is TagMemoListUiState.Loading) { + items( + count = 5, + contentType = { "Memo" }, + ) { + MemoListItem( + uiState = null, + onClick = {}, + modifier = Modifier.animateItem(), + ) + } + } else if (listUiState is TagMemoListUiState.State) { + if (listUiState.list.isEmpty()) { + item( + key = "Empty", + contentType = "Empty", + ) { + Box( + modifier = Modifier + .fillParentMaxSize() + .animateItem(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "๋ฉ”๋ชจ๊ฐ€ ์—†์–ด์š” ๐Ÿฐ", + style = DiaryTheme.typography.headlineMedium, + ) + } + } + } else { + items( + items = listUiState.list, + key = { it.id }, + contentType = { "Memo" }, + ) { + MemoListItem( + uiState = it, + onClick = { onMemo(it.id) }, + modifier = Modifier.animateItem(), + ) + } + } + } else if (listUiState is TagMemoListUiState.NetworkError) { + item( + key = "NetworkError", + contentType = "NetworkError", + ) { + NetworkError( + onRetry = listUiState.retry, + modifier = Modifier + .fillParentMaxSize() + .animateItem(), + ) + } + } else if (listUiState is TagMemoListUiState.UnknownError) { + item( + key = "UnknownError", + contentType = "UnknownError", + ) { + UnknownError( + onRetry = listUiState.retry, + modifier = Modifier + .fillParentMaxSize() + .animateItem(), + ) + } + } + } +} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreenState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoScaffoldState.kt similarity index 80% rename from app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreenState.kt rename to app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoScaffoldState.kt index 4d095a10..7f74ec19 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreenState.kt +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoScaffoldState.kt @@ -1,4 +1,4 @@ -package io.github.taetae98coding.diary.feature.tag.list +package io.github.taetae98coding.diary.core.compose.tag.memo import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState @@ -8,14 +8,14 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -internal class TagListScreenState( +public class TagMemoScaffoldState( private val coroutineScope: CoroutineScope, ) { private var messageJob: Job? = null - val hostState: SnackbarHostState = SnackbarHostState() + internal val hostState: SnackbarHostState = SnackbarHostState() - fun showMessage( + internal fun showMessage( message: String, actionLabel: String, onResult: (SnackbarResult) -> Unit, @@ -36,7 +36,7 @@ internal class TagListScreenState( } } - fun showMessage( + internal fun showMessage( message: String, ) { messageJob?.cancel() diff --git a/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoScaffoldUiState.kt b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoScaffoldUiState.kt new file mode 100644 index 00000000..fb1e9d50 --- /dev/null +++ b/app/core/compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/compose/tag/memo/TagMemoScaffoldUiState.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.core.compose.tag.memo + +public data class TagMemoScaffoldUiState( + val isFinish: Boolean = false, + val isDelete: Boolean = false, + val isUnknownError: Boolean = false, + val restartTag: () -> Unit = {}, + val restoreTag: () -> Unit = {}, + val clearState: () -> 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 index e3c50b99..6e30fb08 100644 --- 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 @@ -7,12 +7,12 @@ import androidx.compose.ui.Modifier @Composable public fun TopBarTitle( - text: String, - modifier: Modifier = Modifier, + text: String, + modifier: Modifier = Modifier, ) { - Text( - text = text, - modifier = modifier.basicMarquee(iterations = Int.MAX_VALUE), - maxLines = 1, - ) + Text( + text = text, + modifier = modifier.basicMarquee(iterations = Int.MAX_VALUE), + maxLines = 1, + ) } 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 index fb1431b4..04de4f2c 100644 --- 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 @@ -4,10 +4,6 @@ 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.start(): CornerBasedShape = copy(topEnd = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)) -public fun CornerBasedShape.end(): CornerBasedShape { - return copy(topStart = CornerSize(0.dp), bottomStart = CornerSize(0.dp)) -} +public fun CornerBasedShape.end(): CornerBasedShape = 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 index 0d206576..d3ff68b0 100644 --- 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 @@ -3,5 +3,5 @@ package io.github.taetae98coding.diary.core.design.system.shape import androidx.compose.foundation.shape.CornerBasedShape public data class DiaryShape( - val large: CornerBasedShape, + val large: CornerBasedShape, ) 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 586cc9da..5868434c 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 @@ -16,9 +16,9 @@ public data object DiaryTheme { @Composable get() = LocalDiaryColor.current - val shape: DiaryShape - @Composable - get() = LocalDiaryShape.current + val shape: DiaryShape + @Composable + get() = LocalDiaryShape.current val typography: DiaryTypography @Composable @@ -47,9 +47,9 @@ public fun DiaryTheme( background = colorScheme.background, onSurface = colorScheme.onSurface, ), - LocalDiaryShape provides DiaryShape( - large = MaterialTheme.shapes.large, - ), + 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/build.gradle.kts b/app/core/diary-database-room/build.gradle.kts index 416144df..23889119 100644 --- a/app/core/diary-database-room/build.gradle.kts +++ b/app/core/diary-database-room/build.gradle.kts @@ -12,9 +12,35 @@ kotlin { implementation(project(":library:room")) } } + + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.room.testing) + } + } + + androidUnitTest { + dependencies { + implementation(libs.test.core) + implementation(libs.roboletric) + } + } } } android { namespace = "${Build.NAMESPACE}.core.diary.database.room" + + sourceSets { + getByName("test") { + assets.srcDir("$projectDir/schemas") + } + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } diff --git a/app/core/diary-database-room/schemas/io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase/3.json b/app/core/diary-database-room/schemas/io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase/3.json new file mode 100644 index 00000000..f4d99694 --- /dev/null +++ b/app/core/diary-database-room/schemas/io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase/3.json @@ -0,0 +1,332 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "119b91868c5683227e0429058746b9ac", + "entities": [ + { + "tableName": "MemoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL DEFAULT '', `title` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', `start` TEXT DEFAULT null, `endInclusive` TEXT DEFAULT null, `color` INTEGER NOT NULL DEFAULT -16777216, `isFinish` INTEGER NOT NULL DEFAULT 0, `isDelete` INTEGER NOT NULL DEFAULT 0, `owner` TEXT DEFAULT null, `primaryTag` TEXT DEFAULT null, `updateAt` INTEGER NOT NULL DEFAULT 0, `serverUpdateAt` INTEGER DEFAULT null, PRIMARY KEY(`id`), FOREIGN KEY(`primaryTag`) REFERENCES `TagEntity`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "endInclusive", + "columnName": "endInclusive", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-16777216" + }, + { + "fieldPath": "isFinish", + "columnName": "isFinish", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isDelete", + "columnName": "isDelete", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "owner", + "columnName": "owner", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "primaryTag", + "columnName": "primaryTag", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "updateAt", + "columnName": "updateAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "serverUpdateAt", + "columnName": "serverUpdateAt", + "affinity": "INTEGER", + "defaultValue": "null" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_MemoEntity_primaryTag", + "unique": false, + "columnNames": [ + "primaryTag" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemoEntity_primaryTag` ON `${TABLE_NAME}` (`primaryTag`)" + } + ], + "foreignKeys": [ + { + "table": "TagEntity", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "primaryTag" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "TagEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL DEFAULT '', `title` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', `color` INTEGER NOT NULL DEFAULT -16777216, `isFinish` INTEGER NOT NULL DEFAULT 0, `isDelete` INTEGER NOT NULL DEFAULT 0, `owner` TEXT DEFAULT null, `updateAt` INTEGER NOT NULL DEFAULT 0, `serverUpdateAt` INTEGER DEFAULT null, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-16777216" + }, + { + "fieldPath": "isFinish", + "columnName": "isFinish", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isDelete", + "columnName": "isDelete", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "owner", + "columnName": "owner", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "updateAt", + "columnName": "updateAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "serverUpdateAt", + "columnName": "serverUpdateAt", + "affinity": "INTEGER", + "defaultValue": "null" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "MemoTagEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memoId` TEXT NOT NULL DEFAULT '', `tagId` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`memoId`, `tagId`), FOREIGN KEY(`memoId`) REFERENCES `MemoEntity`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`tagId`) REFERENCES `TagEntity`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "memoId", + "columnName": "memoId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "memoId", + "tagId" + ] + }, + "indices": [ + { + "name": "index_MemoTagEntity_memoId", + "unique": false, + "columnNames": [ + "memoId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemoTagEntity_memoId` ON `${TABLE_NAME}` (`memoId`)" + }, + { + "name": "index_MemoTagEntity_tagId", + "unique": false, + "columnNames": [ + "tagId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemoTagEntity_tagId` ON `${TABLE_NAME}` (`tagId`)" + } + ], + "foreignKeys": [ + { + "table": "MemoEntity", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "memoId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "TagEntity", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tagId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MemoBuddyGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memoId` TEXT NOT NULL DEFAULT '', `buddyGroupId` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`memoId`, `buddyGroupId`), FOREIGN KEY(`memoId`) REFERENCES `MemoEntity`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "memoId", + "columnName": "memoId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "buddyGroupId", + "columnName": "buddyGroupId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "memoId", + "buddyGroupId" + ] + }, + "indices": [ + { + "name": "index_MemoBuddyGroupEntity_memoId", + "unique": false, + "columnNames": [ + "memoId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemoBuddyGroupEntity_memoId` ON `${TABLE_NAME}` (`memoId`)" + }, + { + "name": "index_MemoBuddyGroupEntity_buddyGroupId", + "unique": false, + "columnNames": [ + "buddyGroupId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MemoBuddyGroupEntity_buddyGroupId` ON `${TABLE_NAME}` (`buddyGroupId`)" + } + ], + "foreignKeys": [ + { + "table": "MemoEntity", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "memoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '119b91868c5683227e0429058746b9ac')" + ] + } +} \ No newline at end of file diff --git a/app/core/diary-database-room/src/androidUnitTest/kotlin/io/github/taetae98coding/diary/core/diary/database/room/MigrationTest.android.kt b/app/core/diary-database-room/src/androidUnitTest/kotlin/io/github/taetae98coding/diary/core/diary/database/room/MigrationTest.android.kt new file mode 100644 index 00000000..eca8aaba --- /dev/null +++ b/app/core/diary-database-room/src/androidUnitTest/kotlin/io/github/taetae98coding/diary/core/diary/database/room/MigrationTest.android.kt @@ -0,0 +1,22 @@ +package io.github.taetae98coding.diary.core.diary.database.room + +import androidx.room.testing.MigrationTestHelper +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.Test + +@RunWith(RobolectricTestRunner::class) +class MigrationTest { + private val helper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + DiaryDatabase::class.java, + ) + + @Test + fun migrate() { + helper.createDatabase("diary", 1).close() + helper.runMigrationsAndValidate("diary", 2, true).close() + helper.runMigrationsAndValidate("diary", 3, true).close() + } +} diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt index 170c2ee3..08aa4bda 100644 --- a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt @@ -5,9 +5,11 @@ import androidx.room.ConstructedBy import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters +import io.github.taetae98coding.diary.core.diary.database.room.dao.MemoBuddyEntityDao import io.github.taetae98coding.diary.core.diary.database.room.dao.MemoEntityDao import io.github.taetae98coding.diary.core.diary.database.room.dao.MemoTagEntityDao import io.github.taetae98coding.diary.core.diary.database.room.dao.TagEntityDao +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoBuddyGroupEntity import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoEntity import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoTagEntity import io.github.taetae98coding.diary.core.diary.database.room.entity.TagEntity @@ -21,10 +23,12 @@ import io.github.taetae98coding.diary.library.room.LocalDataConverter MemoEntity::class, TagEntity::class, MemoTagEntity::class, + MemoBuddyGroupEntity::class, ], - version = 2, + version = 3, autoMigrations = [ AutoMigration(from = 1, to = 2, AutoMigration1To2::class), + AutoMigration(from = 2, to = 3), ], ) @ConstructedBy(DiaryDatabaseConstructor::class) @@ -38,4 +42,6 @@ internal abstract class DiaryDatabase : RoomDatabase() { abstract fun tag(): TagEntityDao abstract fun memoTag(): MemoTagEntityDao + + abstract fun memoBuddyGroup(): MemoBuddyEntityDao } diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBuddyEntityDao.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBuddyEntityDao.kt new file mode 100644 index 00000000..f7840e70 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBuddyEntityDao.kt @@ -0,0 +1,29 @@ +package io.github.taetae98coding.diary.core.diary.database.room.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoBuddyGroupEntity +import io.github.taetae98coding.diary.library.room.dao.EntityDao +import kotlinx.coroutines.flow.Flow + +@Dao +internal abstract class MemoBuddyEntityDao : EntityDao() { + @Transaction + open suspend fun upsert(memoIds: Set, groupId: String) { + memoIds.forEach { memoId -> + upsert(MemoBuddyGroupEntity(memoId, groupId)) + } + } + + @Query( + """ + SELECT EXISTS( + SELECT * + FROM MemoBuddyGroupEntity + WHERE memoId = :memoId + ) + """, + ) + abstract fun isBuddyGroupMemo(memoId: String): Flow +} diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBuddyRoomGroupDao.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBuddyRoomGroupDao.kt new file mode 100644 index 00000000..1bf688b4 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBuddyRoomGroupDao.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary.core.diary.database.room.dao + +import io.github.taetae98coding.diary.core.diary.database.MemoBuddyGroupDao +import io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase +import kotlinx.coroutines.flow.Flow +import org.koin.core.annotation.Factory + +@Factory +internal class MemoBuddyRoomGroupDao( + private val database: DiaryDatabase, +) : MemoBuddyGroupDao { + override suspend fun upsert(memoIds: Set, groupId: String) { + database.memoBuddyGroup().upsert(memoIds, groupId) + } + + override fun isBuddyGroupMemo(memoId: String): Flow = database.memoBuddyGroup().isBuddyGroupMemo(memoId) +} 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 14b4997f..78db9514 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 @@ -4,6 +4,7 @@ import io.github.taetae98coding.diary.core.diary.database.MemoDao import io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoEntity import io.github.taetae98coding.diary.core.diary.database.room.mapper.toDto +import io.github.taetae98coding.diary.core.diary.database.room.mapper.toEntity import io.github.taetae98coding.diary.core.model.memo.MemoAndTagIds import io.github.taetae98coding.diary.core.model.memo.MemoDetail import io.github.taetae98coding.diary.core.model.memo.MemoDto @@ -19,67 +20,71 @@ 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 upsertMemo(dto: List) { + database.memo().upsert(dto.map(MemoDto::toEntity)) + } - 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 upsert(dto: MemoAndTagIds) { + database.memo().upsertMemoAndTagIds(dto) + } - override suspend fun updatePrimaryTag(memoId: String, tagId: String?) { - database.memo().updatePrimaryTag(memoId, tagId, 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 updateFinish(memoId: String, isFinish: Boolean) { - database.memo().updateFinish(memoId, isFinish, clock.now()) - } + override suspend fun updatePrimaryTag(memoId: String, tagId: String?) { + database.memo().updatePrimaryTag(memoId, tagId, clock.now()) + } - override suspend fun updateDelete(memoId: String, isDelete: Boolean) { - database.memo().updateDelete(memoId, isDelete, clock.now()) - } + override suspend fun updateFinish(memoId: String, isFinish: Boolean) { + database.memo().updateFinish(memoId, isFinish, clock.now()) + } - override fun getById(memoId: String): Flow = - database - .memo() - .getById(memoId) - .mapLatest { it?.toDto() } + override suspend fun updateDelete(memoId: String, isDelete: Boolean) { + database.memo().updateDelete(memoId, isDelete, clock.now()) + } - 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 getById(memoId: String): Flow = + database + .memo() + .getById(memoId) + .mapLatest { it?.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 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 suspend fun upsert(memoList: List) { - database.memo().upsertMemoAndTagIds(memoList) - } + 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 getLastServerUpdateAt(owner: String?): Flow = database.memo().getLastUpdateAt(owner) + override suspend fun upsert(memoList: List) { + database.memo().upsertMemoAndTagIds(memoList) + } + + 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/entity/MemoBuddyGroupEntity.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/MemoBuddyGroupEntity.kt new file mode 100644 index 00000000..ef3ce211 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/MemoBuddyGroupEntity.kt @@ -0,0 +1,28 @@ +package io.github.taetae98coding.diary.core.diary.database.room.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +@Entity( + indices = [ + Index("memoId"), Index("buddyGroupId"), + ], + primaryKeys = ["memoId", "buddyGroupId"], + foreignKeys = [ + ForeignKey( + entity = MemoEntity::class, + parentColumns = ["id"], + childColumns = ["memoId"], + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE, + ), + ], +) +internal data class MemoBuddyGroupEntity( + @ColumnInfo(defaultValue = "") + val memoId: String, + @ColumnInfo(defaultValue = "") + val buddyGroupId: String, +) 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 b16374ca..a8972059 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-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoBuddyGroupDao.kt b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoBuddyGroupDao.kt new file mode 100644 index 00000000..cf1e5372 --- /dev/null +++ b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoBuddyGroupDao.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.core.diary.database + +import kotlinx.coroutines.flow.Flow + +public interface MemoBuddyGroupDao { + public suspend fun upsert(memoIds: Set, groupId: String) + + public fun isBuddyGroupMemo(memoId: String): Flow +} diff --git a/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoDao.kt b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoDao.kt index 138db84b..f6fbcf96 100644 --- a/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoDao.kt +++ b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoDao.kt @@ -8,6 +8,8 @@ import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate public interface MemoDao { + public suspend fun upsertMemo(dto: List) + public suspend fun upsert(dto: MemoAndTagIds) public suspend fun upsert(memoList: List) 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 e8dccb2c..98e1235b 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 @@ -26,76 +26,78 @@ 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 upsert( - groupId: String, - memoAndTagIds: MemoAndTagIds, - ) { - client.post("/buddy/group/$groupId/upsertMemo") { - setBody(memoAndTagIds.toEntity()) - contentType(ContentType.Application.Json) - }.getOrThrow() - } + public suspend fun upsert( + groupId: String, + memoAndTagIds: MemoAndTagIds, + ) { + client + .post("/buddy/group/$groupId/upsertMemo") { + setBody(memoAndTagIds.toEntity()) + contentType(ContentType.Application.Json) + }.getOrThrow() + } - public suspend fun findBuddyGroup(): List { - val response = client.get("/buddy/group/find").getOrThrow>() + public suspend fun findBuddyGroup(): List { + val response = client.get("/buddy/group/find").getOrThrow>() - return response.map { - BuddyGroup( - id = it.id, - detail = BuddyGroupDetail( - title = it.title, - description = it.description, - ), - ) - } - } + return response.map { + BuddyGroup( + id = it.id, + detail = BuddyGroupDetail( + title = it.title, + description = it.description, + ), + ) + } + } - public suspend fun findBuddyByEmail(email: String): List { - val response = client - .get("/buddy/find") { - parameter("email", email) - }.getOrThrow>() + 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, - ) - } - } + 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>() + 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) - } + return response.map(MemoEntity::toDto) + } } diff --git a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/ext/HttpClientExt.kt b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/ext/HttpClientExt.kt index 3d505461..f6627075 100644 --- a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/ext/HttpClientExt.kt +++ b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/ext/HttpClientExt.kt @@ -8,18 +8,29 @@ import io.ktor.client.call.body import io.ktor.client.statement.HttpResponse import io.ktor.http.isSuccess -internal suspend inline fun HttpResponse.getOrThrow(): T = try { - if (status.isSuccess()) { +internal suspend inline fun HttpResponse.getOrThrow(): T = if (status.isSuccess()) { + try { val body = body>() requireNotNull(body.body) - } else { - throw when (val errorBody = body>()) { - DiaryResponse.AlreadyExistEmail -> ExistEmailException() - DiaryResponse.AccountNotFound -> AccountNotFoundException() - else -> ApiException(message = errorBody.message) - } + } catch (throwable: Throwable) { + throw wrapApiException(throwable = throwable) + } +} else { + val errorBody = try { + body>() + } catch (throwable: Throwable) { + throw wrapApiException(throwable = throwable) + } + + throw when (errorBody) { + DiaryResponse.AlreadyExistEmail -> throw ExistEmailException() + DiaryResponse.AccountNotFound -> throw AccountNotFoundException() + else -> throw ApiException(message = errorBody.message) } -} catch (e: Throwable) { - throw ApiException(message = "Unknown error", cause = e) } + +private fun wrapApiException( + message: String = "Unknown error", + throwable: Throwable, +): ApiException = ApiException(message = message, cause = throwable) 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 index 65a203e5..e7a2495a 100644 --- 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 @@ -1,64 +1,67 @@ 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.MemoDetailEntity 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 MemoAndTagIds.toEntity(): LegacyMemoEntity = 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 LegacyMemoEntity.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 = 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, - ) -} +internal fun MemoEntity.toDto(): MemoDto = 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, +) + +internal fun MemoDetail.toEntity(): MemoDetailEntity = MemoDetailEntity( + title = title, + description = description, + start = start, + endInclusive = endInclusive, + color = color, +) 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 823a1ac9..25933635 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,15 +1,19 @@ package io.github.taetae98coding.diary.core.diary.service.memo import io.github.taetae98coding.diary.common.model.memo.LegacyMemoEntity +import io.github.taetae98coding.diary.common.model.memo.MemoEntity 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 import io.ktor.client.request.post +import io.ktor.client.request.put import io.ktor.client.request.setBody import io.ktor.http.ContentType import io.ktor.http.contentType @@ -19,23 +23,53 @@ 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") { - contentType(ContentType.Application.Json) - setBody(list.map(MemoAndTagIds::toEntity)) - }.getOrThrow() - } - - public suspend fun fetch(updateAt: Instant): List { - val response = client.get("/memo/fetch") { - parameter("updateAt", updateAt) - }.getOrThrow>() - - return response.map { - MemoAndTagIds(it.toDto(), it.tagIds) - } - } + public suspend fun update(memoId: String, detail: MemoDetail) { + client + .put("/memo/$memoId/update") { + contentType(ContentType.Application.Json) + setBody(detail.toEntity()) + }.getOrThrow() + } + + public suspend fun upsert(list: List) { + client + .post("/memo/upsert") { + contentType(ContentType.Application.Json) + setBody(list.map(MemoAndTagIds::toEntity)) + }.getOrThrow() + } + + public suspend fun fetch(updateAt: Instant): List { + val response = client + .get("/memo/fetch") { + parameter("updateAt", updateAt) + }.getOrThrow>() + + return response.map { + MemoAndTagIds(it.toDto(), it.tagIds) + } + } + + public suspend fun updateFinish(memoId: String, isFinish: Boolean) { + client + .put("/memo/$memoId/updateFinish") { + parameter("isFinish", isFinish) + }.getOrThrow() + } + + public suspend fun updateDelete(memoId: String, isDelete: Boolean) { + client + .put("/memo/$memoId/updateDelete") { + parameter("isDelete", isDelete) + }.getOrThrow() + } + + public suspend fun findById(id: String): MemoDto? { + val response = client.get("/memo/$id").getOrThrow() + + return response?.toDto() + } } diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDetail.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDetail.kt index a89e8667..c92ad2e6 100644 --- a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDetail.kt +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDetail.kt @@ -8,4 +8,12 @@ public data class MemoDetail( val start: LocalDate?, val endInclusive: LocalDate?, val color: Int, -) +) { + val dateRange: ClosedRange? + get() { + if (start == null) return null + if (endInclusive == null) return null + + return start..endInclusive + } +} diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyTagHomeDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyTagHomeDestination.kt new file mode 100644 index 00000000..8f1c87ab --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/buddy/BuddyTagHomeDestination.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 BuddyTagHomeDestination( + @SerialName(GROUP_ID) + val groupId: String, +) { + public companion object { + public const val GROUP_ID: String = "GROUP_ID" + } +} diff --git a/app/data/buddy/build.gradle.kts b/app/data/buddy/build.gradle.kts index 7d00546f..581381b5 100644 --- a/app/data/buddy/build.gradle.kts +++ b/app/data/buddy/build.gradle.kts @@ -6,6 +6,7 @@ kotlin { sourceSets { commonMain { dependencies { + implementation(project(":app:core:diary-database")) implementation(project(":app:core:diary-service")) implementation(project(":app:domain:buddy")) } 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 20ac1038..74fc1bbb 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 @@ -1,5 +1,7 @@ package io.github.taetae98coding.diary.data.buddy.repository +import io.github.taetae98coding.diary.core.diary.database.MemoBuddyGroupDao +import io.github.taetae98coding.diary.core.diary.database.MemoDao 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 @@ -19,10 +21,12 @@ import kotlin.uuid.Uuid @OptIn(ExperimentalUuidApi::class) @Factory internal class BuddyRepositoryImpl( - private val remoteDataSource: BuddyService, + private val memoLocalDataSource: MemoDao, + private val memoBuddyGroupLocalDataSource: MemoBuddyGroupDao, + private val buddyRemoteDataSource: BuddyService, ) : BuddyRepository { override suspend fun upsert(buddyGroup: BuddyGroup, buddyIds: Set) { - remoteDataSource.upsert(buddyGroup, buddyIds) + buddyRemoteDataSource.upsert(buddyGroup, buddyIds) } override suspend fun upsert( @@ -30,19 +34,21 @@ internal class BuddyRepositoryImpl( memo: Memo, tagIds: Set, ) { - remoteDataSource.upsert(groupId, MemoAndTagIds(memo.toDto(), tagIds)) + buddyRemoteDataSource.upsert(groupId, MemoAndTagIds(memo.toDto(), tagIds)) } override fun findMemoByDateRange(groupId: String, dateRange: ClosedRange): Flow> = flow { - remoteDataSource + buddyRemoteDataSource .findMemoByDateRange(groupId, dateRange) + .also { memoLocalDataSource.upsertMemo(it) } + .also { memoBuddyGroupLocalDataSource.upsert(it.map(MemoDto::id).toSet(), groupId) } .map(MemoDto::toMemo) .also { emit(it) } } - override fun findBuddyGroup(): Flow> = flow { emit(remoteDataSource.findBuddyGroup()) } + override fun findBuddyGroup(): Flow> = flow { emit(buddyRemoteDataSource.findBuddyGroup()) } - override fun findBuddyByEmail(email: String): Flow> = flow { emit(remoteDataSource.findBuddyByEmail(email)) } + override fun findBuddyByEmail(email: String): Flow> = flow { emit(buddyRemoteDataSource.findBuddyByEmail(email)) } override suspend fun getNextBuddyGroupId(): String = Uuid.random().toString() diff --git a/app/data/memo/build.gradle.kts b/app/data/memo/build.gradle.kts index 31002b1c..5d85994b 100644 --- a/app/data/memo/build.gradle.kts +++ b/app/data/memo/build.gradle.kts @@ -7,6 +7,7 @@ kotlin { commonMain { dependencies { implementation(project(":app:core:diary-database")) + implementation(project(":app:core:diary-service")) implementation(project(":app:domain:memo")) } } diff --git a/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoBuddyGroupRepositoryImpl.kt b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoBuddyGroupRepositoryImpl.kt new file mode 100644 index 00000000..dd284b23 --- /dev/null +++ b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoBuddyGroupRepositoryImpl.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.data.memo.repository + +import io.github.taetae98coding.diary.core.diary.database.MemoBuddyGroupDao +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyGroupRepository +import kotlinx.coroutines.flow.Flow +import org.koin.core.annotation.Factory + +@Factory +internal class MemoBuddyGroupRepositoryImpl( + private val localDataSource: MemoBuddyGroupDao, +) : MemoBuddyGroupRepository { + override fun isBuddyGroupMemo(memoId: String): Flow = localDataSource.isBuddyGroupMemo(memoId) +} diff --git a/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt index 0c07fdd2..cd878c95 100644 --- a/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt +++ b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt @@ -1,6 +1,8 @@ package io.github.taetae98coding.diary.data.memo.repository +import io.github.taetae98coding.diary.core.diary.database.MemoBuddyGroupDao import io.github.taetae98coding.diary.core.diary.database.MemoDao +import io.github.taetae98coding.diary.core.diary.service.memo.MemoService 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 @@ -11,6 +13,7 @@ import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.mapLatest import kotlinx.datetime.LocalDate import org.koin.core.annotation.Factory @@ -20,35 +23,66 @@ import kotlin.uuid.Uuid @OptIn(ExperimentalCoroutinesApi::class, ExperimentalUuidApi::class) @Factory internal class MemoRepositoryImpl( - private val localDataSource: MemoDao, + private val memoLocalDataSource: MemoDao, + private val memoBuddyGroupLocalDataSource: MemoBuddyGroupDao, + private val memoRemoteDataSource: MemoService, ) : MemoRepository { + override suspend fun fetch(memoId: String) { + } + override suspend fun upsert(memo: Memo, tagIds: Set) { - localDataSource.upsert(MemoAndTagIds(memo.toDto(), tagIds)) + memoLocalDataSource.upsert(MemoAndTagIds(memo.toDto(), tagIds)) } override suspend fun update(memoId: String, detail: MemoDetail) { - localDataSource.update(memoId, detail) + if (memoBuddyGroupLocalDataSource.isBuddyGroupMemo(memoId).first()) { + try { + memoRemoteDataSource.update(memoId, detail) + memoLocalDataSource.update(memoId, detail) + } catch (throwable: Throwable) { + memoLocalDataSource.update(memoId, detail) + throw throwable + } + } else { + memoLocalDataSource.update(memoId, detail) + } } override suspend fun updatePrimaryTag(memoId: String, tagId: String?) { - localDataSource.updatePrimaryTag(memoId, tagId) + memoLocalDataSource.updatePrimaryTag(memoId, tagId) } override suspend fun updateFinish(memoId: String, isFinish: Boolean) { - localDataSource.updateFinish(memoId, isFinish) + memoLocalDataSource.updateFinish(memoId, isFinish) + if (memoBuddyGroupLocalDataSource.isBuddyGroupMemo(memoId).first()) { + try { + memoRemoteDataSource.updateFinish(memoId, isFinish) + } catch (throwable: Throwable) { + memoLocalDataSource.updateFinish(memoId, !isFinish) + throw throwable + } + } } override suspend fun updateDelete(memoId: String, isDelete: Boolean) { - localDataSource.updateDelete(memoId, isDelete) + memoLocalDataSource.updateDelete(memoId, isDelete) + if (memoBuddyGroupLocalDataSource.isBuddyGroupMemo(memoId).first()) { + try { + memoRemoteDataSource.updateDelete(memoId, isDelete) + } catch (throwable: Throwable) { + memoLocalDataSource.updateDelete(memoId, !isDelete) + throw throwable + } + } } override fun getById(memoId: String): Flow = - localDataSource + memoLocalDataSource .getById(memoId) .mapLatest { it?.toMemo() } override fun findByDateRange(owner: String?, dateRange: ClosedRange, tagFilter: Set): Flow> = - localDataSource + memoLocalDataSource .findByDateRange(owner, dateRange, tagFilter) .mapCollectionLatest(MemoDto::toMemo) 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 d0e502d2..8978e5e2 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 @@ -8,14 +8,16 @@ 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 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 + + 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 index 74c406fd..7f03043a 100644 --- 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 @@ -9,30 +9,30 @@ import org.koin.core.annotation.Factory @Factory public class AddBuddyGroupMemoUseCase internal constructor( - private val clock: Clock, - private val repository: BuddyRepository, + 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() + 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(), - ) + val memo = Memo( + id = repository.getNextMemoId(), + detail = detail, + primaryTag = primaryTag, + owner = groupId, + isFinish = false, + isDelete = false, + updateAt = clock.now(), + ) - repository.upsert(groupId, memo, tagIds) - } - } + 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 da8516e5..7b331362 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 @@ -15,21 +15,21 @@ import org.koin.core.annotation.Factory @OptIn(ExperimentalCoroutinesApi::class) @Factory public class FindBuddyUseCase internal constructor( - private val getAccountUseCase: GetAccountUseCase, - 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 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)) } - } + 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 index 14f52920..1244d563 100644 --- 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 @@ -16,19 +16,17 @@ import org.koin.core.annotation.Factory @OptIn(ExperimentalCoroutinesApi::class) @Factory public class PageBuddyGroupCalendarMemo internal constructor( - private val getAccountUseCase: GetAccountUseCase, - private val repository: BuddyRepository, + 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)) } - } + public operator fun invoke(groupId: String, dateRange: ClosedRange): Flow>> = 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/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyGroupUseCase.kt b/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/PageBuddyGroupUseCase.kt similarity index 95% rename from app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyGroupUseCase.kt rename to app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/PageBuddyGroupUseCase.kt index c03131a5..90d48054 100644 --- a/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/FindBuddyGroupUseCase.kt +++ b/app/domain/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/buddy/usecase/PageBuddyGroupUseCase.kt @@ -14,7 +14,7 @@ import org.koin.core.annotation.Factory @OptIn(ExperimentalCoroutinesApi::class) @Factory -public class FindBuddyGroupUseCase internal constructor( +public class PageBuddyGroupUseCase internal constructor( private val getAccountUseCase: GetAccountUseCase, private val repository: BuddyRepository, ) { diff --git a/app/domain/buddy/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/buddy/FindBuddyUseCaseTest.kt b/app/domain/buddy/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/buddy/FindBuddyUseCaseTest.kt index f1acc848..1b9b36c3 100644 --- a/app/domain/buddy/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/buddy/FindBuddyUseCaseTest.kt +++ b/app/domain/buddy/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/buddy/FindBuddyUseCaseTest.kt @@ -15,10 +15,10 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf class FindBuddyUseCaseTest : BehaviorSpec() { - private val getAccountUseCase = mockk() + private val getAccountUseCase = mockk() private val buddyRepository = mockk(relaxed = true, relaxUnitFun = true) private val useCase = FindBuddyUseCase( - getAccountUseCase = getAccountUseCase, + getAccountUseCase = getAccountUseCase, repository = buddyRepository, ) @@ -26,47 +26,47 @@ class FindBuddyUseCaseTest : BehaviorSpec() { Given("given email") { val email = "email" - And("account is Login") { - every { getAccountUseCase() } returns flowOf(Result.success(mockk())) + And("account is Login") { + every { getAccountUseCase() } returns flowOf(Result.success(mockk())) - When("call useCase") { - every { buddyRepository.findBuddyByEmail(any()) } returns flowOf(emptyList()) + When("call useCase") { + every { buddyRepository.findBuddyByEmail(any()) } returns flowOf(emptyList()) - val result = useCase(email).first() + val result = useCase(email).first() - Then("result is success") { - result.shouldBeSuccess() - } + Then("result is success") { + result.shouldBeSuccess() + } - Then("do find") { - verify { buddyRepository.findBuddyByEmail(email) } - } + Then("do find") { + verify { buddyRepository.findBuddyByEmail(email) } + } - clearAllMocks() - } - } + clearAllMocks() + } + } - And("account is not login") { - every { getAccountUseCase() } returns flowOf(Result.success(mockk())) + And("account is not login") { + every { getAccountUseCase() } returns flowOf(Result.success(mockk())) - When("call useCase") { - every { buddyRepository.findBuddyByEmail(any()) } returns flowOf(emptyList()) + When("call useCase") { + every { buddyRepository.findBuddyByEmail(any()) } returns flowOf(emptyList()) - val result = useCase(email).first() + val result = useCase(email).first() - Then("result is success") { - result.shouldBeSuccess() - } + Then("result is success") { + result.shouldBeSuccess() + } - Then("do nothing") { - verify { - buddyRepository wasNot Called - } - } + Then("do nothing") { + verify { + buddyRepository wasNot Called + } + } - clearAllMocks() - } - } + clearAllMocks() + } + } } Given("blank email") { diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoBuddyGroupRepository.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoBuddyGroupRepository.kt new file mode 100644 index 00000000..940ced5d --- /dev/null +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoBuddyGroupRepository.kt @@ -0,0 +1,7 @@ +package io.github.taetae98coding.diary.domain.memo.repository + +import kotlinx.coroutines.flow.Flow + +public interface MemoBuddyGroupRepository { + public fun isBuddyGroupMemo(memoId: String): Flow +} diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt index 7cde5afb..6c8bdfb7 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt @@ -6,6 +6,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.datetime.LocalDate public interface MemoRepository { + public suspend fun fetch(memoId: String) + public suspend fun upsert(memo: Memo, tagIds: Set) public suspend fun update(memoId: String, detail: MemoDetail) diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt index ff576ec6..d8e5628e 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt @@ -1,20 +1,25 @@ package io.github.taetae98coding.diary.domain.memo.usecase import io.github.taetae98coding.diary.domain.backup.usecase.PushMemoBackupQueueUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyGroupRepository import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first import org.koin.core.annotation.Factory @Factory public class DeleteMemoUseCase internal constructor( private val pushMemoBackupQueueUseCase: PushMemoBackupQueueUseCase, - private val repository: MemoRepository, + private val memoRepository: MemoRepository, + private val memoBuddyGroupRepository: MemoBuddyGroupRepository, ) { public suspend operator fun invoke(memoId: String?): Result { return runCatching { if (memoId.isNullOrBlank()) return@runCatching - repository.updateDelete(memoId, true) - pushMemoBackupQueueUseCase(memoId) + memoRepository.updateDelete(memoId, true) + if (!memoBuddyGroupRepository.isBuddyGroupMemo(memoId).first()) { + pushMemoBackupQueueUseCase(memoId) + } } } } diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindMemoUseCase.kt index b7482ced..65353952 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindMemoUseCase.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindMemoUseCase.kt @@ -14,14 +14,18 @@ import org.koin.core.annotation.Factory @OptIn(ExperimentalCoroutinesApi::class) @Factory public class FindMemoUseCase internal constructor( - private val repository: MemoRepository, + private val memoRepository: MemoRepository, ) { public operator fun invoke(memoId: String?): Flow> { if (memoId.isNullOrBlank()) return flowOf(Result.success(null)) - return flow { emitAll(repository.getById(memoId)) } - .mapLatest { memo -> memo?.takeUnless { it.isDelete } } - .mapLatest { Result.success(it) } - .catch { emit(Result.failure(it)) } + return flow { + memoRepository + .getById(memoId) + .mapLatest { memo -> memo?.takeUnless { it.isDelete } } + .also { emitAll(it) } + }.mapLatest { + Result.success(it) + }.catch { emit(Result.failure(it)) } } } diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt index ba8e8a57..4d044904 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt @@ -1,20 +1,25 @@ package io.github.taetae98coding.diary.domain.memo.usecase import io.github.taetae98coding.diary.domain.backup.usecase.PushMemoBackupQueueUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyGroupRepository import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first import org.koin.core.annotation.Factory @Factory public class FinishMemoUseCase internal constructor( private val pushMemoBackupQueueUseCase: PushMemoBackupQueueUseCase, - private val repository: MemoRepository, + private val memoRepository: MemoRepository, + private val memoBuddyGroupRepository: MemoBuddyGroupRepository, ) { public suspend operator fun invoke(memoId: String?): Result { return runCatching { if (memoId.isNullOrBlank()) return@runCatching - repository.updateFinish(memoId, true) - pushMemoBackupQueueUseCase(memoId) + memoRepository.updateFinish(memoId, true) + if (!memoBuddyGroupRepository.isBuddyGroupMemo(memoId).first()) { + pushMemoBackupQueueUseCase(memoId) + } } } } diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt index c413acf8..8f6d3711 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt @@ -1,20 +1,25 @@ package io.github.taetae98coding.diary.domain.memo.usecase import io.github.taetae98coding.diary.domain.backup.usecase.PushMemoBackupQueueUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyGroupRepository import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first import org.koin.core.annotation.Factory @Factory public class RestartMemoUseCase internal constructor( private val pushMemoBackupQueueUseCase: PushMemoBackupQueueUseCase, - private val repository: MemoRepository, + private val memoRepository: MemoRepository, + private val memoBuddyGroupRepository: MemoBuddyGroupRepository, ) { public suspend operator fun invoke(memoId: String?): Result { return runCatching { if (memoId.isNullOrBlank()) return@runCatching - repository.updateFinish(memoId, false) - pushMemoBackupQueueUseCase(memoId) + memoRepository.updateFinish(memoId, false) + if (!memoBuddyGroupRepository.isBuddyGroupMemo(memoId).first()) { + pushMemoBackupQueueUseCase(memoId) + } } } } diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt index 813e25b4..35fb8043 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt @@ -2,6 +2,7 @@ package io.github.taetae98coding.diary.domain.memo.usecase import io.github.taetae98coding.diary.core.model.memo.MemoDetail import io.github.taetae98coding.diary.domain.backup.usecase.PushMemoBackupQueueUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyGroupRepository import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository import kotlinx.coroutines.flow.first import org.koin.core.annotation.Factory @@ -9,19 +10,22 @@ import org.koin.core.annotation.Factory @Factory public class UpdateMemoUseCase internal constructor( private val pushMemoBackupQueueUseCase: PushMemoBackupQueueUseCase, - private val repository: MemoRepository, + private val memoRepository: MemoRepository, + private val memoBuddyGroupRepository: MemoBuddyGroupRepository, ) { public suspend operator fun invoke(memoId: String?, detail: MemoDetail): Result { return runCatching { if (memoId.isNullOrBlank()) return@runCatching - val memo = repository.getById(memoId).first() ?: return@runCatching + val memo = memoRepository.getById(memoId).first() ?: return@runCatching val validDetail = detail.copy(title = detail.title.ifBlank { memo.detail.title }) if (memo.detail == validDetail) return@runCatching - repository.update(memoId, validDetail) - pushMemoBackupQueueUseCase(memoId) + memoRepository.update(memoId, validDetail) + if (!memoBuddyGroupRepository.isBuddyGroupMemo(memoId).first()) { + pushMemoBackupQueueUseCase(memoId) + } } } } diff --git a/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/DeleteMemoUseCaseTest.kt b/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/DeleteMemoUseCaseTest.kt index db37f075..9657d9f9 100644 --- a/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/DeleteMemoUseCaseTest.kt +++ b/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/DeleteMemoUseCaseTest.kt @@ -1,6 +1,7 @@ package io.github.taetae98coding.diary.domain.memo import io.github.taetae98coding.diary.domain.backup.usecase.PushMemoBackupQueueUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyGroupRepository import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository import io.github.taetae98coding.diary.domain.memo.usecase.DeleteMemoUseCase import io.kotest.core.spec.style.BehaviorSpec @@ -8,15 +9,21 @@ import io.kotest.matchers.result.shouldBeSuccess import io.mockk.Called import io.mockk.clearAllMocks import io.mockk.coVerifyOrder +import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.flow.flowOf class DeleteMemoUseCaseTest : BehaviorSpec() { private val pushMemoBackupQueueUseCase = mockk(relaxed = true, relaxUnitFun = true) private val memoRepository = mockk(relaxed = true, relaxUnitFun = true) + private val memoBuddyGroupRepository = mockk { + every { isBuddyGroupMemo(any()) } returns flowOf(false) + } private val useCase = DeleteMemoUseCase( pushMemoBackupQueueUseCase = pushMemoBackupQueueUseCase, - repository = memoRepository, + memoRepository = memoRepository, + memoBuddyGroupRepository = memoBuddyGroupRepository, ) init { @@ -37,7 +44,7 @@ class DeleteMemoUseCaseTest : BehaviorSpec() { } } - clearAllMocks() + clearAllMocks(answers = false) } } @@ -58,7 +65,7 @@ class DeleteMemoUseCaseTest : BehaviorSpec() { } } - clearAllMocks() + clearAllMocks(answers = false) } } } diff --git a/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/FindMemoUseCaseTest.kt b/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/FindMemoUseCaseTest.kt index fb26e2de..c295ac16 100644 --- a/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/FindMemoUseCaseTest.kt +++ b/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/FindMemoUseCaseTest.kt @@ -15,7 +15,9 @@ import kotlinx.coroutines.flow.flowOf class FindMemoUseCaseTest : BehaviorSpec() { private val memoRepository = mockk() - private val useCase = FindMemoUseCase(repository = memoRepository) + private val useCase = FindMemoUseCase( + memoRepository = memoRepository, + ) init { Given("memoId is null") { diff --git a/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/FinishMemoUseCaseTest.kt b/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/FinishMemoUseCaseTest.kt index 013db6b5..32693d4e 100644 --- a/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/FinishMemoUseCaseTest.kt +++ b/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/FinishMemoUseCaseTest.kt @@ -1,6 +1,7 @@ package io.github.taetae98coding.diary.domain.memo import io.github.taetae98coding.diary.domain.backup.usecase.PushMemoBackupQueueUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyGroupRepository import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository import io.github.taetae98coding.diary.domain.memo.usecase.FinishMemoUseCase import io.kotest.core.spec.style.BehaviorSpec @@ -8,15 +9,21 @@ import io.kotest.matchers.result.shouldBeSuccess import io.mockk.Called import io.mockk.clearAllMocks import io.mockk.coVerifyOrder +import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.flow.flowOf class FinishMemoUseCaseTest : BehaviorSpec() { private val pushMemoBackupQueueUseCase = mockk(relaxed = true, relaxUnitFun = true) private val memoRepository = mockk(relaxed = true, relaxUnitFun = true) + private val memoBuddyGroupRepository = mockk { + every { isBuddyGroupMemo(any()) } returns flowOf(false) + } private val useCase = FinishMemoUseCase( pushMemoBackupQueueUseCase = pushMemoBackupQueueUseCase, - repository = memoRepository, + memoRepository = memoRepository, + memoBuddyGroupRepository = memoBuddyGroupRepository, ) init { @@ -36,7 +43,8 @@ class FinishMemoUseCaseTest : BehaviorSpec() { pushMemoBackupQueueUseCase wasNot Called } } - clearAllMocks() + + clearAllMocks(answers = false) } } @@ -56,7 +64,8 @@ class FinishMemoUseCaseTest : BehaviorSpec() { pushMemoBackupQueueUseCase(memoId) } } - clearAllMocks() + + clearAllMocks(answers = false) } } } diff --git a/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/RestartMemoUseCaseTest.kt b/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/RestartMemoUseCaseTest.kt index 59fa545e..2a839569 100644 --- a/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/RestartMemoUseCaseTest.kt +++ b/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/RestartMemoUseCaseTest.kt @@ -1,6 +1,7 @@ package io.github.taetae98coding.diary.domain.memo import io.github.taetae98coding.diary.domain.backup.usecase.PushMemoBackupQueueUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyGroupRepository import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository import io.github.taetae98coding.diary.domain.memo.usecase.RestartMemoUseCase import io.kotest.core.spec.style.BehaviorSpec @@ -8,15 +9,21 @@ import io.kotest.matchers.result.shouldBeSuccess import io.mockk.Called import io.mockk.clearAllMocks import io.mockk.coVerifyOrder +import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.flow.flowOf class RestartMemoUseCaseTest : BehaviorSpec() { private val pushMemoBackupQueueUseCase = mockk(relaxed = true, relaxUnitFun = true) private val memoRepository = mockk(relaxed = true, relaxUnitFun = true) + private val memoBuddyGroupRepository = mockk { + every { isBuddyGroupMemo(any()) } returns flowOf(false) + } private val useCase = RestartMemoUseCase( pushMemoBackupQueueUseCase = pushMemoBackupQueueUseCase, - repository = memoRepository, + memoRepository = memoRepository, + memoBuddyGroupRepository = memoBuddyGroupRepository, ) init { @@ -36,7 +43,8 @@ class RestartMemoUseCaseTest : BehaviorSpec() { pushMemoBackupQueueUseCase wasNot Called } } - clearAllMocks() + + clearAllMocks(answers = false) } } @@ -56,7 +64,8 @@ class RestartMemoUseCaseTest : BehaviorSpec() { pushMemoBackupQueueUseCase(memoId) } } - clearAllMocks() + + clearAllMocks(answers = false) } } } diff --git a/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/UpdateMemoUseCaseTest.kt b/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/UpdateMemoUseCaseTest.kt index a4f4d849..f56b5c39 100644 --- a/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/UpdateMemoUseCaseTest.kt +++ b/app/domain/memo/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/memo/UpdateMemoUseCaseTest.kt @@ -2,6 +2,7 @@ package io.github.taetae98coding.diary.domain.memo import io.github.taetae98coding.diary.core.model.memo.MemoDetail import io.github.taetae98coding.diary.domain.backup.usecase.PushMemoBackupQueueUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyGroupRepository import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository import io.github.taetae98coding.diary.domain.memo.usecase.UpdateMemoUseCase import io.kotest.core.spec.style.BehaviorSpec @@ -15,9 +16,13 @@ import kotlinx.coroutines.flow.flowOf class UpdateMemoUseCaseTest : BehaviorSpec() { private val pushMemoBackupQueueUseCase = mockk(relaxed = true, relaxUnitFun = true) private val memoRepository = mockk(relaxed = true, relaxUnitFun = true) + private val memoBuddyGroupRepository = mockk { + every { isBuddyGroupMemo(any()) } returns flowOf(false) + } private val useCase = UpdateMemoUseCase( pushMemoBackupQueueUseCase = pushMemoBackupQueueUseCase, - repository = memoRepository, + memoRepository = memoRepository, + memoBuddyGroupRepository = memoBuddyGroupRepository, ) init { @@ -47,7 +52,7 @@ class UpdateMemoUseCaseTest : BehaviorSpec() { } } - clearAllMocks() + clearAllMocks(answers = false) } } @@ -72,7 +77,7 @@ class UpdateMemoUseCaseTest : BehaviorSpec() { } } - clearAllMocks() + clearAllMocks(answers = false) } } } @@ -105,7 +110,7 @@ class UpdateMemoUseCaseTest : BehaviorSpec() { coVerify { memoRepository.update(any(), detail.copy(title = "title")) } } - clearAllMocks() + clearAllMocks(answers = false) } } } diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinScreen.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinScreen.kt index 9ddc1fb1..aa943958 100644 --- a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinScreen.kt +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinScreen.kt @@ -223,7 +223,7 @@ private fun Message( when { uiState.isLoginFinish -> onLoginFinish() uiState.isExistEmail -> state.showMessage("์ด๋ฏธ ์‚ฌ์šฉ๋˜๋Š” ์ด๋ฉ”์ผ์ด์—์š” ${Emoji.invalid.random()}") - uiState.isNetworkError -> state.showMessage("๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š” ${Emoji.fail.random()}") + uiState.isNetworkError -> state.showMessage("๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š” ${Emoji.fail.random()}") uiState.isUnknownError -> state.showMessage("์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š” ${Emoji.error.random()}") } diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginScreen.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginScreen.kt index 733f0ada..bdbde26c 100644 --- a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginScreen.kt +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginScreen.kt @@ -114,7 +114,7 @@ private fun Message( when { uiState.isLoginFinish -> onLoginFinish() uiState.isAccountNotFound -> state.showMessage("๊ณ„์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์–ด์š” ${Emoji.fail.random()}") - uiState.isNetworkError -> state.showMessage("๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š” ${Emoji.fail.random()}") + uiState.isNetworkError -> state.showMessage("๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š” ${Emoji.fail.random()}") uiState.isUnknownError -> state.showMessage("์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š” ${Emoji.error.random()}") } diff --git a/app/feature/buddy/build.gradle.kts b/app/feature/buddy/build.gradle.kts index a87512ea..68e0f5ac 100644 --- a/app/feature/buddy/build.gradle.kts +++ b/app/feature/buddy/build.gradle.kts @@ -9,6 +9,7 @@ kotlin { implementation(project(":app:core:calendar-compose")) implementation(project(":app:domain:account")) + implementation(project(":app:domain:memo")) 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 812d8e20..249c3cdd 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 @@ -12,50 +12,59 @@ import io.github.taetae98coding.diary.core.navigation.buddy.BuddyGroupCalendarDe 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.core.navigation.buddy.BuddyTagHomeDestination 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.feature.buddy.tag.BuddyTagHomeRoute import io.github.taetae98coding.diary.library.navigation.LocalDateNavType -import kotlin.reflect.typeOf import kotlinx.datetime.LocalDate +import kotlin.reflect.typeOf @OptIn(ExperimentalMaterial3AdaptiveApi::class) public fun NavGraphBuilder.buddyNavigation( - navController: NavController, + navController: NavController, ) { - navigation( - startDestination = BuddyHomeDestination, - ) { - composable { backStackEntry -> - BuddyHomeRoute( - navigateToBuddyCalendar = { navController.navigate(BuddyGroupCalendarDestination(it)) }, - onScaffoldValueChange = { backStackEntry.savedStateHandle["app_navigation_visible"] = it.isListVisible() }, - ) - } + navigation( + startDestination = BuddyHomeDestination, + ) { + composable { backStackEntry -> + BuddyHomeRoute( + navigateToBuddyCalendarHome = { navController.navigate(BuddyGroupCalendarDestination(it)) }, + navigateToBuddyTagHome = { navController.navigate(BuddyTagHomeDestination(it)) }, + onScaffoldValueChange = { backStackEntry.savedStateHandle["app_navigation_visible"] = it.isListVisible() }, + ) + } + + composable { + val route = it.toRoute() - composable { - val route = it.toRoute() + BuddyGroupCalendarRoute( + navigateUp = navController::popBackStack, + navigateToBuddyGroupMemoAdd = { navController.navigate(BuddyGroupMemoAddDestination(route.groupId, it.start, it.endInclusive)) }, + navigateToBuddyGroupMemoDetail = { navController.navigate(BuddyGroupMemoDetailDestination(it)) }, + ) + } - 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( - typeMap = mapOf(typeOf() to LocalDateNavType), - ) { - BuddyGroupMemoAddRoute( - navigateUp = navController::popBackStack, - ) - } + composable { + BuddyGroupMemoDetailRoute( + navigateUp = navController::popBackStack, + ) + } - composable { - BuddyGroupMemoDetailRoute( - navigateUp = navController::popBackStack, - ) - } - } + composable { + BuddyTagHomeRoute( + 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 75b9e3d4..13892bbb 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,7 +1,5 @@ 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 @@ -11,15 +9,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 index b0420b71..248a1969 100644 --- 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 @@ -23,31 +23,31 @@ import org.koin.android.annotation.KoinViewModel @OptIn(ExperimentalCoroutinesApi::class) @KoinViewModel internal class BuddyGroupCalendarHolidayViewModel( - findHolidayUseCase: FindHolidayUseCase, + findHolidayUseCase: FindHolidayUseCase, ) : ViewModel() { - private val yearAndMonth = MutableStateFlow?>(null) + 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(), - ) + 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) - } - } + 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 index eb2e32b3..280a208b 100644 --- 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 @@ -26,45 +26,45 @@ import org.koin.android.annotation.KoinViewModel @OptIn(ExperimentalCoroutinesApi::class) @KoinViewModel internal class BuddyGroupCalendarMemoViewModel( - savedStateHandle: SavedStateHandle, - private val pageBuddyGroupCalendarMemo: PageBuddyGroupCalendarMemo, + savedStateHandle: SavedStateHandle, + private val pageBuddyGroupCalendarMemo: PageBuddyGroupCalendarMemo, ) : ViewModel() { - private val route = savedStateHandle.toRoute() - private val yearAndMonth = MutableStateFlow?>(null) + private val route = savedStateHandle.toRoute() + private val yearAndMonth = MutableStateFlow?>(null) - private val refreshCount = MutableStateFlow(0) + 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(), - ) + 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() } + .filterNotNull() + .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 fetchMemo(year: Int, month: Month) { + viewModelScope.launch { + yearAndMonth.emit(year to month) + } + } - fun refresh() { - refreshCount.value++ - } + 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 index 9a98f6ac..dad83983 100644 --- 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 @@ -16,59 +16,58 @@ 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(), + 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() + 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() - } - }, - ) + 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, - ) + Fetch( + state = state, + memoViewModel = memoViewModel, + holidayViewModel = holidayViewModel, + ) - LifecycleStartEffect(memoViewModel) { - memoViewModel.refresh() - onStopOrDispose { - - } - } + LifecycleStartEffect(memoViewModel) { + memoViewModel.refresh() + onStopOrDispose { + } + } } @Composable private fun Fetch( - state: CalendarScaffoldState, - memoViewModel: BuddyGroupCalendarMemoViewModel, - holidayViewModel: BuddyGroupCalendarHolidayViewModel, + 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) - } + 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/common/BuddyBottomSheet.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/common/BuddyBottomSheet.kt index 6256f04f..b1c00513 100644 --- a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/common/BuddyBottomSheet.kt +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/common/BuddyBottomSheet.kt @@ -90,7 +90,7 @@ internal fun BuddyBottomSheet( is BuddyBottomSheetUiState.NetworkError -> { item(contentType = "MessageBox") { MessageBox( - message = "๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๐Ÿน", + message = "๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š” ๐Ÿน", modifier = Modifier.animateItem(), ) } 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 70e71e10..42e86029 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 @@ -37,201 +37,201 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun BuddyDetailScreen( - state: BuddyDetailScreenState, - uiStateProvider: () -> BuddyDetailScreenUiState, - buddyUiStateProvider: () -> List?, - buddyBottomSheetUiStateProvider: () -> BuddyBottomSheetUiState, - modifier: Modifier = Modifier, - topBarTitle: @Composable () -> Unit = {}, - navigationIcon: @Composable () -> Unit = {}, - floatingActionButton: @Composable () -> Unit = {}, + state: BuddyDetailScreenState, + uiStateProvider: () -> BuddyDetailScreenUiState, + buddyUiStateProvider: () -> List?, + buddyBottomSheetUiStateProvider: () -> BuddyBottomSheetUiState, + modifier: Modifier = Modifier, + topBarTitle: @Composable () -> Unit = {}, + navigationIcon: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, ) { - 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, - ) + 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 = state.onTag, + 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 = {}, + 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), - ) - } + 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 9237d561..844d94d5 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 @@ -10,51 +10,52 @@ 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() - - 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) } - } + 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 onTag: () -> Unit, + 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 index 2e6d980c..9f1abccf 100644 --- 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 @@ -10,20 +10,22 @@ import io.github.taetae98coding.diary.feature.buddy.common.rememberBuddyBottomSh @Composable internal fun rememberBuddyDetailScreenDetailState( - onCalendar: () -> Unit, + onTag: () -> Unit, + onCalendar: () -> Unit, ): BuddyDetailScreenState.Detail { - val coroutineScope = rememberCoroutineScope() - val drawerState = rememberDrawerState(DrawerValue.Closed) - val componentState = rememberDiaryComponentState() - val buddyBottomSheetState = rememberBuddyBottomSheetState() + 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, - ) - } + return remember { + BuddyDetailScreenState.Detail( + onTag = onTag, + 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 aa9f5881..6a4cea50 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,5 +1,8 @@ package io.github.taetae98coding.diary.feature.buddy.home +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut import androidx.compose.material3.IconButton import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -21,6 +24,7 @@ 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.back.KBackHandler 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 @@ -30,138 +34,182 @@ 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.BuddyListUiState import io.github.taetae98coding.diary.feature.buddy.list.BuddyListViewModel import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun BuddyHomeRoute( - navigateToBuddyCalendar: (String) -> Unit, - onScaffoldValueChange: (ThreePaneScaffoldValue) -> Unit, - modifier: Modifier = Modifier, - listViewModel: BuddyListViewModel = koinViewModel(), - addViewModel: BuddyAddViewModel = koinViewModel(), + navigateToBuddyCalendarHome: (String) -> Unit, + navigateToBuddyTagHome: (String) -> Unit, + onScaffoldValueChange: (ThreePaneScaffoldValue) -> Unit, + modifier: Modifier = Modifier, + listViewModel: BuddyListViewModel = koinViewModel(), + addViewModel: BuddyAddViewModel = koinViewModel(), ) { - val windowAdaptiveInfo = currentWindowAdaptiveInfo() - val navigator = rememberListDetailPaneScaffoldNavigator(scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)) - - 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( - 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( - 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 } } - - FloatingAddButton( - onClick = { addViewModel.add(state.detail) }, - progressProvider = { isProgress }, - ) - } - } - ) - - FetchAccount( - addViewModel = addViewModel, - state = state, - ) - } - }, - modifier = modifier, - ) - - LaunchedScaffoldValue( - navigator = navigator, - onScaffoldValueChange = onScaffoldValueChange, - ) + val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val navigator = rememberListDetailPaneScaffoldNavigator(scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)) + + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + val listUiState by listViewModel.listUiState.collectAsStateWithLifecycle() + + BuddyListScreen( + listUiStateProvider = { listUiState }, + onGroup = { navigator.navigateTo(ThreePaneScaffoldRole.Primary, it) }, + floatingActionButton = { + val isVisible by remember { + derivedStateOf { + val isAdd = if (navigator.isDetailVisible()) { + navigator.currentDestination?.content != null + } else { + true + } + val isValidUiState = listUiState is BuddyListUiState.State || listUiState is BuddyListUiState.Loading + + isAdd && isValidUiState + } + } + + AnimatedVisibility( + visible = isVisible, + enter = scaleIn(), + exit = scaleOut(), + ) { + 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( + onTag = { navigator.currentDestination?.content?.let(navigateToBuddyTagHome) }, + onCalendar = { navigator.currentDestination?.content?.let(navigateToBuddyCalendarHome) }, + ) + } + val uiState by addViewModel.uiState.collectAsStateWithLifecycle() + val buddyUiState by addViewModel.buddyUiState.collectAsStateWithLifecycle() + val buddyBottomSheetState by addViewModel.buddyBottomSheetUiState.collectAsStateWithLifecycle() + + 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 = { + AnimatedVisibility( + visible = isAdd, + enter = scaleIn(), + exit = scaleOut(), + ) { + val isProgress by remember { derivedStateOf { uiState.isProgress } } + + FloatingAddButton( + onClick = { addViewModel.add(state.detail) }, + progressProvider = { isProgress }, + ) + } + }, + ) + + FetchAccount( + addViewModel = addViewModel, + state = state, + ) + + LaunchedDrawerState( + navigator = navigator, + state = state, + ) + } + }, + modifier = modifier, + ) + + LaunchedScaffoldValue( + navigator = navigator, + onScaffoldValueChange = onScaffoldValueChange, + ) + + NavigateUp(navigator = navigator) +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +private fun NavigateUp( + navigator: ThreePaneScaffoldNavigator<*>, +) { + KBackHandler( + isEnabled = navigator.canNavigateBack(), + onBack = navigator::navigateBack, + ) } @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) + } +} + +@ExperimentalMaterial3AdaptiveApi +@Composable +private fun LaunchedDrawerState( + navigator: ThreePaneScaffoldNavigator, + state: BuddyDetailScreenState, ) { - LaunchedEffect(state.buddyBottomSheetState.email) { - addViewModel.fetch(state.buddyBottomSheetState.email) - } + LaunchedEffect(navigator.currentDestination?.content, state) { + if (state is BuddyDetailScreenState.Detail) { + state.drawerState.close() + } + } } 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 140964e5..19e7380d 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 @@ -15,107 +15,143 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.compose.error.NetworkError +import io.github.taetae98coding.diary.core.compose.error.UnknownError 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( - listProvider: () -> List?, - onGroup: (String) -> Unit, - modifier: Modifier = Modifier, - floatingActionButton: @Composable () -> Unit = {}, + listUiStateProvider: () -> BuddyListUiState, + onGroup: (String) -> Unit, + modifier: Modifier = Modifier, + floatingActionButton: @Composable () -> Unit = {}, ) { - Scaffold( - modifier = modifier, - topBar = { TopAppBar(title = { Text(text = "๋ฒ„๋””") }) }, - floatingActionButton = floatingActionButton, - ) { - Content( - listProvider = listProvider, - onGroup = onGroup, - modifier = Modifier.fillMaxSize() - .padding(it), - ) - } + Scaffold( + modifier = modifier, + topBar = { TopAppBar(title = { Text(text = "๋ฒ„๋””") }) }, + floatingActionButton = floatingActionButton, + ) { + Content( + listUiStateProvider = listUiStateProvider, + onGroup = onGroup, + modifier = Modifier + .fillMaxSize() + .padding(it), + ) + } } @Composable private fun Content( - listProvider: () -> List?, - onGroup: (String) -> Unit, - modifier: Modifier = Modifier, + listUiStateProvider: () -> BuddyListUiState, + onGroup: (String) -> Unit, + modifier: Modifier = Modifier, ) { - LazyColumn( - modifier = modifier, - contentPadding = DiaryTheme.dimen.screenPaddingValues, - verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), - ) { - val list = listProvider() + LazyColumn( + modifier = modifier, + contentPadding = DiaryTheme.dimen.screenPaddingValues, + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + when (val uiState = listUiStateProvider()) { + is BuddyListUiState.Loading -> { + items( + count = 5, + contentType = { "BuddyGroup" }, + ) { + BuddyListItem( + group = null, + onClick = {}, + modifier = Modifier.animateItem(), + ) + } + } - 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(), - ) - } - } - } + is BuddyListUiState.NetworkError -> { + item( + key = "NetworkError", + contentType = "NetworkError", + ) { + NetworkError( + onRetry = uiState.retry, + modifier = Modifier + .fillParentMaxSize() + .animateItem(), + ) + } + } + + is BuddyListUiState.UnknownError -> { + item( + key = "UnknownError", + contentType = "UnknownError", + ) { + UnknownError( + onRetry = uiState.retry, + modifier = Modifier + .fillParentMaxSize() + .animateItem(), + ) + } + } + + is BuddyListUiState.State -> { + if (uiState.list.isEmpty()) { + item( + key = "Empty", + contentType = "Empty", + ) { + Box( + modifier = Modifier + .fillParentMaxSize() + .animateItem(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "๊ทธ๋ฃน์ด ์—†์–ด์š” ๐Ÿญ", + style = DiaryTheme.typography.headlineMedium, + ) + } + } + } else { + items( + items = uiState.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, + 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, - ) - } - } + 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/list/BuddyListUiState.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/list/BuddyListUiState.kt new file mode 100644 index 00000000..970f1ff1 --- /dev/null +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/list/BuddyListUiState.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.feature.buddy.list + +import io.github.taetae98coding.diary.core.model.buddy.BuddyGroup + +internal sealed class BuddyListUiState { + data object Loading : BuddyListUiState() + + data class NetworkError( + val retry: () -> Unit, + ) : BuddyListUiState() + + data class UnknownError( + val retry: () -> Unit, + ) : BuddyListUiState() + + data class State( + val list: List, + ) : BuddyListUiState() +} diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/list/BuddyListViewModel.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/list/BuddyListViewModel.kt index 7ca8e914..be8b904a 100644 --- a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/list/BuddyListViewModel.kt +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/list/BuddyListViewModel.kt @@ -2,23 +2,44 @@ package io.github.taetae98coding.diary.feature.buddy.list import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.github.taetae98coding.diary.domain.buddy.usecase.FindBuddyGroupUseCase +import io.github.taetae98coding.diary.common.exception.ext.isNetworkException +import io.github.taetae98coding.diary.domain.buddy.usecase.PageBuddyGroupUseCase import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel @OptIn(ExperimentalCoroutinesApi::class) @KoinViewModel internal class BuddyListViewModel( - findBuddyGroupUseCase: FindBuddyGroupUseCase, + pageBuddyGroupUseCase: PageBuddyGroupUseCase, ) : ViewModel() { - val groupList = findBuddyGroupUseCase() - .mapLatest { it.getOrNull() } + private val retryFlow = MutableStateFlow(0) + + val listUiState = retryFlow.flatMapLatest { pageBuddyGroupUseCase() } + .mapLatest { result -> + if (result.isSuccess) { + BuddyListUiState.State(result.getOrThrow()) + } else { + when (val exception = result.exceptionOrNull()) { + is Exception if (exception.isNetworkException()) -> BuddyListUiState.NetworkError(::retry) + else -> BuddyListUiState.UnknownError(::retry) + } + } + } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, + initialValue = BuddyListUiState.Loading, ) + + private fun retry() { + viewModelScope.launch { + retryFlow.emit(retryFlow.value + 1) + } + } } 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 index b5a0b6eb..3e613e6d 100644 --- 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 @@ -8,7 +8,7 @@ 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.MemoDetailMultiPaneScaffold 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 @@ -16,33 +16,33 @@ import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun BuddyGroupMemoAddRoute( - navigateUp: () -> Unit, - modifier: Modifier = Modifier, - addViewModel: BuddyGroupMemoAddViewModel = koinViewModel(), + 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() + 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 } } + MemoDetailMultiPaneScaffold( + 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 }, - ) - }, - ) + 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 index 832d1af3..ca4ab489 100644 --- 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 @@ -7,14 +7,13 @@ 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.compose.tag.card.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 @@ -24,118 +23,119 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate import org.koin.android.annotation.KoinViewModel +import kotlin.reflect.typeOf @KoinViewModel internal class BuddyGroupMemoAddViewModel( - savedStateHandle: SavedStateHandle, - private val addBuddyGroupMemoUseCase: AddBuddyGroupMemoUseCase, + savedStateHandle: SavedStateHandle, + private val addBuddyGroupMemoUseCase: AddBuddyGroupMemoUseCase, ) : ViewModel() { - val route = savedStateHandle.toRoute( - typeMap = mapOf(typeOf() to LocalDateNavType), - ) + val route = savedStateHandle.toRoute( + typeMap = mapOf(typeOf() to LocalDateNavType), + ) - private val _uiState = MutableStateFlow(MemoDetailScaffoldUiState(onMessageShow = ::clearMessage)) - val uiState = _uiState.asStateFlow() + 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 selectedTag = MutableStateFlow(setOfNotNull(savedStateHandle.get(MemoAddDestination.SELECTED_TAG))) + private val primaryTag = MutableStateFlow(null) - private val tagPageList = MutableStateFlow(emptyList()) + 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 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, - ) + 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 + 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) - } - } + 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 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 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 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/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 index 1dc88813..fcb886f2 100644 --- 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 @@ -2,14 +2,104 @@ package io.github.taetae98coding.diary.feature.buddy.memo.detail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import io.github.taetae98coding.diary.common.exception.ext.isNetworkException +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.buddy.BuddyGroupMemoDetailDestination +import io.github.taetae98coding.diary.domain.memo.usecase.DeleteMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.FindMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.FinishMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.RestartMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.UpdateMemoUseCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel +@OptIn(ExperimentalCoroutinesApi::class) @KoinViewModel internal class BuddyGroupMemoDetailViewModel( - savedStateHandle: SavedStateHandle, + savedStateHandle: SavedStateHandle, + findMemoUseCase: FindMemoUseCase, + private val finishMemoUseCase: FinishMemoUseCase, + private val restartMemoUseCase: RestartMemoUseCase, + private val deleteMemoUseCase: DeleteMemoUseCase, + private val updateMemoUseCase: UpdateMemoUseCase, ) : ViewModel() { - private val route = savedStateHandle.toRoute() + private val route = savedStateHandle.toRoute() + private val _uiState = MutableStateFlow(MemoDetailScaffoldUiState(onMessageShow = ::clearMessage)) + val uiState = _uiState.asStateFlow() + + val memo = findMemoUseCase(route.memoId) + .mapLatest { it.getOrNull() } + .filterNotNull() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) + + val detail = memo.mapLatest { it?.detail } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) + + fun onFinishChange(isFinish: Boolean) { + viewModelScope.launch { + if (isFinish) { + finishMemoUseCase(route.memoId) + .onFailure { handleThrowable(it) } + } else { + restartMemoUseCase(route.memoId) + .onFailure { handleThrowable(it) } + } + } + } + + fun delete() { + viewModelScope.launch { + deleteMemoUseCase(route.memoId) + .onSuccess { _uiState.update { it.copy(isDelete = true) } } + .onFailure { handleThrowable(it) } + } + } + + fun update(detail: MemoDetail) { + viewModelScope.launch { + updateMemoUseCase(route.memoId, detail) + .onSuccess { _uiState.update { it.copy(isUpdate = true) } } + .onFailure { handleThrowable(it) } + } + } + + private fun clearMessage() { + _uiState.update { + it.copy( + isDelete = false, + isUpdate = false, + isNetworkError = false, + isUnknownError = false, + ) + } + } + + private fun handleThrowable(throwable: Throwable) { + _uiState.update { + when (throwable) { + is Exception if throwable.isNetworkException() -> it.copy(isNetworkError = true) + else -> it.copy(isUnknownError = true) + } + } + } } 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 index 6cb82c91..27daaa4d 100644 --- 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 @@ -1,34 +1,58 @@ package io.github.taetae98coding.diary.feature.buddy.memo.detail +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconToggleButton +import androidx.compose.material3.Text 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 io.github.taetae98coding.diary.core.compose.memo.MemoListDetailPaneScaffold -import io.github.taetae98coding.diary.core.compose.memo.detail.MemoDetailScaffoldUiState +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.taetae98coding.diary.core.compose.memo.MemoDetailMultiPaneScaffold import io.github.taetae98coding.diary.core.compose.memo.detail.rememberMemoDetailScaffoldDetailState -import io.github.taetae98coding.diary.core.compose.topbar.TopBarTitle +import io.github.taetae98coding.diary.core.design.system.icon.DeleteIcon +import io.github.taetae98coding.diary.core.design.system.icon.FinishIcon import org.koin.compose.viewmodel.koinViewModel @Composable internal fun BuddyGroupMemoDetailRoute( - navigateUp: () -> Unit, - modifier: Modifier = Modifier, - detailViewModel: BuddyGroupMemoDetailViewModel = koinViewModel(), + navigateUp: () -> Unit, + modifier: Modifier = Modifier, + detailViewModel: BuddyGroupMemoDetailViewModel = koinViewModel(), ) { - val state = rememberMemoDetailScaffoldDetailState( - onDelete = { }, - onUpdate = { }, - detailProvider = { null }, - ) + val memo by detailViewModel.memo.collectAsStateWithLifecycle() + val detail by detailViewModel.detail.collectAsStateWithLifecycle() + val state = rememberMemoDetailScaffoldDetailState( + onDelete = navigateUp, + onUpdate = navigateUp, + detailProvider = { detail }, + ) + val uiState by detailViewModel.uiState.collectAsStateWithLifecycle() - MemoListDetailPaneScaffold( - onNavigateUp = navigateUp, - onDetailTag = { }, - detailScaffoldStateProvider = { state }, - detailUiStateProvider = { MemoDetailScaffoldUiState() }, - detailTagListProvider = { null }, - detailTitle = { TopBarTitle(text = "") }, - onTagAdd = {}, - tagListProvider = { null }, - modifier = modifier, - ) + MemoDetailMultiPaneScaffold( + onNavigateUp = { detailViewModel.update(state.detail) }, + onDetailTag = { }, + detailScaffoldStateProvider = { state }, + detailUiStateProvider = { uiState }, + detailTagListProvider = { null }, + detailTitle = { Text(text = detail?.title.orEmpty()) }, + onTagAdd = {}, + tagListProvider = { null }, + modifier = modifier, + detailActions = { + val isFinished by remember { derivedStateOf { memo?.isFinish == true } } + + IconToggleButton( + checked = isFinished, + onCheckedChange = { detailViewModel.onFinishChange(it) }, + ) { + FinishIcon() + } + + IconButton(onClick = detailViewModel::delete) { + DeleteIcon() + } + }, + ) } diff --git a/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/tag/BuddyTagHomeRoute.kt b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/tag/BuddyTagHomeRoute.kt new file mode 100644 index 00000000..21c49b25 --- /dev/null +++ b/app/feature/buddy/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/buddy/tag/BuddyTagHomeRoute.kt @@ -0,0 +1,26 @@ +package io.github.taetae98coding.diary.feature.buddy.tag + +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +internal fun BuddyTagHomeRoute( + navigateUp: () -> Unit, + modifier: Modifier = Modifier, +) { +// TagListMultiPaneScaffold( +// navigator = rememberListDetailPaneScaffoldNavigator(), +// onListTagRestart = {}, +// onListTagRestore = {}, +// listScaffoldUiStateProvider = { TagListScaffoldUiState() }, +// listUiStateProvider = { TagListUiState.Loading }, +// modifier = modifier, +// listNavigationIcon = { +// IconButton(onClick = navigateUp) { +// NavigateUpIcon() +// } +// }, +// ) +} 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 3431169e..3ad2f7db 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 @@ -13,49 +13,49 @@ 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 = rememberCalendarScaffoldState( - onFilter = navigateToCalendarFilter, - ) - 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() - CalendarScaffold( - state = state, - onSelectDate = navigateToMemoAdd, - 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: CalendarScaffoldState, - 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/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 1eac9076..04870f02 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 @@ -8,7 +8,7 @@ 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.MemoDetailMultiPaneScaffold 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 @@ -16,36 +16,36 @@ 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 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() + 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 } } + MemoDetailMultiPaneScaffold( + 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 }, - ) - }, - ) + 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 89c950f7..2d9c9ac6 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 @@ -7,13 +7,12 @@ 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.compose.tag.card.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.library.navigation.LocalDateNavType -import kotlin.reflect.typeOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -25,122 +24,123 @@ 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(MemoDetailScaffoldUiState(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 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 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, - ) + 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/detail/MemoDetailRoute.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailRoute.kt index 97df92a1..f9a80a7e 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,43 +1,65 @@ package io.github.taetae98coding.diary.feature.memo.detail +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconToggleButton import androidx.compose.material3.Text 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.memo.MemoListDetailPaneScaffold +import io.github.taetae98coding.diary.core.compose.memo.MemoDetailMultiPaneScaffold import io.github.taetae98coding.diary.core.compose.memo.detail.rememberMemoDetailScaffoldDetailState +import io.github.taetae98coding.diary.core.design.system.icon.DeleteIcon +import io.github.taetae98coding.diary.core.design.system.icon.FinishIcon 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 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() + val memo by detailViewModel.memo.collectAsStateWithLifecycle() + 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 }, - ) + MemoDetailMultiPaneScaffold( + onNavigateUp = { detailViewModel.update(state.detail) }, + onDetailTag = navigateToTagDetail, + detailScaffoldStateProvider = { state }, + detailUiStateProvider = { uiState }, + detailTagListProvider = { primaryTagList }, + detailTitle = { Text(text = detail?.title.orEmpty()) }, + onTagAdd = navigateToTagAdd, + tagListProvider = { tagList }, + modifier = modifier, + detailActions = { + val isFinished by remember { derivedStateOf { memo?.isFinish == true } } + + IconToggleButton( + checked = isFinished, + onCheckedChange = { detailViewModel.onFinishChange(it) }, + ) { + FinishIcon() + } + + IconButton(onClick = detailViewModel::delete) { + DeleteIcon() + } + }, + ) } 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 f4f7d79c..3614f6d8 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,7 +4,7 @@ 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.compose.tag.card.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 @@ -63,7 +63,7 @@ internal class MemoDetailTagViewModel( val tagList = memoTagList .mapCollectionLatest { - TagCardItemUiState( + 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 6c44521a..7669eddc 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 @@ -38,7 +38,7 @@ internal class MemoDetailViewModel( private val _uiState = MutableStateFlow(MemoDetailScaffoldUiState(onMessageShow = ::clearMessage)) val uiState = _uiState.asStateFlow() - private val memo = + val memo = findMemoUseCase(route.memoId) .mapLatest { it.getOrNull() } .filterNotNull() @@ -57,7 +57,7 @@ internal class MemoDetailViewModel( initialValue = null, ) - private fun onFinishChange(isFinish: Boolean) { + fun onFinishChange(isFinish: Boolean) { viewModelScope.launch { if (isFinish) { finishMemoUseCase(route.memoId).onFailure { handleThrowable() } @@ -67,7 +67,7 @@ internal class MemoDetailViewModel( } } - private fun delete() { + fun delete() { viewModelScope.launch { deleteMemoUseCase(route.memoId) .onSuccess { _uiState.update { it.copy(isDelete = true) } } diff --git a/app/feature/tag/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenPreview.kt b/app/feature/tag/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenPreview.kt deleted file mode 100644 index d268f916..00000000 --- a/app/feature/tag/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenPreview.kt +++ /dev/null @@ -1,55 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.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.tag.TagDetail -import io.github.taetae98coding.diary.feature.tag.add.rememberTagDetailScreenAddState -import io.github.taetae98coding.diary.library.color.randomArgb - -@DiaryPreview -@Composable -private fun AddPreview() { - DiaryTheme { - TagDetailScreen( - state = rememberTagDetailScreenAddState(), - titleProvider = { "Title" }, - navigateButtonProvider = { TagDetailNavigationButton.NavigateUp(onNavigateUp = {}) }, - actionButtonProvider = { TagDetailActionButton.None }, - floatingButtonProvider = { TagDetailFloatingButton.Add(onAdd = {}) }, - uiStateProvider = { TagDetailScreenUiState() }, - ) - } -} - -@DiaryPreview -@Composable -private fun DetailPreview() { - DiaryTheme { - TagDetailScreen( - state = rememberTagDetailScreenDetailState( - onUpdate = {}, - onDelete = {}, - onMemo = {}, - detailProvider = { - TagDetail( - title = "Title", - description = "Description", - color = randomArgb(), - ) - }, - ), - titleProvider = { "Title" }, - navigateButtonProvider = { TagDetailNavigationButton.NavigateUp(onNavigateUp = {}) }, - actionButtonProvider = { - TagDetailActionButton.FinishAndDetail( - isFinish = true, - onFinishChange = {}, - delete = {}, - ) - }, - floatingButtonProvider = { TagDetailFloatingButton.None }, - uiStateProvider = { TagDetailScreenUiState() }, - ) - } -} diff --git a/app/feature/tag/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreenPreview.kt b/app/feature/tag/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreenPreview.kt deleted file mode 100644 index 8ebdc040..00000000 --- a/app/feature/tag/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreenPreview.kt +++ /dev/null @@ -1,57 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.list - -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 - -@DiaryPreview -@Composable -private fun LoadingPreview() { - DiaryTheme { - TagListScreen( - state = rememberTagListScreenState(), - floatingButtonProvider = { TagListFloatingButton.Add(onAdd = {}) }, - listProvider = { null }, - onTag = {}, - uiStateProvider = { TagListScreenUiState() }, - ) - } -} - -@DiaryPreview -@Composable -private fun EmptyPreview() { - DiaryTheme { - TagListScreen( - state = rememberTagListScreenState(), - floatingButtonProvider = { TagListFloatingButton.Add(onAdd = {}) }, - listProvider = { emptyList() }, - onTag = {}, - uiStateProvider = { TagListScreenUiState() }, - ) - } -} - -@DiaryPreview -@Composable -private fun Preview() { - DiaryTheme { - TagListScreen( - state = rememberTagListScreenState(), - floatingButtonProvider = { TagListFloatingButton.Add(onAdd = {}) }, - listProvider = { - List(10) { - TagListItemUiState( - id = it.toString(), - title = "Title $it", - finish = SkipProperty {}, - delete = SkipProperty {}, - ) - } - }, - onTag = {}, - uiStateProvider = { TagListScreenUiState() }, - ) - } -} diff --git a/app/feature/tag/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreenPreview.kt b/app/feature/tag/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreenPreview.kt deleted file mode 100644 index cdf73bbb..00000000 --- a/app/feature/tag/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreenPreview.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.memo - -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 kotlinx.datetime.LocalDate - -@DiaryPreview -@Composable -private fun LoadingPreview() { - DiaryTheme { - TagMemoScreen( - state = rememberTagMemoScreenState(), - navigateButtonProvider = { TagMemoNavigateButton.NavigateUp(onNavigateUp = {}) }, - uiStateProvider = { TagMemoScreenUiState() }, - onAdd = {}, - listProvider = { null }, - onMemo = {}, - ) - } -} - -@DiaryPreview -@Composable -private fun EmptyPreview() { - DiaryTheme { - TagMemoScreen( - state = rememberTagMemoScreenState(), - navigateButtonProvider = { TagMemoNavigateButton.NavigateUp(onNavigateUp = {}) }, - uiStateProvider = { TagMemoScreenUiState() }, - onAdd = {}, - listProvider = { emptyList() }, - onMemo = {}, - ) - } -} - -@DiaryPreview -@Composable -private fun Preview() { - DiaryTheme { - TagMemoScreen( - state = rememberTagMemoScreenState(), - navigateButtonProvider = { TagMemoNavigateButton.NavigateUp(onNavigateUp = {}) }, - uiStateProvider = { TagMemoScreenUiState() }, - onAdd = {}, - listProvider = { - List(10) { - MemoListItemUiState( - id = it.toString(), - title = "Title $it", - dateRange = if (it % 3 == 0) { - null - } else if (it % 3 == 1) { - LocalDate(2000, 1, 1)..LocalDate(2000, 1, 1) - } else { - LocalDate(2000, 1, 1)..LocalDate(2000, 1, 2) - }, - finish = SkipProperty {}, - delete = SkipProperty {}, - ) - } - }, - onMemo = {}, - ) - } -} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagNavigation.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagNavigation.kt index 1434d95d..55b801b9 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagNavigation.kt +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/TagNavigation.kt @@ -28,12 +28,7 @@ public fun NavGraphBuilder.tagNavigation( composable { backStackEntry -> TagHomeRoute( navigateToMemoAdd = { navController.navigate(MemoAddDestination(selectedTag = it)) }, - navigateToMemoDetail = { - navController.navigate( - route = MemoDetailDestination(it), - navOptions = navOptions { launchSingleTop = true }, - ) - }, + navigateToMemoDetail = { navController.navigate(MemoDetailDestination(it),) }, onScaffoldValueChange = { backStackEntry.savedStateHandle["app_navigation_visible"] = it.isListVisible() }, ) } diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddRoute.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddRoute.kt index 8aa0e745..f6cb4479 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddRoute.kt +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddRoute.kt @@ -9,43 +9,47 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.github.taetae98coding.diary.feature.tag.detail.TagDetailActionButton -import io.github.taetae98coding.diary.feature.tag.detail.TagDetailFloatingButton -import io.github.taetae98coding.diary.feature.tag.detail.TagDetailNavigationButton -import io.github.taetae98coding.diary.feature.tag.detail.TagDetailScreen +import io.github.taetae98coding.diary.core.compose.tag.add.rememberTagDetailScaffoldAddState +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffold +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldActions +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldFloatingButton +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldNavigationIcon import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun TagAddRoute( - navigateUp: () -> Unit, - modifier: Modifier = Modifier, - addViewModel: TagAddViewModel = koinViewModel(), + navigateUp: () -> Unit, + modifier: Modifier = Modifier, + addViewModel: TagAddViewModel = koinViewModel(), ) { - val navigator = rememberListDetailPaneScaffoldNavigator() + val navigator = rememberListDetailPaneScaffoldNavigator() - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), - value = navigator.scaffoldValue, - listPane = { - AnimatedPane { - val state = rememberTagDetailScreenAddState() - val uiState by addViewModel.uiState.collectAsStateWithLifecycle() + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + val state = rememberTagDetailScaffoldAddState() + val uiState by addViewModel.uiState.collectAsStateWithLifecycle() - TagDetailScreen( - state = state, - titleProvider = { "ํƒœ๊ทธ ์ถ”๊ฐ€" }, - navigateButtonProvider = { - TagDetailNavigationButton.NavigateUp(onNavigateUp = navigateUp) - }, - actionButtonProvider = { TagDetailActionButton.None }, - floatingButtonProvider = { TagDetailFloatingButton.Add(onAdd = { addViewModel.add(state.tagDetail) }) }, - uiStateProvider = { uiState }, - ) - } - }, - detailPane = { - }, - modifier = modifier, - ) + TagDetailScaffold( + state = state, + titleProvider = { "ํƒœ๊ทธ ์ถ”๊ฐ€" }, + navigationIconProvider = { TagDetailScaffoldNavigationIcon.NavigateUp(navigateUp = navigateUp) }, + actionsProvider = { TagDetailScaffoldActions.None }, + floatingButtonProvider = { + TagDetailScaffoldFloatingButton.Add( + isInProgress = uiState.isAddInProgress, + add = { uiState.add(state.tagDetail) }, + ) + }, + uiStateProvider = { uiState }, + ) + } + }, + detailPane = { + }, + modifier = modifier, + ) } diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddViewModel.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddViewModel.kt index 338837b8..86ec0e1d 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddViewModel.kt +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/add/TagAddViewModel.kt @@ -3,9 +3,9 @@ package io.github.taetae98coding.diary.feature.tag.add import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.github.taetae98coding.diary.common.exception.tag.TagTitleBlankException +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldUiState import io.github.taetae98coding.diary.core.model.tag.TagDetail import io.github.taetae98coding.diary.domain.tag.usecase.AddTagUseCase -import io.github.taetae98coding.diary.feature.tag.detail.TagDetailScreenUiState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -14,34 +14,39 @@ import org.koin.android.annotation.KoinViewModel @KoinViewModel internal class TagAddViewModel( - private val addTagUseCase: AddTagUseCase, + private val addTagUseCase: AddTagUseCase, ) : ViewModel() { - private val _uiState = MutableStateFlow(TagDetailScreenUiState(onMessageShow = ::clearMessage)) - val uiState = _uiState.asStateFlow() + private val _uiState = MutableStateFlow( + TagDetailScaffoldUiState.Add( + add = ::add, + clearState = ::clearState, + ), + ) + val uiState = _uiState.asStateFlow() - fun add(detail: TagDetail) { - viewModelScope.launch { - _uiState.update { it.copy(isProgress = true) } - addTagUseCase(detail) - .onSuccess { _uiState.update { it.copy(isProgress = false, isAdd = true) } } - .onFailure(::handleThrowable) - } - } + private fun add(detail: TagDetail) { + viewModelScope.launch { + _uiState.update { it.copy(isAddInProgress = true) } + addTagUseCase(detail) + .onSuccess { _uiState.update { it.copy(isAddInProgress = false, isAddFinish = true) } } + .onFailure(::handleThrowable) + } + } - private fun handleThrowable(throwable: Throwable) { - when (throwable) { - is TagTitleBlankException -> _uiState.update { it.copy(isProgress = false, isTitleBlankError = true) } - else -> _uiState.update { it.copy(isProgress = false, isUnknownError = true) } - } - } + private fun handleThrowable(throwable: Throwable) { + when (throwable) { + is TagTitleBlankException -> _uiState.update { it.copy(isAddInProgress = false, isTitleBlankError = true) } + else -> _uiState.update { it.copy(isAddInProgress = false, isUnknownError = true) } + } + } - private fun clearMessage() { - _uiState.update { - it.copy( - isAdd = false, - isTitleBlankError = false, - isUnknownError = false, - ) - } - } + private fun clearState() { + _uiState.update { + it.copy( + isAddFinish = false, + isTitleBlankError = false, + isUnknownError = false, + ) + } + } } diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailActionButton.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailActionButton.kt deleted file mode 100644 index 2116362a..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailActionButton.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.detail - -internal sealed class TagDetailActionButton { - data object None : TagDetailActionButton() - - data class FinishAndDetail( - val isFinish: Boolean, - val onFinishChange: (Boolean) -> Unit, - val delete: () -> Unit, - ) : TagDetailActionButton() -} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailFloatingButton.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailFloatingButton.kt deleted file mode 100644 index 5e77de56..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailFloatingButton.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.detail - -internal sealed class TagDetailFloatingButton { - data object None : TagDetailFloatingButton() - - data class Add( - val onAdd: () -> Unit, - ) : TagDetailFloatingButton() -} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailNavigationButton.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailNavigationButton.kt deleted file mode 100644 index a79b8477..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailNavigationButton.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.detail - -internal sealed class TagDetailNavigationButton { - data object None : TagDetailNavigationButton() - - data class NavigateUp( - val onNavigateUp: () -> Unit, - ) : TagDetailNavigationButton() -} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailRoute.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailRoute.kt index 12236608..5fea5592 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailRoute.kt +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailRoute.kt @@ -6,93 +6,107 @@ 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.ThreePaneScaffoldNavigator 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.feature.tag.memo.TagMemoNavigateButton -import io.github.taetae98coding.diary.feature.tag.memo.TagMemoScreen +import io.github.taetae98coding.diary.core.compose.back.KBackHandler +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffold +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldActions +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldNavigationIcon +import io.github.taetae98coding.diary.core.compose.tag.detail.rememberTagDetailScaffoldDetailState +import io.github.taetae98coding.diary.core.compose.tag.memo.TagMemoNavigateIcon +import io.github.taetae98coding.diary.core.compose.tag.memo.TagMemoScaffold +import io.github.taetae98coding.diary.core.compose.tag.memo.rememberTagMemoScaffoldState import io.github.taetae98coding.diary.feature.tag.memo.TagMemoViewModel -import io.github.taetae98coding.diary.feature.tag.memo.rememberTagMemoScreenState import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun TagDetailRoute( - navigateUp: () -> Unit, - navigateToMemoAdd: () -> Unit, - navigateToMemoDetail: (String) -> Unit, - modifier: Modifier = Modifier, - detailViewModel: TagDetailViewModel = koinViewModel(), - memoViewModel: TagMemoViewModel = koinViewModel(), + navigateUp: () -> Unit, + navigateToMemoAdd: () -> Unit, + navigateToMemoDetail: (String) -> Unit, + modifier: Modifier = Modifier, + detailViewModel: TagDetailViewModel = koinViewModel(), + memoViewModel: TagMemoViewModel = koinViewModel(), ) { - val windowAdaptiveInfo = currentWindowAdaptiveInfo() - val navigator = rememberListDetailPaneScaffoldNavigator(scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)) + val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val navigator = rememberListDetailPaneScaffoldNavigator(scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)) - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), - value = navigator.scaffoldValue, - listPane = { - AnimatedPane { - val tagDetail by detailViewModel.tagDetail.collectAsStateWithLifecycle() - val state = rememberTagDetailScreenDetailState( - onUpdate = navigateUp, - onDelete = navigateUp, - onMemo = { navigator.navigateTo(ThreePaneScaffoldRole.Primary) }, - detailProvider = { tagDetail }, - ) - val actionButton by detailViewModel.actionButton.collectAsStateWithLifecycle() - val uiState by detailViewModel.uiState.collectAsStateWithLifecycle() + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective.copy(defaultPanePreferredWidth = 500.dp), + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + val tagDetail by detailViewModel.tagDetail.collectAsStateWithLifecycle() + val uiState by detailViewModel.uiState.collectAsStateWithLifecycle() + val actionsUiState by detailViewModel.actionsUiState.collectAsStateWithLifecycle() + val state = rememberTagDetailScaffoldDetailState( + onUpdate = navigateUp, + onDelete = navigateUp, + navigateToMemo = { navigator.navigateTo(ThreePaneScaffoldRole.Primary) }, + detailProvider = { tagDetail }, + ) - TagDetailScreen( - state = state, - titleProvider = { tagDetail?.title }, - navigateButtonProvider = { - TagDetailNavigationButton.NavigateUp( - onNavigateUp = { detailViewModel.update(state.tagDetail) }, - ) - }, - actionButtonProvider = { actionButton }, - floatingButtonProvider = { TagDetailFloatingButton.None }, - uiStateProvider = { uiState }, - ) - } - }, - detailPane = { - val isNavigateUpVisible by remember(windowAdaptiveInfo) { - derivedStateOf { - if (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { - !navigator.isListVisible() - } else { - true - } - } - } + TagDetailScaffold( + state = state, + uiStateProvider = { uiState }, + titleProvider = { tagDetail?.title }, + navigationIconProvider = { + TagDetailScaffoldNavigationIcon.NavigateUp( + navigateUp = { detailViewModel.update(state.tagDetail) }, + ) + }, + actionsProvider = { + TagDetailScaffoldActions.FinishAndDelete( + isFinish = actionsUiState.isFinish, + finish = actionsUiState.finish, + restart = actionsUiState.restart, + delete = actionsUiState.delete, + ) + }, + ) + } + }, + detailPane = { + AnimatedPane { + val uiState by memoViewModel.uiState.collectAsStateWithLifecycle() + val memoList by memoViewModel.memoList.collectAsStateWithLifecycle() - val uiState by memoViewModel.uiState.collectAsStateWithLifecycle() - val list by memoViewModel.memoList.collectAsStateWithLifecycle() + TagMemoScaffold( + state = rememberTagMemoScaffoldState(), + uiStateProvider = { uiState }, + listUiStateProvider = { memoList }, + onAdd = navigateToMemoAdd, + onMemo = navigateToMemoDetail, + navigateIconProvider = { + if (navigator.isListVisible()) { + TagMemoNavigateIcon.None + } else { + TagMemoNavigateIcon.NavigateUp(navigator::navigateBack) + } + }, + ) + } + }, + modifier = modifier, + ) - TagMemoScreen( - state = rememberTagMemoScreenState(), - navigateButtonProvider = { - if (isNavigateUpVisible) { - TagMemoNavigateButton.NavigateUp(onNavigateUp = navigator::navigateBack) - } else { - TagMemoNavigateButton.None - } - }, - uiStateProvider = { uiState }, - onAdd = navigateToMemoAdd, - listProvider = { list }, - onMemo = navigateToMemoDetail, - ) - }, - modifier = modifier, - ) + NavigateUp(navigator = navigator) +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +private fun NavigateUp( + navigator: ThreePaneScaffoldNavigator<*>, +) { + KBackHandler( + isEnabled = navigator.canNavigateBack(), + onBack = navigator::navigateBack, + ) } diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenUiState.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenUiState.kt deleted file mode 100644 index 64fd3af0..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailScreenUiState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.detail - -internal data class TagDetailScreenUiState( - val isProgress: Boolean = false, - val isAdd: Boolean = false, - val isDelete: Boolean = false, - val isUpdate: Boolean = false, - val isTitleBlankError: Boolean = false, - val isUnknownError: Boolean = false, - val onMessageShow: () -> Unit = {}, -) { - val hasMessage = isAdd || isDelete || isUpdate || isTitleBlankError || isUnknownError -} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailViewModel.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailViewModel.kt index d0d51743..1677250e 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailViewModel.kt +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/detail/TagDetailViewModel.kt @@ -3,6 +3,8 @@ package io.github.taetae98coding.diary.feature.tag.detail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldActionsUiState +import io.github.taetae98coding.diary.core.compose.tag.detail.TagDetailScaffoldUiState import io.github.taetae98coding.diary.core.model.tag.TagDetail import io.github.taetae98coding.diary.core.navigation.tag.TagDetailDestination import io.github.taetae98coding.diary.domain.tag.usecase.DeleteTagUseCase @@ -26,99 +28,104 @@ import org.koin.android.annotation.KoinViewModel @OptIn(ExperimentalCoroutinesApi::class) @KoinViewModel internal class TagDetailViewModel( - private val savedStateHandle: SavedStateHandle, - private val findTagUseCase: FindTagUseCase, - private val finishTagUseCase: FinishTagUseCase, - private val restartTagUseCase: RestartTagUseCase, - private val deleteTagUseCase: DeleteTagUseCase, - private val updateTagUseCase: UpdateTagUseCase, + private val savedStateHandle: SavedStateHandle, + private val findTagUseCase: FindTagUseCase, + private val finishTagUseCase: FinishTagUseCase, + private val restartTagUseCase: RestartTagUseCase, + private val deleteTagUseCase: DeleteTagUseCase, + private val updateTagUseCase: UpdateTagUseCase, ) : ViewModel() { - private val tagId = savedStateHandle.getStateFlow(TagDetailDestination.TAG_ID, null) + private val tagId = savedStateHandle.getStateFlow(TagDetailDestination.TAG_ID, null) - private val tag = - tagId - .flatMapLatest { findTagUseCase(it) } - .filterNotNull() - .mapLatest { it.getOrNull() } - .mapNotNull { it } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, - ) + private val tag = tagId + .flatMapLatest { findTagUseCase(it) } + .filterNotNull() + .mapLatest { it.getOrNull() } + .mapNotNull { it } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) - private val _uiState = MutableStateFlow(TagDetailScreenUiState(onMessageShow = ::clearMessage)) - val uiState = _uiState.asStateFlow() + private val _uiState = MutableStateFlow( + TagDetailScaffoldUiState.Detail( + update = ::update, + clearState = ::clearState, + ), + ) - val tagDetail = - tag - .mapLatest { it?.detail } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, - ) + val uiState = _uiState.asStateFlow() - val actionButton = - tag - .mapLatest { - TagDetailActionButton.FinishAndDetail( - isFinish = it?.isFinish ?: false, - onFinishChange = ::onFinishChange, - delete = ::delete, - ) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = - TagDetailActionButton.FinishAndDetail( - isFinish = false, - onFinishChange = ::onFinishChange, - delete = ::delete, - ), - ) + val actionsUiState = tag.mapLatest { + TagDetailScaffoldActionsUiState( + isFinish = it?.isFinish == true, + finish = ::finish, + restart = ::restart, + delete = ::delete, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = TagDetailScaffoldActionsUiState( + isFinish = false, + finish = ::finish, + restart = ::restart, + delete = ::delete, + ), + ) - private fun onFinishChange(isFinish: Boolean) { - viewModelScope.launch { - if (isFinish) { - finishTagUseCase(tagId.value).onFailure { handleThrowable() } - } else { - restartTagUseCase(tagId.value).onFailure { handleThrowable() } - } - } - } + val tagDetail = tag + .mapLatest { it?.detail } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) - private fun delete() { - viewModelScope.launch { - deleteTagUseCase(tagId.value) - .onSuccess { _uiState.update { it.copy(isDelete = true) } } - .onFailure { handleThrowable() } - } - } + private fun finish() { + viewModelScope.launch { + finishTagUseCase(tagId.value).onFailure { handleThrowable() } + } + } - private fun handleThrowable() { - _uiState.update { it.copy(isUnknownError = true) } - } + private fun restart() { + viewModelScope.launch { + restartTagUseCase(tagId.value).onFailure { handleThrowable() } + } + } - private fun clearMessage() { - _uiState.update { - it.copy( - isUpdate = false, - isDelete = false, - isUnknownError = false, - ) - } - } + private fun delete() { + viewModelScope.launch { + deleteTagUseCase(tagId.value) + .onSuccess { _uiState.update { it.copy(isDeleteFinish = true) } } + .onFailure { handleThrowable() } + } + } - fun update(detail: TagDetail) { - viewModelScope.launch { - updateTagUseCase(tagId.value, detail) - .onSuccess { _uiState.update { it.copy(isUpdate = true) } } - .onFailure { handleThrowable() } - } - } + private fun handleThrowable() { + _uiState.update { it.copy(isUnknownError = true) } + } - fun fetch(tagId: String?) { - savedStateHandle[TagDetailDestination.TAG_ID] = tagId - } + private fun clearState() { + _uiState.update { + it.copy( + isUpdateFinish = false, + isDeleteFinish = false, + isUnknownError = false, + ) + } + } + + fun update(detail: TagDetail) { + viewModelScope.launch { + updateTagUseCase(tagId.value, detail) + .onSuccess { _uiState.update { it.copy(isUpdateFinish = true) } } + .onFailure { handleThrowable() } + } + } + + fun fetch(tagId: String?) { + savedStateHandle[TagDetailDestination.TAG_ID] = tagId + } } diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/home/TagHomeNavigate.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/home/TagHomeNavigate.kt deleted file mode 100644 index 6052e120..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/home/TagHomeNavigate.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.home - -internal sealed class TagHomeNavigate { - data object None : TagHomeNavigate() - - data object Add : TagHomeNavigate() - - data class Tag( - val tagId: String, - ) : TagHomeNavigate() -} 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 8fadc532..72de4e8b 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 @@ -1,44 +1,19 @@ package io.github.taetae98coding.diary.feature.tag.home 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.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue 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.back.KBackHandler +import io.github.taetae98coding.diary.core.compose.tag.TagListMultiPaneScaffold import io.github.taetae98coding.diary.feature.tag.add.TagAddViewModel -import io.github.taetae98coding.diary.feature.tag.add.rememberTagDetailScreenAddState -import io.github.taetae98coding.diary.feature.tag.detail.TagDetailActionButton -import io.github.taetae98coding.diary.feature.tag.detail.TagDetailFloatingButton -import io.github.taetae98coding.diary.feature.tag.detail.TagDetailNavigationButton -import io.github.taetae98coding.diary.feature.tag.detail.TagDetailScreen import io.github.taetae98coding.diary.feature.tag.detail.TagDetailViewModel -import io.github.taetae98coding.diary.feature.tag.detail.rememberTagDetailScreenDetailState -import io.github.taetae98coding.diary.feature.tag.list.TagListFloatingButton -import io.github.taetae98coding.diary.feature.tag.list.TagListScreen import io.github.taetae98coding.diary.feature.tag.list.TagListViewModel -import io.github.taetae98coding.diary.feature.tag.list.rememberTagListScreenState -import io.github.taetae98coding.diary.feature.tag.memo.TagMemoNavigateButton -import io.github.taetae98coding.diary.feature.tag.memo.TagMemoScreen import io.github.taetae98coding.diary.feature.tag.memo.TagMemoViewModel -import io.github.taetae98coding.diary.feature.tag.memo.rememberTagMemoScreenState import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3AdaptiveApi::class) @@ -53,199 +28,28 @@ internal fun TagHomeRoute( detailViewModel: TagDetailViewModel = koinViewModel(), memoViewModel: TagMemoViewModel = koinViewModel(), ) { - val windowAdaptiveInfo = currentWindowAdaptiveInfo() - val navigator = rememberListDetailPaneScaffoldNavigator(scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)) - - 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 - } - } - } - - 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) - } - }, - 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.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 }, - ) - - LaunchedEffect(tagHomeNavigate) { - when (tagHomeNavigate) { - is TagHomeNavigate.Add -> { - 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() - - 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, - ) - } - }, - ) - - LaunchedScaffoldValue( + val navigator = rememberListDetailPaneScaffoldNavigator() + val listScreenUiState by listViewModel.uiState.collectAsStateWithLifecycle() + val listUiState by listViewModel.list.collectAsStateWithLifecycle() + val addUiState by addViewModel.uiState.collectAsStateWithLifecycle() + val tagDetail by detailViewModel.tagDetail.collectAsStateWithLifecycle() + val detailUiState by detailViewModel.uiState.collectAsStateWithLifecycle() + val detailActionsUiState by detailViewModel.actionsUiState.collectAsStateWithLifecycle() + val tagMemoListUiState by memoViewModel.memoList.collectAsStateWithLifecycle() + val tagMemoUiState by memoViewModel.uiState.collectAsStateWithLifecycle() + + TagListMultiPaneScaffold( + navigateToMemoAdd = navigateToMemoAdd, + navigateToMemoDetail = navigateToMemoDetail, navigator = navigator, - onScaffoldValueChange = onScaffoldValueChange, + listScaffoldUiStateProvider = { listScreenUiState }, + listUiStateProvider = { listUiState }, + addUiStateProvider = { addUiState }, + detailProvider = { tagDetail }, + detailUiStateProvider = { detailUiState }, + detailActionsUiStateProvider = { detailActionsUiState }, + tagMemoUiStateProvider = { tagMemoUiState }, + tagMemoListUiStateProvider = { tagMemoListUiState }, ) LaunchedFetch( @@ -254,12 +58,13 @@ internal fun TagHomeRoute( memoViewModel = memoViewModel, ) - KBackHandler( - isEnabled = navigator.canNavigateBack(), - onBack = navigator::navigateBack, + LaunchedScaffoldValue( + navigator = navigator, + onScaffoldValueChange = onScaffoldValueChange, ) } + @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable private fun LaunchedFetch( diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListFloatingButton.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListFloatingButton.kt deleted file mode 100644 index 2958713e..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListFloatingButton.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.list - -internal sealed class TagListFloatingButton { - data object None : TagListFloatingButton() - - data class Add( - val onAdd: () -> Unit, - ) : TagListFloatingButton() -} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreen.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreen.kt deleted file mode 100644 index a818b9c9..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreen.kt +++ /dev/null @@ -1,200 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.list - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.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.compose.swipe.FinishAndDeleteSwipeBox -import io.github.taetae98coding.diary.core.design.system.emoji.Emoji -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, -) { - 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), - ) - } - - Message( - state = state, - uiStateProvider = uiStateProvider, - ) -} - -@Composable -private fun Message( - state: TagListScreenState, - uiStateProvider: () -> TagListScreenUiState, -) { - val uiState = uiStateProvider() - - 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) - } - } - - !uiState.deleteTagId.isNullOrBlank() -> { - state.showMessage( - message = "ํƒœ๊ทธ ์‚ญ์ œ ${Emoji.congratulate.random()}", - actionLabel = "์ทจ์†Œ", - ) { - uiState.restoreTag(uiState.deleteTagId) - } - } - - uiState.isUnknownError -> state.showMessage("์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š” ${Emoji.error.random()}") - } - - uiState.onMessageShow() - } -} - -@Composable -private fun Content( - listProvider: () -> List?, - onTag: (String) -> Unit, - modifier: Modifier = Modifier, -) { - LazyColumn( - modifier = modifier, - contentPadding = DiaryTheme.dimen.screenPaddingValues, - verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), - ) { - val list = listProvider() - - if (list == null) { - items( - count = 5, - contentType = { "Tag" }, - ) { - TagItem( - 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, -) { - 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/list/TagListScreenUiState.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreenUiState.kt deleted file mode 100644 index bd7ea03a..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListScreenUiState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.list - -internal data class TagListScreenUiState( - val finishTagId: String? = null, - val deleteTagId: String? = null, - val isUnknownError: Boolean = false, - val restartTag: (String) -> Unit = {}, - val restoreTag: (String) -> Unit = {}, - val onMessageShow: () -> Unit = {}, -) { - val hasMessage = !finishTagId.isNullOrBlank() || !deleteTagId.isNullOrBlank() || isUnknownError -} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListViewModel.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListViewModel.kt index 7cb9a76e..a8e0a7d9 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListViewModel.kt +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/list/TagListViewModel.kt @@ -2,17 +2,21 @@ package io.github.taetae98coding.diary.feature.tag.list import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.common.exception.ext.isNetworkException import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty +import io.github.taetae98coding.diary.core.compose.tag.list.TagListItemUiState +import io.github.taetae98coding.diary.core.compose.tag.list.TagListScaffoldUiState +import io.github.taetae98coding.diary.core.compose.tag.list.TagListUiState import io.github.taetae98coding.diary.domain.tag.usecase.DeleteTagUseCase import io.github.taetae98coding.diary.domain.tag.usecase.FinishTagUseCase import io.github.taetae98coding.diary.domain.tag.usecase.PageTagUseCase import io.github.taetae98coding.diary.domain.tag.usecase.RestartTagUseCase import io.github.taetae98coding.diary.domain.tag.usecase.RestoreTagUseCase -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.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -22,73 +26,100 @@ import org.koin.android.annotation.KoinViewModel @OptIn(ExperimentalCoroutinesApi::class) @KoinViewModel internal class TagListViewModel( - pageTagUseCase: PageTagUseCase, - private val finishTagUseCase: FinishTagUseCase, - private val deleteTagUseCase: DeleteTagUseCase, - private val restartTagUseCase: RestartTagUseCase, - private val restoreTagUseCase: RestoreTagUseCase, + pageTagUseCase: PageTagUseCase, + private val finishTagUseCase: FinishTagUseCase, + private val deleteTagUseCase: DeleteTagUseCase, + private val restartTagUseCase: RestartTagUseCase, + private val restoreTagUseCase: RestoreTagUseCase, ) : ViewModel() { - private val _uiState = - MutableStateFlow( - TagListScreenUiState( - restartTag = ::restart, - restoreTag = ::restore, - onMessageShow = ::clearMessage, - ), - ) - val uiState = _uiState.asStateFlow() + private val _uiState = MutableStateFlow(TagListScaffoldUiState(clearState = ::clearState)) + val uiState = _uiState.asStateFlow() - val list = - pageTagUseCase() - .mapLatest { it.getOrNull() } - .mapCollectionLatest { - TagListItemUiState( - id = it.id, - title = it.detail.title, - finish = SkipProperty { finish(it.id) }, - delete = SkipProperty { delete(it.id) }, - ) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, - ) + private val refreshFlow = MutableStateFlow(0) + val list = refreshFlow.flatMapLatest { pageTagUseCase() } + .mapLatest { result -> + if (result.isSuccess) { + TagListUiState.State( + list = result.getOrNull() + .orEmpty() + .map { + TagListItemUiState( + id = it.id, + title = it.detail.title, + finish = SkipProperty { finish(it.id) }, + delete = SkipProperty { delete(it.id) }, + ) + }, + ) + } else { + when (val exception = result.exceptionOrNull()) { + is Exception if exception.isNetworkException() -> TagListUiState.NetworkError(::refresh) + else -> TagListUiState.UnknownError(::refresh) + } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = TagListUiState.Loading, + ) - private fun finish(tagId: String) { - viewModelScope.launch { - finishTagUseCase(tagId) - .onSuccess { _uiState.update { it.copy(finishTagId = tagId) } } - .onFailure { _uiState.update { it.copy(isUnknownError = true) } } - } - } + private fun finish(tagId: String) { + viewModelScope.launch { + finishTagUseCase(tagId) + .onSuccess { + _uiState.update { + it.copy( + isTagFinish = true, + restart = SkipProperty { restart(tagId) }, + ) + } + } + .onFailure { _uiState.update { it.copy(isUnknownError = true) } } + } + } - private fun delete(tagId: String) { - viewModelScope.launch { - deleteTagUseCase(tagId) - .onSuccess { _uiState.update { it.copy(deleteTagId = tagId) } } - .onFailure { _uiState.update { it.copy(isUnknownError = true) } } - } - } + private fun delete(tagId: String) { + viewModelScope.launch { + deleteTagUseCase(tagId) + .onSuccess { + _uiState.update { + it.copy( + isTagDelete = true, + restore = SkipProperty { restore(tagId) }, + ) + } + } + .onFailure { _uiState.update { it.copy(isUnknownError = true) } } + } + } - private fun restart(id: String) { - viewModelScope.launch { - restartTagUseCase(id) - } - } + private fun restart(id: String) { + viewModelScope.launch { + restartTagUseCase(id) + } + } - private fun restore(id: String) { - viewModelScope.launch { - restoreTagUseCase(id) - } - } + private fun restore(id: String) { + viewModelScope.launch { + restoreTagUseCase(id) + } + } - private fun clearMessage() { - _uiState.update { - it.copy( - finishTagId = null, - deleteTagId = null, - isUnknownError = false, - ) - } - } + private fun clearState() { + _uiState.update { + it.copy( + isTagFinish = false, + isTagDelete = false, + isUnknownError = false, + restart = SkipProperty {}, + restore = SkipProperty {}, + ) + } + } + + private fun refresh() { + viewModelScope.launch { + refreshFlow.emit(refreshFlow.value + 1) + } + } } diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/RememberTagMemoScreenState.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/RememberTagMemoScreenState.kt deleted file mode 100644 index 695b9cba..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/RememberTagMemoScreenState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.memo - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope - -@Composable -internal fun rememberTagMemoScreenState(): TagMemoScreenState { - val coroutineScope = rememberCoroutineScope() - - return remember { - TagMemoScreenState(coroutineScope = coroutineScope) - } -} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoNavigateButton.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoNavigateButton.kt deleted file mode 100644 index 5eea12e1..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoNavigateButton.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.memo - -internal sealed class TagMemoNavigateButton { - data object None : TagMemoNavigateButton() - - data class NavigateUp( - val onNavigateUp: () -> Unit, - ) : TagMemoNavigateButton() -} 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 deleted file mode 100644 index 601dd5ea..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreen.kt +++ /dev/null @@ -1,208 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.memo - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.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.compose.swipe.FinishAndDeleteSwipeBox -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.theme.DiaryTheme -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, -) { - 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), - ) - } - - Message( - state = state, - uiStateProvider = uiStateProvider, - ) -} - -@Composable -private fun Message( - state: TagMemoScreenState, - uiStateProvider: () -> TagMemoScreenUiState, -) { - val uiState = uiStateProvider() - - 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) - } - } - - !uiState.deleteTagId.isNullOrBlank() -> { - state.showMessage( - message = "๋ฉ”๋ชจ ์‚ญ์ œ ${Emoji.congratulate.random()}", - actionLabel = "์ทจ์†Œ", - ) { - uiState.restoreTag(uiState.deleteTagId) - } - } - - uiState.isUnknownError -> state.showMessage("์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š” ${Emoji.error.random()}") - } - - uiState.onMessageShow() - } -} - -@Composable -private fun Content( - listProvider: () -> List?, - onMemo: (String) -> Unit, - modifier: Modifier = Modifier, -) { - LazyColumn( - modifier = modifier, - contentPadding = DiaryTheme.dimen.screenPaddingValues, - verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), - ) { - val list = listProvider() - - if (list == null) { - items( - count = 5, - contentType = { "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, -) { - 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/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreenUiState.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreenUiState.kt deleted file mode 100644 index a57f859b..00000000 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoScreenUiState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.taetae98coding.diary.feature.tag.memo - -internal data class TagMemoScreenUiState( - val finishTagId: String? = null, - val deleteTagId: String? = null, - val isUnknownError: Boolean = false, - val restartTag: (String) -> Unit = {}, - val restoreTag: (String) -> Unit = {}, - val onMessageShow: () -> Unit = {}, -) { - val hasMessage = !finishTagId.isNullOrBlank() || !deleteTagId.isNullOrBlank() || isUnknownError -} diff --git a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoViewModel.kt b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoViewModel.kt index 5cac9183..e446a03a 100644 --- a/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoViewModel.kt +++ b/app/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/memo/TagMemoViewModel.kt @@ -3,14 +3,17 @@ package io.github.taetae98coding.diary.feature.tag.memo import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.common.exception.ext.isNetworkException +import io.github.taetae98coding.diary.core.compose.memo.list.MemoListItemUiState import io.github.taetae98coding.diary.core.compose.runtime.SkipProperty +import io.github.taetae98coding.diary.core.compose.tag.memo.TagMemoListUiState +import io.github.taetae98coding.diary.core.compose.tag.memo.TagMemoScaffoldUiState import io.github.taetae98coding.diary.core.navigation.tag.TagDetailDestination import io.github.taetae98coding.diary.domain.memo.usecase.DeleteMemoUseCase import io.github.taetae98coding.diary.domain.memo.usecase.FinishMemoUseCase import io.github.taetae98coding.diary.domain.memo.usecase.RestartMemoUseCase import io.github.taetae98coding.diary.domain.memo.usecase.RestoreMemoUseCase import io.github.taetae98coding.diary.domain.tag.usecase.PageTagMemoUseCase -import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -25,88 +28,108 @@ import org.koin.android.annotation.KoinViewModel @OptIn(ExperimentalCoroutinesApi::class) @KoinViewModel internal class TagMemoViewModel( - private val savedStateHandle: SavedStateHandle, - private val pageTagMemoUseCase: PageTagMemoUseCase, - private val finishMemoUseCase: FinishMemoUseCase, - private val deleteMemoUseCase: DeleteMemoUseCase, - private val restartMemoUseCase: RestartMemoUseCase, - private val restoreMemoUseCase: RestoreMemoUseCase, + private val savedStateHandle: SavedStateHandle, + private val pageTagMemoUseCase: PageTagMemoUseCase, + private val finishMemoUseCase: FinishMemoUseCase, + private val deleteMemoUseCase: DeleteMemoUseCase, + private val restartMemoUseCase: RestartMemoUseCase, + private val restoreMemoUseCase: RestoreMemoUseCase, ) : ViewModel() { - private val tagId = savedStateHandle.getStateFlow(TagDetailDestination.TAG_ID, null) + private val tagId = savedStateHandle.getStateFlow(TagDetailDestination.TAG_ID, null) - private val _uiState = - MutableStateFlow( - TagMemoScreenUiState( - restartTag = ::restart, - restoreTag = ::restore, - onMessageShow = ::clearMessage, - ), - ) - val uiState = _uiState.asStateFlow() + private val _uiState = MutableStateFlow(TagMemoScaffoldUiState(clearState = ::clearState)) + val uiState = _uiState.asStateFlow() - val memoList = tagId - .flatMapLatest { pageTagMemoUseCase(it) } - .mapLatest { it.getOrNull() } - .mapCollectionLatest { - val start = it.detail.start - val endInclusive = it.detail.endInclusive + private val refreshFlow = MutableStateFlow(0) + val memoList = refreshFlow.flatMapLatest { + tagId.flatMapLatest { pageTagMemoUseCase(it) } + }.mapLatest { result -> + if (result.isSuccess) { + val list = result.getOrThrow() + .map { + MemoListItemUiState( + id = it.id, + title = it.detail.title, + dateRange = it.detail.dateRange, + finish = SkipProperty { finish(it.id) }, + delete = SkipProperty { delete(it.id) }, + ) + } + TagMemoListUiState.State(list) + } else { + when (val throwable = result.exceptionOrNull()) { + is Exception if throwable.isNetworkException() -> TagMemoListUiState.NetworkError(::refresh) + else -> TagMemoListUiState.UnknownError(::refresh) + } + } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = TagMemoListUiState.Loading, + ) - MemoListItemUiState( - id = it.id, - title = it.detail.title, - dateRange = if (start != null && endInclusive != null) { - start..endInclusive - } else { - null - }, - finish = SkipProperty { finish(it.id) }, - delete = SkipProperty { delete(it.id) }, - ) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, - ) + private fun finish(memoId: String) { + viewModelScope.launch { + finishMemoUseCase(memoId) + .onSuccess { + _uiState.update { + it.copy( + isFinish = true, + restartTag = { restart(memoId) }, + ) + } + } + .onFailure { _uiState.update { it.copy(isUnknownError = true) } } + } + } - private fun finish(memoId: String) { - viewModelScope.launch { - finishMemoUseCase(memoId) - .onSuccess { _uiState.update { it.copy(finishTagId = memoId) } } - .onFailure { _uiState.update { it.copy(isUnknownError = true) } } - } - } + private fun delete(memoId: String) { + viewModelScope.launch { + deleteMemoUseCase(memoId) + .onSuccess { + _uiState.update { + it.copy( + isDelete = true, + restoreTag = { restore(memoId) }, + ) + } + } + .onFailure { _uiState.update { it.copy(isUnknownError = true) } } + } + } - private fun delete(memoId: String) { - viewModelScope.launch { - deleteMemoUseCase(memoId) - .onSuccess { _uiState.update { it.copy(deleteTagId = memoId) } } - .onFailure { _uiState.update { it.copy(isUnknownError = true) } } - } - } + private fun restart(id: String) { + viewModelScope.launch { + restartMemoUseCase(id) + } + } - private fun restart(id: String) { - viewModelScope.launch { - restartMemoUseCase(id) - } - } + private fun restore(id: String) { + viewModelScope.launch { + restoreMemoUseCase(id) + } + } - private fun restore(id: String) { - viewModelScope.launch { - restoreMemoUseCase(id) - } - } + private fun clearState() { + _uiState.update { + it.copy( + isFinish = false, + isDelete = false, + isUnknownError = false, + restartTag = {}, + restoreTag = {}, + ) + } + } - private fun clearMessage() { - _uiState.update { - it.copy( - finishTagId = null, - deleteTagId = null, - isUnknownError = false, - ) - } - } + private fun refresh() { + viewModelScope.launch { + refreshFlow.value++ + } + } - fun fetch(tagId: String?) { - savedStateHandle[TagDetailDestination.TAG_ID] = tagId - } + fun fetch(tagId: String?) { + savedStateHandle[TagDetailDestination.TAG_ID] = tagId + } } diff --git a/app/platform/android/build.gradle.kts b/app/platform/android/build.gradle.kts index a572472b..3da4a07a 100644 --- a/app/platform/android/build.gradle.kts +++ b/app/platform/android/build.gradle.kts @@ -35,8 +35,8 @@ android { defaultConfig { applicationId = "io.github.taetae98coding.diary" - versionCode = 7 - versionName = "1.3.1" + versionCode = 8 + versionName = "1.3.2" } buildTypes { diff --git a/app/platform/android/dependencies/realReleaseRuntimeClasspath.txt b/app/platform/android/dependencies/realReleaseRuntimeClasspath.txt index 5118e410..ea181442 100644 --- a/app/platform/android/dependencies/realReleaseRuntimeClasspath.txt +++ b/app/platform/android/dependencies/realReleaseRuntimeClasspath.txt @@ -116,18 +116,18 @@ androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05 androidx.profileinstaller:profileinstaller:1.3.1 androidx.recyclerview:recyclerview:1.1.0 androidx.resourceinspection:resourceinspection-annotation:1.0.1 -androidx.room:room-common-jvm:2.7.0-alpha11 -androidx.room:room-common:2.7.0-alpha11 -androidx.room:room-runtime-android:2.7.0-alpha11 -androidx.room:room-runtime:2.7.0-alpha11 +androidx.room:room-common-jvm:2.7.0-alpha12 +androidx.room:room-common:2.7.0-alpha12 +androidx.room:room-runtime-android:2.7.0-alpha12 +androidx.room:room-runtime:2.7.0-alpha12 androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate:1.2.1 -androidx.sqlite:sqlite-android:2.5.0-alpha11 -androidx.sqlite:sqlite-bundled-android:2.5.0-alpha11 -androidx.sqlite:sqlite-bundled:2.5.0-alpha11 -androidx.sqlite:sqlite-framework-android:2.5.0-alpha11 -androidx.sqlite:sqlite-framework:2.5.0-alpha11 -androidx.sqlite:sqlite:2.5.0-alpha11 +androidx.sqlite:sqlite-android:2.5.0-alpha12 +androidx.sqlite:sqlite-bundled-android:2.5.0-alpha12 +androidx.sqlite:sqlite-bundled:2.5.0-alpha12 +androidx.sqlite:sqlite-framework-android:2.5.0-alpha12 +androidx.sqlite:sqlite-framework:2.5.0-alpha12 +androidx.sqlite:sqlite:2.5.0-alpha12 androidx.startup:startup-runtime:1.2.0 androidx.tracing:tracing:1.0.0 androidx.transition:transition:1.5.0 diff --git a/app/platform/android/src/main/AndroidManifest.xml b/app/platform/android/src/main/AndroidManifest.xml index 76ff8a94..02974b7f 100644 --- a/app/platform/android/src/main/AndroidManifest.xml +++ b/app/platform/android/src/main/AndroidManifest.xml @@ -1,55 +1,56 @@ + xmlns:tools="http://schemas.android.com/tools"> - - + + + android:name=".DiaryApplication" + android:enableOnBackInvokedCallback="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher" + android:theme="@style/Theme.Material3.DayNight.NoActionBar" + android:usesCleartextTraffic="true"> + android:name=".DiaryActivity" + android:launchMode="singleInstancePerTask" + android:exported="true"> - - + + + android:name=".service.DiaryFirebaseMessagingService" + android:exported="false"> - + + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + android:exported="false" + tools:node="merge"> + android:name="io.github.taetae98coding.diary.initializer.BackupManagerInitializer" + android:value="androidx.startup"/> + android:name="io.github.taetae98coding.diary.initializer.FCMManagerInitializer" + android:value="androidx.startup"/> + android:name="io.github.taetae98coding.diary.initializer.FetchManagerInitializer" + android:value="androidx.startup"/> + android:name="io.github.taetae98coding.diary.initializer.KoinInitializer" + android:value="androidx.startup"/> \ No newline at end of file diff --git a/app/platform/jvm/build.gradle.kts b/app/platform/jvm/build.gradle.kts index b180f59f..06fc0b80 100644 --- a/app/platform/jvm/build.gradle.kts +++ b/app/platform/jvm/build.gradle.kts @@ -47,7 +47,7 @@ compose { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "Diary" - packageVersion = "1.3.1" + packageVersion = "1.3.2" macOS { appStore = true diff --git a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/MemoDetailEntity.kt b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/MemoDetailEntity.kt new file mode 100644 index 00000000..cd0ab2f9 --- /dev/null +++ b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/MemoDetailEntity.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.common.model.memo + +import kotlinx.datetime.LocalDate +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class MemoDetailEntity( + @SerialName("title") + val title: String, + @SerialName("description") + val description: String, + @SerialName("start") + val start: LocalDate?, + @SerialName("endInclusive") + val endInclusive: LocalDate?, + @SerialName("color") + val color: Int, +) diff --git a/docs/images/graphs/dep_graph_app_core_account_preferences.svg b/docs/images/graphs/dep_graph_app_core_account_preferences.svg index e69de29b..9bffdb5b 100644 --- a/docs/images/graphs/dep_graph_app_core_account_preferences.svg +++ b/docs/images/graphs/dep_graph_app_core_account_preferences.svg @@ -0,0 +1,9 @@ + + + + + + :app:core:account-preferences + + + diff --git a/docs/images/graphs/dep_graph_app_core_account_preferences_datastore.svg b/docs/images/graphs/dep_graph_app_core_account_preferences_datastore.svg index e69de29b..61354813 100644 --- a/docs/images/graphs/dep_graph_app_core_account_preferences_datastore.svg +++ b/docs/images/graphs/dep_graph_app_core_account_preferences_datastore.svg @@ -0,0 +1,25 @@ + + + + + + :app:core:account-preferences-datastore + + + + :library:koin-datastore + + + + + + + + :app:core:account-preferences + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_backup_database.svg b/docs/images/graphs/dep_graph_app_core_backup_database.svg index e69de29b..260f5d31 100644 --- a/docs/images/graphs/dep_graph_app_core_backup_database.svg +++ b/docs/images/graphs/dep_graph_app_core_backup_database.svg @@ -0,0 +1,17 @@ + + + + + + :app:core:backup-database + + + + :app:core:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_backup_database_room.svg b/docs/images/graphs/dep_graph_app_core_backup_database_room.svg index e69de29b..7bd31a73 100644 --- a/docs/images/graphs/dep_graph_app_core_backup_database_room.svg +++ b/docs/images/graphs/dep_graph_app_core_backup_database_room.svg @@ -0,0 +1,57 @@ + + + + + + :app:core:backup-database-room + + + + :library:koin-room + + + + + + + + :app:core:backup-database + + + + + + + + :library:coroutines + + + + + + + + :library:room + + + + + + + + :app:core:model + + + + + + + + :library:datetime + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_calendar_compose.svg b/docs/images/graphs/dep_graph_app_core_calendar_compose.svg index e69de29b..8e9a2f5a 100644 --- a/docs/images/graphs/dep_graph_app_core_calendar_compose.svg +++ b/docs/images/graphs/dep_graph_app_core_calendar_compose.svg @@ -0,0 +1,41 @@ + + + + + + :app:core:calendar-compose + + + + :app:core:design-system + + + + + + + + :library:color + + + + + + + + :library:datetime + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_coroutines.svg b/docs/images/graphs/dep_graph_app_core_coroutines.svg index e69de29b..4b9225c7 100644 --- a/docs/images/graphs/dep_graph_app_core_coroutines.svg +++ b/docs/images/graphs/dep_graph_app_core_coroutines.svg @@ -0,0 +1,9 @@ + + + + + + :app:core:coroutines + + + diff --git a/docs/images/graphs/dep_graph_app_core_design_system.svg b/docs/images/graphs/dep_graph_app_core_design_system.svg index e69de29b..363ccbb4 100644 --- a/docs/images/graphs/dep_graph_app_core_design_system.svg +++ b/docs/images/graphs/dep_graph_app_core_design_system.svg @@ -0,0 +1,25 @@ + + + + + + :app:core:design-system + + + + :library:color + + + + + + + + :library:datetime + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_diary_database.svg b/docs/images/graphs/dep_graph_app_core_diary_database.svg index e69de29b..ef09f95c 100644 --- a/docs/images/graphs/dep_graph_app_core_diary_database.svg +++ b/docs/images/graphs/dep_graph_app_core_diary_database.svg @@ -0,0 +1,17 @@ + + + + + + :app:core:diary-database + + + + :app:core:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_diary_database_room.svg b/docs/images/graphs/dep_graph_app_core_diary_database_room.svg index e69de29b..47e4357e 100644 --- a/docs/images/graphs/dep_graph_app_core_diary_database_room.svg +++ b/docs/images/graphs/dep_graph_app_core_diary_database_room.svg @@ -0,0 +1,57 @@ + + + + + + :app:core:diary-database-room + + + + :library:koin-room + + + + + + + + :app:core:diary-database + + + + + + + + :library:coroutines + + + + + + + + :library:room + + + + + + + + :app:core:model + + + + + + + + :library:datetime + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_diary_service.svg b/docs/images/graphs/dep_graph_app_core_diary_service.svg index e69de29b..829d47e3 100644 --- a/docs/images/graphs/dep_graph_app_core_diary_service.svg +++ b/docs/images/graphs/dep_graph_app_core_diary_service.svg @@ -0,0 +1,41 @@ + + + + + + :app:core:diary-service + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_filter_database.svg b/docs/images/graphs/dep_graph_app_core_filter_database.svg index e69de29b..35054e2d 100644 --- a/docs/images/graphs/dep_graph_app_core_filter_database.svg +++ b/docs/images/graphs/dep_graph_app_core_filter_database.svg @@ -0,0 +1,9 @@ + + + + + + :app:core:filter-database + + + diff --git a/docs/images/graphs/dep_graph_app_core_filter_database_room.svg b/docs/images/graphs/dep_graph_app_core_filter_database_room.svg index e69de29b..f6b7c8b9 100644 --- a/docs/images/graphs/dep_graph_app_core_filter_database_room.svg +++ b/docs/images/graphs/dep_graph_app_core_filter_database_room.svg @@ -0,0 +1,49 @@ + + + + + + :app:core:filter-database-room + + + + :library:koin-room + + + + + + + + :app:core:filter-database + + + + + + + + :library:coroutines + + + + + + + + :library:room + + + + + + + + :library:datetime + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_holiday_database.svg b/docs/images/graphs/dep_graph_app_core_holiday_database.svg index e69de29b..9109e744 100644 --- a/docs/images/graphs/dep_graph_app_core_holiday_database.svg +++ b/docs/images/graphs/dep_graph_app_core_holiday_database.svg @@ -0,0 +1,17 @@ + + + + + + :app:core:holiday-database + + + + :app:core:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_holiday_database_room.svg b/docs/images/graphs/dep_graph_app_core_holiday_database_room.svg index e69de29b..056af84b 100644 --- a/docs/images/graphs/dep_graph_app_core_holiday_database_room.svg +++ b/docs/images/graphs/dep_graph_app_core_holiday_database_room.svg @@ -0,0 +1,57 @@ + + + + + + :app:core:holiday-database-room + + + + :library:koin-room + + + + + + + + :app:core:holiday-database + + + + + + + + :library:room + + + + + + + + :library:coroutines + + + + + + + + :app:core:model + + + + + + + + :library:datetime + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_holiday_preferences.svg b/docs/images/graphs/dep_graph_app_core_holiday_preferences.svg index e69de29b..1ce3fa19 100644 --- a/docs/images/graphs/dep_graph_app_core_holiday_preferences.svg +++ b/docs/images/graphs/dep_graph_app_core_holiday_preferences.svg @@ -0,0 +1,9 @@ + + + + + + :app:core:holiday-preferences + + + diff --git a/docs/images/graphs/dep_graph_app_core_holiday_preferences_datastore.svg b/docs/images/graphs/dep_graph_app_core_holiday_preferences_datastore.svg index e69de29b..f6cb9ebc 100644 --- a/docs/images/graphs/dep_graph_app_core_holiday_preferences_datastore.svg +++ b/docs/images/graphs/dep_graph_app_core_holiday_preferences_datastore.svg @@ -0,0 +1,33 @@ + + + + + + :app:core:holiday-preferences-datastore + + + + :library:koin-datastore + + + + + + + + :app:core:holiday-preferences + + + + + + + + :library:datetime + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_holiday_service.svg b/docs/images/graphs/dep_graph_app_core_holiday_service.svg index e69de29b..4c38263a 100644 --- a/docs/images/graphs/dep_graph_app_core_holiday_service.svg +++ b/docs/images/graphs/dep_graph_app_core_holiday_service.svg @@ -0,0 +1,17 @@ + + + + + + :app:core:holiday-service + + + + :app:core:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_model.svg b/docs/images/graphs/dep_graph_app_core_model.svg index e69de29b..9a93db0f 100644 --- a/docs/images/graphs/dep_graph_app_core_model.svg +++ b/docs/images/graphs/dep_graph_app_core_model.svg @@ -0,0 +1,9 @@ + + + + + + :app:core:model + + + diff --git a/docs/images/graphs/dep_graph_app_core_navigation.svg b/docs/images/graphs/dep_graph_app_core_navigation.svg index e69de29b..16a862be 100644 --- a/docs/images/graphs/dep_graph_app_core_navigation.svg +++ b/docs/images/graphs/dep_graph_app_core_navigation.svg @@ -0,0 +1,17 @@ + + + + + + :app:core:navigation + + + + :library:datetime + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_account.svg b/docs/images/graphs/dep_graph_app_data_account.svg index e69de29b..2a8edad1 100644 --- a/docs/images/graphs/dep_graph_app_data_account.svg +++ b/docs/images/graphs/dep_graph_app_data_account.svg @@ -0,0 +1,73 @@ + + + + + + :app:data:account + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:account-preferences + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_backup.svg b/docs/images/graphs/dep_graph_app_data_backup.svg index e69de29b..e4dc9936 100644 --- a/docs/images/graphs/dep_graph_app_data_backup.svg +++ b/docs/images/graphs/dep_graph_app_data_backup.svg @@ -0,0 +1,149 @@ + + + + + + :app:data:backup + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:backup-database + + + + + + + + :app:core:diary-database + + + + + + + + :app:core:diary-service + + + + + + + + :app:domain:backup + + + + + + + + :app:core:model + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + + + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_buddy.svg b/docs/images/graphs/dep_graph_app_data_buddy.svg index e69de29b..5fd35302 100644 --- a/docs/images/graphs/dep_graph_app_data_buddy.svg +++ b/docs/images/graphs/dep_graph_app_data_buddy.svg @@ -0,0 +1,137 @@ + + + + + + :app:data:buddy + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:diary-database + + + + + + + + :app:core:diary-service + + + + + + + + :app:domain:buddy + + + + + + + + :app:core:model + + + + + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + + + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_calendar.svg b/docs/images/graphs/dep_graph_app_data_calendar.svg index e69de29b..a720b8d9 100644 --- a/docs/images/graphs/dep_graph_app_data_calendar.svg +++ b/docs/images/graphs/dep_graph_app_data_calendar.svg @@ -0,0 +1,205 @@ + + + + + + :app:data:calendar + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:filter-database + + + + + + + + :app:domain:calendar + + + + + + + + + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + :app:domain:memo + + + + + + + + :app:domain:tag + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:domain:backup + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_credential.svg b/docs/images/graphs/dep_graph_app_data_credential.svg index e69de29b..d604d9d7 100644 --- a/docs/images/graphs/dep_graph_app_data_credential.svg +++ b/docs/images/graphs/dep_graph_app_data_credential.svg @@ -0,0 +1,225 @@ + + + + + + :app:data:credential + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:account-preferences + + + + + + + + :app:core:diary-service + + + + + + + + :app:domain:credential + + + + + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :common:model + + + + + + + + + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + + :app:domain:fetch + + + + + + + + :app:domain:backup + + + + + + + + :app:domain:fcm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_fcm.svg b/docs/images/graphs/dep_graph_app_data_fcm.svg index e69de29b..7bae6781 100644 --- a/docs/images/graphs/dep_graph_app_data_fcm.svg +++ b/docs/images/graphs/dep_graph_app_data_fcm.svg @@ -0,0 +1,141 @@ + + + + + + :app:data:fcm + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:diary-service + + + + + + + + :app:domain:fcm + + + + + + + + :library:firebase-messaging + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + + + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + :library:firebase-common + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_fetch.svg b/docs/images/graphs/dep_graph_app_data_fetch.svg index e69de29b..2ecc3dfc 100644 --- a/docs/images/graphs/dep_graph_app_data_fetch.svg +++ b/docs/images/graphs/dep_graph_app_data_fetch.svg @@ -0,0 +1,169 @@ + + + + + + :app:data:fetch + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:diary-database + + + + + + + + :app:core:diary-service + + + + + + + + :app:domain:fetch + + + + + + + + :app:core:model + + + + + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + + + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + :app:domain:backup + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_holiday.svg b/docs/images/graphs/dep_graph_app_data_holiday.svg index e69de29b..0a78ba3d 100644 --- a/docs/images/graphs/dep_graph_app_data_holiday.svg +++ b/docs/images/graphs/dep_graph_app_data_holiday.svg @@ -0,0 +1,97 @@ + + + + + + :app:data:holiday + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:holiday-preferences + + + + + + + + :app:core:holiday-database + + + + + + + + :app:core:holiday-service + + + + + + + + :app:domain:holiday + + + + + + + + :app:core:model + + + + + + + + + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_memo.svg b/docs/images/graphs/dep_graph_app_data_memo.svg index e69de29b..f7ce19ca 100644 --- a/docs/images/graphs/dep_graph_app_data_memo.svg +++ b/docs/images/graphs/dep_graph_app_data_memo.svg @@ -0,0 +1,205 @@ + + + + + + :app:data:memo + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:diary-database + + + + + + + + :app:core:diary-service + + + + + + + + :app:domain:memo + + + + + + + + :app:core:model + + + + + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + + + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + :app:domain:backup + + + + + + + + :app:domain:tag + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_tag.svg b/docs/images/graphs/dep_graph_app_data_tag.svg index e69de29b..93a2204b 100644 --- a/docs/images/graphs/dep_graph_app_data_tag.svg +++ b/docs/images/graphs/dep_graph_app_data_tag.svg @@ -0,0 +1,137 @@ + + + + + + :app:data:tag + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:diary-database + + + + + + + + :app:domain:tag + + + + + + + + :app:core:model + + + + + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + :app:domain:backup + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_account.svg b/docs/images/graphs/dep_graph_app_domain_account.svg index e69de29b..f80a0c6d 100644 --- a/docs/images/graphs/dep_graph_app_domain_account.svg +++ b/docs/images/graphs/dep_graph_app_domain_account.svg @@ -0,0 +1,49 @@ + + + + + + :app:domain:account + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_backup.svg b/docs/images/graphs/dep_graph_app_domain_backup.svg index e69de29b..5ba19d9b 100644 --- a/docs/images/graphs/dep_graph_app_domain_backup.svg +++ b/docs/images/graphs/dep_graph_app_domain_backup.svg @@ -0,0 +1,77 @@ + + + + + + :app:domain:backup + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_buddy.svg b/docs/images/graphs/dep_graph_app_domain_buddy.svg index e69de29b..d4cf3aca 100644 --- a/docs/images/graphs/dep_graph_app_domain_buddy.svg +++ b/docs/images/graphs/dep_graph_app_domain_buddy.svg @@ -0,0 +1,77 @@ + + + + + + :app:domain:buddy + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_calendar.svg b/docs/images/graphs/dep_graph_app_domain_calendar.svg index e69de29b..b2df94de 100644 --- a/docs/images/graphs/dep_graph_app_domain_calendar.svg +++ b/docs/images/graphs/dep_graph_app_domain_calendar.svg @@ -0,0 +1,181 @@ + + + + + + :app:domain:calendar + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + :app:domain:memo + + + + + + + + :app:domain:tag + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:domain:backup + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_credential.svg b/docs/images/graphs/dep_graph_app_domain_credential.svg index e69de29b..9c2bc32f 100644 --- a/docs/images/graphs/dep_graph_app_domain_credential.svg +++ b/docs/images/graphs/dep_graph_app_domain_credential.svg @@ -0,0 +1,173 @@ + + + + + + :app:domain:credential + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + + :app:domain:fetch + + + + + + + + :app:domain:backup + + + + + + + + :app:domain:fcm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_fcm.svg b/docs/images/graphs/dep_graph_app_domain_fcm.svg index e69de29b..4abb57f6 100644 --- a/docs/images/graphs/dep_graph_app_domain_fcm.svg +++ b/docs/images/graphs/dep_graph_app_domain_fcm.svg @@ -0,0 +1,77 @@ + + + + + + :app:domain:fcm + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_fetch.svg b/docs/images/graphs/dep_graph_app_domain_fetch.svg index e69de29b..fae505eb 100644 --- a/docs/images/graphs/dep_graph_app_domain_fetch.svg +++ b/docs/images/graphs/dep_graph_app_domain_fetch.svg @@ -0,0 +1,109 @@ + + + + + + :app:domain:fetch + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + :app:domain:backup + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_holiday.svg b/docs/images/graphs/dep_graph_app_domain_holiday.svg index e69de29b..dec9f8bb 100644 --- a/docs/images/graphs/dep_graph_app_domain_holiday.svg +++ b/docs/images/graphs/dep_graph_app_domain_holiday.svg @@ -0,0 +1,49 @@ + + + + + + :app:domain:holiday + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_memo.svg b/docs/images/graphs/dep_graph_app_domain_memo.svg index e69de29b..ff041402 100644 --- a/docs/images/graphs/dep_graph_app_domain_memo.svg +++ b/docs/images/graphs/dep_graph_app_domain_memo.svg @@ -0,0 +1,145 @@ + + + + + + :app:domain:memo + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + :app:domain:backup + + + + + + + + :app:domain:tag + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_tag.svg b/docs/images/graphs/dep_graph_app_domain_tag.svg index e69de29b..8322c2f7 100644 --- a/docs/images/graphs/dep_graph_app_domain_tag.svg +++ b/docs/images/graphs/dep_graph_app_domain_tag.svg @@ -0,0 +1,109 @@ + + + + + + :app:domain:tag + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + :app:domain:backup + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_feature_buddy.svg b/docs/images/graphs/dep_graph_app_feature_buddy.svg index e42b9b8e..a5eb2b8e 100644 --- a/docs/images/graphs/dep_graph_app_feature_buddy.svg +++ b/docs/images/graphs/dep_graph_app_feature_buddy.svg @@ -1,237 +1,337 @@ - + - - - - :app:feature:buddy + + + + :app:feature:buddy - - - :app:core:compose + + + :app:core:compose - - - + + + - - - :app:core:design-system + + + :app:core:design-system - - + + - - - :app:core:navigation + + + :app:core:navigation - - + + - - - :library:color + + + :library:color - - + + - - - :library:kotlin + + + :library:kotlin - - + + - - - :library:navigation + + + :library:navigation - - + + - - - :library:coroutines + + + :library:coroutines - - + + - - - :library:datetime + + + :library:datetime - - + + - - - :library:shimmer-m3 + + + :library:shimmer-m3 - - + + - - - :app:core:calendar-compose + + + :app:core:calendar-compose - - + + - - - :app:domain:account + + + :app:domain:account - - + + - - - :app:domain:buddy + + + :app:domain:memo + + + + + + + + :app:domain:buddy - - + + - - - :app:domain:holiday + + + :app:domain:holiday - - + + - - + + - - + + - - + + - - - + + + + + + + :app:core:model + + + + + + + + + + + + + + + + - - - :app:core:model + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + :common:exception - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + :app:domain:backup - - + + + + + + :app:domain:tag - - + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - - :common:exception + + + - - + + - - + + - - + + - - + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + diff --git a/docs/images/graphs/dep_graph_app_platform_android.svg b/docs/images/graphs/dep_graph_app_platform_android.svg index db36de8f..7efeb899 100644 --- a/docs/images/graphs/dep_graph_app_platform_android.svg +++ b/docs/images/graphs/dep_graph_app_platform_android.svg @@ -1,1349 +1,1361 @@ - + - + - - :app:platform:android + + :app:platform:android - - :app:platform:common + + :app:platform:common - - + + - - :app:core:diary-service + + :app:core:diary-service - - + + - - :app:core:holiday-service + + :app:core:holiday-service - - + + - - :app:domain:fcm + + :app:domain:fcm - - + + - - + + - - + + - - + + - - :app:core:compose + + :app:core:compose - - + + - - :app:core:design-system + + :app:core:design-system - - + + - - :app:core:navigation + + :app:core:navigation - - + + - - :library:color + + :library:color - - + + - - :library:kotlin + + :library:kotlin - - + + - - :library:navigation + + :library:navigation - - + + - - :library:coroutines + + :library:coroutines - - + + - - :library:datetime + + :library:datetime - - + + - - :library:shimmer-m3 + + :library:shimmer-m3 - - + + - - :app:data:memo + + :app:data:memo - - + + - - :app:data:tag + + :app:data:tag - - + + - - :app:data:account + + :app:data:account - - + + - - :app:data:credential + + :app:data:credential - - + + - - :app:data:holiday + + :app:data:holiday - - + + - - :app:data:backup + + :app:data:backup - - + + - - :app:data:fetch + + :app:data:fetch - - + + - - :app:data:fcm + + :app:data:fcm - - + + - - :app:data:calendar + + :app:data:calendar - - + + - - :app:data:buddy + + :app:data:buddy - - + + - - :app:domain:memo + + :app:domain:memo - - + + - - :app:domain:tag + + :app:domain:tag - - + + - - :app:domain:account + + :app:domain:account - - + + - - :app:domain:credential + + :app:domain:credential - - + + - - :app:domain:holiday + + :app:domain:holiday - - + + - - :app:domain:backup + + :app:domain:backup - - + + - - :app:domain:fetch + + :app:domain:fetch - - + + - - :app:domain:calendar + + :app:domain:calendar - - + + - - :app:domain:buddy + + :app:domain:buddy - - + + - - :app:core:coroutines + + :app:core:coroutines - - + + - - :app:core:filter-database-room + + :app:core:filter-database-room - - + + - - :app:core:backup-database-room + + :app:core:backup-database-room - - + + - - :app:core:account-preferences-datastore + + :app:core:account-preferences-datastore - - + + - - :app:core:diary-database-room + + :app:core:diary-database-room - - + + - - :app:core:holiday-preferences-datastore + + :app:core:holiday-preferences-datastore - - + + - - :app:core:holiday-database-room + + :app:core:holiday-database-room - - + + - - :app:feature:memo + + :app:feature:memo - - + + - - :app:feature:tag + + :app:feature:tag - - + + - - :app:feature:calendar + + :app:feature:calendar - - + + - - :app:feature:buddy + + :app:feature:buddy - - + + - - :app:feature:more + + :app:feature:more - - + + - - :app:feature:account + + :app:feature:account - - + + - - :library:firebase-messaging + + :library:firebase-messaging - - + + - - :app:core:model + + :app:core:model - - + + - - :common:exception + + :common:exception - - + + - - :app:core:account-preferences + + :app:core:account-preferences - - + + - - :common:model + + :common:model - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:calendar-compose + + :app:core:calendar-compose - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + - - :app:core:diary-database + + :app:core:diary-database - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:holiday-preferences + + :app:core:holiday-preferences - - + + - - :app:core:holiday-database + + :app:core:holiday-database - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:backup-database + + :app:core:backup-database - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:filter-database + + :app:core:filter-database + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :library:koin-room + + :library:koin-room - - + + - - :library:room + + :library:room - - + + - - + + - - + + - - + + - - + + - - + + - - :library:koin-datastore + + :library:koin-datastore + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :library:firebase-common + + :library:firebase-common - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/docs/images/graphs/dep_graph_app_platform_common.svg b/docs/images/graphs/dep_graph_app_platform_common.svg index 155c3420..3402a35d 100644 --- a/docs/images/graphs/dep_graph_app_platform_common.svg +++ b/docs/images/graphs/dep_graph_app_platform_common.svg @@ -1,1329 +1,1341 @@ - + - + - - :app:platform:common + + :app:platform:common - - :app:core:compose + + :app:core:compose - - + + - - :app:core:design-system + + :app:core:design-system - - + + - - :app:core:navigation + + :app:core:navigation - - + + - - :library:color + + :library:color - - + + - - :library:kotlin + + :library:kotlin - - + + :library:navigation - - + + - - :library:coroutines + + :library:coroutines - - + + - - :library:datetime + + :library:datetime - - + + :library:shimmer-m3 - - + + - - :app:data:memo + + :app:data:memo - - + + - - :app:data:tag + + :app:data:tag - - + + - - :app:data:account + + :app:data:account - - + + - - :app:data:credential + + :app:data:credential - - + + - - :app:data:holiday + + :app:data:holiday - - + + - - :app:data:backup + + :app:data:backup - - + + - - :app:data:fetch + + :app:data:fetch - - + + - - :app:data:fcm + + :app:data:fcm - - + + - - :app:data:calendar + + :app:data:calendar - - + + - - :app:data:buddy + + :app:data:buddy - - + + - - :app:domain:memo + + :app:domain:memo - - + + - - :app:domain:tag + + :app:domain:tag - - + + - - :app:domain:account + + :app:domain:account - - + + - - :app:domain:credential + + :app:domain:credential - - + + - - :app:domain:holiday + + :app:domain:holiday - - + + - - :app:domain:backup + + :app:domain:backup - - + + - - :app:domain:fetch + + :app:domain:fetch - - + + - - :app:domain:fcm + + :app:domain:fcm - - + + - - :app:domain:calendar + + :app:domain:calendar - - + + - - :app:domain:buddy + + :app:domain:buddy - - + + - - :app:core:coroutines + + :app:core:coroutines - - + + - - :app:core:filter-database-room + + :app:core:filter-database-room - - + + - - :app:core:backup-database-room + + :app:core:backup-database-room - - + + - - :app:core:account-preferences-datastore + + :app:core:account-preferences-datastore - - + + - - :app:core:diary-database-room + + :app:core:diary-database-room - - + + - - :app:core:diary-service + + :app:core:diary-service - - + + - - :app:core:holiday-service + + :app:core:holiday-service - - + + - - :app:core:holiday-preferences-datastore + + :app:core:holiday-preferences-datastore - - + + - - :app:core:holiday-database-room + + :app:core:holiday-database-room - - + + - - :app:feature:memo + + :app:feature:memo - - + + - - :app:feature:tag + + :app:feature:tag - - + + - - :app:feature:calendar + + :app:feature:calendar - - + + - - :app:feature:buddy + + :app:feature:buddy - - + + - - :app:feature:more + + :app:feature:more - - + + - - :app:feature:account + + :app:feature:account - - + + - - :library:firebase-messaging + + :library:firebase-messaging - - + + - - + + - - + + - - + + :app:core:calendar-compose - - + + - - :app:core:model + + :app:core:model - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + - - :app:core:diary-database + + :app:core:diary-database - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:account-preferences + + :app:core:account-preferences - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:holiday-preferences + + :app:core:holiday-preferences - - + + - - :app:core:holiday-database + + :app:core:holiday-database - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:backup-database + + :app:core:backup-database - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:filter-database + + :app:core:filter-database + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :common:exception + + :common:exception - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :library:koin-room + + :library:koin-room - - + + - - :library:room + + :library:room - - + + - - + + - - + + - - + + - - + + - - + + - - :library:koin-datastore + + :library:koin-datastore - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :common:model + + :common:model + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :library:firebase-common + + :library:firebase-common - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/docs/images/graphs/dep_graph_app_platform_ios.svg b/docs/images/graphs/dep_graph_app_platform_ios.svg index bd6c479b..2e1592a4 100644 --- a/docs/images/graphs/dep_graph_app_platform_ios.svg +++ b/docs/images/graphs/dep_graph_app_platform_ios.svg @@ -1,1349 +1,1361 @@ - + - + - - :app:platform:ios + + :app:platform:ios - - :app:platform:common + + :app:platform:common - - + + - - :app:core:coroutines + + :app:core:coroutines - - + + - - :app:core:diary-service + + :app:core:diary-service - - + + - - :app:core:holiday-service + + :app:core:holiday-service - - + + - - + + - - + + - - + + - - :app:core:compose + + :app:core:compose - - + + - - :app:core:design-system + + :app:core:design-system - - + + - - :app:core:navigation + + :app:core:navigation - - + + - - :library:color + + :library:color - - + + - - :library:kotlin + + :library:kotlin - - + + - - :library:navigation + + :library:navigation - - + + - - :library:coroutines + + :library:coroutines - - + + - - :library:datetime + + :library:datetime - - + + - - :library:shimmer-m3 + + :library:shimmer-m3 - - + + - - :app:data:memo + + :app:data:memo - - + + - - :app:data:tag + + :app:data:tag - - + + - - :app:data:account + + :app:data:account - - + + - - :app:data:credential + + :app:data:credential - - + + - - :app:data:holiday + + :app:data:holiday - - + + - - :app:data:backup + + :app:data:backup - - + + - - :app:data:fetch + + :app:data:fetch - - + + - - :app:data:fcm + + :app:data:fcm - - + + - - :app:data:calendar + + :app:data:calendar - - + + - - :app:data:buddy + + :app:data:buddy - - + + - - :app:domain:memo + + :app:domain:memo - - + + - - :app:domain:tag + + :app:domain:tag - - + + - - :app:domain:account + + :app:domain:account - - + + - - :app:domain:credential + + :app:domain:credential - - + + - - :app:domain:holiday + + :app:domain:holiday - - + + - - :app:domain:backup + + :app:domain:backup - - + + - - :app:domain:fetch + + :app:domain:fetch - - + + - - :app:domain:fcm + + :app:domain:fcm - - + + - - :app:domain:calendar + + :app:domain:calendar - - + + - - :app:domain:buddy + + :app:domain:buddy - - + + - - :app:core:filter-database-room + + :app:core:filter-database-room - - + + - - :app:core:backup-database-room + + :app:core:backup-database-room - - + + - - :app:core:account-preferences-datastore + + :app:core:account-preferences-datastore - - + + - - :app:core:diary-database-room + + :app:core:diary-database-room - - + + - - :app:core:holiday-preferences-datastore + + :app:core:holiday-preferences-datastore - - + + - - :app:core:holiday-database-room + + :app:core:holiday-database-room - - + + - - :app:feature:memo + + :app:feature:memo - - + + - - :app:feature:tag + + :app:feature:tag - - + + - - :app:feature:calendar + + :app:feature:calendar - - + + - - :app:feature:buddy + + :app:feature:buddy - - + + - - :app:feature:more + + :app:feature:more - - + + - - :app:feature:account + + :app:feature:account - - + + - - :library:firebase-messaging + + :library:firebase-messaging - - + + - - :app:core:model + + :app:core:model - - + + - - :common:exception + + :common:exception - - + + - - :app:core:account-preferences + + :app:core:account-preferences - - + + - - :common:model + + :common:model - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:calendar-compose + + :app:core:calendar-compose - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + - - :app:core:diary-database + + :app:core:diary-database - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:holiday-preferences + + :app:core:holiday-preferences - - + + - - :app:core:holiday-database + + :app:core:holiday-database - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:backup-database + + :app:core:backup-database - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:filter-database + + :app:core:filter-database + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :library:koin-room + + :library:koin-room - - + + - - :library:room + + :library:room - - + + - - + + - - + + - - + + - - + + - - + + - - :library:koin-datastore + + :library:koin-datastore + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :library:firebase-common + + :library:firebase-common - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/docs/images/graphs/dep_graph_app_platform_jvm.svg b/docs/images/graphs/dep_graph_app_platform_jvm.svg index 4688024b..48eace3d 100644 --- a/docs/images/graphs/dep_graph_app_platform_jvm.svg +++ b/docs/images/graphs/dep_graph_app_platform_jvm.svg @@ -1,1357 +1,1369 @@ - + - + - - :app:platform:jvm + + :app:platform:jvm - - :app:platform:common + + :app:platform:common - - + + - - :app:core:coroutines + + :app:core:coroutines - - + + - - :app:core:diary-service + + :app:core:diary-service - - + + :app:core:holiday-service - - + + :library:koin-room - - + + - - :library:koin-datastore + + :library:koin-datastore - - + + - - + + - - + + - - + + - - :app:core:compose + + :app:core:compose - - + + - - :app:core:design-system + + :app:core:design-system - - + + - - :app:core:navigation + + :app:core:navigation - - + + - - :library:color + + :library:color - - + + - - :library:kotlin + + :library:kotlin - - + + - - :library:navigation + + :library:navigation - - + + - - :library:coroutines + + :library:coroutines - - + + - - :library:datetime + + :library:datetime - - + + - - :library:shimmer-m3 + + :library:shimmer-m3 - - + + - - :app:data:memo + + :app:data:memo - - + + - - :app:data:tag + + :app:data:tag - - + + - - :app:data:account + + :app:data:account - - + + - - :app:data:credential + + :app:data:credential - - + + - - :app:data:holiday + + :app:data:holiday - - + + - - :app:data:backup + + :app:data:backup - - + + - - :app:data:fetch + + :app:data:fetch - - + + - - :app:data:fcm + + :app:data:fcm - - + + - - :app:data:calendar + + :app:data:calendar - - + + - - :app:data:buddy + + :app:data:buddy - - + + - - :app:domain:memo + + :app:domain:memo - - + + - - :app:domain:tag + + :app:domain:tag - - + + - - :app:domain:account + + :app:domain:account - - + + - - :app:domain:credential + + :app:domain:credential - - + + - - :app:domain:holiday + + :app:domain:holiday - - + + - - :app:domain:backup + + :app:domain:backup - - + + - - :app:domain:fetch + + :app:domain:fetch - - + + - - :app:domain:fcm + + :app:domain:fcm - - + + - - :app:domain:calendar + + :app:domain:calendar - - + + - - :app:domain:buddy + + :app:domain:buddy - - + + - - :app:core:filter-database-room + + :app:core:filter-database-room - - + + - - :app:core:backup-database-room + + :app:core:backup-database-room - - + + - - :app:core:account-preferences-datastore + + :app:core:account-preferences-datastore - - + + - - :app:core:diary-database-room + + :app:core:diary-database-room - - + + - - :app:core:holiday-preferences-datastore + + :app:core:holiday-preferences-datastore - - + + - - :app:core:holiday-database-room + + :app:core:holiday-database-room - - + + - - :app:feature:memo + + :app:feature:memo - - + + - - :app:feature:tag + + :app:feature:tag - - + + - - :app:feature:calendar + + :app:feature:calendar - - + + - - :app:feature:buddy + + :app:feature:buddy - - + + - - :app:feature:more + + :app:feature:more - - + + - - :app:feature:account + + :app:feature:account - - + + - - :library:firebase-messaging + + :library:firebase-messaging - - + + - - :app:core:model + + :app:core:model - - + + - - :common:exception + + :common:exception - - + + - - :app:core:account-preferences + + :app:core:account-preferences - - + + - - :common:model + + :common:model - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:calendar-compose + + :app:core:calendar-compose - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + - - :app:core:diary-database + + :app:core:diary-database - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:holiday-preferences + + :app:core:holiday-preferences - - + + - - :app:core:holiday-database + + :app:core:holiday-database - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:backup-database + + :app:core:backup-database - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :app:core:filter-database + + :app:core:filter-database + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :library:room + + :library:room + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - :library:firebase-common + + :library:firebase-common - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/docs/images/graphs/dep_graph_app_platform_macos.svg b/docs/images/graphs/dep_graph_app_platform_macos.svg index f306c89c..e69de29b 100644 --- a/docs/images/graphs/dep_graph_app_platform_macos.svg +++ b/docs/images/graphs/dep_graph_app_platform_macos.svg @@ -1,1337 +0,0 @@ - - - - - - :app:platform:macos - - - - :app:platform:common - - - - - - - - :app:core:compose - - - - - - - - :app:core:design-system - - - - - - - - :app:core:navigation - - - - - - - - :library:color - - - - - - - - :library:kotlin - - - - - - - - :library:navigation - - - - - - - - :library:coroutines - - - - - - - - :library:datetime - - - - - - - - :library:shimmer-m3 - - - - - - - - :app:data:memo - - - - - - - - :app:data:tag - - - - - - - - :app:data:account - - - - - - - - :app:data:credential - - - - - - - - :app:data:holiday - - - - - - - - :app:data:backup - - - - - - - - :app:data:fetch - - - - - - - - :app:data:fcm - - - - - - - - :app:data:calendar - - - - - - - - :app:data:buddy - - - - - - - - :app:domain:memo - - - - - - - - :app:domain:tag - - - - - - - - :app:domain:account - - - - - - - - :app:domain:credential - - - - - - - - :app:domain:holiday - - - - - - - - :app:domain:backup - - - - - - - - :app:domain:fetch - - - - - - - - :app:domain:fcm - - - - - - - - :app:domain:calendar - - - - - - - - :app:domain:buddy - - - - - - - - :app:core:coroutines - - - - - - - - :app:core:filter-database-room - - - - - - - - :app:core:backup-database-room - - - - - - - - :app:core:account-preferences-datastore - - - - - - - - :app:core:diary-database-room - - - - - - - - :app:core:diary-service - - - - - - - - :app:core:holiday-service - - - - - - - - :app:core:holiday-preferences-datastore - - - - - - - - :app:core:holiday-database-room - - - - - - - - :app:feature:memo - - - - - - - - :app:feature:tag - - - - - - - - :app:feature:calendar - - - - - - - - :app:feature:buddy - - - - - - - - :app:feature:more - - - - - - - - :app:feature:account - - - - - - - - :library:firebase-messaging - - - - - - - - - - - - - - - - - - - - :app:core:calendar-compose - - - - - - - - :app:core:model - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - :app:core:diary-database - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - :app:core:account-preferences - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - :app:core:holiday-preferences - - - - - - - - :app:core:holiday-database - - - - - - - - - - - - - - - - - - - - - - - - - - - - :app:core:backup-database - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - :app:core:filter-database - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - :common:exception - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - :library:koin-room - - - - - - - - :library:room - - - - - - - - - - - - - - - - - - - - - - - - - - - - :library:koin-datastore - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - :common:model - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - :library:firebase-common - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_common_exception.svg b/docs/images/graphs/dep_graph_common_exception.svg index e69de29b..9fc01b65 100644 --- a/docs/images/graphs/dep_graph_common_exception.svg +++ b/docs/images/graphs/dep_graph_common_exception.svg @@ -0,0 +1,9 @@ + + + + + + :common:exception + + + diff --git a/docs/images/graphs/dep_graph_common_model.svg b/docs/images/graphs/dep_graph_common_model.svg index e69de29b..10f453c6 100644 --- a/docs/images/graphs/dep_graph_common_model.svg +++ b/docs/images/graphs/dep_graph_common_model.svg @@ -0,0 +1,9 @@ + + + + + + :common:model + + + diff --git a/docs/images/graphs/dep_graph_library_color.svg b/docs/images/graphs/dep_graph_library_color.svg index e69de29b..5fa2d0a8 100644 --- a/docs/images/graphs/dep_graph_library_color.svg +++ b/docs/images/graphs/dep_graph_library_color.svg @@ -0,0 +1,9 @@ + + + + + + :library:color + + + diff --git a/docs/images/graphs/dep_graph_library_coroutines.svg b/docs/images/graphs/dep_graph_library_coroutines.svg index e69de29b..831e504b 100644 --- a/docs/images/graphs/dep_graph_library_coroutines.svg +++ b/docs/images/graphs/dep_graph_library_coroutines.svg @@ -0,0 +1,9 @@ + + + + + + :library:coroutines + + + diff --git a/docs/images/graphs/dep_graph_library_datetime.svg b/docs/images/graphs/dep_graph_library_datetime.svg index e69de29b..94bc4256 100644 --- a/docs/images/graphs/dep_graph_library_datetime.svg +++ b/docs/images/graphs/dep_graph_library_datetime.svg @@ -0,0 +1,9 @@ + + + + + + :library:datetime + + + diff --git a/docs/images/graphs/dep_graph_library_firebase_common.svg b/docs/images/graphs/dep_graph_library_firebase_common.svg index e69de29b..78fcd833 100644 --- a/docs/images/graphs/dep_graph_library_firebase_common.svg +++ b/docs/images/graphs/dep_graph_library_firebase_common.svg @@ -0,0 +1,9 @@ + + + + + + :library:firebase-common + + + diff --git a/docs/images/graphs/dep_graph_library_firebase_messaging.svg b/docs/images/graphs/dep_graph_library_firebase_messaging.svg index e69de29b..65cd3e1b 100644 --- a/docs/images/graphs/dep_graph_library_firebase_messaging.svg +++ b/docs/images/graphs/dep_graph_library_firebase_messaging.svg @@ -0,0 +1,17 @@ + + + + + + :library:firebase-messaging + + + + :library:firebase-common + + + + + + + diff --git a/docs/images/graphs/dep_graph_library_koin_datastore.svg b/docs/images/graphs/dep_graph_library_koin_datastore.svg index e69de29b..a9cdf8cb 100644 --- a/docs/images/graphs/dep_graph_library_koin_datastore.svg +++ b/docs/images/graphs/dep_graph_library_koin_datastore.svg @@ -0,0 +1,9 @@ + + + + + + :library:koin-datastore + + + diff --git a/docs/images/graphs/dep_graph_library_koin_room.svg b/docs/images/graphs/dep_graph_library_koin_room.svg index e69de29b..35267461 100644 --- a/docs/images/graphs/dep_graph_library_koin_room.svg +++ b/docs/images/graphs/dep_graph_library_koin_room.svg @@ -0,0 +1,9 @@ + + + + + + :library:koin-room + + + diff --git a/docs/images/graphs/dep_graph_library_kotlin.svg b/docs/images/graphs/dep_graph_library_kotlin.svg index e69de29b..9f42633b 100644 --- a/docs/images/graphs/dep_graph_library_kotlin.svg +++ b/docs/images/graphs/dep_graph_library_kotlin.svg @@ -0,0 +1,9 @@ + + + + + + :library:kotlin + + + diff --git a/docs/images/graphs/dep_graph_library_navigation.svg b/docs/images/graphs/dep_graph_library_navigation.svg index e69de29b..98993cd7 100644 --- a/docs/images/graphs/dep_graph_library_navigation.svg +++ b/docs/images/graphs/dep_graph_library_navigation.svg @@ -0,0 +1,17 @@ + + + + + + :library:navigation + + + + :library:datetime + + + + + + + diff --git a/docs/images/graphs/dep_graph_library_room.svg b/docs/images/graphs/dep_graph_library_room.svg index e69de29b..e880c6f8 100644 --- a/docs/images/graphs/dep_graph_library_room.svg +++ b/docs/images/graphs/dep_graph_library_room.svg @@ -0,0 +1,17 @@ + + + + + + :library:room + + + + :library:datetime + + + + + + + diff --git a/docs/images/graphs/dep_graph_library_shimmer_m3.svg b/docs/images/graphs/dep_graph_library_shimmer_m3.svg index e69de29b..4e31ccd8 100644 --- a/docs/images/graphs/dep_graph_library_shimmer_m3.svg +++ b/docs/images/graphs/dep_graph_library_shimmer_m3.svg @@ -0,0 +1,17 @@ + + + + + + :library:shimmer-m3 + + + + :library:color + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_app.svg b/docs/images/graphs/dep_graph_server_app.svg index e69de29b..32f59b31 100644 --- a/docs/images/graphs/dep_graph_server_app.svg +++ b/docs/images/graphs/dep_graph_server_app.svg @@ -0,0 +1,397 @@ + + + + + + :server:app + + + + :server:core:database + + + + + + + + :server:data:account + + + + + + + + :server:data:memo + + + + + + + + :server:data:tag + + + + + + + + :server:data:fcm + + + + + + + + :server:data:buddy + + + + + + + + :server:domain:account + + + + + + + + :server:domain:memo + + + + + + + + :server:domain:tag + + + + + + + + :server:domain:fcm + + + + + + + + :server:domain:buddy + + + + + + + + :server:feature:home + + + + + + + + :server:feature:account + + + + + + + + :server:feature:memo + + + + + + + + :server:feature:tag + + + + + + + + :server:feature:fcm + + + + + + + + :server:feature:buddy + + + + + + + + :common:model + + + + + + + + :server:core:model + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_core_database.svg b/docs/images/graphs/dep_graph_server_core_database.svg index e69de29b..dfbf2f18 100644 --- a/docs/images/graphs/dep_graph_server_core_database.svg +++ b/docs/images/graphs/dep_graph_server_core_database.svg @@ -0,0 +1,17 @@ + + + + + + :server:core:database + + + + :server:core:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_core_model.svg b/docs/images/graphs/dep_graph_server_core_model.svg index e69de29b..7483a13d 100644 --- a/docs/images/graphs/dep_graph_server_core_model.svg +++ b/docs/images/graphs/dep_graph_server_core_model.svg @@ -0,0 +1,9 @@ + + + + + + :server:core:model + + + diff --git a/docs/images/graphs/dep_graph_server_data_account.svg b/docs/images/graphs/dep_graph_server_data_account.svg index e69de29b..ae0ff61b 100644 --- a/docs/images/graphs/dep_graph_server_data_account.svg +++ b/docs/images/graphs/dep_graph_server_data_account.svg @@ -0,0 +1,57 @@ + + + + + + :server:data:account + + + + :server:core:model + + + + + + + + :server:core:database + + + + + + + + :server:domain:account + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_data_buddy.svg b/docs/images/graphs/dep_graph_server_data_buddy.svg index e69de29b..2f63c8e6 100644 --- a/docs/images/graphs/dep_graph_server_data_buddy.svg +++ b/docs/images/graphs/dep_graph_server_data_buddy.svg @@ -0,0 +1,97 @@ + + + + + + :server:data:buddy + + + + :server:core:model + + + + + + + + :server:core:database + + + + + + + + :server:domain:buddy + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + + :server:domain:fcm + + + + + + + + :server:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_data_fcm.svg b/docs/images/graphs/dep_graph_server_data_fcm.svg index e69de29b..0ccbfb26 100644 --- a/docs/images/graphs/dep_graph_server_data_fcm.svg +++ b/docs/images/graphs/dep_graph_server_data_fcm.svg @@ -0,0 +1,57 @@ + + + + + + :server:data:fcm + + + + :server:core:model + + + + + + + + :server:core:database + + + + + + + + :server:domain:fcm + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_data_memo.svg b/docs/images/graphs/dep_graph_server_data_memo.svg index e69de29b..3b4b59af 100644 --- a/docs/images/graphs/dep_graph_server_data_memo.svg +++ b/docs/images/graphs/dep_graph_server_data_memo.svg @@ -0,0 +1,97 @@ + + + + + + :server:data:memo + + + + :server:core:model + + + + + + + + :server:core:database + + + + + + + + :server:domain:memo + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + + :server:domain:account + + + + + + + + :server:domain:fcm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_data_tag.svg b/docs/images/graphs/dep_graph_server_data_tag.svg index e69de29b..28ce38c7 100644 --- a/docs/images/graphs/dep_graph_server_data_tag.svg +++ b/docs/images/graphs/dep_graph_server_data_tag.svg @@ -0,0 +1,57 @@ + + + + + + :server:data:tag + + + + :server:core:model + + + + + + + + :server:core:database + + + + + + + + :server:domain:tag + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_domain_account.svg b/docs/images/graphs/dep_graph_server_domain_account.svg index e69de29b..5e392609 100644 --- a/docs/images/graphs/dep_graph_server_domain_account.svg +++ b/docs/images/graphs/dep_graph_server_domain_account.svg @@ -0,0 +1,33 @@ + + + + + + :server:domain:account + + + + :server:core:model + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_domain_buddy.svg b/docs/images/graphs/dep_graph_server_domain_buddy.svg index e69de29b..057a1eed 100644 --- a/docs/images/graphs/dep_graph_server_domain_buddy.svg +++ b/docs/images/graphs/dep_graph_server_domain_buddy.svg @@ -0,0 +1,73 @@ + + + + + + :server:domain:buddy + + + + :server:core:model + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + + :server:domain:fcm + + + + + + + + :server:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_domain_fcm.svg b/docs/images/graphs/dep_graph_server_domain_fcm.svg index e69de29b..559a4e63 100644 --- a/docs/images/graphs/dep_graph_server_domain_fcm.svg +++ b/docs/images/graphs/dep_graph_server_domain_fcm.svg @@ -0,0 +1,33 @@ + + + + + + :server:domain:fcm + + + + :server:core:model + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_domain_memo.svg b/docs/images/graphs/dep_graph_server_domain_memo.svg index e69de29b..a256c672 100644 --- a/docs/images/graphs/dep_graph_server_domain_memo.svg +++ b/docs/images/graphs/dep_graph_server_domain_memo.svg @@ -0,0 +1,73 @@ + + + + + + :server:domain:memo + + + + :server:core:model + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + + :server:domain:account + + + + + + + + :server:domain:fcm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_domain_tag.svg b/docs/images/graphs/dep_graph_server_domain_tag.svg index e69de29b..bb37cf21 100644 --- a/docs/images/graphs/dep_graph_server_domain_tag.svg +++ b/docs/images/graphs/dep_graph_server_domain_tag.svg @@ -0,0 +1,33 @@ + + + + + + :server:domain:tag + + + + :server:core:model + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_feature_account.svg b/docs/images/graphs/dep_graph_server_feature_account.svg index e69de29b..bd36fe75 100644 --- a/docs/images/graphs/dep_graph_server_feature_account.svg +++ b/docs/images/graphs/dep_graph_server_feature_account.svg @@ -0,0 +1,57 @@ + + + + + + :server:feature:account + + + + :server:core:model + + + + + + + + :common:model + + + + + + + + :common:exception + + + + + + + + :server:domain:account + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_feature_buddy.svg b/docs/images/graphs/dep_graph_server_feature_buddy.svg index e69de29b..3f81ddf1 100644 --- a/docs/images/graphs/dep_graph_server_feature_buddy.svg +++ b/docs/images/graphs/dep_graph_server_feature_buddy.svg @@ -0,0 +1,97 @@ + + + + + + :server:feature:buddy + + + + :server:core:model + + + + + + + + :common:model + + + + + + + + :common:exception + + + + + + + + :server:domain:buddy + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + + :server:domain:fcm + + + + + + + + :server:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_feature_fcm.svg b/docs/images/graphs/dep_graph_server_feature_fcm.svg index e69de29b..012aef53 100644 --- a/docs/images/graphs/dep_graph_server_feature_fcm.svg +++ b/docs/images/graphs/dep_graph_server_feature_fcm.svg @@ -0,0 +1,57 @@ + + + + + + :server:feature:fcm + + + + :server:core:model + + + + + + + + :common:model + + + + + + + + :common:exception + + + + + + + + :server:domain:fcm + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_feature_home.svg b/docs/images/graphs/dep_graph_server_feature_home.svg index e69de29b..0123a03c 100644 --- a/docs/images/graphs/dep_graph_server_feature_home.svg +++ b/docs/images/graphs/dep_graph_server_feature_home.svg @@ -0,0 +1,33 @@ + + + + + + :server:feature:home + + + + :server:core:model + + + + + + + + :common:model + + + + + + + + :common:exception + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_feature_memo.svg b/docs/images/graphs/dep_graph_server_feature_memo.svg index e69de29b..ec32de50 100644 --- a/docs/images/graphs/dep_graph_server_feature_memo.svg +++ b/docs/images/graphs/dep_graph_server_feature_memo.svg @@ -0,0 +1,97 @@ + + + + + + :server:feature:memo + + + + :server:core:model + + + + + + + + :common:model + + + + + + + + :common:exception + + + + + + + + :server:domain:memo + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + + :server:domain:account + + + + + + + + :server:domain:fcm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_feature_tag.svg b/docs/images/graphs/dep_graph_server_feature_tag.svg index e69de29b..221848ca 100644 --- a/docs/images/graphs/dep_graph_server_feature_tag.svg +++ b/docs/images/graphs/dep_graph_server_feature_tag.svg @@ -0,0 +1,57 @@ + + + + + + :server:feature:tag + + + + :server:core:model + + + + + + + + :common:model + + + + + + + + :common:exception + + + + + + + + :server:domain:tag + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5df9af0..3479ca1b 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" @@ -21,8 +21,8 @@ compose-markdown = "0.27.0" # https://github.com/mikepenz/multip koin = "4.1.0-Beta1" # https://github.com/InsertKoinIO/koin/releases koin-annotations = "2.0.0-Beta2" # https://github.com/InsertKoinIO/koin-annotations/releases datastore = "1.1.1" # https://developer.android.com/jetpack/androidx/releases/datastore?hl=en -room = "2.7.0-alpha11" # https://developer.android.com/jetpack/androidx/releases/room?hl=en -sqlite = "2.5.0-alpha11" # https://developer.android.com/jetpack/androidx/releases/sqlite?hl=en +room = "2.7.0-alpha12" # https://developer.android.com/jetpack/androidx/releases/room?hl=en +sqlite = "2.5.0-alpha12" # https://developer.android.com/jetpack/androidx/releases/sqlite?hl=en kotest = "6.0.0.M1" # https://github.com/kotest/kotest/releases mockk = "1.13.13" # https://github.com/mockk/mockk/releases @@ -37,6 +37,9 @@ android-firebase-crashlytics-plugin = "3.0.2" android-firebase-perf-plugin = "1.4.2" google-services = "4.4.2" +test-core = "1.6.1" # https://developer.android.com/jetpack/androidx/releases/test?hl=en +roboletric = "4.14.1" # https://github.com/robolectric/robolectric/releases + leakcanary = "2.14" # https://github.com/square/leakcanary/releases ### server @@ -87,6 +90,7 @@ datastore-preferences = { group = "androidx.datastore", name = "datastore-prefer room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } sqlite-bundled = { group = "androidx.sqlite", name = "sqlite-bundled", version.ref = "sqlite" } +room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" } koin-bom = { group = "io.insert-koin", name = "koin-bom", version.ref = "koin" } koin-core = { group = "io.insert-koin", name = "koin-core" } @@ -116,6 +120,8 @@ android-firebase-crashlytics = { group = "com.google.firebase", name = "firebase android-firebase-perf = { group = "com.google.firebase", name = "firebase-perf" } android-firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } +test-core = { group = "androidx.test", name = "core", version.ref = "test-core" } +roboletric = { group = "org.robolectric", name = "robolectric", version.ref = "roboletric" } leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanary" } ### ktor 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 67b8e442..c6c5e564 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 @@ -45,6 +45,13 @@ public data object AccountTable : IdTable(name = "Account") { } }.map { it.toAccount() } + public fun findByUid(uid: String): Account? { + return selectAll() + .where { UID eq uid } + .singleOrNull() + ?.toAccount() + } + private fun ResultRow.toAccount(): Account = Account( email = get(EMAIL), 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 index 00229c52..ae32f942 100644 --- 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 @@ -2,6 +2,7 @@ package io.github.taetae98coding.diary.core.database import org.jetbrains.exposed.sql.ReferenceOption import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.upsert public data object MemoBuddyGroupTable : Table("MemoBuddyGroup") { @@ -26,4 +27,12 @@ public data object MemoBuddyGroupTable : Table("MemoBuddyGroup") { it[BUDDY_GROUP] = buddyGroup } } + + public fun findGroupIdsByMemoId(memoId: String): String? { + return selectAll() + .where { MEMO_ID eq memoId } + .singleOrNull() + ?.get(BUDDY_GROUP) + ?.value + } } 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 506e1a8f..d5676e53 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 @@ -1,6 +1,7 @@ package io.github.taetae98coding.diary.core.database import io.github.taetae98coding.diary.core.model.Memo +import io.github.taetae98coding.diary.core.model.MemoDetail import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate @@ -14,6 +15,7 @@ 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.update import org.jetbrains.exposed.sql.upsert public data object MemoTable : IdTable(name = "Memo") { @@ -51,6 +53,37 @@ public data object MemoTable : IdTable(name = "Memo") { } } + public fun update(id: String, memo: MemoDetail) { + update({ ID eq id }) { + it[TITLE] = memo.title + it[DESCRIPTION] = memo.description + it[START] = memo.start + it[END_INCLUSIVE] = memo.endInclusive + it[COLOR] = memo.color + } + } + + public fun updateFinish(id: String, isFinish: Boolean) { + update({ ID eq id }) { + it[IS_FINISH] = isFinish + it[UPDATE_AT] = Clock.System.now() + } + } + + public fun updateDelete(id: String, isDelete: Boolean) { + update({ ID eq id }) { + it[IS_DELETE] = isDelete + it[UPDATE_AT] = Clock.System.now() + } + } + + public fun findById(id: String): Memo? { + return selectAll() + .where { ID eq id } + .singleOrNull() + ?.toMemo() + } + public fun findByIds(ids: Set): List = selectAll() .where { ID.inList(ids) } @@ -70,6 +103,7 @@ public data object MemoTable : IdTable(name = "Memo") { .selectAll() .where { (MemoBuddyGroupTable.BUDDY_GROUP eq groupId) and + (IS_DELETE eq false) and (START.isNotNull() and (START lessEq dateRange.endInclusive)) and (END_INCLUSIVE.isNotNull() and (END_INCLUSIVE greaterEq dateRange.start)) } diff --git a/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/MemoDetail.kt b/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/MemoDetail.kt new file mode 100644 index 00000000..7f2fd189 --- /dev/null +++ b/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/MemoDetail.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.core.model + +import kotlinx.datetime.LocalDate + +public data class MemoDetail( + val title: String, + val description: String, + val start: LocalDate?, + val endInclusive: LocalDate?, + val color: Int, +) diff --git a/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt b/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt index 78843895..b1cc2954 100644 --- a/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt +++ b/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt @@ -3,18 +3,32 @@ package io.github.taetae98coding.diary.data.account.repository import io.github.taetae98coding.diary.core.database.AccountTable import io.github.taetae98coding.diary.core.model.Account import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.koin.core.annotation.Singleton @Singleton internal class AccountRepositoryImpl : AccountRepository { - override suspend fun contains(email: String): Boolean = newSuspendedTransaction { AccountTable.contains(email) } + override suspend fun contains(email: String): Boolean = newSuspendedTransaction { AccountTable.contains(email) } - override suspend fun upsert(account: Account) { - newSuspendedTransaction { - AccountTable.insert(account) - } - } + override suspend fun upsert(account: Account) { + newSuspendedTransaction { + AccountTable.insert(account) + } + } - override suspend fun findByEmail(email: String, password: String): Account? = newSuspendedTransaction { AccountTable.findByEmail(email, password) } + override fun findByEmail(email: String, password: String): Flow { + return flow { + newSuspendedTransaction { AccountTable.findByEmail(email, password) } + .also { emit(it) } + } + } + + override fun findByUid(uid: String): Flow { + return flow { + newSuspendedTransaction { AccountTable.findByUid(uid) } + .also { emit(it) } + } + } } diff --git a/server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt b/server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt index acb5c47b..c8c0c56d 100644 --- a/server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt +++ b/server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt @@ -39,6 +39,6 @@ internal class FCMRepositoryImpl : FCMRepository { .build() } - FirebaseMessaging.getInstance().sendEach(messageList) + FirebaseMessaging.getInstance().sendEachAsync(messageList) } } diff --git a/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoBuddyRepositoryImpl.kt b/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoBuddyRepositoryImpl.kt new file mode 100644 index 00000000..89cfefd8 --- /dev/null +++ b/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoBuddyRepositoryImpl.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.data.memo.repository + +import io.github.taetae98coding.diary.core.database.BuddyGroupAccountRelation +import io.github.taetae98coding.diary.core.database.MemoBuddyGroupTable +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.koin.core.annotation.Factory + +@Factory +internal class MemoBuddyRepositoryImpl : MemoBuddyRepository { + override fun findBuddyIdByMemoId(memoId: String): Flow> { + return flow { + newSuspendedTransaction> { + val groupId = MemoBuddyGroupTable.findGroupIdsByMemoId(memoId) ?: return@newSuspendedTransaction emptyList() + BuddyGroupAccountRelation.findBuddyIdByGroupId(groupId) + }.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 234aafad..e8578e3f 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 @@ -6,6 +6,7 @@ import io.github.taetae98coding.diary.core.database.MemoTable import io.github.taetae98coding.diary.core.database.MemoTagTable import io.github.taetae98coding.diary.core.model.Memo import io.github.taetae98coding.diary.core.model.MemoAndTagIds +import io.github.taetae98coding.diary.core.model.MemoDetail import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -15,7 +16,7 @@ import org.koin.core.annotation.Factory @Factory internal class MemoRepositoryImpl : MemoRepository { - override suspend fun upsert(list: List, owner: String) { + override suspend fun upsert(list: List, owner: String) { newSuspendedTransaction { list.forEach { memoAndTagIds -> MemoQuery.upsertMemo(memoAndTagIds.memo, memoAndTagIds.tagIds) @@ -24,7 +25,32 @@ internal class MemoRepositoryImpl : MemoRepository { } } - override fun findByIds(ids: Set): Flow> = flow { emit(newSuspendedTransaction { MemoTable.findByIds(ids) }) } + override suspend fun update(id: String, detail: MemoDetail) { + newSuspendedTransaction { + MemoTable.update(id, detail) + } + } + + override suspend fun updateFinish(id: String, isFinish: Boolean) { + newSuspendedTransaction { + MemoTable.updateFinish(id, isFinish) + } + } + + override suspend fun updateDelete(id: String, isDelete: Boolean) { + newSuspendedTransaction { + MemoTable.updateDelete(id, isDelete) + } + } + + override fun findById(id: String): Flow { + return flow { + newSuspendedTransaction { MemoTable.findById(id) } + .also { emit(it) } + } + } + + override fun findByIds(ids: Set): Flow> = flow { emit(newSuspendedTransaction { MemoTable.findByIds(ids) }) } override fun findMemoAndTagIdsByUpdateAt(uid: String, updateAt: Instant): Flow> = flow { diff --git a/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt index 176e9754..8be1be14 100644 --- a/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt +++ b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt @@ -1,11 +1,13 @@ package io.github.taetae98coding.diary.domain.account.repository import io.github.taetae98coding.diary.core.model.Account +import kotlinx.coroutines.flow.Flow public interface AccountRepository { public suspend fun contains(email: String): Boolean public suspend fun upsert(account: Account) - public suspend fun findByEmail(email: String, password: String): Account? + public fun findByEmail(email: String, password: String): Flow + public fun findByUid(uid: String): Flow } diff --git a/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/FindAccountUseCase.kt b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/FindAccountUseCase.kt index 2656ce7f..c7084d53 100644 --- a/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/FindAccountUseCase.kt +++ b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/FindAccountUseCase.kt @@ -2,18 +2,32 @@ package io.github.taetae98coding.diary.domain.account.usecase import io.github.taetae98coding.diary.core.model.Account import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +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 org.koin.core.annotation.Factory +@OptIn(ExperimentalCoroutinesApi::class) @Factory public class FindAccountUseCase internal constructor( - private val hashingPasswordUseCase: HashingPasswordUseCase, - private val repository: AccountRepository, + private val hashingPasswordUseCase: HashingPasswordUseCase, + private val repository: AccountRepository, ) { - public suspend operator fun invoke(email: String, password: String): Result = - runCatching { - repository.findByEmail( - email = email, - password = hashingPasswordUseCase(email, password).getOrThrow(), - ) - } + public operator fun invoke(email: String, password: String): Flow> { + return flow { + repository.findByEmail( + email = email, + password = hashingPasswordUseCase(email, password).getOrThrow(), + ).also { + emitAll(it) + } + }.mapLatest { + Result.success(it) + }.catch { + emit(Result.failure(it)) + } + } } diff --git a/server/domain/buddy/build.gradle.kts b/server/domain/buddy/build.gradle.kts index 3e3592b2..b5d6bd93 100644 --- a/server/domain/buddy/build.gradle.kts +++ b/server/domain/buddy/build.gradle.kts @@ -4,4 +4,5 @@ plugins { dependencies { implementation(project(":server:domain:fcm")) + implementation(project(":server:domain:account")) } 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 index f209765f..a7fc90e1 100644 --- 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 @@ -1,6 +1,7 @@ package io.github.taetae98coding.diary.domain.buddy.usecase import io.github.taetae98coding.diary.core.model.Memo +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository import io.github.taetae98coding.diary.domain.buddy.repository.BuddyRepository import io.github.taetae98coding.diary.domain.fcm.repository.FCMRepository import kotlinx.coroutines.flow.first @@ -10,19 +11,24 @@ import org.koin.core.annotation.Factory public class UpsertBuddyGroupMemoUseCase internal constructor( private val buddyRepository: BuddyRepository, private val fcmRepository: FCMRepository, + private val accountRepository: AccountRepository, ) { public suspend operator fun invoke( groupId: String, memo: Memo, tagIds: Set, + requesterUid: String, ): Result { return runCatching { // TODO Permission Check + val account = accountRepository.findByUid(requesterUid).first() ?: return@runCatching + buddyRepository.upsert(groupId, memo, tagIds) buddyRepository.findBuddyIdByGroupId(groupId).first() + .filter { it != requesterUid } .forEach { - fcmRepository.send(it, "๊ทธ๋ฃน ๋ฉ”๋ชจ", "${memo.title}๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + fcmRepository.send(it, "๊ทธ๋ฃน ๋ฉ”๋ชจ", "\'${memo.title}\' ๋ฉ”๋ชจ๊ฐ€ ์ถ”๊ฐ€๋์Šต๋‹ˆ๋‹ค. (${account.email})") } } } diff --git a/server/domain/memo/build.gradle.kts b/server/domain/memo/build.gradle.kts index e10cade2..ade19241 100644 --- a/server/domain/memo/build.gradle.kts +++ b/server/domain/memo/build.gradle.kts @@ -1,3 +1,8 @@ plugins { id("diary.server.domain") } + +dependencies { + implementation(project(":server:domain:account")) + implementation(project(":server:domain:fcm")) +} diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoBuddyRepository.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoBuddyRepository.kt new file mode 100644 index 00000000..e567ed60 --- /dev/null +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoBuddyRepository.kt @@ -0,0 +1,7 @@ +package io.github.taetae98coding.diary.domain.memo.repository + +import kotlinx.coroutines.flow.Flow + +public interface MemoBuddyRepository { + public fun findBuddyIdByMemoId(memoId: String): Flow> +} 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 4dfbb0e4..36715a7a 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 @@ -2,13 +2,18 @@ package io.github.taetae98coding.diary.domain.memo.repository import io.github.taetae98coding.diary.core.model.Memo import io.github.taetae98coding.diary.core.model.MemoAndTagIds +import io.github.taetae98coding.diary.core.model.MemoDetail import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant public interface MemoRepository { public suspend fun upsert(list: List, owner: String) + public suspend fun update(id: String, detail: MemoDetail) + public suspend fun updateFinish(id: String, isFinish: Boolean) + public suspend fun updateDelete(id: String, isDelete: Boolean) - public fun findByIds(ids: Set): Flow> + public fun findById(id: String): Flow + public fun findByIds(ids: Set): Flow> public fun findMemoAndTagIdsByUpdateAt(uid: String, updateAt: Instant): Flow> } diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt new file mode 100644 index 00000000..23dd49df --- /dev/null +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt @@ -0,0 +1,32 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import io.github.taetae98coding.diary.domain.fcm.repository.FCMRepository +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyRepository +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class DeleteMemoUseCase internal constructor( + private val memoRepository: MemoRepository, + private val memoBuddyRepository: MemoBuddyRepository, + private val fcmRepository: FCMRepository, + private val accountRepository: AccountRepository, +) { + public suspend operator fun invoke(id: String?, requesterUid: String): Result { + return runCatching { + // TODO permission check + if (id.isNullOrBlank()) return@runCatching + val memo = memoRepository.findById(id).first() ?: return@runCatching + val account = accountRepository.findByUid(requesterUid).first() ?: return@runCatching + + memoRepository.updateDelete(id, true) + memoBuddyRepository.findBuddyIdByMemoId(id).first() + .filter { it != requesterUid } + .forEach { + fcmRepository.send(it, "๊ทธ๋ฃน ๋ฉ”๋ชจ", "'${memo.title}' ๋ฉ”๋ชจ๊ฐ€ ์‚ญ์ œ๋์Šต๋‹ˆ๋‹ค. (${account.email})") + } + } + } +} diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindMemoUseCase.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindMemoUseCase.kt new file mode 100644 index 00000000..da397d6d --- /dev/null +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindMemoUseCase.kt @@ -0,0 +1,27 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.core.model.Memo +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class FindMemoUseCase internal constructor( + private val repository: MemoRepository, +) { + public operator fun invoke(id: String?): Flow> { + // TODO permission check + if (id.isNullOrBlank()) return flowOf(Result.success(null)) + + return flow { emitAll(repository.findById(id)) } + .mapLatest { Result.success(it) } + .catch { emit(Result.failure(it)) } + } +} diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt new file mode 100644 index 00000000..803f9027 --- /dev/null +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt @@ -0,0 +1,33 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import io.github.taetae98coding.diary.domain.fcm.repository.FCMRepository +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyRepository +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class FinishMemoUseCase internal constructor( + private val memoRepository: MemoRepository, + private val memoBuddyRepository: MemoBuddyRepository, + private val fcmRepository: FCMRepository, + private val accountRepository: AccountRepository, +) { + public suspend operator fun invoke(id: String?, requesterUid: String): Result { + return runCatching { + // TODO permission check + if (id.isNullOrBlank()) return@runCatching + + val memo = memoRepository.findById(id).first() ?: return@runCatching + val account = accountRepository.findByUid(requesterUid).first() ?: return@runCatching + + memoRepository.updateFinish(id, true) + memoBuddyRepository.findBuddyIdByMemoId(id).first() + .filter { it != requesterUid } + .forEach { + fcmRepository.send(it, "๊ทธ๋ฃน ๋ฉ”๋ชจ", "'${memo.title}' ๋ฉ”๋ชจ๊ฐ€ ์™„๋ฃŒ๋์Šต๋‹ˆ๋‹ค. (${account.email})") + } + } + } +} diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt new file mode 100644 index 00000000..1de2bf2b --- /dev/null +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt @@ -0,0 +1,32 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import io.github.taetae98coding.diary.domain.fcm.repository.FCMRepository +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyRepository +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class RestartMemoUseCase internal constructor( + private val memoRepository: MemoRepository, + private val memoBuddyRepository: MemoBuddyRepository, + private val fcmRepository: FCMRepository, + private val accountRepository: AccountRepository, +) { + public suspend operator fun invoke(id: String?, requesterUid: String): Result { + return runCatching { + // TODO permission check + if (id.isNullOrBlank()) return@runCatching + val memo = memoRepository.findById(id).first() ?: return@runCatching + val account = accountRepository.findByUid(requesterUid).first() ?: return@runCatching + + memoRepository.updateFinish(id, false) + memoBuddyRepository.findBuddyIdByMemoId(id).first() + .filter { it != requesterUid } + .forEach { + fcmRepository.send(it, "๊ทธ๋ฃน ๋ฉ”๋ชจ", "'${memo.title}' ๋ฉ”๋ชจ๊ฐ€ ์‹œ์ž‘๋์Šต๋‹ˆ๋‹ค. (${account.email})") + } + } + } +} diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt new file mode 100644 index 00000000..0dddbffd --- /dev/null +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt @@ -0,0 +1,33 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.core.model.MemoDetail +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import io.github.taetae98coding.diary.domain.fcm.repository.FCMRepository +import io.github.taetae98coding.diary.domain.memo.repository.MemoBuddyRepository +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class UpdateMemoUseCase internal constructor( + private val memoRepository: MemoRepository, + private val memoBuddyRepository: MemoBuddyRepository, + private val fcmRepository: FCMRepository, + private val accountRepository: AccountRepository, +){ + public suspend operator fun invoke(memoId: String?, detail: MemoDetail, requesterUid: String): Result { + return runCatching { + // TODO permission check + if (memoId.isNullOrBlank()) return@runCatching + val memo = memoRepository.findById(memoId).first() ?: return@runCatching + val account = accountRepository.findByUid(requesterUid).first() ?: return@runCatching + + memoRepository.update(memoId, detail) + memoBuddyRepository.findBuddyIdByMemoId(memoId).first() + .filter { it != requesterUid } + .forEach { + fcmRepository.send(it, "๊ทธ๋ฃน ๋ฉ”๋ชจ", "'${memo.title}' ๋ฉ”๋ชจ๊ฐ€ ์ˆ˜์ •๋์Šต๋‹ˆ๋‹ค. (${account.email})") + } + } + } +} diff --git a/server/feature/account/src/main/kotlin/io/github/taetae98coding/diary/feature/account/AccountRouting.kt b/server/feature/account/src/main/kotlin/io/github/taetae98coding/diary/feature/account/AccountRouting.kt index a453acf4..f78ab19a 100644 --- a/server/feature/account/src/main/kotlin/io/github/taetae98coding/diary/feature/account/AccountRouting.kt +++ b/server/feature/account/src/main/kotlin/io/github/taetae98coding/diary/feature/account/AccountRouting.kt @@ -14,6 +14,7 @@ import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.post import io.ktor.server.routing.route +import kotlinx.coroutines.flow.first import org.koin.ktor.plugin.scope public fun Route.accountRouting() { @@ -34,7 +35,7 @@ public fun Route.accountRouting() { post("/login") { request -> val useCase = call.scope.get() - useCase(request.email, request.password) + useCase(request.email, request.password).first() .onSuccess { account -> if (account == null) { call.respond(HttpStatusCode.NotFound, DiaryResponse.AccountNotFound) 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 707b3102..6c8ff801 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 @@ -113,6 +113,8 @@ public fun Route.buddyRouting() { return@post } + val uid = principal.payload.getClaim("uid").asString() + val groupId = call.parameters["groupId"] ?: run { call.respond(HttpStatusCode.BadRequest, DiaryResponse.BadRequest) return@post @@ -120,7 +122,7 @@ public fun Route.buddyRouting() { val useCase = call.scope.get() - useCase(groupId, request.toMemo(), request.tagIds) + useCase(groupId, request.toMemo(), request.tagIds, uid) .onSuccess { call.respond(DiaryResponse.Success) } .onFailure { call.respond(DiaryResponse.InternalServerError) } } 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 27a925b2..a09e4bde 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,10 +1,18 @@ package io.github.taetae98coding.diary.feature.memo import io.github.taetae98coding.diary.common.model.memo.LegacyMemoEntity +import io.github.taetae98coding.diary.common.model.memo.MemoDetailEntity +import io.github.taetae98coding.diary.common.model.memo.MemoEntity import io.github.taetae98coding.diary.common.model.response.DiaryResponse -import io.github.taetae98coding.diary.core.model.MemoAndTagIds import io.github.taetae98coding.diary.core.model.Memo +import io.github.taetae98coding.diary.core.model.MemoAndTagIds +import io.github.taetae98coding.diary.core.model.MemoDetail +import io.github.taetae98coding.diary.domain.memo.usecase.DeleteMemoUseCase import io.github.taetae98coding.diary.domain.memo.usecase.FetchMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.FindMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.FinishMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.RestartMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.UpdateMemoUseCase import io.github.taetae98coding.diary.domain.memo.usecase.UpsertMemoUseCase import io.ktor.http.HttpStatusCode import io.ktor.server.auth.authenticate @@ -14,6 +22,7 @@ import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.post +import io.ktor.server.routing.put import io.ktor.server.routing.route import kotlinx.coroutines.flow.first import kotlinx.datetime.Instant @@ -61,6 +70,105 @@ public fun Route.memoRouting() { } .onFailure { call.respond(DiaryResponse.InternalServerError) } } + + get("/{id}") { + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@get + } + + val id = call.parameters["id"] + val useCase = call.scope.get() + + useCase(id).first() + .onSuccess { memo -> + val entity = memo?.let { + 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(entity)) + } + .onFailure { call.respond(DiaryResponse.InternalServerError) } + } + + put("/{id}/update") { request -> + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@put + } + + val uid = principal.payload.getClaim("uid").asString() + val id = call.parameters["id"] + val useCase = call.scope.get() + + useCase(id, request.toDetail(), uid) + .onSuccess { call.respond(DiaryResponse.Success) } + .onFailure { call.respond(DiaryResponse.InternalServerError) } + } + + put("/{id}/updateDelete") { + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@put + } + + val uid = principal.payload.getClaim("uid").asString() + val id = call.parameters["id"] + val isDelete = call.parameters["isDelete"]?.toBoolean() ?: run { + call.respond(DiaryResponse.BadRequest) + return@put + } + + if (isDelete) { + val useCase = call.scope.get() + + useCase(id, uid) + .onSuccess { call.respond(DiaryResponse.Success) } + .onFailure { call.respond(DiaryResponse.InternalServerError) } + } + } + + put("/{id}/updateFinish") { + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@put + } + + val uid = principal.payload.getClaim("uid").asString() + val id = call.parameters["id"] + val isFinish = call.parameters["isFinish"]?.toBoolean() ?: run { + call.respond(DiaryResponse.BadRequest) + return@put + } + + if (isFinish) { + val useCase = call.scope.get() + + useCase(id, uid) + .onSuccess { call.respond(DiaryResponse.Success) } + .onFailure { call.respond(DiaryResponse.InternalServerError) } + } else { + val useCase = call.scope.get() + + useCase(id, uid) + .onSuccess { call.respond(DiaryResponse.Success) } + .onFailure { call.respond(DiaryResponse.InternalServerError) } + } + } } } } @@ -79,6 +187,16 @@ private fun LegacyMemoEntity.toMemo(): Memo = updateAt = updateAt, ) +private fun MemoDetailEntity.toDetail(): MemoDetail { + return MemoDetail( + title = title, + description = description, + start = start, + endInclusive = endInclusive, + color = color, + ) +} + private fun MemoAndTagIds.toEntity(owner: String): LegacyMemoEntity = LegacyMemoEntity( id = memo.id,