diff --git a/app/src/androidTest/java/com/into/websoso/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/into/websoso/ExampleInstrumentedTest.kt
deleted file mode 100644
index 426080809..000000000
--- a/app/src/androidTest/java/com/into/websoso/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.into.websoso
-
-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("com.teamwss.websoso", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/com/into/websoso/ExampleUnitTest.kt b/app/src/test/java/com/into/websoso/ExampleUnitTest.kt
deleted file mode 100644
index 99b42c258..000000000
--- a/app/src/test/java/com/into/websoso/ExampleUnitTest.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.into.websoso
-
-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)
- }
-}
\ No newline at end of file
diff --git a/core/resource/src/main/res/drawable/ic_collection_novel_add.xml b/core/resource/src/main/res/drawable/ic_collection_novel_add.xml
new file mode 100644
index 000000000..32a6762f5
--- /dev/null
+++ b/core/resource/src/main/res/drawable/ic_collection_novel_add.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/core/resource/src/main/res/drawable/ic_collection_novel_delete.xml b/core/resource/src/main/res/drawable/ic_collection_novel_delete.xml
new file mode 100644
index 000000000..abccbfe26
--- /dev/null
+++ b/core/resource/src/main/res/drawable/ic_collection_novel_delete.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/core/resource/src/main/res/values/strings.xml b/core/resource/src/main/res/values/strings.xml
index 40ae57467..ab00c6277 100644
--- a/core/resource/src/main/res/values/strings.xml
+++ b/core/resource/src/main/res/values/strings.xml
@@ -459,6 +459,12 @@
컬렉션에 관련한 설명을 간단하게 작성해주세요
작품 리스트
작품 추가
+ 추가
+ 삭제
+ 추가한 작품
+ %1$d개
+ 서재에서 추가
+ 작품 제목, 작가를 검색하세요
(%1$d/%2$d)
보는 중
봤어요
diff --git a/data/novel/build.gradle.kts b/data/novel/build.gradle.kts
index 2b6edc225..4b49cff1e 100644
--- a/data/novel/build.gradle.kts
+++ b/data/novel/build.gradle.kts
@@ -15,6 +15,4 @@ dependencies {
implementation(libs.paging.runtime)
implementation(libs.retrofit)
implementation(libs.serialization.json)
-
- testImplementation(libs.junit)
}
diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchRepository.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchRepository.kt
new file mode 100644
index 000000000..bdd085a9a
--- /dev/null
+++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchRepository.kt
@@ -0,0 +1,33 @@
+package com.into.websoso.data.novel
+
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import com.into.websoso.data.novel.model.NovelSearchEntity
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class NovelSearchRepository
+ @Inject
+ internal constructor(
+ private val api: NovelSearchApi,
+ ) {
+ fun searchNovels(query: String): Flow> =
+ Pager(
+ config = PagingConfig(
+ pageSize = PAGE_SIZE,
+ initialLoadSize = PAGE_SIZE,
+ enablePlaceholders = false,
+ ),
+ pagingSourceFactory = {
+ NovelSearchPagingSource(
+ query = query,
+ api = api,
+ )
+ },
+ ).flow
+
+ private companion object {
+ const val PAGE_SIZE = 20
+ }
+ }
diff --git a/feature/collection/build.gradle.kts b/feature/collection/build.gradle.kts
index 163a265f6..bcdee05eb 100644
--- a/feature/collection/build.gradle.kts
+++ b/feature/collection/build.gradle.kts
@@ -10,6 +10,7 @@ android {
dependencies {
implementation(projects.data.library)
+ implementation(projects.data.novel)
implementation(libs.navigation.compose)
implementation(libs.paging.compose)
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt
index e5f0bd184..a1e334e53 100644
--- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt
@@ -1,34 +1,24 @@
package com.into.websoso.feature.collection
-import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
-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.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -37,39 +27,36 @@ import androidx.paging.LoadState
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
-import com.into.websoso.core.designsystem.theme.Black
-import com.into.websoso.core.designsystem.theme.Gray300
-import com.into.websoso.core.designsystem.theme.Primary100
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.core.designsystem.theme.White
-import com.into.websoso.core.resource.R.drawable.img_load_fail
-import com.into.websoso.core.resource.R.string.load_fail_description
-import com.into.websoso.core.resource.R.string.load_fail_reload
-import com.into.websoso.core.resource.R.string.load_fail_title
+import com.into.websoso.core.resource.R.string.collection_create_add
+import com.into.websoso.core.resource.R.string.library
import com.into.websoso.feature.collection.component.CollectionAppBar
import com.into.websoso.feature.collection.component.CollectionLibraryNovelItem
+import com.into.websoso.feature.collection.component.CollectionNetworkError
import com.into.websoso.feature.collection.model.CollectionLibraryNovelUiModel
+import com.into.websoso.feature.collection.model.CollectionSelectedNovel
import kotlinx.coroutines.flow.flowOf
@Composable
internal fun CollectionLibraryNovelSelectionRoute(
- initialSelectedNovelIds: Set,
- onAddClick: (Set) -> Unit,
+ initialSelectedNovels: List,
+ onAddClick: (List) -> Unit,
onNavigateBack: () -> Unit,
viewModel: CollectionLibraryNovelSelectionViewModel = hiltViewModel(),
) {
val novels = viewModel.novels.collectAsLazyPagingItems()
- val selectedNovelIds by viewModel.selectedNovelIds.collectAsStateWithLifecycle()
+ val selectedNovels by viewModel.selectedNovels.collectAsStateWithLifecycle()
- LaunchedEffect(initialSelectedNovelIds) {
- viewModel.setSelectedNovelIds(initialSelectedNovelIds)
+ LaunchedEffect(initialSelectedNovels) {
+ viewModel.setSelectedNovels(initialSelectedNovels)
}
CollectionLibraryNovelSelectionScreen(
novels = novels,
- selectedNovelIds = selectedNovelIds,
+ selectedNovelIds = selectedNovels.mapTo(mutableSetOf()) { it.novelId },
onNovelSelectionChange = viewModel::toggleNovelSelection,
- onAddClick = { onAddClick(selectedNovelIds) },
+ onAddClick = { onAddClick(selectedNovels) },
onNavigateBack = onNavigateBack,
)
}
@@ -78,7 +65,7 @@ internal fun CollectionLibraryNovelSelectionRoute(
internal fun CollectionLibraryNovelSelectionScreen(
novels: LazyPagingItems,
selectedNovelIds: Set,
- onNovelSelectionChange: (Long) -> Unit,
+ onNovelSelectionChange: (CollectionLibraryNovelUiModel) -> Unit,
onAddClick: () -> Unit,
onNavigateBack: () -> Unit,
modifier: Modifier = Modifier,
@@ -90,8 +77,8 @@ internal fun CollectionLibraryNovelSelectionScreen(
.statusBarsPadding(),
) {
CollectionAppBar(
- title = "서재",
- actionLabel = "추가",
+ title = stringResource(library),
+ actionLabel = stringResource(collection_create_add),
onNavigateBack = onNavigateBack,
onActionClick = onAddClick,
isActionEnabled = selectedNovelIds.isNotEmpty(),
@@ -108,44 +95,10 @@ internal fun CollectionLibraryNovelSelectionScreen(
(novels.itemCount == 0 && novels.loadState.refresh is LoadState.Error) ||
novels.loadState.append is LoadState.Error -> {
- Column(
+ CollectionNetworkError(
+ onRetryClick = novels::retry,
modifier = Modifier.align(Alignment.Center),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Image(
- painter = painterResource(img_load_fail),
- contentDescription = null,
- modifier = Modifier.size(width = 166.dp, height = 160.dp),
- )
- Spacer(modifier = Modifier.height(40.dp))
- Text(
- text = stringResource(load_fail_title),
- color = Black,
- style = WebsosoTheme.typography.title1,
- textAlign = TextAlign.Center,
- )
- Spacer(modifier = Modifier.height(10.dp))
- Text(
- text = stringResource(load_fail_description),
- color = Gray300,
- style = WebsosoTheme.typography.body2,
- textAlign = TextAlign.Center,
- )
- Spacer(modifier = Modifier.height(40.dp))
- Button(
- onClick = novels::retry,
- shape = RoundedCornerShape(8.dp),
- colors = ButtonDefaults.buttonColors(containerColor = Primary100),
- contentPadding = PaddingValues(horizontal = 38.dp, vertical = 14.dp),
- elevation = null,
- ) {
- Text(
- text = stringResource(load_fail_reload),
- color = White,
- style = WebsosoTheme.typography.label1,
- )
- }
- }
+ )
}
else -> {
@@ -165,7 +118,7 @@ internal fun CollectionLibraryNovelSelectionScreen(
CollectionLibraryNovelItem(
novel = novel,
isSelected = novel.novelId in selectedNovelIds,
- onSelectionChange = { onNovelSelectionChange(novel.novelId) },
+ onSelectionChange = { onNovelSelectionChange(novel) },
)
}
}
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionViewModel.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionViewModel.kt
index b3644b9cb..594436510 100644
--- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionViewModel.kt
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionViewModel.kt
@@ -9,6 +9,7 @@ import com.into.websoso.data.library.LibraryRepository
import com.into.websoso.data.library.model.NovelEntity
import com.into.websoso.feature.collection.mapper.toUiModel
import com.into.websoso.feature.collection.model.CollectionLibraryNovelUiModel
+import com.into.websoso.feature.collection.model.CollectionSelectedNovel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -24,8 +25,8 @@ internal class CollectionLibraryNovelSelectionViewModel
constructor(
libraryRepository: LibraryRepository,
) : ViewModel() {
- private val _selectedNovelIds = MutableStateFlow>(emptySet())
- val selectedNovelIds: StateFlow> = _selectedNovelIds.asStateFlow()
+ private val _selectedNovels = MutableStateFlow>(emptyList())
+ val selectedNovels: StateFlow> = _selectedNovels.asStateFlow()
val novels: Flow> =
libraryRepository
@@ -33,17 +34,25 @@ internal class CollectionLibraryNovelSelectionViewModel
.map { pagingData -> pagingData.map(NovelEntity::toUiModel) }
.cachedIn(viewModelScope)
- fun toggleNovelSelection(novelId: Long) {
- _selectedNovelIds.update { selectedNovelIds ->
- if (novelId in selectedNovelIds) {
- selectedNovelIds - novelId
+ fun toggleNovelSelection(novel: CollectionLibraryNovelUiModel) {
+ _selectedNovels.update { selectedNovels ->
+ if (selectedNovels.any { it.novelId == novel.novelId }) {
+ selectedNovels.filterNot { it.novelId == novel.novelId }
} else {
- selectedNovelIds + novelId
+ selectedNovels + novel.toSelectedNovel()
}
}
}
- fun setSelectedNovelIds(novelIds: Set) {
- _selectedNovelIds.value = novelIds
+ fun setSelectedNovels(novels: List) {
+ _selectedNovels.value = novels
}
}
+
+private fun CollectionLibraryNovelUiModel.toSelectedNovel(): CollectionSelectedNovel =
+ CollectionSelectedNovel(
+ novelId = novelId,
+ title = title,
+ author = "",
+ imageUrl = imageUrl,
+ )
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNavHost.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNavHost.kt
index f6dc8c7ae..da8476577 100644
--- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNavHost.kt
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNavHost.kt
@@ -1,9 +1,9 @@
package com.into.websoso.feature.collection
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@@ -13,7 +13,6 @@ private const val COLLECTION_CREATE_ROUTE = "collection/create"
private const val COLLECTION_NOVEL_SEARCH_ROUTE = "collection/create/novel-search"
private const val COLLECTION_LIBRARY_NOVEL_SELECTION_ROUTE =
"collection/create/novel-search/library"
-private const val SELECTED_NOVEL_IDS_KEY = "selectedNovelIds"
@Composable
fun CollectionNavHost(
@@ -44,31 +43,29 @@ fun CollectionNavHost(
)
}
composable(route = COLLECTION_NOVEL_SEARCH_ROUTE) { backStackEntry ->
- val selectedNovelIds by backStackEntry.savedStateHandle
- .getStateFlow(SELECTED_NOVEL_IDS_KEY, longArrayOf())
- .collectAsStateWithLifecycle()
+ val createBackStackEntry = remember(backStackEntry) {
+ navController.getBackStackEntry(COLLECTION_CREATE_ROUTE)
+ }
+ val novelSearchViewModel: CollectionNovelSearchViewModel = hiltViewModel(createBackStackEntry)
- CollectionNovelSearchScreen(
- addedNovelCount = selectedNovelIds.size,
+ CollectionNovelSearchRoute(
+ viewModel = novelSearchViewModel,
onNavigateBack = navController::popBackStack,
onNavigateToLibraryNovelSelection = {
navController.navigate(COLLECTION_LIBRARY_NOVEL_SELECTION_ROUTE)
},
)
}
- composable(route = COLLECTION_LIBRARY_NOVEL_SELECTION_ROUTE) {
- val initialSelectedNovelIds = navController.previousBackStackEntry
- ?.savedStateHandle
- ?.get(SELECTED_NOVEL_IDS_KEY)
- ?.toSet()
- .orEmpty()
+ composable(route = COLLECTION_LIBRARY_NOVEL_SELECTION_ROUTE) { backStackEntry ->
+ val createBackStackEntry = remember(backStackEntry) {
+ navController.getBackStackEntry(COLLECTION_CREATE_ROUTE)
+ }
+ val novelSearchViewModel: CollectionNovelSearchViewModel = hiltViewModel(createBackStackEntry)
CollectionLibraryNovelSelectionRoute(
- initialSelectedNovelIds = initialSelectedNovelIds,
- onAddClick = { selectedNovelIds ->
- navController.previousBackStackEntry
- ?.savedStateHandle
- ?.set(SELECTED_NOVEL_IDS_KEY, selectedNovelIds.toLongArray())
+ initialSelectedNovels = novelSearchViewModel.selectedNovels.value,
+ onAddClick = { selectedNovels ->
+ novelSearchViewModel.updateSelectedNovels(selectedNovels)
navController.popBackStack()
},
onNavigateBack = navController::popBackStack,
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt
index 52262404f..eb8c0faab 100644
--- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt
@@ -1,10 +1,24 @@
package com.into.websoso.feature.collection
+import androidx.compose.foundation.Image
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.statusBarsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -12,21 +26,80 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.LoadState
+import androidx.paging.PagingData
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.into.websoso.core.designsystem.theme.Gray200
+import com.into.websoso.core.designsystem.theme.Gray50
+import com.into.websoso.core.designsystem.theme.Primary100
+import com.into.websoso.core.designsystem.theme.Primary50
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.core.designsystem.theme.White
+import com.into.websoso.core.resource.R.drawable.ic_explore_not_exist_result
+import com.into.websoso.core.resource.R.string.collection_create_complete
+import com.into.websoso.core.resource.R.string.collection_create_novel_list
+import com.into.websoso.core.resource.R.string.normal_explore_add_novel_inquire
+import com.into.websoso.core.resource.R.string.normal_explore_not_exist_result
+import com.into.websoso.core.resource.R.string.novel_inquire_link
+import com.into.websoso.data.novel.model.NovelSearchEntity
import com.into.websoso.feature.collection.component.CollectionAppBar
+import com.into.websoso.feature.collection.component.CollectionNetworkError
import com.into.websoso.feature.collection.component.CollectionNovelSearchField
+import com.into.websoso.feature.collection.component.CollectionNovelSearchItem
import com.into.websoso.feature.collection.component.CollectionNovelSelectionInfo
+import com.into.websoso.feature.collection.component.CollectionSelectedNovelItem
+import com.into.websoso.feature.collection.model.CollectionSelectedNovel
+import kotlinx.coroutines.flow.flowOf
+
+@Composable
+internal fun CollectionNovelSearchRoute(
+ onNavigateBack: () -> Unit,
+ onNavigateToLibraryNovelSelection: () -> Unit,
+ viewModel: CollectionNovelSearchViewModel,
+) {
+ val selectedNovels by viewModel.selectedNovels.collectAsStateWithLifecycle()
+ val submittedQuery by viewModel.submittedQuery.collectAsStateWithLifecycle()
+ val searchResults = viewModel.searchResults.collectAsLazyPagingItems()
+
+ CollectionNovelSearchScreen(
+ searchResults = searchResults,
+ submittedQuery = submittedQuery,
+ selectedNovels = selectedNovels,
+ onSearch = { query ->
+ if (query.trim() == submittedQuery && query.isNotBlank()) {
+ searchResults.refresh()
+ } else {
+ viewModel.search(query)
+ }
+ },
+ onAddNovel = viewModel::addNovel,
+ onDeleteNovel = viewModel::removeNovel,
+ onNavigateBack = onNavigateBack,
+ onNavigateToLibraryNovelSelection = onNavigateToLibraryNovelSelection,
+ )
+}
@Composable
internal fun CollectionNovelSearchScreen(
- addedNovelCount: Int,
+ searchResults: LazyPagingItems,
+ submittedQuery: String,
+ selectedNovels: List,
+ onSearch: (String) -> Unit,
+ onAddNovel: (NovelSearchEntity) -> Unit,
+ onDeleteNovel: (Long) -> Unit,
onNavigateBack: () -> Unit,
onNavigateToLibraryNovelSelection: () -> Unit,
modifier: Modifier = Modifier,
@@ -36,6 +109,11 @@ internal fun CollectionNovelSearchScreen(
}
val searchFocusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
+ val selectedNovelIds = selectedNovels.mapTo(mutableSetOf()) { it.novelId }
+ val isInitialLoading =
+ submittedQuery.isNotBlank() &&
+ searchResults.itemCount == 0 &&
+ searchResults.loadState.refresh is LoadState.Loading
LaunchedEffect(Unit) {
searchFocusRequester.requestFocus()
@@ -49,15 +127,23 @@ internal fun CollectionNovelSearchScreen(
.statusBarsPadding(),
) {
CollectionAppBar(
- title = "작품 리스트",
- actionLabel = "완료",
+ title = stringResource(collection_create_novel_list),
+ actionLabel = stringResource(collection_create_complete),
onNavigateBack = onNavigateBack,
- isActionEnabled = addedNovelCount > 0,
+ onActionClick = onNavigateBack,
+ isActionEnabled = selectedNovels.isNotEmpty(),
)
CollectionNovelSearchField(
value = searchQuery,
onValueChange = { searchQuery = it },
- onClearClick = { searchQuery = TextFieldValue() },
+ onClearClick = {
+ searchQuery = TextFieldValue()
+ onSearch("")
+ },
+ onSearchClick = {
+ keyboardController?.hide()
+ onSearch(searchQuery.text)
+ },
focusRequester = searchFocusRequester,
modifier = Modifier.padding(
start = 20.dp,
@@ -66,10 +152,129 @@ internal fun CollectionNovelSearchScreen(
),
)
CollectionNovelSelectionInfo(
- addedNovelCount = addedNovelCount,
+ addedNovelCount = selectedNovels.size,
onAddFromLibraryClick = onNavigateToLibraryNovelSelection,
modifier = Modifier.padding(top = 16.dp),
)
+ PullToRefreshBox(
+ isRefreshing =
+ searchResults.loadState.refresh is LoadState.Loading &&
+ searchResults.itemCount > 0,
+ onRefresh = searchResults::refresh,
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ ) {
+ when {
+ searchResults.loadState.refresh is LoadState.Error -> {
+ CollectionNetworkError(
+ onRetryClick = searchResults::retry,
+ modifier = Modifier.align(Alignment.Center),
+ )
+ }
+
+ isInitialLoading -> {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+
+ else -> {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = androidx.compose.foundation.layout.PaddingValues(
+ horizontal = 20.dp,
+ vertical = 10.dp,
+ ),
+ ) {
+ items(
+ count = selectedNovels.size,
+ key = { index -> "selected-${selectedNovels[index].novelId}" },
+ ) { index ->
+ val novel = selectedNovels[index]
+ CollectionSelectedNovelItem(
+ novel = novel,
+ onDeleteClick = { onDeleteNovel(novel.novelId) },
+ )
+ }
+
+ if (selectedNovels.isNotEmpty() && submittedQuery.isNotBlank()) {
+ item {
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 10.dp),
+ color = Gray50,
+ )
+ }
+ }
+
+ if (submittedQuery.isNotBlank() && searchResults.itemCount == 0) {
+ item { CollectionNovelSearchEmpty() }
+ }
+
+ items(searchResults.itemCount) { index ->
+ searchResults[index]
+ ?.takeUnless { it.novelId in selectedNovelIds }
+ ?.let { novel ->
+ CollectionNovelSearchItem(
+ novel = novel,
+ onAddClick = { onAddNovel(novel) },
+ )
+ }
+ }
+
+ if (searchResults.loadState.append is LoadState.Loading) {
+ item {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun CollectionNovelSearchEmpty() {
+ val uriHandler = LocalUriHandler.current
+ val inquireUrl = stringResource(novel_inquire_link)
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 100.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ painter = painterResource(ic_explore_not_exist_result),
+ contentDescription = null,
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = stringResource(normal_explore_not_exist_result),
+ color = Gray200,
+ style = WebsosoTheme.typography.body1,
+ textAlign = TextAlign.Center,
+ )
+ Spacer(modifier = Modifier.height(36.dp))
+ Button(
+ onClick = { uriHandler.openUri(inquireUrl) },
+ shape = RoundedCornerShape(14.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = Primary50),
+ contentPadding = PaddingValues(horizontal = 40.dp, vertical = 20.dp),
+ elevation = null,
+ ) {
+ Text(
+ text = stringResource(normal_explore_add_novel_inquire),
+ color = Primary100,
+ style = WebsosoTheme.typography.title2,
+ )
+ }
}
}
@@ -78,7 +283,12 @@ internal fun CollectionNovelSearchScreen(
private fun CollectionNovelSearchScreenPreview() {
WebsosoTheme {
CollectionNovelSearchScreen(
- addedNovelCount = 0,
+ searchResults = flowOf(PagingData.empty()).collectAsLazyPagingItems(),
+ submittedQuery = "",
+ selectedNovels = emptyList(),
+ onSearch = {},
+ onAddNovel = {},
+ onDeleteNovel = {},
onNavigateBack = {},
onNavigateToLibraryNovelSelection = {},
)
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchViewModel.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchViewModel.kt
new file mode 100644
index 000000000..e62e164af
--- /dev/null
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchViewModel.kt
@@ -0,0 +1,75 @@
+package com.into.websoso.feature.collection
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import com.into.websoso.data.novel.NovelSearchRepository
+import com.into.websoso.data.novel.model.NovelSearchEntity
+import com.into.websoso.feature.collection.model.CollectionSelectedNovel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.update
+import javax.inject.Inject
+
+@HiltViewModel
+internal class CollectionNovelSearchViewModel
+ @Inject
+ constructor(
+ novelSearchRepository: NovelSearchRepository,
+ ) : ViewModel() {
+ private val _submittedQuery = MutableStateFlow("")
+ val submittedQuery: StateFlow = _submittedQuery.asStateFlow()
+
+ private val _selectedNovels = MutableStateFlow>(emptyList())
+ val selectedNovels: StateFlow> = _selectedNovels.asStateFlow()
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ val searchResults: Flow> =
+ submittedQuery
+ .flatMapLatest { query ->
+ if (query.isBlank()) {
+ flowOf(PagingData.empty())
+ } else {
+ novelSearchRepository.searchNovels(query)
+ }
+ }.cachedIn(viewModelScope)
+
+ fun search(query: String) {
+ _submittedQuery.value = query.trim()
+ }
+
+ fun addNovel(novel: NovelSearchEntity) {
+ _selectedNovels.update { selectedNovels ->
+ if (selectedNovels.any { it.novelId == novel.novelId }) {
+ selectedNovels
+ } else {
+ selectedNovels + novel.toSelectedNovel()
+ }
+ }
+ }
+
+ fun removeNovel(novelId: Long) {
+ _selectedNovels.update { selectedNovels ->
+ selectedNovels.filterNot { it.novelId == novelId }
+ }
+ }
+
+ fun updateSelectedNovels(novels: List) {
+ _selectedNovels.value = novels
+ }
+ }
+
+private fun NovelSearchEntity.toSelectedNovel(): CollectionSelectedNovel =
+ CollectionSelectedNovel(
+ novelId = novelId,
+ title = title,
+ author = author,
+ imageUrl = imageUrl,
+ )
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNetworkError.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNetworkError.kt
new file mode 100644
index 000000000..aae52038f
--- /dev/null
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNetworkError.kt
@@ -0,0 +1,73 @@
+package com.into.websoso.feature.collection.component
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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 com.into.websoso.core.designsystem.theme.Black
+import com.into.websoso.core.designsystem.theme.Gray300
+import com.into.websoso.core.designsystem.theme.Primary100
+import com.into.websoso.core.designsystem.theme.WebsosoTheme
+import com.into.websoso.core.designsystem.theme.White
+import com.into.websoso.core.resource.R.drawable.img_load_fail
+import com.into.websoso.core.resource.R.string.load_fail_description
+import com.into.websoso.core.resource.R.string.load_fail_reload
+import com.into.websoso.core.resource.R.string.load_fail_title
+
+@Composable
+internal fun CollectionNetworkError(
+ onRetryClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ painter = painterResource(img_load_fail),
+ contentDescription = null,
+ modifier = Modifier.size(width = 166.dp, height = 160.dp),
+ )
+ Spacer(modifier = Modifier.height(40.dp))
+ Text(
+ text = stringResource(load_fail_title),
+ color = Black,
+ style = WebsosoTheme.typography.title1,
+ textAlign = TextAlign.Center,
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ text = stringResource(load_fail_description),
+ color = Gray300,
+ style = WebsosoTheme.typography.body2,
+ textAlign = TextAlign.Center,
+ )
+ Spacer(modifier = Modifier.height(40.dp))
+ Button(
+ onClick = onRetryClick,
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = Primary100),
+ contentPadding = PaddingValues(horizontal = 38.dp, vertical = 14.dp),
+ elevation = null,
+ ) {
+ Text(
+ text = stringResource(load_fail_reload),
+ color = White,
+ style = WebsosoTheme.typography.label1,
+ )
+ }
+ }
+}
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt
index e0a8bcc56..d75982640 100644
--- a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt
@@ -2,6 +2,7 @@ package com.into.websoso.feature.collection.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
@@ -21,6 +23,8 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
@@ -32,12 +36,14 @@ import com.into.websoso.core.designsystem.theme.Primary100
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.core.resource.R.drawable.ic_common_search
import com.into.websoso.core.resource.R.drawable.ic_common_search_clear
+import com.into.websoso.core.resource.R.string.collection_create_search_hint
@Composable
internal fun CollectionNovelSearchField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
onClearClick: () -> Unit,
+ onSearchClick: () -> Unit,
focusRequester: FocusRequester,
modifier: Modifier = Modifier,
) {
@@ -65,11 +71,12 @@ internal fun CollectionNovelSearchField(
textStyle = WebsosoTheme.typography.body4.copy(color = Black),
cursorBrush = SolidColor(Primary100),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ keyboardActions = KeyboardActions(onSearch = { onSearchClick() }),
decorationBox = { innerTextField ->
Box {
if (value.text.isEmpty()) {
Text(
- text = "작품 제목, 작가를 검색하세요",
+ text = stringResource(collection_create_search_hint),
color = Gray100,
style = WebsosoTheme.typography.body4,
)
@@ -85,13 +92,18 @@ internal fun CollectionNovelSearchField(
) {
Image(
painter = painterResource(id = ic_common_search_clear),
- contentDescription = "검색어 지우기",
+ contentDescription = null,
modifier = Modifier.size(36.dp),
)
}
}
Box(
- modifier = Modifier.size(36.dp),
+ modifier = Modifier
+ .size(36.dp)
+ .clickable(
+ onClick = onSearchClick,
+ role = Role.Button,
+ ),
contentAlignment = Alignment.Center,
) {
Image(
@@ -111,6 +123,7 @@ private fun CollectionNovelSearchFieldPreview() {
value = TextFieldValue(),
onValueChange = {},
onClearClick = {},
+ onSearchClick = {},
focusRequester = remember { FocusRequester() },
modifier = Modifier.padding(20.dp),
)
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt
new file mode 100644
index 000000000..27cb19c1e
--- /dev/null
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt
@@ -0,0 +1,143 @@
+package com.into.websoso.feature.collection.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+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.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.into.websoso.core.designsystem.component.NetworkImage
+import com.into.websoso.core.designsystem.theme.Black
+import com.into.websoso.core.designsystem.theme.Gray200
+import com.into.websoso.core.designsystem.theme.LightPink
+import com.into.websoso.core.designsystem.theme.Primary100
+import com.into.websoso.core.designsystem.theme.Secondary100
+import com.into.websoso.core.designsystem.theme.WebsosoTheme
+import com.into.websoso.core.designsystem.theme.White
+import com.into.websoso.core.resource.R.drawable.ic_collection_novel_add
+import com.into.websoso.core.resource.R.drawable.ic_collection_novel_delete
+import com.into.websoso.core.resource.R.string.collection_create_add
+import com.into.websoso.core.resource.R.string.collection_create_delete
+import com.into.websoso.data.novel.model.NovelSearchEntity
+import com.into.websoso.feature.collection.model.CollectionSelectedNovel
+
+@Composable
+internal fun CollectionNovelSearchItem(
+ title: String,
+ author: String,
+ imageUrl: String,
+ isAdd: Boolean,
+ onActionClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ NetworkImage(
+ imageUrl = imageUrl,
+ contentDescription = title,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .size(width = 73.dp, height = 98.dp)
+ .clip(RoundedCornerShape(8.dp)),
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ color = Black,
+ style = WebsosoTheme.typography.title3,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+ if (author.isNotEmpty()) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = author,
+ color = Gray200,
+ style = WebsosoTheme.typography.body5,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(22.dp))
+ .background(if (isAdd) Primary100 else LightPink)
+ .clickable(onClick = onActionClick)
+ .padding(horizontal = 10.dp, vertical = 7.dp),
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ painter = painterResource(
+ id = if (isAdd) ic_collection_novel_add else ic_collection_novel_delete,
+ ),
+ tint = if (isAdd) White else Secondary100,
+ contentDescription = null,
+ modifier = Modifier.size(12.dp),
+ )
+ Text(
+ text = stringResource(if (isAdd) collection_create_add else collection_create_delete),
+ color = if (isAdd) White else Secondary100,
+ style = WebsosoTheme.typography.body5,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+internal fun CollectionNovelSearchItem(
+ novel: NovelSearchEntity,
+ onAddClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ CollectionNovelSearchItem(
+ title = novel.title,
+ author = novel.author,
+ imageUrl = novel.imageUrl,
+ isAdd = true,
+ onActionClick = onAddClick,
+ modifier = modifier,
+ )
+}
+
+@Composable
+internal fun CollectionSelectedNovelItem(
+ novel: CollectionSelectedNovel,
+ onDeleteClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ CollectionNovelSearchItem(
+ title = novel.title,
+ author = novel.author,
+ imageUrl = novel.imageUrl,
+ isAdd = false,
+ onActionClick = onDeleteClick,
+ modifier = modifier,
+ )
+}
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSelectionInfo.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSelectionInfo.kt
index f2ff57cb9..e2d153381 100644
--- a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSelectionInfo.kt
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSelectionInfo.kt
@@ -9,6 +9,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
@@ -16,6 +17,9 @@ import androidx.compose.ui.unit.dp
import com.into.websoso.core.designsystem.theme.Gray200
import com.into.websoso.core.designsystem.theme.Primary100
import com.into.websoso.core.designsystem.theme.WebsosoTheme
+import com.into.websoso.core.resource.R.string.collection_create_add_from_library
+import com.into.websoso.core.resource.R.string.collection_create_added_novel_count
+import com.into.websoso.core.resource.R.string.collection_create_added_novels
@Composable
internal fun CollectionNovelSelectionInfo(
@@ -32,18 +36,18 @@ internal fun CollectionNovelSelectionInfo(
) {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
- text = "추가한 작품",
+ text = stringResource(collection_create_added_novels),
color = Gray200,
style = WebsosoTheme.typography.body4,
)
Text(
- text = "${addedNovelCount}개",
+ text = stringResource(collection_create_added_novel_count, addedNovelCount),
color = Primary100,
style = WebsosoTheme.typography.body4,
)
}
Text(
- text = "서재에서 추가",
+ text = stringResource(collection_create_add_from_library),
color = Gray200,
style = WebsosoTheme.typography.body4.copy(
textDecoration = TextDecoration.Underline,
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/model/CollectionSelectedNovel.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/model/CollectionSelectedNovel.kt
new file mode 100644
index 000000000..766d6e579
--- /dev/null
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/model/CollectionSelectedNovel.kt
@@ -0,0 +1,8 @@
+package com.into.websoso.feature.collection.model
+
+internal data class CollectionSelectedNovel(
+ val novelId: Long,
+ val title: String,
+ val author: String,
+ val imageUrl: String,
+)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3405ee8af..62d014da9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -19,7 +19,7 @@ google-services = "4.4.4"
firebase-bom = "34.7.0"
# Analytics
-amplitude = "1.+"
+amplitude = "1.30.1"
# AndroidX Libraries
androidx = "1.15.0"