Просмотр исходного кода

feat: 阅读界面相关问题

HapeLee 3 месяцев назад
Родитель
Сommit
5ee48b7660

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt

@@ -533,6 +533,7 @@ sealed interface ReadBookIntent {
     data object OpenSourceEdit : ReadBookIntent
     data class OpenSourceEditByUrl(val sourceUrl: String) : ReadBookIntent
     data object OpenBookInfo : ReadBookIntent
+    data object OpenBookInfoDirect : ReadBookIntent
     data object OpenChapterList : ReadBookIntent
     data object OpenChapterUrl : ReadBookIntent
     data class SourceCustomButton(val longClick: Boolean) : ReadBookIntent

+ 46 - 24
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarTitle.kt

@@ -117,6 +117,7 @@ internal fun MenuTitleBar(
     val labelStyle = LegadoTheme.typography.labelSmallEmphasized.copy(
         shadow = menuTextShadow
     )
+    val useTitleCapsule = state.menuConfig.readMenuTopBarTitleCapsule
 
     Column(
         modifier = Modifier
@@ -153,7 +154,7 @@ internal fun MenuTitleBar(
                 }
             )
             .then(
-                if (topBarBorderWidth > 0) {
+                if (topBarBorderWidth > 0 && !useTitleCapsule) {
                     Modifier.drawBehind {
                         val strokeWidth = topBarBorderWidth.dp.toPx()
                         drawLine(
@@ -171,7 +172,6 @@ internal fun MenuTitleBar(
                 )
             )
     ) {
-        val useTitleCapsule = state.menuConfig.readMenuTopBarTitleCapsule
         val capsuleIconColor = LegadoTheme.colorScheme.onSurfaceVariant
 
         // Title row: left group (back + capsule/title) + right group (actions)
@@ -426,6 +426,8 @@ private fun MenuTitleGlassButton(
         iconStyle = state.menuConfig.titleBarIconStyle,
         modifier = modifier,
         onLongClick = onLongClick,
+        menuBorderEnabled = state.menuConfig.readMenuBorderWidth > 0 &&
+                state.menuConfig.readMenuTopBarTitleCapsule,
         contentDescription = contentDescription,
     )
 }
@@ -443,6 +445,7 @@ internal fun ReadMenuGlassIconButton(
     modifier: Modifier = Modifier,
     onLongClick: (() -> Unit)? = null,
     selected: Boolean = false,
+    menuBorderEnabled: Boolean = false,
     contentDescription: String? = null,
 ) {
     ReadMenuGlassButtonSurface(
@@ -455,6 +458,7 @@ internal fun ReadMenuGlassIconButton(
         modifier = modifier,
         onLongClick = onLongClick,
         selected = selected,
+        menuBorderEnabled = menuBorderEnabled,
         contentDescription = contentDescription,
     ) { tint ->
         Icon(
@@ -478,6 +482,7 @@ internal fun ReadMenuGlassButtonSurface(
     modifier: Modifier = Modifier,
     onLongClick: (() -> Unit)? = null,
     selected: Boolean = false,
+    menuBorderEnabled: Boolean = false,
     contentDescription: String? = null,
     content: @Composable (Color) -> Unit,
 ) {
@@ -492,6 +497,10 @@ internal fun ReadMenuGlassButtonSurface(
     )
     val border = when {
         selected -> BorderStroke(1.5.dp, LegadoTheme.colorScheme.secondary)
+        menuBorderEnabled -> BorderStroke(
+            menuConfig.readMenuBorderWidth.dp,
+            Color(readMenuBorderColor(menuConfig)),
+        )
         !glassEnabled && iconStyle == 2 -> BorderStroke(1.dp, tint.copy(alpha = 0.45f))
         else -> null
     }
@@ -559,6 +568,19 @@ private fun RowScope.TitleCapsuleGlassLayout(
     val glassEnabled = readerMenuLiquidGlassAvailable(backdrop)
             && state.menuConfig.readMenuTopBarLiquidGlassButtons
     val iconStyle = state.menuConfig.titleBarIconStyle
+    val border = when {
+        state.menuConfig.readMenuBorderWidth > 0 -> BorderStroke(
+            state.menuConfig.readMenuBorderWidth.dp,
+            Color(readMenuBorderColor(state.menuConfig)),
+        )
+
+        iconStyle == 2 -> BorderStroke(
+            1.dp,
+            LegadoTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.45f)
+        )
+
+        else -> null
+    }
 
     Row(
         modifier = Modifier
@@ -581,34 +603,19 @@ private fun RowScope.TitleCapsuleGlassLayout(
                         1 -> LegadoTheme.colorScheme.surfaceContainerLow
                         else -> Color.Transparent
                     }
-                    val border = when (iconStyle) {
-                        2 -> BorderStroke(
-                            1.dp,
-                            LegadoTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.45f)
-                        )
-
-                        else -> null
-                    }
                     Modifier
                         .clip(pillShape)
                         .background(containerColor, pillShape)
-                        .then(if (border != null) Modifier.border(border, pillShape) else Modifier)
                 }
             )
+            .then(if (border != null) Modifier.border(border, pillShape) else Modifier)
             .then(
-                if (!state.isLocalBook) {
-                    Modifier.combinedClickable(
-                        indication = if (glassEnabled) null else LocalIndication.current,
-                        interactionSource = remember { MutableInteractionSource() },
-                        onClick = { onIntent(ReadBookIntent.OpenBookInfo) },
-                        onLongClick = { onIntent(ReadBookIntent.OpenChapterUrl) },
-                    )
-                } else {
-                    Modifier.clickable(
-                        indication = if (glassEnabled) null else LocalIndication.current,
-                        interactionSource = remember { MutableInteractionSource() },
-                    ) { onIntent(ReadBookIntent.OpenBookInfo) }
-                }
+                Modifier.combinedClickable(
+                    indication = if (glassEnabled) null else LocalIndication.current,
+                    interactionSource = remember { MutableInteractionSource() },
+                    onClick = { onIntent(ReadBookIntent.OpenBookInfo) },
+                    onLongClick = { onIntent(ReadBookIntent.OpenBookInfoDirect) },
+                )
             )
             .padding(horizontal = 12.dp),
         verticalAlignment = Alignment.CenterVertically,
@@ -749,6 +756,21 @@ private fun MenuTitleBarMergedGlassButton(
                     blurRadius = 32.dp,
                     interactive = true,
                     menuConfig = state.menuConfig,
+                )
+                .then(
+                    if (state.menuConfig.readMenuBorderWidth > 0 &&
+                        state.menuConfig.readMenuTopBarTitleCapsule
+                    ) {
+                        Modifier.border(
+                            BorderStroke(
+                                state.menuConfig.readMenuBorderWidth.dp,
+                                Color(readMenuBorderColor(state.menuConfig)),
+                            ),
+                            pillShape,
+                        )
+                    } else {
+                        Modifier
+                    }
                 ),
             verticalAlignment = Alignment.CenterVertically,
         ) {

+ 12 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt

@@ -59,6 +59,7 @@ import io.legado.app.ui.book.read.page.ReaderEventListener
 import io.legado.app.ui.book.read.page.ReaderPageSource
 import io.legado.app.ui.book.read.page.entities.PageDirection
 import io.legado.app.ui.book.read.sheet.ReaderBookSheetRoute
+import io.legado.app.ui.book.read.sheet.ReaderBookSourceActions
 import io.legado.app.ui.book.read.sheet.TextSelectMenuConfigSheet
 import io.legado.app.ui.book.searchContent.SearchContentResult
 import io.legado.app.ui.book.toc.TocActivityResult
@@ -626,6 +627,17 @@ fun ReadBookRouteScreen(
                         }
                     }
                 },
+                bookSource = state.bookSource,
+                onOpenChapterUrl = { viewModel.onIntent(ReadBookIntent.OpenChapterUrl) },
+                onToggleReadUrlInBrowser = {
+                    viewModel.onIntent(ReadBookIntent.ToggleReadUrlInBrowser)
+                },
+                sourceActions = ReaderBookSourceActions(
+                    onLogin = { viewModel.onIntent(ReadBookIntent.ShowLogin) },
+                    onPay = { viewModel.onIntent(ReadBookIntent.PayAction) },
+                    onEdit = { viewModel.onIntent(ReadBookIntent.OpenSourceEdit) },
+                    onDisable = { viewModel.onIntent(ReadBookIntent.DisableSource) },
+                ),
             )
             TextActionSelectionMenu(
                 menuState = textMenuState,

+ 10 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -185,6 +185,15 @@ class ReadBookViewModel(
         _uiState.update { it.copy(menuState = ReadBookMenuState()) }
     }
 
+    /**
+     * 直接打开书籍详情(跳过阅读信息 Sheet,长按标题胶囊时使用)。
+     */
+    private fun openBookInfoDirect() {
+        val book = ReadBook.book ?: return
+        closeReadMenu()
+        _effects.tryEmit(ReadBookEffect.OpenBookInfo(book.name, book.author, book.bookUrl))
+    }
+
     // --- 正文处理域 ---
 
     private val contentProcessDelegate = ReadContentProcessDelegate(
@@ -933,6 +942,7 @@ class ReadBookViewModel(
             is ReadBookIntent.OpenBookInfo -> openBookNavigation(ReaderBookSheetTab.Information) {
                 ReadBookEffect.OpenBookInfo(it.name, it.author, it.bookUrl)
             }
+            is ReadBookIntent.OpenBookInfoDirect -> openBookInfoDirect()
 
             is ReadBookIntent.OpenChapterList -> openBookNavigation(ReaderBookSheetTab.Toc) {
                 ReadBookEffect.OpenChapterList(it.bookUrl)

+ 132 - 3
app/src/main/java/io/legado/app/ui/book/read/sheet/ReaderBookSheet.kt

@@ -11,6 +11,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -32,16 +33,20 @@ import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Login
 import androidx.compose.material.icons.automirrored.filled.MenuOpen
+import androidx.compose.material.icons.filled.Block
 import androidx.compose.material.icons.filled.BookmarkAdd
 import androidx.compose.material.icons.filled.CheckCircle
 import androidx.compose.material.icons.filled.Clear
 import androidx.compose.material.icons.filled.Download
+import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.ExpandMore
 import androidx.compose.material.icons.filled.LocationOn
 import androidx.compose.material.icons.filled.Lock
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Numbers
+import androidx.compose.material.icons.filled.Payment
 import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.material.icons.filled.SelectAll
@@ -74,6 +79,7 @@ import androidx.compose.ui.unit.sp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocalTxt
@@ -94,6 +100,7 @@ import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.image.cover.CoilBookCover
+import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -121,6 +128,10 @@ fun ReaderBookSheetRoute(
     onDismissRequest: () -> Unit,
     onChapterClick: (chapterIndex: Int, chapterPos: Int) -> Unit,
     onOpenFullBookInfo: () -> Unit,
+    bookSource: BookSource? = null,
+    onOpenChapterUrl: () -> Unit = {},
+    onToggleReadUrlInBrowser: () -> Unit = {},
+    sourceActions: ReaderBookSourceActions? = null,
     viewModel: TocViewModel = koinViewModel(key = "reader-book-sheet-$bookUrl"),
 ) {
     val state by viewModel.screenState.collectAsStateWithLifecycle()
@@ -203,6 +214,11 @@ fun ReaderBookSheetRoute(
             pendingExportMarkdown = isMarkdown
             exportLauncher.launch(fileName)
         },
+        bookSource = bookSource,
+        onOpenFullBookInfo = onOpenFullBookInfo,
+        onOpenChapterUrl = onOpenChapterUrl,
+        onToggleReadUrlInBrowser = onToggleReadUrlInBrowser,
+        sourceActions = sourceActions,
     )
 }
 
@@ -218,6 +234,11 @@ private fun ReaderBookSheet(
     onOpenFullScreen: (ReaderBookSheetTab) -> Unit,
     onEditLocalTocRule: (String?) -> Unit,
     onExportBookmarks: (isMarkdown: Boolean, fileName: String) -> Unit,
+    bookSource: BookSource? = null,
+    onOpenFullBookInfo: () -> Unit = {},
+    onOpenChapterUrl: () -> Unit = {},
+    onToggleReadUrlInBrowser: () -> Unit = {},
+    sourceActions: ReaderBookSourceActions? = null,
 ) {
     val initialPage = initialTab.ordinal
     val pagerState = rememberPagerState(initialPage = initialPage) { ReaderBookSheetTab.entries.size }
@@ -245,7 +266,14 @@ private fun ReaderBookSheet(
             .heightIn(max = maxHeight),
     ) {
         Box(modifier = Modifier.padding(horizontal = 16.dp)) {
-            ReaderBookHeader(book = state.book)
+            ReaderBookHeader(
+                book = state.book,
+                bookSource = bookSource,
+                onOpenBookInfo = onOpenFullBookInfo,
+                onOpenChapterUrl = onOpenChapterUrl,
+                onToggleReadUrlInBrowser = onToggleReadUrlInBrowser,
+                sourceActions = sourceActions,
+            )
         }
         CardTabRow(
             tabTitles = listOf(
@@ -333,10 +361,27 @@ private fun ReaderBookSheet(
     )
 }
 
+data class ReaderBookSourceActions(
+    val onLogin: () -> Unit = {},
+    val onPay: () -> Unit = {},
+    val onEdit: () -> Unit = {},
+    val onDisable: () -> Unit = {},
+)
+
+@OptIn(ExperimentalFoundationApi::class)
 @Composable
-internal fun ReaderBookHeader(book: Book?) {
+internal fun ReaderBookHeader(
+    book: Book?,
+    bookSource: BookSource? = null,
+    onOpenBookInfo: (() -> Unit)? = null,
+    onOpenChapterUrl: (() -> Unit)? = null,
+    onToggleReadUrlInBrowser: (() -> Unit)? = null,
+    sourceActions: ReaderBookSourceActions? = null,
+) {
     val current = ((book?.durChapterIndex ?: -1) + 1).coerceAtLeast(0)
     val total = book?.totalChapterNum?.coerceAtLeast(0) ?: 0
+    val isLocal = book?.isLocal == true
+    var sourceMenuExpanded by remember { mutableStateOf(false) }
 
     Row(
         modifier = Modifier
@@ -356,6 +401,11 @@ internal fun ReaderBookHeader(book: Book?) {
         ) {
             AppText(
                 text = book?.name.orEmpty(),
+                modifier = if (onOpenBookInfo != null) {
+                    Modifier.clickable { onOpenBookInfo() }
+                } else {
+                    Modifier
+                },
                 style = LegadoTheme.typography.labelLargeEmphasized,
                 maxLines = 2,
                 overflow = TextOverflow.Ellipsis,
@@ -367,7 +417,17 @@ internal fun ReaderBookHeader(book: Book?) {
                     modifier = Modifier
                         .padding(end = 16.dp)
                         .basicMarquee()
-                        .weight(1f),
+                        .weight(1f)
+                        .then(
+                            if (!isLocal && onOpenChapterUrl != null) {
+                                Modifier.combinedClickable(
+                                    onClick = { onOpenChapterUrl() },
+                                    onLongClick = onToggleReadUrlInBrowser,
+                                )
+                            } else {
+                                Modifier
+                            }
+                        ),
                     text = book?.durChapterTitle.orEmpty(),
                     style = LegadoTheme.typography.labelSmallEmphasized,
                     color = LegadoTheme.colorScheme.onSurfaceVariant,
@@ -379,8 +439,77 @@ internal fun ReaderBookHeader(book: Book?) {
                     text = if (total > 0) "$current / $total" else "--",
                 )
             }
+            if (!isLocal && sourceActions != null && !book?.originName.isNullOrBlank()) {
+                Box {
+                    AppText(
+                        text = book.originName,
+                        modifier = Modifier
+                            .clickable { sourceMenuExpanded = true }
+                            .padding(top = 4.dp),
+                        style = LegadoTheme.typography.labelSmall,
+                        color = LegadoTheme.colorScheme.onSurfaceVariant,
+                        maxLines = 1,
+                        overflow = TextOverflow.Ellipsis,
+                    )
+                    ReaderBookSourceDropdown(
+                        expanded = sourceMenuExpanded,
+                        onDismiss = { sourceMenuExpanded = false },
+                        bookSource = bookSource,
+                        sourceActions = sourceActions,
+                    )
+                }
+            }
+        }
+    }
+}
 
+@Composable
+private fun ReaderBookSourceDropdown(
+    expanded: Boolean,
+    onDismiss: () -> Unit,
+    bookSource: BookSource?,
+    sourceActions: ReaderBookSourceActions,
+) {
+    RoundDropdownMenu(
+        expanded = expanded,
+        onDismissRequest = onDismiss,
+    ) {
+        if (!bookSource?.loginUrl.isNullOrBlank()) {
+            RoundDropdownMenuItem(
+                leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
+                text = stringResource(R.string.login),
+                onClick = {
+                    onDismiss()
+                    sourceActions.onLogin()
+                },
+            )
         }
+        if (!bookSource?.getContentRule()?.payAction.isNullOrBlank()) {
+            RoundDropdownMenuItem(
+                leadingIcon = { MenuItemIcon(Icons.Default.Payment) },
+                text = stringResource(R.string.chapter_pay),
+                onClick = {
+                    onDismiss()
+                    sourceActions.onPay()
+                },
+            )
+        }
+        RoundDropdownMenuItem(
+            leadingIcon = { MenuItemIcon(Icons.Default.Edit) },
+            text = stringResource(R.string.edit_source),
+            onClick = {
+                onDismiss()
+                sourceActions.onEdit()
+            },
+        )
+        RoundDropdownMenuItem(
+            leadingIcon = { MenuItemIcon(Icons.Default.Block) },
+            text = stringResource(R.string.disable_source),
+            onClick = {
+                onDismiss()
+                sourceActions.onDisable()
+            },
+        )
     }
 }
 

+ 4 - 0
app/src/main/java/io/legado/app/ui/main/MainIntent.kt

@@ -65,6 +65,10 @@ object MainIntent {
         sourceKey: String? = null,
         bookUrl: String? = null,
     ): Intent = createLauncherIntent(context).apply {
+        // NEW_TASK: 支持从 Application context(如 RssJsExtensions)启动;
+        // SINGLE_TOP: MainActivity 已在栈顶时复用现有实例走 onNewIntent,直接把登录路由
+        // 压进 nav3 back stack,避免 standard launchMode 下新建 MainActivity 先回主页面。
+        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
         putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_SOURCE_LOGIN)
         putExtra(EXTRA_SOURCE_LOGIN_TYPE, type.name)
         putExtra(EXTRA_SOURCE_LOGIN_KEY, sourceKey)