From b86eadce045d09cc5a395296d742cdcd707c2f81 Mon Sep 17 00:00:00 2001 From: Sehwan Yun <39579912+l5x5l@users.noreply.github.com> Date: Sat, 27 Jul 2024 16:39:13 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20#21=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [BASE] #21 검색 화면 모듈 생성 * [FEATURE] #21 필터 영역 제외한 검색 화면 UI 구현 및 PokitInput에서 아이콘 클릭 이벤트를 수신할 수 있도록 변경 * [FEATURE] #21 필터 영역 및 달력 제외한 filter bottomsheet ui 구현 * [FEATURE] #21 달력 ui 구현 * [FEATURE] #21 달력 ui 수정 * [FEATURE] #21 검색 화면에 달력 UI 연결 및 viewModel 구현 * [UI] #21 PokitInput의 singleline 속성 추가 및 KeyboardAction와 focusRequester를 인자 추가 (기본값 존재) * [CHORE] #21 세부 사항 수정 - 아래 문제점 수정 : "모아보기", "안읽음" 필터 클릭시 포킷 선택으로 bottomSheet가 호출되던 문제 : bottomSheet에서 저장하기 클릭시 필터가 전부 취소되어 있을 때 필터 버튼만 표시되던 문제 - 아래 기능 구현 : 키보드 엔터 클릭시 검색 결과 화면으로 이동 : 검색어 제거 클릭시 검색창 활성화 : 정렬 기준 변경 로직 추가 * [CHORE] #21 ktlint 적용 --- .../ui/components/atom/input/PokitInput.kt | 24 +- .../subcomponents/icon/PokitInputIcon.kt | 14 +- .../pokit/core/ui/utils/ModifierUtils.kt | 11 + feature/search/.gitignore | 1 + feature/search/build.gradle.kts | 64 ++++ feature/search/consumer-rules.pro | 0 feature/search/proguard-rules.pro | 21 ++ .../pokit/search/ExampleInstrumentedTest.kt | 22 ++ feature/search/src/main/AndroidManifest.xml | 4 + .../java/pokitmons/pokit/search/Preview.kt | 20 ++ .../pokitmons/pokit/search/SearchScreen.kt | 127 ++++++++ .../pokitmons/pokit/search/SearchViewModel.kt | 129 ++++++++ .../search/components/atom/CalendarCell.kt | 91 ++++++ .../search/components/calendar/Calendar.kt | 149 +++++++++ .../search/components/calendar/Preview.kt | 52 +++ .../pokit/search/components/filter/Filter.kt | 169 ++++++++++ .../pokit/search/components/filter/Preview.kt | 45 +++ .../filterbottomsheet/FilterBottomSheet.kt | 104 ++++++ .../FilterBottomSheetContent.kt | 298 ++++++++++++++++++ .../components/filterbottomsheet/Preview.kt | 23 ++ .../components/recentsearchword/Preview.kt | 27 ++ .../recentsearchword/RecentSearchWord.kt | 118 +++++++ .../searchitemlist/SearchItemList.kt | 79 +++++ .../search/components/toolbar/Toolbar.kt | 86 +++++ .../pokit/search/model/CalendarCell.kt | 8 + .../pokit/search/model/CalendarPage.kt | 41 +++ .../java/pokitmons/pokit/search/model/Date.kt | 15 + .../pokitmons/pokit/search/model/Filter.kt | 39 +++ .../java/pokitmons/pokit/search/model/Link.kt | 83 +++++ .../pokitmons/pokit/search/model/Pokit.kt | 15 + .../pokit/search/model/SearchScreenState.kt | 15 + .../pokit/search/utils/CalendarUtils.kt | 115 +++++++ feature/search/src/main/res/values/string.xml | 35 ++ .../pokitmons/pokit/search/ExampleUnitTest.kt | 16 + 34 files changed, 2053 insertions(+), 7 deletions(-) create mode 100644 core/ui/src/main/java/pokitmons/pokit/core/ui/utils/ModifierUtils.kt create mode 100644 feature/search/.gitignore create mode 100644 feature/search/build.gradle.kts create mode 100644 feature/search/consumer-rules.pro create mode 100644 feature/search/proguard-rules.pro create mode 100644 feature/search/src/androidTest/java/pokitmons/pokit/search/ExampleInstrumentedTest.kt create mode 100644 feature/search/src/main/AndroidManifest.xml create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/Preview.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/atom/CalendarCell.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Calendar.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Preview.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/filter/Preview.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/Preview.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/Preview.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/RecentSearchWord.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/components/toolbar/Toolbar.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/CalendarCell.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/CalendarPage.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/Filter.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt create mode 100644 feature/search/src/main/java/pokitmons/pokit/search/utils/CalendarUtils.kt create mode 100644 feature/search/src/main/res/values/string.xml create mode 100644 feature/search/src/test/java/pokitmons/pokit/search/ExampleUnitTest.kt diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/PokitInput.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/PokitInput.kt index cfe8f5e9..79e24e41 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/PokitInput.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/PokitInput.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -11,6 +12,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -21,6 +24,7 @@ import pokitmons.pokit.core.ui.components.atom.input.attributes.PokitInputState import pokitmons.pokit.core.ui.components.atom.input.subcomponents.container.PokitInputContainer import pokitmons.pokit.core.ui.components.atom.input.subcomponents.icon.PokitInputIcon import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.core.ui.utils.conditional @Composable fun PokitInput( @@ -29,10 +33,13 @@ fun PokitInput( onChangeText: (String) -> Unit, icon: PokitInputIcon?, modifier: Modifier = Modifier, + onClickIcon: (() -> Unit)? = null, shape: PokitInputShape = PokitInputShape.RECTANGLE, readOnly: Boolean = false, enable: Boolean = true, isError: Boolean = false, + keyboardActions: KeyboardActions = KeyboardActions.Default, + focusRequester: FocusRequester? = null, ) { var focused by remember { mutableStateOf(false) } val state = remember(focused, isError, readOnly, enable) { @@ -52,10 +59,15 @@ fun PokitInput( onValueChange = onChangeText, textStyle = textStyle, enabled = (enable && !readOnly), - maxLines = 1, - modifier = Modifier.onFocusChanged { focusState -> - focused = focusState.isFocused - }, + singleLine = true, + modifier = Modifier + .onFocusChanged { focusState -> + focused = focusState.isFocused + } + .conditional(focusRequester != null) { + focusRequester(focusRequester!!) + }, + keyboardActions = keyboardActions, decorationBox = { innerTextField -> PokitInputContainer( iconPosition = icon?.position, @@ -68,7 +80,7 @@ fun PokitInput( } if (icon?.position == PokitInputIconPosition.LEFT) { - PokitInputIcon(state = state, resourceId = icon.resourceId) + PokitInputIcon(state = state, resourceId = icon.resourceId, onClick = onClickIcon) Box(modifier = Modifier.width(8.dp)) } @@ -81,7 +93,7 @@ fun PokitInput( } if (icon?.position == PokitInputIconPosition.RIGHT) { - PokitInputIcon(state = state, resourceId = icon.resourceId) + PokitInputIcon(state = state, resourceId = icon.resourceId, onClick = onClickIcon) } } } diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/subcomponents/icon/PokitInputIcon.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/subcomponents/icon/PokitInputIcon.kt index b3b89e04..0dec8891 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/subcomponents/icon/PokitInputIcon.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/input/subcomponents/icon/PokitInputIcon.kt @@ -1,8 +1,11 @@ package pokitmons.pokit.core.ui.components.atom.input.subcomponents.icon +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource @@ -14,6 +17,7 @@ import pokitmons.pokit.core.ui.theme.PokitTheme internal fun PokitInputIcon( state: PokitInputState, resourceId: Int, + onClick: (() -> Unit)? = null, ) { val iconColor = getColor(state = state) @@ -21,7 +25,15 @@ internal fun PokitInputIcon( painter = painterResource(id = resourceId), contentDescription = null, tint = iconColor, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(24.dp).then( + other = onClick?.let { method -> + Modifier.clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = method + ) + } ?: Modifier + ) ) } diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/utils/ModifierUtils.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/utils/ModifierUtils.kt new file mode 100644 index 00000000..6220a9eb --- /dev/null +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/utils/ModifierUtils.kt @@ -0,0 +1,11 @@ +package pokitmons.pokit.core.ui.utils + +import androidx.compose.ui.Modifier + +internal fun Modifier.conditional(condition: Boolean, modifier: Modifier.() -> Modifier): Modifier { + return if (condition) { + then(modifier(Modifier)) + } else { + this + } +} diff --git a/feature/search/.gitignore b/feature/search/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/search/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts new file mode 100644 index 00000000..ef9a8247 --- /dev/null +++ b/feature/search/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + alias(libs.plugins.com.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) +} + +android { + namespace = "pokitmons.pokit.search" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + implementation(libs.orbit.compose) + implementation(libs.orbit.core) + implementation(libs.orbit.viewmodel) + + implementation(project(":core:ui")) +} diff --git a/feature/search/consumer-rules.pro b/feature/search/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/search/proguard-rules.pro b/feature/search/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/search/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/search/src/androidTest/java/pokitmons/pokit/search/ExampleInstrumentedTest.kt b/feature/search/src/androidTest/java/pokitmons/pokit/search/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..b07df8db --- /dev/null +++ b/feature/search/src/androidTest/java/pokitmons/pokit/search/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package pokitmons.pokit.search + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("pokitmons.pokit.search.test", appContext.packageName) + } +} diff --git a/feature/search/src/main/AndroidManifest.xml b/feature/search/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/search/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt new file mode 100644 index 00000000..d549c85f --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt @@ -0,0 +1,20 @@ +package pokitmons.pokit.search + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Preview(showBackground = true) +@Composable +private fun Preview() { + PokitTheme { + Column( + modifier = Modifier.fillMaxSize() + ) { + SearchScreen() + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt b/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt new file mode 100644 index 00000000..545e0dec --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt @@ -0,0 +1,127 @@ +package pokitmons.pokit.search + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.search.components.filter.FilterArea +import pokitmons.pokit.search.components.filterbottomsheet.FilterBottomSheet +import pokitmons.pokit.search.components.recentsearchword.RecentSearchWord +import pokitmons.pokit.search.components.searchitemlist.SearchItemList +import pokitmons.pokit.search.components.toolbar.Toolbar +import pokitmons.pokit.search.model.Filter +import pokitmons.pokit.search.model.FilterType +import pokitmons.pokit.search.model.Link +import pokitmons.pokit.search.model.SearchScreenState +import pokitmons.pokit.search.model.SearchScreenStep + +@Composable +fun SearchScreenContainer( + viewModel: SearchViewModel, + onBackPressed: () -> Unit, +) { + val state by viewModel.state.collectAsState() + val searchWord by viewModel.searchWord.collectAsState() + val linkList by viewModel.linkList.collectAsState() + + SearchScreen( + state = state, + currentSearchWord = searchWord, + linkList = linkList, + onClickBack = onBackPressed, + inputSearchWord = viewModel::inputSearchWord, + onClickSearch = viewModel::applyCurrentSearchWord, + onClickRecentSearchWord = viewModel::applySearchWord, + onClickUseRecentSearchWord = viewModel::toggleUseRecentSearchWord, + onClickRemoveAllRecentSearchWord = viewModel::removeAllRecentSearchWord, + onClickRemoveRecentSearchWord = viewModel::removeRecentSearchWord, + onClickFilterSelect = viewModel::showFilterBottomSheet, + onClickFilterItem = viewModel::showFilterBottomSheetWithType, + hideBottomSheet = viewModel::hideFilterBottomSheet, + onClickFilterSave = viewModel::setFilter, + toggleSortOrder = viewModel::toggleSortOrder + ) +} + +@Composable +fun SearchScreen( + state: SearchScreenState = SearchScreenState(), + currentSearchWord: String = "", + linkList: List = emptyList(), + onClickBack: () -> Unit = {}, + inputSearchWord: (String) -> Unit = {}, + onClickSearch: () -> Unit = {}, + onClickRecentSearchWord: (String) -> Unit = {}, + onClickUseRecentSearchWord: () -> Unit = {}, + onClickRemoveAllRecentSearchWord: () -> Unit = {}, + onClickRemoveRecentSearchWord: (String) -> Unit = {}, + onClickFilterSelect: () -> Unit = {}, + onClickFilterItem: (FilterType) -> Unit = {}, + hideBottomSheet: () -> Unit = {}, + onClickFilterSave: (Filter) -> Unit = {}, + toggleSortOrder: () -> Unit = {}, +) { + Column( + modifier = Modifier.fillMaxSize() + ) { + Toolbar( + onClickBack = onClickBack, + inputSearchWord = inputSearchWord, + currentSearchWord = currentSearchWord, + onClickSearch = onClickSearch, + onClickRemove = remember { { inputSearchWord("") } } + ) + + if (state.step == SearchScreenStep.INPUT) { + RecentSearchWord( + onClickRemoveAll = onClickRemoveAllRecentSearchWord, + onToggleAutoSave = onClickUseRecentSearchWord, + useAutoSave = state.useRecentSearchWord, + recentSearchWords = state.recentSearchWords, + onClickRemoveSearchWord = onClickRemoveRecentSearchWord, + onClickSearchWord = onClickRecentSearchWord + ) + } + + if (state.step == SearchScreenStep.RESULT) { + FilterArea( + filter = state.filter, + onClickFilter = onClickFilterSelect, + onClickBookmark = remember { { onClickFilterItem(FilterType.Collect) } }, + onClickPokitName = remember { { onClickFilterItem(FilterType.Pokit) } }, + onClickPeriod = remember { { onClickFilterItem(FilterType.Period) } } + ) + } + + HorizontalDivider( + thickness = 6.dp, + color = PokitTheme.colors.backgroundPrimary + ) + + if (state.step == SearchScreenStep.RESULT) { + SearchItemList( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + onToggleSort = toggleSortOrder, + useRecentOrder = state.sortRecent, + links = linkList + ) + } + + FilterBottomSheet( + filter = state.filter ?: Filter(), + firstShowType = state.firstBottomSheetFilterType, + show = state.showFilterBottomSheet, + onDismissRequest = hideBottomSheet, + onSaveClilck = onClickFilterSave + ) + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt b/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt new file mode 100644 index 00000000..b8c0055a --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt @@ -0,0 +1,129 @@ +package pokitmons.pokit.search + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import pokitmons.pokit.search.model.Filter +import pokitmons.pokit.search.model.FilterType +import pokitmons.pokit.search.model.Link +import pokitmons.pokit.search.model.SearchScreenState +import pokitmons.pokit.search.model.SearchScreenStep +import pokitmons.pokit.search.model.sampleLinks + +class SearchViewModel : ViewModel() { + private val _searchWord = MutableStateFlow("") + val searchWord = _searchWord.asStateFlow() + + private val _state = MutableStateFlow(SearchScreenState()) + val state = _state.asStateFlow() + + private var appliedSearchWord = "" + + private val _linkList = MutableStateFlow>(emptyList()) + val linkList = _linkList.asStateFlow() + + init { + _linkList.update { sampleLinks } + } + + fun inputSearchWord(newSearchWord: String) { + _searchWord.update { newSearchWord } + val currentState = state.value + + if (newSearchWord.isEmpty() && currentState.step == SearchScreenStep.RESULT) { + _state.update { state -> + state.copy(step = SearchScreenStep.INPUT) + } + } + } + + fun applyCurrentSearchWord() { + appliedSearchWord = searchWord.value + + if (appliedSearchWord.isNotEmpty()) { + _state.update { state -> + state.copy(step = SearchScreenStep.RESULT) + } + } + } + + fun applySearchWord(word: String) { + appliedSearchWord = word + _searchWord.update { word } + + if (appliedSearchWord.isNotEmpty()) { + _state.update { state -> + state.copy(step = SearchScreenStep.RESULT) + } + } + } + + fun toggleUseRecentSearchWord() { + _state.update { state -> + state.copy(useRecentSearchWord = !state.useRecentSearchWord) + } + } + + fun removeRecentSearchWord(word: String) { + _state.update { state -> + state.copy(recentSearchWords = state.recentSearchWords.filter { name -> name != word }) + } + } + + fun removeAllRecentSearchWord() { + _state.update { state -> + state.copy( + recentSearchWords = emptyList() + ) + } + } + + fun showFilterBottomSheet() { + _state.update { state -> + state.copy( + showFilterBottomSheet = true + ) + } + } + + fun showFilterBottomSheetWithType(type: FilterType) { + _state.update { state -> + state.copy( + showFilterBottomSheet = true, + firstBottomSheetFilterType = type + ) + } + } + + fun hideFilterBottomSheet() { + _state.update { state -> + state.copy( + showFilterBottomSheet = false + ) + } + } + + fun setFilter(filter: Filter) { + _state.update { state -> + state.copy( + showFilterBottomSheet = false, + filter = if (filter == Filter.DefaultFilter) { + null + } else { + filter + } + ) + } + + // todo refresh 기능 구현 + } + + fun toggleSortOrder() { + _state.update { state -> + state.copy(sortRecent = !state.sortRecent) + } + + // todo refresh 기능 구현 + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/atom/CalendarCell.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/atom/CalendarCell.kt new file mode 100644 index 00000000..2cb7dc3d --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/atom/CalendarCell.kt @@ -0,0 +1,91 @@ +package pokitmons.pokit.search.components.atom + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.core.ui.theme.color.Orange100 +import pokitmons.pokit.search.model.Date + +@Composable +internal fun CalendarCellView( + date: Date, + onClick: ((Date) -> Unit)? = null, + state: CalendarCellState = CalendarCellState.ACTIVE, +) { + val backgroundColor = getBackgroundColor(state = state) + val textColor = getTextColor(state = state) + + Box( + modifier = Modifier + .size(40.dp) + .then( + other = onClick?.let { method -> + Modifier.clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { method(date) } + ) + } ?: Modifier + ) + .background( + color = backgroundColor, + shape = RoundedCornerShape(8.dp) + ) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + textAlign = TextAlign.Center, + text = date.day.toString(), + style = PokitTheme.typography.body1Medium.copy(color = textColor) + ) + } +} + +enum class CalendarCellState { + INACTIVE, ACTIVE, SELECTED, IN_RANGE +} + +@Composable +private fun getTextColor(state: CalendarCellState): Color { + return when (state) { + CalendarCellState.INACTIVE -> { + PokitTheme.colors.textTertiary + } + CalendarCellState.ACTIVE -> { + PokitTheme.colors.textSecondary + } + CalendarCellState.IN_RANGE -> { + PokitTheme.colors.inverseWh + } + CalendarCellState.SELECTED -> { + PokitTheme.colors.inverseWh + } + } +} + +@Composable +private fun getBackgroundColor(state: CalendarCellState): Color { + return when (state) { + CalendarCellState.IN_RANGE -> { + Orange100 + } + CalendarCellState.SELECTED -> { + PokitTheme.colors.brand + } + else -> { + Color.Unspecified + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Calendar.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Calendar.kt new file mode 100644 index 00000000..d2c92eb2 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Calendar.kt @@ -0,0 +1,149 @@ +package pokitmons.pokit.search.components.calendar + +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.search.R +import pokitmons.pokit.search.components.atom.CalendarCellState +import pokitmons.pokit.search.components.atom.CalendarCellView +import pokitmons.pokit.search.model.CalendarPage +import pokitmons.pokit.search.model.Date +import pokitmons.pokit.search.utils.getCells +import pokitmons.pokit.search.utils.weekDayStringResources + +@Composable +internal fun CalendarView( + calendarPage: CalendarPage, + modifier: Modifier = Modifier, + startDate: Date? = null, + endDate: Date? = null, + onClickCell: (Date) -> Unit = {}, +) { + var currentPage by remember { mutableStateOf(calendarPage) } + val calendarCells = remember( + currentPage, + startDate, + endDate + ) { + getCells(year = currentPage.year, month = currentPage.month, startDate = startDate, endDate = endDate) + } + + Column( + modifier = modifier + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.format_calendar, currentPage.year, currentPage.month), + style = PokitTheme.typography.body1Bold.copy(color = PokitTheme.colors.textPrimary) + ) + + Spacer(modifier = Modifier.weight(1f)) + + IconButton( + modifier = Modifier.size(40.dp), + onClick = remember { + { + currentPage = currentPage.prevPage() + } + } + ) { + Icon( + painter = painterResource(id = pokitmons.pokit.core.ui.R.drawable.icon_24_arrow_left), + contentDescription = "prev month" + ) + } + + Spacer(modifier = Modifier.width(4.dp)) + + IconButton( + modifier = Modifier.size(40.dp), + onClick = remember { + { + currentPage = currentPage.nextPage() + } + } + ) { + Icon( + painter = painterResource(id = pokitmons.pokit.core.ui.R.drawable.icon_24_arrow_right), + contentDescription = "next month" + ) + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + LazyVerticalGrid( + columns = GridCells.Fixed(count = 7), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(weekDayStringResources) { stringRes -> + Box( + modifier = Modifier.size(40.dp) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + textAlign = TextAlign.Center, + text = stringResource(id = stringRes), + style = PokitTheme.typography.detail1.copy(color = PokitTheme.colors.textTertiary) + ) + } + } + + items(calendarCells) { cell -> + if (cell.date.year != currentPage.year || cell.date.month != currentPage.month) { + CalendarCellView( + date = cell.date, + onClick = null, + state = CalendarCellState.INACTIVE + ) + } else if (cell.selected) { + CalendarCellView( + date = cell.date, + onClick = onClickCell, + state = CalendarCellState.SELECTED + ) + } else if (cell.inSelectRange) { + CalendarCellView( + date = cell.date, + onClick = onClickCell, + state = CalendarCellState.IN_RANGE + ) + } else { + CalendarCellView( + date = cell.date, + onClick = onClickCell, + state = CalendarCellState.ACTIVE + ) + } + } + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Preview.kt new file mode 100644 index 00000000..89781404 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/calendar/Preview.kt @@ -0,0 +1,52 @@ +package pokitmons.pokit.search.components.calendar + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.search.model.CalendarPage +import pokitmons.pokit.search.model.Date + +@Preview(showBackground = true) +@Composable +private fun Preview() { + var startDate by remember { mutableStateOf(null) } + var endDate by remember { mutableStateOf(null) } + + PokitTheme { + Column( + modifier = Modifier.width(300.dp).fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + CalendarView( + calendarPage = CalendarPage(endDate), + startDate = startDate, + endDate = endDate, + onClickCell = remember { + { date -> + val currentStartDate = startDate + if (currentStartDate != null && endDate == null) { + if (date <= currentStartDate) { + startDate = date + } else { + endDate = date + } + } else { + startDate = date + endDate = null + } + } + } + ) + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt new file mode 100644 index 00000000..4f18074a --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt @@ -0,0 +1,169 @@ +package pokitmons.pokit.search.components.filter + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.components.atom.button.PokitButton +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIcon +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIconPosition +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonShape +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonStyle +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonType +import pokitmons.pokit.search.R +import pokitmons.pokit.search.model.Filter +import pokitmons.pokit.core.ui.R.drawable as coreDrawable + +@Composable +fun FilterArea( + filter: Filter? = null, + onClickFilter: () -> Unit = {}, + onClickPeriod: () -> Unit = {}, + onClickPokitName: () -> Unit = {}, + onClickBookmark: () -> Unit = {}, +) { + val scrollState = rememberScrollState() + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, bottom = 24.dp) + .horizontalScroll(scrollState), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Spacer(modifier = Modifier.width(20.dp)) + + if (filter == null) { + PokitButton( + text = stringResource(id = R.string.filter), + icon = PokitButtonIcon( + resourceId = coreDrawable.icon_24_filter, + position = PokitButtonIconPosition.LEFT + ), + onClick = onClickFilter, + size = PokitButtonSize.SMALL, + shape = PokitButtonShape.ROUND, + style = PokitButtonStyle.STROKE, + type = PokitButtonType.SECONDARY + ) + + PokitButton( + text = stringResource(id = R.string.pokit_name), + icon = PokitButtonIcon( + resourceId = coreDrawable.icon_24_arrow_down, + position = PokitButtonIconPosition.RIGHT + ), + onClick = onClickPokitName, + size = PokitButtonSize.SMALL, + shape = PokitButtonShape.ROUND, + style = PokitButtonStyle.STROKE, + type = PokitButtonType.SECONDARY + ) + + PokitButton( + text = stringResource(id = R.string.collect_show), + icon = PokitButtonIcon( + resourceId = coreDrawable.icon_24_arrow_down, + position = PokitButtonIconPosition.RIGHT + ), + onClick = onClickBookmark, + size = PokitButtonSize.SMALL, + shape = PokitButtonShape.ROUND, + style = PokitButtonStyle.STROKE, + type = PokitButtonType.SECONDARY + ) + + PokitButton( + text = stringResource(id = R.string.period), + icon = PokitButtonIcon( + resourceId = coreDrawable.icon_24_arrow_down, + position = PokitButtonIconPosition.RIGHT + ), + onClick = onClickPeriod, + size = PokitButtonSize.SMALL, + shape = PokitButtonShape.ROUND, + style = PokitButtonStyle.STROKE, + type = PokitButtonType.SECONDARY + ) + } else { + PokitButton( + text = stringResource(id = R.string.filter), + icon = PokitButtonIcon( + resourceId = coreDrawable.icon_24_filter, + position = PokitButtonIconPosition.LEFT + ), + onClick = onClickFilter, + size = PokitButtonSize.SMALL, + shape = PokitButtonShape.ROUND, + style = PokitButtonStyle.FILLED + ) + + filter.selectedPokits.map { pokit -> + PokitButton( + text = pokit.title, + icon = PokitButtonIcon( + resourceId = coreDrawable.icon_24_arrow_down, + position = PokitButtonIconPosition.RIGHT + ), + onClick = onClickPokitName, + size = PokitButtonSize.SMALL, + shape = PokitButtonShape.ROUND, + style = PokitButtonStyle.STROKE + ) + } + + if (filter.bookmark) { + PokitButton( + text = stringResource(id = R.string.bookmark), + icon = PokitButtonIcon( + resourceId = coreDrawable.icon_24_arrow_down, + position = PokitButtonIconPosition.RIGHT + ), + onClick = onClickBookmark, + size = PokitButtonSize.SMALL, + shape = PokitButtonShape.ROUND, + style = PokitButtonStyle.STROKE + ) + } + + if (filter.notRead) { + PokitButton( + text = stringResource(id = R.string.not_read), + icon = PokitButtonIcon( + resourceId = coreDrawable.icon_24_arrow_down, + position = PokitButtonIconPosition.RIGHT + ), + onClick = onClickBookmark, + size = PokitButtonSize.SMALL, + shape = PokitButtonShape.ROUND, + style = PokitButtonStyle.STROKE + ) + } + + filter.getDateString()?.let { dateString -> + PokitButton( + text = dateString, + icon = PokitButtonIcon( + resourceId = coreDrawable.icon_24_arrow_down, + position = PokitButtonIconPosition.RIGHT + ), + onClick = onClickPeriod, + size = PokitButtonSize.SMALL, + shape = PokitButtonShape.ROUND, + style = PokitButtonStyle.STROKE + ) + } + } + + Spacer(modifier = Modifier.width(20.dp)) + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Preview.kt new file mode 100644 index 00000000..176015e0 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Preview.kt @@ -0,0 +1,45 @@ +package pokitmons.pokit.search.components.filter + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.search.model.Date +import pokitmons.pokit.search.model.Filter +import pokitmons.pokit.search.model.samplePokits + +@Preview(showBackground = true) +@Composable +private fun Preview() { + PokitTheme { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + FilterArea() + + FilterArea( + filter = Filter( + selectedPokits = samplePokits + ) + ) + + FilterArea( + filter = Filter( + startDate = Date(year = 2024, month = 7, day = 20), + endDate = Date(year = 2024, month = 7, day = 20) + ) + ) + + FilterArea( + filter = Filter( + bookmark = true + ) + ) + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt new file mode 100644 index 00000000..648c08ae --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt @@ -0,0 +1,104 @@ +package pokitmons.pokit.search.components.filterbottomsheet + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.search.model.Filter +import pokitmons.pokit.search.model.FilterType +import pokitmons.pokit.search.model.Pokit +import pokitmons.pokit.search.model.samplePokits + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun FilterBottomSheet( + filter: Filter = Filter(), + firstShowType: FilterType = FilterType.Pokit, + onSaveClilck: (Filter) -> Unit = {}, + pokits: List = samplePokits, + show: Boolean = false, + onDismissRequest: () -> Unit = {}, +) { + val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var visibility by remember { mutableStateOf(show) } + val scope = rememberCoroutineScope() + + LaunchedEffect(show) { + if (visibility && !show) { + scope.launch { + bottomSheetState.hide() + }.invokeOnCompletion { + onDismissRequest() + visibility = false + } + } else { + visibility = show + } + } + + if (visibility) { + ModalBottomSheet( + onDismissRequest = remember { + { + onDismissRequest() + visibility = false + } + }, + sheetState = bottomSheetState, + scrimColor = Color.Transparent, + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + containerColor = PokitTheme.colors.backgroundBase, + dragHandle = { + Column { + Spacer(modifier = Modifier.height(8.dp)) + + Box( + Modifier + .clip(RoundedCornerShape(4.dp)) + .width(36.dp) + .height(4.dp) + .background(color = PokitTheme.colors.iconTertiary) + ) + + Spacer(modifier = Modifier.height(12.dp)) + } + } + ) { + FilterBottomSheetContent( + filter = filter, + firstShowType = firstShowType, + onSaveClilck = onSaveClilck, + pokits = pokits + ) + + Spacer( + Modifier.windowInsetsBottomHeight( + WindowInsets.navigationBarsIgnoringVisibility + ) + ) + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt new file mode 100644 index 00000000..63584cd0 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt @@ -0,0 +1,298 @@ +package pokitmons.pokit.search.components.filterbottomsheet + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.interaction.MutableInteractionSource +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.components.atom.button.PokitButton +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize +import pokitmons.pokit.core.ui.components.atom.checkbox.PokitCheckbox +import pokitmons.pokit.core.ui.components.atom.chip.PokitChip +import pokitmons.pokit.core.ui.components.atom.chip.attributes.PokitChipIconPosiion +import pokitmons.pokit.core.ui.components.atom.chip.attributes.PokitChipState +import pokitmons.pokit.core.ui.components.block.pokitlist.PokitList +import pokitmons.pokit.core.ui.components.block.tap.PokitTap +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.search.R +import pokitmons.pokit.search.components.calendar.CalendarView +import pokitmons.pokit.search.model.CalendarPage +import pokitmons.pokit.search.model.Filter +import pokitmons.pokit.search.model.FilterType +import pokitmons.pokit.search.model.Pokit +import pokitmons.pokit.search.model.samplePokits +import pokitmons.pokit.core.ui.R.string as coreString + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun FilterBottomSheetContent( + filter: Filter = Filter(), + firstShowType: FilterType = FilterType.Pokit, + onSaveClilck: (Filter) -> Unit = {}, + pokits: List = samplePokits, +) { + var currentFilter by remember { mutableStateOf(filter) } + var currentShowType by remember { mutableStateOf(firstShowType) } + val scrollState = rememberScrollState() + val pagerState = rememberPagerState( + initialPage = firstShowType.index + ) { + FilterType.entries.size + } + + Column( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + FilterType.entries.map { entry -> + PokitTap( + modifier = Modifier.weight(1f), + text = stringResource(id = entry.stringResourceId), + data = entry, + onClick = remember { + { type -> + currentShowType = type + } + }, + selectedItem = currentShowType + ) + } + } + + HorizontalPager( + modifier = Modifier + .fillMaxWidth() + .height(390.dp), + state = pagerState, + userScrollEnabled = false + ) { + when (currentShowType) { + FilterType.Pokit -> { + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items(pokits) { pokit -> + PokitList( + item = pokit, + title = pokit.title, + sub = stringResource(id = coreString.pokit_count_format, pokit.count), + onClickItem = { item -> + currentFilter = currentFilter.addPokit(item) + } + ) + } + } + } + + FilterType.Collect -> { + Column( + modifier = Modifier.fillMaxSize() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + currentFilter = currentFilter.copy(bookmark = !currentFilter.bookmark) + } + .padding(12.dp) + ) { + PokitCheckbox( + checked = currentFilter.bookmark, + onClick = { currentFilter = currentFilter.copy(bookmark = !currentFilter.bookmark) } + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(id = R.string.bookmark), + style = PokitTheme.typography.body1Medium.copy(color = PokitTheme.colors.textPrimary) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + currentFilter = currentFilter.copy(notRead = !currentFilter.notRead) + } + .padding(12.dp) + ) { + PokitCheckbox( + checked = currentFilter.notRead, + onClick = { + currentFilter = currentFilter.copy(notRead = !currentFilter.notRead) + } + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(id = R.string.not_read), + style = PokitTheme.typography.body1Medium.copy(color = PokitTheme.colors.textPrimary) + ) + } + } + } + + FilterType.Period -> { + CalendarView( + calendarPage = CalendarPage(filter.endDate), + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + startDate = currentFilter.startDate, + endDate = currentFilter.endDate, + onClickCell = remember { + { date -> + val startDate = currentFilter.startDate + val endDate = currentFilter.endDate + currentFilter = if (startDate != null && endDate == null) { + if (date <= startDate) { + currentFilter.copy(startDate = date) + } else { + currentFilter.copy(endDate = date) + } + } else { + currentFilter.copy( + startDate = date, + endDate = null + ) + } + } + } + ) + } + } + } + + HorizontalDivider( + thickness = 1.dp, + color = PokitTheme.colors.borderTertiary + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + .horizontalScroll(scrollState), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Spacer(modifier = Modifier.width(20.dp).height(32.dp)) + + currentFilter.selectedPokits.map { pokit -> + PokitChip( + data = pokit, + text = pokit.title, + removeIconPosition = PokitChipIconPosiion.RIGHT, + state = PokitChipState.STROKE, + onClickRemove = remember { + { item -> + currentFilter = currentFilter.copy( + selectedPokits = currentFilter.selectedPokits.filter { it.id != item.id } + ) + } + }, + onClickItem = null + ) + } + + if (currentFilter.bookmark) { + PokitChip( + data = stringResource(id = R.string.bookmark), + text = stringResource(id = R.string.bookmark), + removeIconPosition = PokitChipIconPosiion.RIGHT, + state = PokitChipState.STROKE, + onClickRemove = remember { + { + currentFilter = currentFilter.copy(bookmark = false) + } + }, + onClickItem = null + ) + } + + if (currentFilter.notRead) { + PokitChip( + data = stringResource(id = R.string.not_read), + text = stringResource(id = R.string.not_read), + removeIconPosition = PokitChipIconPosiion.RIGHT, + state = PokitChipState.STROKE, + onClickRemove = remember { + { + currentFilter = currentFilter.copy(notRead = false) + } + }, + onClickItem = null + ) + } + + currentFilter.getDateString()?.let { dateString -> + PokitChip( + data = dateString, + text = dateString, + removeIconPosition = PokitChipIconPosiion.RIGHT, + state = PokitChipState.STROKE, + onClickRemove = remember { + { + currentFilter = currentFilter.copy(startDate = null, endDate = null) + } + }, + onClickItem = null + ) + } + + Spacer(modifier = Modifier.width(20.dp)) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + PokitButton( + modifier = Modifier.fillMaxWidth(), + size = PokitButtonSize.LARGE, + text = stringResource(id = R.string.search), + icon = null, + onClick = remember { + { + onSaveClilck(currentFilter) + } + } + ) + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/Preview.kt new file mode 100644 index 00000000..16a5bcef --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/Preview.kt @@ -0,0 +1,23 @@ +package pokitmons.pokit.search.components.filterbottomsheet + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Preview(showBackground = true) +@Composable +private fun Preview() { + PokitTheme { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + FilterBottomSheetContent() + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/Preview.kt new file mode 100644 index 00000000..002c3898 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/Preview.kt @@ -0,0 +1,27 @@ +package pokitmons.pokit.search.components.recentsearchword + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Preview(showBackground = true) +@Composable +private fun Preview() { + PokitTheme { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + RecentSearchWord(useAutoSave = false) + + RecentSearchWord(useAutoSave = true) + + RecentSearchWord(useAutoSave = true, recentSearchWords = listOf("Android", "IOS", "Server", "Design", "PM")) + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/RecentSearchWord.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/RecentSearchWord.kt new file mode 100644 index 00000000..b15b4939 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/recentsearchword/RecentSearchWord.kt @@ -0,0 +1,118 @@ +package pokitmons.pokit.search.components.recentsearchword + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.components.atom.chip.PokitChip +import pokitmons.pokit.core.ui.components.atom.chip.attributes.PokitChipIconPosiion +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.search.R.string as SearchString + +@Composable +internal fun RecentSearchWord( + onClickRemoveAll: () -> Unit = {}, + onToggleAutoSave: () -> Unit = {}, + useAutoSave: Boolean = true, + recentSearchWords: List = emptyList(), + onClickRemoveSearchWord: (String) -> Unit = {}, + onClickSearchWord: (String) -> Unit = {}, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 28.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = SearchString.recent_search_word), + style = PokitTheme.typography.body2Bold.copy(color = PokitTheme.colors.textPrimary) + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onClickRemoveAll + ) + .padding(4.dp), + text = stringResource(id = SearchString.remove_all), + style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.textTertiary) + ) + + Text( + text = "|", + style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.textTertiary) + ) + + Text( + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onToggleAutoSave + ) + .padding(4.dp), + text = stringResource( + id = if (useAutoSave) SearchString.off_use_auto_save else SearchString.on_auto_save + ), + style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.textTertiary) + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + if (!useAutoSave) { + Text( + modifier = Modifier.fillMaxWidth().padding(vertical = 7.dp), + textAlign = TextAlign.Center, + text = stringResource(id = SearchString.describe_auto_save_is_off), + style = PokitTheme.typography.body3Regular.copy(color = PokitTheme.colors.textTertiary) + ) + } else if (recentSearchWords.isEmpty()) { + Text( + modifier = Modifier.fillMaxWidth().padding(vertical = 7.dp), + textAlign = TextAlign.Center, + text = stringResource(id = SearchString.describe_recent_search_word_is_empty), + style = PokitTheme.typography.body3Regular.copy(color = PokitTheme.colors.textTertiary) + ) + } else { + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + items(recentSearchWords) { searchWord -> + PokitChip( + data = searchWord, + text = searchWord, + removeIconPosition = PokitChipIconPosiion.RIGHT, + onClickRemove = onClickRemoveSearchWord, + onClickItem = onClickSearchWord + ) + } + } + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt new file mode 100644 index 00000000..c462b3f3 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt @@ -0,0 +1,79 @@ +package pokitmons.pokit.search.components.searchitemlist + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.search.model.Link +import pokitmons.pokit.core.ui.R.drawable as coreDrawable +import pokitmons.pokit.search.R.string as SearchString + +@Composable +internal fun SearchItemList( + modifier: Modifier = Modifier, + onToggleSort: () -> Unit = {}, + useRecentOrder: Boolean = true, + links: List = emptyList(), + onClickLinkKebab: (Link) -> Unit = {}, + onClickLink: (Link) -> Unit = {}, +) { + Column( + modifier = modifier + ) { + Row( + modifier = Modifier + .padding(start = 12.dp, top = 16.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onToggleSort + ) + .padding(all = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + modifier = Modifier.size(18.dp), + painter = painterResource(id = coreDrawable.icon_24_align), + contentDescription = "change sort order", + colorFilter = ColorFilter.tint(color = PokitTheme.colors.iconPrimary) + ) + + Text( + text = stringResource(id = if (useRecentOrder) SearchString.sort_order_recent else SearchString.sort_order_older), + style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.textSecondary) + ) + } + + LazyColumn { + items(links) { link -> + LinkCard( + item = link, + title = link.title, + sub = "${link.dateString} · ${link.domainUrl}", + painter = painterResource(id = coreDrawable.icon_24_google), + notRead = link.isRead, + badgeText = stringResource(id = link.linkType.textResourceId), + onClickKebab = onClickLinkKebab, + onClickItem = onClickLink, + modifier = Modifier.padding(20.dp) + ) + } + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/toolbar/Toolbar.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/toolbar/Toolbar.kt new file mode 100644 index 00000000..40dc69b0 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/toolbar/Toolbar.kt @@ -0,0 +1,86 @@ +package pokitmons.pokit.search.components.toolbar + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.components.atom.input.PokitInput +import pokitmons.pokit.core.ui.components.atom.input.attributes.PokitInputIcon +import pokitmons.pokit.core.ui.components.atom.input.attributes.PokitInputIconPosition +import pokitmons.pokit.core.ui.components.atom.input.attributes.PokitInputShape +import pokitmons.pokit.core.ui.R.drawable as coreDrawable +import pokitmons.pokit.search.R.string as SearchString + +@Composable +internal fun Toolbar( + onClickBack: () -> Unit = {}, + inputSearchWord: (String) -> Unit = {}, + currentSearchWord: String = "", + onClickSearch: () -> Unit = {}, + onClickRemove: () -> Unit = {}, +) { + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + modifier = Modifier.size(40.dp), + onClick = onClickBack + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(id = coreDrawable.icon_24_arrow_left), + contentDescription = "back button" + ) + } + + Spacer(modifier = Modifier.width(4.dp)) + + PokitInput( + text = currentSearchWord, + hintText = stringResource(id = SearchString.placeholder_input_search_word), + onChangeText = inputSearchWord, + shape = PokitInputShape.ROUND, + icon = if (currentSearchWord.isNotEmpty()) { + PokitInputIcon( + position = PokitInputIconPosition.RIGHT, + resourceId = coreDrawable.icon_24_x + ) + } else { + null + }, + onClickIcon = remember { + { + onClickRemove() + focusRequester.requestFocus() + } + }, + keyboardActions = remember { + KeyboardActions { + onClickSearch() + focusManager.clearFocus() + } + }, + focusRequester = focusRequester + ) + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarCell.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarCell.kt new file mode 100644 index 00000000..a19b2db3 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarCell.kt @@ -0,0 +1,8 @@ +package pokitmons.pokit.search.model + +data class CalendarCell( + val date: Date = Date(), + val selected: Boolean = false, + val inSelectRange: Boolean = false, + val currentMonth: Boolean = true, +) diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarPage.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarPage.kt new file mode 100644 index 00000000..92a20ebb --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/model/CalendarPage.kt @@ -0,0 +1,41 @@ +package pokitmons.pokit.search.model + +import java.util.Calendar + +data class CalendarPage( + val year: Int, + val month: Int, +) { + constructor(date: Date?) : this( + year = date?.year ?: Calendar.getInstance().get(Calendar.YEAR), + month = date?.month ?: (Calendar.getInstance().get(Calendar.MONTH) + 1) + ) + + fun prevPage(): CalendarPage { + return if (month == 1) { + CalendarPage( + year = year - 1, + month = 12 + ) + } else { + CalendarPage( + year = year, + month = month - 1 + ) + } + } + + fun nextPage(): CalendarPage { + return if (month == 12) { + CalendarPage( + year = year + 1, + month = 1 + ) + } else { + CalendarPage( + year = year, + month = month + 1 + ) + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt new file mode 100644 index 00000000..aa65a803 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt @@ -0,0 +1,15 @@ +package pokitmons.pokit.search.model + +data class Date( + val year: Int = 2024, + val month: Int = 1, + val day: Int = 1, +) : Comparable { + override fun compareTo(other: Date): Int { + return compareValuesBy(this, other, { it.year }, { it.month }, { it.day }) + } + + override fun toString(): String { + return "${year % 1000}.$month.$day" + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/Filter.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/Filter.kt new file mode 100644 index 00000000..aad44146 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/model/Filter.kt @@ -0,0 +1,39 @@ +package pokitmons.pokit.search.model + +import androidx.compose.runtime.Immutable +import pokitmons.pokit.search.R + +@Immutable +data class Filter( + val selectedPokits: List = emptyList(), + val bookmark: Boolean = false, + val notRead: Boolean = false, + val startDate: Date? = null, + val endDate: Date? = null, +) { + fun addPokit(pokit: Pokit): Filter { + return if (selectedPokits.contains(pokit)) { + this + } else { + this.copy(selectedPokits = listOf(pokit) + selectedPokits) + } + } + + fun getDateString(): String? { + return if (startDate != null && endDate != null) { + "$startDate~$endDate" + } else if (startDate != null) { + "$startDate" + } else { + null + } + } + + companion object { + val DefaultFilter = Filter() + } +} + +enum class FilterType(val stringResourceId: Int, val index: Int) { + Pokit(R.string.pokit, 0), Collect(R.string.collect_show, 1), Period(R.string.period, 2) +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt new file mode 100644 index 00000000..15121d73 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt @@ -0,0 +1,83 @@ +package pokitmons.pokit.search.model + +import pokitmons.pokit.search.R + +data class Link( + val id: String, + val title: String, + val dateString: String, + val domainUrl: String, + val isRead: Boolean, + val linkType: LinkType, + val url: String, + val memo: String, + val bookmark: Boolean, + val imageUrl: String? = null, +) + +enum class LinkType(val textResourceId: Int) { + TEXT(R.string.text), +} + +internal val sampleLinks = listOf( + Link( + id = "1", + title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", + imageUrl = null, + dateString = "2024.04.12", + domainUrl = "youtu.be", + isRead = true, + linkType = LinkType.TEXT, + url = "", + memo = "", + bookmark = true + ), + Link( + id = "2", + title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", + imageUrl = null, + dateString = "2024.05.12", + domainUrl = "youtu.be", + isRead = false, + linkType = LinkType.TEXT, + url = "", + memo = "", + bookmark = true + ), + Link( + id = "3", + title = "포킷포킷", + imageUrl = null, + dateString = "2024.04.12", + domainUrl = "pokitmons.pokit", + isRead = true, + linkType = LinkType.TEXT, + url = "", + memo = "", + bookmark = true + ), + Link( + id = "4", + title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", + imageUrl = null, + dateString = "2024.06.12", + domainUrl = "youtu.be", + isRead = true, + linkType = LinkType.TEXT, + url = "", + memo = "", + bookmark = true + ), + Link( + id = "5", + title = "마지막 링크입니다.", + imageUrl = null, + dateString = "2024.07.14", + domainUrl = "youtu.be", + isRead = false, + linkType = LinkType.TEXT, + url = "", + memo = "", + bookmark = true + ) +) diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt new file mode 100644 index 00000000..bcc29728 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt @@ -0,0 +1,15 @@ +package pokitmons.pokit.search.model + +data class Pokit( + val title: String = "", + val id: String = "", + val count: Int = 0, +) + +internal val samplePokits = listOf( + Pokit(title = "안드로이드", id = "1", count = 2), + Pokit(title = "IOS", id = "2", count = 2), + Pokit(title = "디자인", id = "3", count = 2), + Pokit(title = "PM", id = "4", count = 1), + Pokit(title = "서버", id = "5", count = 2) +) diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt new file mode 100644 index 00000000..034bf214 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt @@ -0,0 +1,15 @@ +package pokitmons.pokit.search.model + +data class SearchScreenState( + val step: SearchScreenStep = SearchScreenStep.INPUT, + val filter: Filter? = null, + val recentSearchWords: List = emptyList(), + val useRecentSearchWord: Boolean = false, + val showFilterBottomSheet: Boolean = false, + val firstBottomSheetFilterType: FilterType = FilterType.Pokit, + val sortRecent: Boolean = true, +) + +enum class SearchScreenStep { + INPUT, RESULT +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/utils/CalendarUtils.kt b/feature/search/src/main/java/pokitmons/pokit/search/utils/CalendarUtils.kt new file mode 100644 index 00000000..0e0c3297 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/utils/CalendarUtils.kt @@ -0,0 +1,115 @@ +package pokitmons.pokit.search.utils + +import pokitmons.pokit.search.R +import pokitmons.pokit.search.model.CalendarCell +import pokitmons.pokit.search.model.Date +import java.util.Calendar + +fun getCells( + year: Int, + month: Int, + startDate: Date?, + endDate: Date?, +): List { + val cells = getDefaultCalendarCells(year = year, month = month) + if (startDate == null && endDate == null) { + return cells + } + + val changedCells = cells.map { cell -> + if (cell.date == startDate || cell.date == endDate) { + cell.copy(selected = true) + } else if (startDate != null && endDate != null && cell.date > startDate && cell.date < endDate) { + cell.copy(inSelectRange = true) + } else { + cell + } + } + + return changedCells +} + +private fun getDayAmountOfMonth(year: Int, month: Int): Int { + return when (month) { + 2 -> { + if (year % 4 != 0 || (year % 100 == 0 && year % 400 != 0)) { + 28 + } else { + 29 + } + } + + in listOf(1, 3, 5, 7, 8, 10, 12) -> { + 31 + } + + else -> { + 30 + } + } +} + +/** + * 달력 상에서 이전달에 해당하는 요일의 리스트를 리턴합니다. + */ +private fun lastDaysOfPrevMonth(year: Int, month: Int): List { + val calendar = Calendar.getInstance() + val prevMonthIndex = month - 1 + + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, prevMonthIndex) + calendar.set(Calendar.DAY_OF_MONTH, 1) + + val prevMonth = if (prevMonthIndex == 0) { + 12 + } else { + prevMonthIndex + } + + val startDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) + val amountOfDayOfPrevMonth = getDayAmountOfMonth(year, prevMonth) + + return List(startDayOfWeek - 1) { i -> amountOfDayOfPrevMonth - (startDayOfWeek - 2 - i) } +} + +/** + * 달력 상에서 다음달에 해당하는 요일의 리스트를 리턴합니다. + */ +private fun firstDaysOfNextMonth(year: Int, month: Int): List { + val calendar = Calendar.getInstance() + + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, 1) + + val startDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) + + return List(8 - startDayOfWeek) { it + 1 } +} + +private fun getDefaultCalendarCells(year: Int, month: Int): List { + val lastMonthCells = lastDaysOfPrevMonth(year, month).map { day -> + if (month == 1) { + CalendarCell(date = Date(year = year - 1, month = 12, day = day), currentMonth = false) + } else { + CalendarCell(date = Date(year = year, month = month - 1, day = day), currentMonth = false) + } + } + + val datOfMonth = getDayAmountOfMonth(year, month) + val currentMonthCells = (1..datOfMonth).map { day -> + CalendarCell(date = Date(year = year, month = month, day = day), currentMonth = true) + } + + val nextMonthCells = firstDaysOfNextMonth(year, month).map { day -> + if (month == 12) { + CalendarCell(date = Date(year = year + 1, month = 1, day = day), currentMonth = false) + } else { + CalendarCell(date = Date(year = year, month = month + 1, day = day), currentMonth = false) + } + } + + return lastMonthCells + currentMonthCells + nextMonthCells +} + +val weekDayStringResources = listOf(R.string.SUN, R.string.MON, R.string.TUE, R.string.WED, R.string.THU, R.string.FRI, R.string.SAT) diff --git a/feature/search/src/main/res/values/string.xml b/feature/search/src/main/res/values/string.xml new file mode 100644 index 00000000..baceab83 --- /dev/null +++ b/feature/search/src/main/res/values/string.xml @@ -0,0 +1,35 @@ + + + 내용을 입력해주세요. + + 최근 검색어 + 전체 삭제 + 자동 저장 켜기 + 자동 저장 끄기 + 최근 검색 저장 기능이 꺼져있습니다. + 검색 내역이 없습니다. + + 최신순 + 오래된순 + + 텍스트 + + 필터 + 포킷명 + 포킷 + 기간 + 모아보기 + 즐겨찾기 + 안읽음 + 검색하기 + + + + + + + + + + %d년 %d월 + \ No newline at end of file diff --git a/feature/search/src/test/java/pokitmons/pokit/search/ExampleUnitTest.kt b/feature/search/src/test/java/pokitmons/pokit/search/ExampleUnitTest.kt new file mode 100644 index 00000000..44a19e7b --- /dev/null +++ b/feature/search/src/test/java/pokitmons/pokit/search/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package pokitmons.pokit.search + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +}