diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 000000000..43de65f84
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,15 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(./gradlew compileAppDebugKotlin)",
+ "Bash(./gradlew test *)",
+ "Bash(./gradlew :app:compileAppDebugUnitTestKotlin)",
+ "Bash(head -n 508 \"app/src/main/java/io/legado/app/model/CacheBook.kt\")",
+ "Bash(mv /tmp/cachebook_tmp.kt \"app/src/main/java/io/legado/app/model/CacheBook.kt\")",
+ "Bash(./gradlew :app:cleanCompileAppDebugKotlin)",
+ "Bash(./gradlew :app:compileAppDebugKotlin --rerun-tasks)",
+ "Bash(./gradlew :app:compileAppDebugKotlin)",
+ "Bash(./gradlew :app:cleanCompileAppDebugKotlin :app:compileAppDebugKotlin)"
+ ]
+ }
+}
diff --git a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt
index 3b8b27b00..5e81f0a8d 100644
--- a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt
@@ -37,7 +37,6 @@ import androidx.compose.material3.WideNavigationRailValue
import androidx.compose.material3.rememberWideNavigationRailState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -51,6 +50,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.HazeState
@@ -102,7 +102,7 @@ fun MainScreen(
val coroutineScope = rememberCoroutineScope()
val bookshelfViewModel: BookshelfViewModel = koinViewModel()
- val bookshelfUiState by bookshelfViewModel.uiState.collectAsState()
+ val bookshelfUiState by bookshelfViewModel.uiState.collectAsStateWithLifecycle()
val hazeState = remember { HazeState() }
val floatingBarSurfaceColor = MaterialTheme.colorScheme.surface
diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
index 87025ada2..3034cd298 100644
--- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
@@ -55,7 +55,6 @@ import androidx.compose.material3.pulltorefresh.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -73,6 +72,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R
import io.legado.app.base.BaseRuleEvent
import io.legado.app.data.entities.BookGroup
@@ -127,7 +127,7 @@ fun BookshelfScreen(
onNavigateToCache: (Long) -> Unit
) {
val context = LocalContext.current
- val uiState by viewModel.uiState.collectAsState()
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
var showAddUrlDialog by remember { mutableStateOf(false) }
@@ -721,7 +721,7 @@ fun BookshelfScreen(
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
- beyondViewportPageCount = 3,
+ beyondViewportPageCount = 1,
key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
) { pageIndex ->
val group = uiState.groups.getOrNull(pageIndex)
@@ -729,7 +729,7 @@ fun BookshelfScreen(
val booksFlow = remember(group.groupId) {
viewModel.getBooksFlow(group.groupId)
}
- val books by booksFlow.collectAsState(emptyList())
+ val books by booksFlow.collectAsStateWithLifecycle(emptyList())
BookshelfPage(
paddingValues = paddingValues,
books = books,
diff --git a/app/src/test/java/io/legado/app/domain/usecase/SearchBooksUseCaseTest.kt b/app/src/test/java/io/legado/app/domain/usecase/SearchBooksUseCaseTest.kt
new file mode 100644
index 000000000..aa9373dad
--- /dev/null
+++ b/app/src/test/java/io/legado/app/domain/usecase/SearchBooksUseCaseTest.kt
@@ -0,0 +1,54 @@
+package io.legado.app.domain.usecase
+
+import io.legado.app.data.entities.BookSource
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SearchBooksUseCaseTest {
+
+ @Test
+ fun `search url without dynamic page does not support next page`() {
+ val source = BookSource(
+ searchUrl = "https://example.com/search?keyword={{key}}&page=1"
+ )
+
+ assertFalse(source.supportsSearchPage())
+ }
+
+ @Test
+ fun `search url with page group supports next page`() {
+ val source = BookSource(
+ searchUrl = "https://example.com/search/{{key}}/<1,2,3>"
+ )
+
+ assertTrue(source.supportsSearchPage())
+ }
+
+ @Test
+ fun `search url with page inner rule supports next page`() {
+ val source = BookSource(
+ searchUrl = "https://example.com/search?keyword={{key}}&page={{page}}"
+ )
+
+ assertTrue(source.supportsSearchPage())
+ }
+
+ @Test
+ fun `search url with page js supports next page`() {
+ val source = BookSource(
+ searchUrl = "baseUrl + '/search?keyword=' + key + '&page=' + page"
+ )
+
+ assertTrue(source.supportsSearchPage())
+ }
+
+ @Test
+ fun `search url with js but without page does not support next page`() {
+ val source = BookSource(
+ searchUrl = "baseUrl + '/search?keyword=' + key"
+ )
+
+ assertFalse(source.supportsSearchPage())
+ }
+}