Przeglądaj źródła

[优化] 正确处理发现页的一些情况

HapeLee 11 miesięcy temu
rodzic
commit
7a1ade76e2

+ 4 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -204,6 +204,10 @@ object PreferKey {
     const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable"
     const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape"
     const val bookshelfLayoutGridPortrait = "bookshelfLayoutGridPortrait"
+
+    const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape"
+
+    const val exploreLayoutGridPortrait = "exploreLayoutGridPortrait"
     const val tabletInterface = "tabletInterface"
     const val pureBlack = "pure_black"
     const val labelVisibilityMode = "labelVisibilityMode"

+ 1 - 1
app/src/main/java/io/legado/app/data/repository/ExploreRepository.kt

@@ -37,7 +37,7 @@ class ExploreRepositoryImpl(
     }
 
     override suspend fun exploreBook(source: BookSource, url: String, page: Int): Result<List<SearchBook>> {
-        return withContext(Dispatchers.IO) {
+        return withContext(IO) {
             try {
                 val books = WebBook.exploreBookSuspend(source, url, page)
                 Result.success(books)

+ 12 - 0
app/src/main/java/io/legado/app/help/config/AppConfig.kt

@@ -237,6 +237,18 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
             appCtx.putPrefInt(PreferKey.bookshelfLayoutGridPortrait, value)
         }
 
+    var exploreLayoutGridLandscape: Int
+        get() = appCtx.getPrefInt(PreferKey.exploreLayoutGridLandscape, 7)
+        set(value) {
+            appCtx.putPrefInt(PreferKey.exploreLayoutGridLandscape, value)
+        }
+
+    var exploreLayoutGridPortrait: Int
+        get() = appCtx.getPrefInt(PreferKey.exploreLayoutGridPortrait, 3)
+        set(value) {
+            appCtx.putPrefInt(PreferKey.exploreLayoutGridPortrait, value)
+        }
+
     var bookshelfLayoutGridLandscape: Int
         get() = appCtx.getPrefInt(PreferKey.bookshelfLayoutGridLandscape, 7)
         set(value) {

+ 151 - 78
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt

@@ -3,8 +3,12 @@ package io.legado.app.ui.book.explore
 import android.annotation.SuppressLint
 import android.content.Intent
 import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.Crossfade
+import androidx.compose.animation.animateContentSize
 import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Arrangement
@@ -40,6 +44,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.FilterList
+import androidx.compose.material.icons.filled.Grid4x4
 import androidx.compose.material.icons.filled.GridView
 import androidx.compose.material.icons.filled.Warning
 import androidx.compose.material.icons.outlined.FilterAlt
@@ -53,8 +58,10 @@ import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.MediumTopAppBar
 import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.Scaffold
 import androidx.compose.material3.SheetValue
+import androidx.compose.material3.Slider
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBarDefaults
@@ -74,7 +81,6 @@ import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
@@ -87,7 +93,7 @@ import io.legado.app.ui.widget.components.AnimatedTextButton
 import io.legado.app.ui.widget.components.AnimatedTextLine
 import io.legado.app.ui.widget.components.Cover
 import io.legado.app.ui.widget.components.EmptyMessageView
-import io.legado.app.utils.bookshelfLayoutGrid
+import kotlinx.coroutines.delay
 import org.koin.androidx.compose.koinViewModel
 
 @SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight")
@@ -106,7 +112,7 @@ fun ExploreShowScreen(
     }
 
     val books by viewModel.uiBooks.collectAsState()
-    val isBookEnd by viewModel.isRawDataEmptyAndEnd.collectAsState()
+    val isBookEnd by viewModel.isEnd.collectAsState()
     val shouldTriggerAutoLoad by viewModel.shouldTriggerAutoLoad.collectAsState()
     val kinds by viewModel.kinds.collectAsState()
     val isLoading by viewModel.isLoading.collectAsState()
@@ -119,16 +125,14 @@ fun ExploreShowScreen(
     var showKindSheet by remember { mutableStateOf(false) }
     val layoutState by viewModel.layoutState.collectAsState()
     val isGridMode = layoutState == 1
-    val context = LocalContext.current
-    val gridColumnCount = remember(context.resources.configuration.orientation) {
-        context.bookshelfLayoutGrid
-    }
+    var showGridCountSheet by remember { mutableStateOf(false) }
+    val gridColumnCount by viewModel.gridCount.collectAsState()
 
     val shouldLoadMoreList = remember {
         derivedStateOf {
             val total = listState.layoutInfo.totalItemsCount
             val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
-            total > 0 && last >= total - 6
+            total > 0 && last >= total - 3
         }
     }
 
@@ -136,7 +140,7 @@ fun ExploreShowScreen(
         derivedStateOf {
             val total = gridState.layoutInfo.totalItemsCount
             val last = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
-            total > 0 && last >= total - 6
+            total > 0 && last >= total - 1
         }
     }
 
@@ -166,6 +170,50 @@ fun ExploreShowScreen(
         }
     }
 
+    if (showGridCountSheet) {
+        val sheetState = rememberModalBottomSheetState(
+            skipPartiallyExpanded = true
+        )
+
+        ModalBottomSheet(
+            sheetState = sheetState,
+            onDismissRequest = { showGridCountSheet = false }
+        ) {
+            Column(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(24.dp)
+            ) {
+
+                Text(
+                    text = "当前:$gridColumnCount 列",
+                    style = MaterialTheme.typography.titleLarge,
+                    modifier = Modifier.padding(bottom = 12.dp)
+                )
+
+                Slider(
+                    value = gridColumnCount.toFloat(),
+                    onValueChange = {
+                        val col = it.toInt().coerceIn(1, 10)
+                        viewModel.saveGridCount(col)
+                    },
+                    valueRange = 1f..10f,
+                    steps = 8,
+                    modifier = Modifier.padding(horizontal = 8.dp)
+                )
+
+                Spacer(Modifier.height(20.dp))
+
+                OutlinedButton (
+                    onClick = { showGridCountSheet = false },
+                    modifier = Modifier.fillMaxWidth()
+                ) {
+                    Text("完成")
+                }
+            }
+        }
+    }
+
     if (showKindSheet) {
         val scrollState = rememberScrollState()
         val sheetState = rememberModalBottomSheetState(
@@ -218,6 +266,7 @@ fun ExploreShowScreen(
                 onSelectKindClick = { showKindSheet = true },
                 onToggleGridMode = { viewModel.setLayout() },
                 isGridMode = isGridMode,
+                onGridCountClick = { showGridCountSheet = true },
                 scrollBehavior = scrollBehavior
             )
         }
@@ -227,74 +276,72 @@ fun ExploreShowScreen(
                 .fillMaxSize()
                 .padding(paddingValues)
         ) {
-            if (!isLoading && isBookEnd && errorMsg == null) {
-                Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-                    EmptyMessageView(message = "暂无书籍")
-                }
-            } else {
-                Crossfade(
-                    targetState = isGridMode,
-                    animationSpec = tween(250),
-                    label = "LayoutCrossfade"
-                ) { isGrid ->
-                    if (isGrid) {
-                        LazyVerticalGrid(
-                            state = gridState,
-                            modifier = Modifier.fillMaxSize(),
-                            columns = GridCells.Fixed(gridColumnCount),
-                            contentPadding = PaddingValues(12.dp),
-                            horizontalArrangement = Arrangement.spacedBy(4.dp),
-                            verticalArrangement = Arrangement.spacedBy(8.dp)
-                        ) {
-                            items(
-                                items = books,
-                                key = { it.bookUrl }
-                            ) { book ->
-                                val shelfState = viewModel.getCurrentBookShelfState(book)
-                                ExploreBookGridItem(
-                                    book = book,
-                                    shelfState = shelfState,
-                                    onClick = { onBookClick(book) },
-                                    modifier = Modifier.animateItem()
-                                )
-                            }
 
-                            item(span = { GridItemSpan(maxLineSpan) }) {
-                                LoadMoreFooter(
-                                    isLoading = isLoading,
-                                    errorMsg = errorMsg,
-                                    onRetry = viewModel::loadMore
-                                )
-                            }
+            Crossfade(
+                targetState = isGridMode,
+                animationSpec = tween(250),
+                label = "LayoutCrossfade"
+            ) { isGrid ->
+                if (isGrid) {
+                    LazyVerticalGrid(
+                        state = gridState,
+                        modifier = Modifier.fillMaxSize(),
+                        columns = GridCells.Fixed(gridColumnCount),
+                        contentPadding = PaddingValues(12.dp),
+                        horizontalArrangement = Arrangement.spacedBy(4.dp),
+                        verticalArrangement = Arrangement.spacedBy(8.dp)
+                    ) {
+                        items(
+                            items = books,
+                            key = { it.bookUrl }
+                        ) { book ->
+                            val shelfState = viewModel.getCurrentBookShelfState(book)
+                            ExploreBookGridItem(
+                                book = book,
+                                shelfState = shelfState,
+                                onClick = { onBookClick(book) },
+                                modifier = Modifier.animateItem()
+                            )
                         }
-                    } else {
-                        LazyColumn(
-                            state = listState,
-                            contentPadding = PaddingValues(bottom = 16.dp)
-                        ) {
-                            items(
-                                items = books,
-                                key = { it.bookUrl }
-                            ) { book ->
-                                val shelfState = viewModel.getCurrentBookShelfState(book)
-                                ExploreBookItem(
-                                    book = book,
-                                    shelfState = shelfState,
-                                    onClick = { onBookClick(book) },
-                                    modifier = Modifier.animateItem()
-                                )
-                            }
 
-                            item {
-                                LoadMoreFooter(
-                                    isLoading = isLoading,
-                                    errorMsg = errorMsg,
-                                    onRetry = viewModel::loadMore
-                                )
-                            }
+                        item(span = { GridItemSpan(maxLineSpan) }) {
+                            LoadMoreFooter(
+                                isLoading = isLoading,
+                                errorMsg = errorMsg,
+                                isEnd = isBookEnd,
+                                onRetry = viewModel::loadMore
+                            )
+                        }
+                    }
+                } else {
+                    LazyColumn(
+                        state = listState,
+                        contentPadding = PaddingValues(bottom = 16.dp)
+                    ) {
+                        items(
+                            items = books,
+                            key = { it.bookUrl }
+                        ) { book ->
+                            val shelfState = viewModel.getCurrentBookShelfState(book)
+                            ExploreBookItem(
+                                book = book,
+                                shelfState = shelfState,
+                                onClick = { onBookClick(book) },
+                                modifier = Modifier.animateItem()
+                            )
+                        }
+
+                        item {
+                            LoadMoreFooter(
+                                isLoading = isLoading,
+                                errorMsg = errorMsg,
+                                isEnd = isBookEnd,
+                                onRetry = viewModel::loadMore
+                            )
                         }
                     }
                 }
+
             }
         }
     }
@@ -341,6 +388,7 @@ fun ExploreTopBar(
     onSelectKindClick: () -> Unit,
     onToggleGridMode: () -> Unit,
     isGridMode: Boolean,
+    onGridCountClick: () -> Unit,
     scrollBehavior: TopAppBarScrollBehavior? = null
 ) {
     var showMenu by remember { mutableStateOf(false) }
@@ -353,11 +401,25 @@ fun ExploreTopBar(
             }
         },
         actions = {
-            IconButton(onClick = { showMenu = true }) {
-                Icon(Icons.Default.FilterList, contentDescription = "Filter")
-            }
-            IconButton(onClick = { onSelectKindClick() }) {
-                Icon(Icons.Outlined.FilterAlt, contentDescription = "分类")
+            Row(
+                horizontalArrangement = Arrangement.End,
+                modifier = Modifier.animateContentSize(tween(300))
+            ) {
+                IconButton(onClick = { showMenu = true }) {
+                    Icon(Icons.Default.FilterList, contentDescription = "Filter")
+                }
+                IconButton(onClick = { onSelectKindClick() }) {
+                    Icon(Icons.Outlined.FilterAlt, contentDescription = "分类")
+                }
+                AnimatedVisibility(
+                    visible = isGridMode,
+                    enter = fadeIn(tween(300)),
+                    exit = fadeOut(tween(300))
+                ) {
+                    IconButton(onClick = onGridCountClick) {
+                        Icon(Icons.Default.Grid4x4, contentDescription = "列数设置")
+                    }
+                }
             }
             IconButton(onClick = { onToggleGridMode() }) {
                 Icon(
@@ -592,22 +654,33 @@ fun TagChip(text: String) {
 fun LoadMoreFooter(
     isLoading: Boolean,
     errorMsg: String?,
+    isEnd: Boolean,
     onRetry: () -> Unit
 ) {
+
+    LaunchedEffect(isLoading, errorMsg, isEnd) {
+        if (!isLoading && errorMsg == null && !isEnd) {
+            while (true) {
+                onRetry()
+                delay(1000L)
+            }
+        }
+    }
+
     Box(
         modifier = Modifier
             .fillMaxWidth()
             .padding(32.dp),
         contentAlignment = Alignment.Center
     ) {
-
         Column(horizontalAlignment = Alignment.CenterHorizontally) {
 
             AnimatedContent(
                 targetState = when {
                     isLoading -> "加载中…"
                     errorMsg != null -> "加载失败: $errorMsg"
-                    else -> "已经到底了~"
+                    isEnd -> "已经到底了~"
+                    else -> "我爱你"
                 },
                 label = "FooterTextChange"
             ) { text ->

+ 24 - 25
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt

@@ -10,6 +10,7 @@ import io.legado.app.data.repository.ExploreRepository
 import io.legado.app.help.config.AppConfig
 import io.legado.app.model.BookShelfState
 import io.legado.app.ui.book.search.BookKey
+import io.legado.app.utils.exploreLayoutGrid
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
+import splitties.init.appCtx
 
 sealed class BookFilterState(val id: Int) {
     data object SHOW_ALL : BookFilterState(0)
@@ -60,7 +62,13 @@ class ExploreShowViewModel(
     val selectedKindTitle = _selectedKindTitle.asStateFlow()
     private val _layoutState = MutableStateFlow(AppConfig.exploreLayoutState) // 0=列表, 1=网格
     val layoutState: StateFlow<Int> = _layoutState.asStateFlow()
-
+    private val _gridCount = MutableStateFlow(appCtx.exploreLayoutGrid)
+    val gridCount = _gridCount.asStateFlow()
+    val isEnd: StateFlow<Boolean> = _isEndStateFlow.asStateFlow()
+    fun saveGridCount(value: Int) {
+        appCtx.exploreLayoutGrid = value
+        _gridCount.value = value
+    }
     val uiBooks = combine(
         _rawBooks,
         _filterState,
@@ -71,13 +79,6 @@ class ExploreShowViewModel(
         }
     }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
 
-    val isRawDataEmptyAndEnd = combine(
-        _rawBooks,
-        _isEndStateFlow
-    ) { rawBooks, isEndValue ->
-        rawBooks.isEmpty() && isEndValue
-    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
-
     val shouldTriggerAutoLoad = combine(
         _isLoading,
         uiBooks,
@@ -138,21 +139,14 @@ class ExploreShowViewModel(
     }
 
     fun loadMore(isRefresh: Boolean = false) {
-        if (_isLoading.value || (_isEndStateFlow.value && !isRefresh)) return
+        val source = bookSource
+        val url = exploreUrl
+        if (source == null || url == null || _isLoading.value || (_isEndStateFlow.value && !isRefresh)) return
 
         viewModelScope.launch {
             _isLoading.value = true
             _errorMsg.value = null
 
-            val source = bookSource
-            val url = exploreUrl
-
-            if (source == null || url == null) {
-                _isLoading.value = false
-                _errorMsg.value = "源或URL为空"
-                return@launch
-            }
-
             if (isRefresh) {
                 page = 1
                 _isEndStateFlow.value = false
@@ -165,16 +159,21 @@ class ExploreShowViewModel(
                         _isEndStateFlow.value = true
                     } else {
                         repository.saveSearchBooks(newBooks)
-                        val currentList = if (page == 1) emptyList() else _rawBooks.value
-                        if (page == 1) {
-                            _rawBooks.value = newBooks.distinctBy { it.bookUrl }
+
+                        val currentList = _rawBooks.value
+                        val existingUrls = currentList.map { it.bookUrl }.toSet()
+
+                        val uniqueNewBooks = newBooks
+                            .filter { it.bookUrl !in existingUrls }
+                            .distinctBy { it.bookUrl }
+
+                        if (uniqueNewBooks.isEmpty()) {
+                            _isEndStateFlow.value = true
                         } else {
-                            val existingUrls = currentList.map { it.bookUrl }.toSet()
-                            val uniqueNewBooks = newBooks.filter { it.bookUrl !in existingUrls }
                             _rawBooks.value = currentList + uniqueNewBooks
+                            page++
+                            _isEndStateFlow.value = false
                         }
-                        page++
-                        _isEndStateFlow.value = false
                     }
                 }
                 .onFailure {

+ 14 - 0
app/src/main/java/io/legado/app/utils/ContextExtensions.kt

@@ -434,6 +434,20 @@ val Context.bookshelfLayoutGrid: Int
         AppConfig.bookshelfLayoutGridPortrait
     }
 
+var Context.exploreLayoutGrid: Int
+    get() = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+        AppConfig.exploreLayoutGridLandscape
+    } else {
+        AppConfig.exploreLayoutGridPortrait
+    }
+    set(value) {
+        if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+            AppConfig.exploreLayoutGridLandscape = value
+        } else {
+            AppConfig.exploreLayoutGridPortrait = value
+        }
+    }
+
 fun Context.themeColor(attr: Int): Int {
     val typedValue = TypedValue()
     theme.resolveAttribute(attr, typedValue, true)