Merge origin/main into feature/private-group

Resolve conflict in BookInfoSheets.kt: keep MediumPlainButton style from main
while preserving showAddGroup logic from PR for correct new group creation.
This commit is contained in:
HapeLee
2026-05-27 01:10:06 +08:00
129 changed files with 6295 additions and 1914 deletions
+44
View File
@@ -0,0 +1,44 @@
name: Deploy Docs
on:
push:
branches: [ main ]
paths: [ 'docs/**' ]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: docs/package-lock.json
- run: cd docs && npm ci
- run: cd docs && npx vitepress build
- uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
+222 -1
View File
@@ -138,7 +138,228 @@ Legacy View-based theme still exists in `lib/theme/` (used by non-migrated scree
The app is mid-migration from Views to Compose. View-based screens (reader, book info, source management) coexist with Compose screens (main tabs, settings, search, RSS, cache management). XML layouts, `viewBinding`, and traditional Activities are still heavily used. The `viewBinding` build feature is enabled but Compose screens are the target. The app is mid-migration from Views to Compose. View-based screens (reader, book info, source management) coexist with Compose screens (main tabs, settings, search, RSS, cache management). XML layouts, `viewBinding`, and traditional Activities are still heavily used. The `viewBinding` build feature is enabled but Compose screens are the target.
Compose code follows **MVI/UDF**: ViewModel owns `StateFlow`/`SharedFlow` state, Screen observes and emits user actions, no business logic in composables. For detailed Compose review conventions and migration patterns, see `.claude/skills/legado-compose-review/`. ## Jetpack Compose Requirements (new screens MUST follow)
All **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not
** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain
until migrated.
### MVI/UDF Architecture
Every Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in
a `*Contract.kt` file:
```
ui/{feature}/
├── XxxContract.kt // UiState, Intent, Effect (and optionally Sheet/Dialog)
├── XxxViewModel.kt // ViewModel
├── XxxScreen.kt // Screen composable
└── XxxRouteScreen.kt // (optional) outer wrapper for activity results / lifecycle
```
**Contract definitions:**
```kotlin
// @Stable data class — all screen state in one place
@Stable
data class XxxUiState(
val loading: Boolean = false,
val items: ImmutableList<ItemUi> = persistentListOf(),
val activeSheet: XxxSheet? = null,
val activeDialog: XxxDialog? = null,
)
// sealed interface — every user action is an Intent
sealed interface XxxIntent {
data class LoadData(val id: Long) : XxxIntent
data object Refresh : XxxIntent
}
// sealed interface — one-shot side effects (navigation, toast, etc.)
sealed interface XxxEffect {
data class ShowToast(val message: String) : XxxEffect
data class NavigateTo(val route: MainRoute) : XxxEffect
}
// (optional) sealed interface for multi-sheet/dialog scenarios
sealed interface XxxSheet { data object Filter : XxxSheet }
sealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }
```
**Naming rules:**
- State: `{Feature}UiState``@Stable data class`
- Intent: `{Feature}Intent``sealed interface` with `data class` / `data object` members
- Effect: `{Feature}Effect``sealed interface`
- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog``sealed interfaces` stored in UiState
### ViewModel
```kotlin
class XxxViewModel(/* injected dependencies */) : ViewModel() {
private val _uiState = MutableStateFlow(XxxUiState())
val uiState = _uiState.asStateFlow()
private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)
val effects = _effects.asSharedFlow()
fun onIntent(intent: XxxIntent) {
when (intent) {
is XxxIntent.LoadData -> loadData(intent.id)
is XxxIntent.Refresh -> refresh()
}
}
private fun loadData(id: Long) {
// Use viewModelScope, update _uiState via update { it.copy(...) }
}
}
```
Key rules:
- Extend `ViewModel()` directly (not `BaseViewModel`).
- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.
- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.
- Emit effects via `_effects.tryEmit(...)`.
- Single `onIntent()` entry point, dispatched via `when`.
### Screen Composable
```kotlin
// Stateless screen — ViewModel wired in entry provider or RouteScreen
@Composable
fun XxxScreen(
state: XxxUiState,
onIntent: (XxxIntent) -> Unit,
effects: Flow<XxxEffect>, // one-shot effects from ViewModel
onBack: () -> Unit,
onNavigateToYyy: (YyyRoute) -> Unit,
) {
// Collect effects
LaunchedEffect(Unit) {
effects.collectLatest { effect ->
when (effect) {
is XxxEffect.ShowToast -> { /* ... */ }
is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)
}
}
}
AppScaffold(
topBar = {
GlassMediumFlexibleTopAppBar(
title = { Text("Title") },
scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),
navigationButton = { TopBarNavigationButton(onBack) },
)
},
) { contentPadding ->
// UI content, no business logic here
}
}
```
Key rules:
- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel
directly.
- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.
- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen
doesn't need them directly.
- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,
`AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,
`TopBarActionButton`, etc.
- No business logic, no direct DB/network calls in composables.
Two input patterns are acceptable:
- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit`
ViewModel wired in entry provider or RouteScreen.
- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for
standalone screens.
### Stability
- All `UiState` and UI item data classes **must** be annotated with `@Stable`.
- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,
not `List` or `MutableList`.
- Prefer `persistentListOf()` / `toImmutableList()` for default values.
### Navigation
Uses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:
```kotlin
// In MainNavKey.kt
@Serializable
data class MainRouteXxx(val id: Long) : MainRoute
```
Entry registered in `MainNavGraph.kt`:
```kotlin
entry<MainRouteXxx> { route ->
val viewModel = koinViewModel<XxxViewModel>()
XxxScreen(
state = viewModel.uiState.collectAsStateWithLifecycle().value,
onIntent = viewModel::onIntent,
onBack = { onNavigateBack() },
onNavigateToYyy = { onNavigateToRoute(it) },
)
}
```
Key rules:
- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.
- Navigation is callback-based, wired by the entry provider.
- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.
### Koin DI
- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.
- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).
- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.
- Repositories/gateways/use cases registered as `singleOf(::...)`.
### Activity Base Class
New standalone Compose activities extend `BaseComposeActivity`:
```kotlin
class XxxActivity : BaseComposeActivity() {
@Composable
override fun Content() {
// Screen content — AppTheme is already applied by the base class
}
}
```
### RouteScreen Wrapper
For screens needing activity result handling, lifecycle observation, or permission requests, use a
two-layer pattern:
- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,
permission requests. Wires ViewModel.
- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.
### Material 3 vs Miuix
The project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:
```kotlin
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
// Miuix implementation
} else {
// Material 3 implementation
}
```
For detailed Compose review conventions and migration patterns, see
`.claude/skills/legado-compose-review/`.
## Rhino JavaScript Engine ## Rhino JavaScript Engine
+68 -14
View File
@@ -1,14 +1,68 @@
* 阅读是一款解析指定规则并获取内容的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。 ---
* 当您搜索一本书的时,阅读会您所使用的规则将该书的书名以关键词的形式提交到各个第三方网络文学网站。
各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。 # 用户协议与免责声明
任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。
第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。 ## 【特别提示】
您应该对使用搜索引擎的结果自行承担风险。
* 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。 在下载、安装或使用本软件前,请您务必仔细阅读并充分理解本协议及免责声明的全部内容。您一旦下载、安装或使用本软件,即视为已阅读、理解并同意接受本声明的全部内容。
因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读,阅读不承担任何法律责任。
阅读尊重并保护所有使用阅读用户的个人隐私权,您注册的用户名、电子邮件地址等个人资料,非经您亲自许可或根据相关法律、法规的强制性规定,阅读不会主动地泄露给第三方。 ---
* 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费,通过专业搜索展示不同网站中网络文学的最新章节。
阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时,也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。 # 一、软件性质说明
阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商,并建议阅读正版图书。
任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明 1. 本软件是一款用户可配置的本地化网页内容浏览工具,提供网页访问、内容解析、文本提取、阅读排版与数据管理等技术功能
阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。
2. 本软件默认状态下不预置、不内置、不提供任何第三方网站内容、数据资源或解析规则。
3. 本软件开发者不提供任何内容运营、内容存储、内容发布或内容传播服务。
4. 用户可基于自身需求,自行配置或导入第三方规则,以实现对公开网页内容的个性化浏览与处理。
---
# 二、用户行为与规则说明
1. 用户可自行创建、编辑、导入或使用第三方分享的解析规则(以下简称“规则”)。
2. 相关规则仅用于定义网页内容的获取、提取与显示方式,其来源、合法性、准确性与适用性均由用户自行判断并承担责任。
3. 用户使用规则访问第三方网站时,相关网络请求由用户设备直接向目标网站发起并接收数据。本软件仅提供本地解析与展示能力,不对第三方网站内容进行修改、编辑或再分发。
4. 用户应遵守所在地法律法规、网络安全要求以及相关网站的服务协议与版权规范,不得利用本软件从事侵犯知识产权、非法传播、未授权数据获取、破坏网络服务或其他违法违规行为。
---
# 三、第三方内容与社区说明
1. 任何由第三方建立或维护的规则分享平台、论坛、交流群组、网站或其他社区,均属于独立运营的第三方平台,与本软件开发者无隶属关系。
2. 开发者不参与第三方规则、内容或社区的制作、发布、运营、维护与传播行为,亦不对相关内容承担主动审查义务。
3. 用户因使用第三方规则或访问第三方网站而产生的风险,包括但不限于版权纠纷、数据安全风险、网络访问风险或其他法律风险,应由相关行为主体依法承担相应责任。
---
# 四、隐私与数据说明
1. 本软件主要功能运行于用户本地设备,不设立用于提供网页内容服务的自有内容服务器。
2. 本软件不会主动收集、上传或存储用户的阅读内容、规则列表、浏览记录或其他个人隐私数据。
3. 为提升软件稳定性与兼容性,本软件可能集成第三方统计或崩溃分析服务(如 Firebase Crashlytics
等),用于收集匿名化的崩溃日志、性能信息及基础设备信息。
4. 部分网络、存储或同步权限,仅用于实现用户主动启用的本地备份、WebDAV 同步或跨设备数据同步等功能。
---
# 五、知识产权保护
1. 开发者尊重并保护知识产权权利人的合法权益,并反对任何侵犯著作权、商标权或其他合法权益的行为。
2. 用户应确保其使用本软件获取、处理或访问相关内容的行为符合适用法律法规及权利要求。
3. 若权利人认为某些第三方规则存在涉嫌侵权情形,可依法向相关内容实际托管方主张权利。
4. 权利人亦可向开发者提交包含身份证明、权属证明、具体规则信息及相关说明的有效通知。开发者将在合理技术能力范围内,对涉嫌违规规则采取必要处理措施。
---
@@ -4,10 +4,13 @@ import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
val Context.localDataStore: DataStore<Preferences> by preferencesDataStore(name = "local_ui_status") val Context.localDataStore: DataStore<Preferences> by preferencesDataStore(name = "local_ui_status")
object LocalPreferencesKeys { object LocalPreferencesKeys {
val SHOW_THEME_REFACTOR_TIP = booleanPreferencesKey("show_theme_refactor_tip") val SHOW_THEME_REFACTOR_TIP = booleanPreferencesKey("show_theme_refactor_tip")
val SEARCH_LAYOUT_MODE = intPreferencesKey("search_layout_mode")
val MATCH_MODE = intPreferencesKey("match_mode")
} }
@@ -80,9 +80,10 @@ class ExploreRepositoryImpl(
override suspend fun exploreBooks( override suspend fun exploreBooks(
bookSource: BookSource, bookSource: BookSource,
url: String, url: String,
page: Int page: Int,
key: String?
): List<SearchBook> { ): List<SearchBook> {
return WebBook.exploreBookSuspend(bookSource, url, page) return WebBook.exploreBookSuspend(bookSource, url, page, key = key, isSearch = key != null)
} }
override suspend fun getSourceExploreKinds(sourceUrl: String): List<ExploreKind> = withContext(IO) { override suspend fun getSourceExploreKinds(sourceUrl: String): List<ExploreKind> = withContext(IO) {
@@ -5,5 +5,10 @@ import io.legado.app.data.entities.SearchBook
interface ExploreBooksGateway { interface ExploreBooksGateway {
suspend fun getBookSource(sourceUrl: String): BookSource? suspend fun getBookSource(sourceUrl: String): BookSource?
suspend fun exploreBooks(bookSource: BookSource, url: String, page: Int): List<SearchBook> suspend fun exploreBooks(
bookSource: BookSource,
url: String,
page: Int,
key: String? = null
): List<SearchBook>
} }
@@ -0,0 +1,10 @@
package io.legado.app.domain.model
enum class MatchMode(val value: Int) {
DEFAULT(0),
EXACT(1);
companion object {
fun of(value: Int) = entries.getOrElse(value) { DEFAULT }
}
}
@@ -20,14 +20,17 @@ class ExploreBooksUseCase(
sourceUrl: String, sourceUrl: String,
moduleUrl: String?, moduleUrl: String?,
args: String?, args: String?,
page: Int = 1 page: Int = 1,
key: String? = null,
): ExploreResult = withContext(Dispatchers.IO) { ): ExploreResult = withContext(Dispatchers.IO) {
val base = gateway.getBookSource(sourceUrl) val base = gateway.getBookSource(sourceUrl)
?: throw SourceNotFound(sourceUrl) ?: throw SourceNotFound(sourceUrl)
val source = args?.let { base.copy().also { s -> s.setVariable(it) } } ?: base val source = args?.let { base.copy().also { s -> s.setVariable(it) } } ?: base
val resolvedUrl = moduleUrl ?: source.exploreUrl val resolvedUrl = moduleUrl
?: throw NoExploreUrl(sourceUrl) ?: (if (key != null) source.searchUrl else null)
val books = gateway.exploreBooks(source, resolvedUrl, page) ?: source.exploreUrl
?: throw NoExploreUrl(sourceUrl)
val books = gateway.exploreBooks(source, resolvedUrl, page, key = key)
ExploreResult(resolvedUrl, books) ExploreResult(resolvedUrl, books)
} }
@@ -6,6 +6,7 @@ import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.domain.gateway.BookSearchGateway import io.legado.app.domain.gateway.BookSearchGateway
import io.legado.app.domain.model.BookSearchScope import io.legado.app.domain.model.BookSearchScope
import io.legado.app.domain.model.MatchMode
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@@ -30,7 +31,7 @@ data class BookSearchRequest(
val keyword: String, val keyword: String,
val page: Int, val page: Int,
val scope: BookSearchScope, val scope: BookSearchScope,
val precision: Boolean, val matchMode: MatchMode,
val concurrency: Int, val concurrency: Int,
val types: Set<Int>? = null, val types: Set<Int>? = null,
) )
@@ -103,7 +104,7 @@ class SearchBooksUseCase(
throw NoStackTraceException("可搜索书源为空") throw NoStackTraceException("可搜索书源为空")
} }
val merger = SearchResultMerger(keyword, request.precision) val merger = SearchResultMerger(keyword, request.matchMode)
val concurrency = request.concurrency.coerceAtLeast(1) val concurrency = request.concurrency.coerceAtLeast(1)
var hasMore = false var hasMore = false
var processedSources = 0 var processedSources = 0
@@ -116,7 +117,7 @@ class SearchBooksUseCase(
.flatMapMerge(concurrency) { searchableSource -> .flatMapMerge(concurrency) { searchableSource ->
flow { flow {
control.awaitResumed() control.awaitResumed()
emit(searchSource(searchableSource, keyword, request.page, request.precision)) emit(searchSource(searchableSource, keyword, request.page, request.matchMode))
}.flowOn(Dispatchers.IO) }.flowOn(Dispatchers.IO)
} }
.collect { result -> .collect { result ->
@@ -177,7 +178,7 @@ class SearchBooksUseCase(
searchableSource: SearchableSource, searchableSource: SearchableSource,
keyword: String, keyword: String,
page: Int, page: Int,
precision: Boolean, matchMode: MatchMode,
): SourceSearchResult { ): SourceSearchResult {
return try { return try {
val source = searchableSource.source val source = searchableSource.source
@@ -191,7 +192,7 @@ class SearchBooksUseCase(
keyword, keyword,
page, page,
filter = { name, author -> filter = { name, author ->
!precision || matchMode == MatchMode.DEFAULT ||
name.contains(keyword, ignoreCase = true) || name.contains(keyword, ignoreCase = true) ||
author.contains(keyword, ignoreCase = true) author.contains(keyword, ignoreCase = true)
} }
@@ -222,7 +223,7 @@ class SearchBooksUseCase(
private class SearchResultMerger( private class SearchResultMerger(
private val keyword: String, private val keyword: String,
private val precision: Boolean, private val matchMode: MatchMode,
) { ) {
private companion object { private companion object {
const val MAX_RETAINED_SEARCH_RESULTS = 1000 const val MAX_RETAINED_SEARCH_RESULTS = 1000
@@ -267,9 +268,13 @@ class SearchBooksUseCase(
book.name.equals(keyword, ignoreCase = true) || book.name.equals(keyword, ignoreCase = true) ||
book.author.equals(keyword, ignoreCase = true) -> equalBooks book.author.equals(keyword, ignoreCase = true) -> equalBooks
book.name.contains(keyword, ignoreCase = true) || book.name.contains(keyword, ignoreCase = true) ||
book.author.contains(keyword, ignoreCase = true) -> containsBooks book.author.contains(
!precision -> otherBooks keyword,
else -> null ignoreCase = true
) -> if (matchMode == MatchMode.EXACT) null else containsBooks
matchMode != MatchMode.DEFAULT -> null
else -> otherBooks
} }
} }
@@ -109,9 +109,12 @@ object WebBook {
url: String, url: String,
page: Int? = 1, page: Int? = 1,
ruleData: RuleDataInterface = RuleData(), ruleData: RuleDataInterface = RuleData(),
key: String? = null,
isSearch: Boolean = false,
): ArrayList<SearchBook> { ): ArrayList<SearchBook> {
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
mUrl = url, mUrl = url,
key = key,
page = page, page = page,
baseUrl = bookSource.bookSourceUrl, baseUrl = bookSource.bookSourceUrl,
source = bookSource, source = bookSource,
@@ -137,7 +140,7 @@ object WebBook {
analyzeUrl = analyzeUrl, analyzeUrl = analyzeUrl,
baseUrl = res.url, baseUrl = res.url,
body = res.body, body = res.body,
isSearch = false isSearch = isSearch
) )
} }
@@ -146,8 +149,10 @@ object WebBook {
url: String, url: String,
page: Int? = 1, page: Int? = 1,
ruleData: RuleDataInterface = RuleData(), ruleData: RuleDataInterface = RuleData(),
key: String? = null,
isSearch: Boolean = false,
): List<SearchBook> { ): List<SearchBook> {
return exploreBookAwait(bookSource, url, page, ruleData) return exploreBookAwait(bookSource, url, page, ruleData, key, isSearch)
} }
suspend fun exploreBookWithResolvedUrl( suspend fun exploreBookWithResolvedUrl(
@@ -40,7 +40,7 @@ import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.AppFloatingActionButton import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallTonalIconButton import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.card.NormalCard 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.card.TextCard
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
@@ -347,7 +347,7 @@ private fun BookCacheBookCard(
color = LegadoTheme.colorScheme.onSurfaceVariant color = LegadoTheme.colorScheme.onSurfaceVariant
) )
if (item.hasDownloadTask || item.cachedCount < item.totalCount) { if (item.hasDownloadTask || item.cachedCount < item.totalCount) {
SmallTonalIconButton( SmallTonalButton(
onClick = { onClick = {
if (item.hasActiveDownload) { if (item.hasActiveDownload) {
onIntent(BookCacheManageIntent.StopBookDownload(item.bookUrl)) onIntent(BookCacheManageIntent.StopBookDownload(item.bookUrl))
@@ -355,7 +355,7 @@ private fun BookCacheBookCard(
onIntent(BookCacheManageIntent.StartBookDownload(item.bookUrl)) onIntent(BookCacheManageIntent.StartBookDownload(item.bookUrl))
} }
}, },
imageVector = if (item.hasActiveDownload) Icons.Default.Stop else Icons.Default.PlayArrow, icon = if (item.hasActiveDownload) Icons.Default.Stop else Icons.Default.PlayArrow,
contentDescription = when { contentDescription = when {
item.hasActiveDownload -> "暂停本书下载" item.hasActiveDownload -> "暂停本书下载"
item.isPaused -> "继续本书下载" item.isPaused -> "继续本书下载"
@@ -363,9 +363,9 @@ private fun BookCacheBookCard(
} }
) )
} }
SmallTonalIconButton( SmallTonalButton(
onClick = { onDeleteBook(item) }, onClick = { onDeleteBook(item) },
imageVector = Icons.Default.Delete, icon = Icons.Default.Delete,
contentDescription = null contentDescription = null
) )
} }
@@ -412,21 +412,21 @@ private fun BookCacheChapterRow(
) )
} }
if (item.isWaiting || item.isDownloading) { if (item.isWaiting || item.isDownloading) {
SmallTonalIconButton( SmallTonalButton(
onClick = onStop, onClick = onStop,
imageVector = Icons.Default.Stop, icon = Icons.Default.Stop,
contentDescription = "暂停章节下载" contentDescription = "暂停章节下载"
) )
} else if (item.isPaused || !item.isCached) { } else if (item.isPaused || !item.isCached) {
SmallTonalIconButton( SmallTonalButton(
onClick = onDownload, onClick = onDownload,
imageVector = Icons.Default.Download, icon = Icons.Default.Download,
contentDescription = if (item.isPaused) "继续章节下载" else "下载章节" contentDescription = if (item.isPaused) "继续章节下载" else "下载章节"
) )
} }
SmallTonalIconButton( SmallTonalButton(
onClick = onDelete, onClick = onDelete,
imageVector = Icons.Default.Delete, icon = Icons.Default.Delete,
contentDescription = null contentDescription = null
) )
} }
@@ -0,0 +1,65 @@
package io.legado.app.ui.book.explore
import androidx.compose.runtime.Stable
import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.domain.model.BookShelfState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Stable
data class ExploreShowUiState(
val sourceUrl: String? = null,
val books: ImmutableList<ExploreBookItemUi> = persistentListOf(),
val kinds: ImmutableList<ExploreKind> = persistentListOf(),
val selectedKindTitle: String? = null,
val layoutState: Int = 0,
val gridCount: Int = 3,
val isLoading: Boolean = false,
val isRefreshing: Boolean = false,
val isEnd: Boolean = false,
val errorMsg: String? = null,
val sheet: ExploreShowSheet = ExploreShowSheet.None,
)
@Stable
data class ExploreBookItemUi(
val book: SearchBook,
val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF,
)
sealed interface ExploreShowSheet {
data object None : ExploreShowSheet
data object KindSelect : ExploreShowSheet
data object GridCount : ExploreShowSheet
}
sealed interface ExploreShowIntent {
data class InitData(
val sourceUrl: String?,
val exploreUrl: String?,
) : ExploreShowIntent
data object LoadMore : ExploreShowIntent
data object Refresh : ExploreShowIntent
data class SwitchKind(val kind: ExploreKind) : ExploreShowIntent
data object ToggleLayout : ExploreShowIntent
data class SaveGridCount(val count: Int) : ExploreShowIntent
data class ShowSheet(val sheet: ExploreShowSheet) : ExploreShowIntent
data object DismissSheet : ExploreShowIntent
data class OpenBook(val book: SearchBook, val sharedCoverKey: String?) : ExploreShowIntent
data class AddToShelf(val book: SearchBook) : ExploreShowIntent
}
sealed interface ExploreShowEffect {
data class OpenBookInfo(
val name: String,
val author: String,
val bookUrl: String,
val origin: String?,
val coverPath: String?,
val sharedCoverKey: String?,
) : ExploreShowEffect
data class ShowMessage(val message: String) : ExploreShowEffect
}
@@ -6,7 +6,6 @@ import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@@ -33,11 +32,8 @@ import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.outlined.FilterAlt import androidx.compose.material.icons.outlined.FilterAlt
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -48,19 +44,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.domain.model.BookShelfState import io.legado.app.domain.model.BookShelfState
import io.legado.app.domain.usecase.ExploreKindUiUseCase
import io.legado.app.ui.config.coverConfig.CoverConfig import io.legado.app.ui.config.coverConfig.CoverConfig
import io.legado.app.ui.main.bookCoverSharedElementKey import io.legado.app.ui.main.bookCoverSharedElementKey
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.responsiveHazeEffect import io.legado.app.ui.theme.responsiveHazeEffect
import io.legado.app.ui.theme.responsiveHazeSource import io.legado.app.ui.theme.responsiveHazeSource
import io.legado.app.ui.widget.components.AppPullToRefresh import io.legado.app.ui.widget.components.AppPullToRefresh
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppSlider
import io.legado.app.ui.widget.components.LoadMoreFooter import io.legado.app.ui.widget.components.LoadMoreFooter
import io.legado.app.ui.widget.components.book.SearchBookGridItem import io.legado.app.ui.widget.components.book.SearchBookGridItem
import io.legado.app.ui.widget.components.book.SearchBookListItem import io.legado.app.ui.widget.components.book.SearchBookListItem
@@ -74,7 +70,17 @@ import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.topbar.TopBarActionButton import io.legado.app.ui.widget.components.topbar.TopBarActionButton
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
private enum class BookFilterState(val id: Int) {
SHOW_ALL(0),
HIDE_IN_SHELF(1),
HIDE_SAME_NAME_AUTHOR(2),
SHOW_NOT_IN_SHELF_ONLY(3);
companion object {
fun fromId(id: Int) = entries.getOrElse(id) { SHOW_ALL }
}
}
@SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight") @SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight")
@OptIn( @OptIn(
@@ -83,55 +89,51 @@ import org.koin.compose.koinInject
) )
@Composable @Composable
fun ExploreShowScreen( fun ExploreShowScreen(
title: String, viewModel: ExploreShowViewModel = koinViewModel(),
sourceUrl: String?, title: String = "",
exploreUrl: String?,
onBack: () -> Unit, onBack: () -> Unit,
onBookClick: (SearchBook, String?) -> Unit, onBookClick: (SearchBook, String?) -> Unit,
viewModel: ExploreShowViewModel = koinViewModel(),
sharedTransitionScope: SharedTransitionScope? = null, sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null,
) { ) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(viewModel) {
viewModel.effects.collect { effect ->
when (effect) {
is ExploreShowEffect.OpenBookInfo -> onBookClick(
SearchBook(
name = effect.name,
author = effect.author,
bookUrl = effect.bookUrl,
origin = effect.origin ?: "",
coverUrl = effect.coverPath,
),
effect.sharedCoverKey,
)
is ExploreShowEffect.ShowMessage -> {}
}
}
}
var previewBook by remember { mutableStateOf<SearchBook?>(null) } var previewBook by remember { mutableStateOf<SearchBook?>(null) }
var previewSharedCoverKey by remember { mutableStateOf<String?>(null) } var previewSharedCoverKey by remember { mutableStateOf<String?>(null) }
LaunchedEffect(sourceUrl, exploreUrl, viewModel) {
viewModel.initData(sourceUrl, exploreUrl)
}
val rawBooks by viewModel.uiBooks.collectAsState()
val filterStateId = CoverConfig.exploreFilterState val filterStateId = CoverConfig.exploreFilterState
val books = remember(rawBooks, filterStateId) { val books = remember(state.books, filterStateId) {
val filter = BookFilterState.fromId(filterStateId) val filter = BookFilterState.fromId(filterStateId)
when (filter) { when (filter) {
BookFilterState.SHOW_ALL -> rawBooks BookFilterState.SHOW_ALL -> state.books
BookFilterState.HIDE_IN_SHELF -> rawBooks.filter { it.shelfState != BookShelfState.IN_SHELF } BookFilterState.HIDE_IN_SHELF -> state.books.filter { it.shelfState != BookShelfState.IN_SHELF }
BookFilterState.HIDE_SAME_NAME_AUTHOR -> rawBooks.filter { it.shelfState != BookShelfState.SAME_NAME_AUTHOR } BookFilterState.HIDE_SAME_NAME_AUTHOR -> state.books.filter { it.shelfState != BookShelfState.SAME_NAME_AUTHOR }
BookFilterState.SHOW_NOT_IN_SHELF_ONLY -> rawBooks.filter { it.shelfState == BookShelfState.NOT_IN_SHELF } BookFilterState.SHOW_NOT_IN_SHELF_ONLY -> state.books.filter { it.shelfState == BookShelfState.NOT_IN_SHELF }
} }
} }
val isBookEnd by viewModel.isEnd.collectAsState()
val shouldTriggerAutoLoad by viewModel.shouldTriggerAutoLoad.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
val errorMsg by viewModel.errorMsg.collectAsState()
val selectedTitle by viewModel.selectedKindTitle.collectAsState()
val listState = rememberLazyListState() val listState = rememberLazyListState()
val gridState = rememberLazyGridState() val gridState = rememberLazyGridState()
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior() val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
var showKindSheet by remember { mutableStateOf(false) } val isGridMode = state.layoutState == 1
val layoutState by viewModel.layoutState.collectAsState()
val isGridMode = layoutState == 1
var showGridCountSheet by remember { mutableStateOf(false) }
val gridColumnCount by viewModel.gridCount.collectAsState()
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
val exploreKindUseCase: ExploreKindUiUseCase = koinInject()
LaunchedEffect(sourceUrl) {
exploreKindUseCase.warmUp(sourceUrl)
}
val isRefreshing by viewModel.isRefreshing.collectAsState()
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
val shouldLoadMoreList = remember { val shouldLoadMoreList = remember {
derivedStateOf { derivedStateOf {
@@ -150,17 +152,11 @@ fun ExploreShowScreen(
} }
LaunchedEffect(shouldLoadMoreList.value, isGridMode) { LaunchedEffect(shouldLoadMoreList.value, isGridMode) {
if (!isGridMode && shouldLoadMoreList.value) viewModel.loadMore() if (!isGridMode && shouldLoadMoreList.value) viewModel.onIntent(ExploreShowIntent.LoadMore)
} }
LaunchedEffect(shouldLoadMoreGrid.value, isGridMode) { LaunchedEffect(shouldLoadMoreGrid.value, isGridMode) {
if (isGridMode && shouldLoadMoreGrid.value) viewModel.loadMore() if (isGridMode && shouldLoadMoreGrid.value) viewModel.onIntent(ExploreShowIntent.LoadMore)
}
LaunchedEffect(shouldTriggerAutoLoad) {
if (shouldTriggerAutoLoad) {
viewModel.loadMore()
}
} }
LaunchedEffect(isGridMode) { LaunchedEffect(isGridMode) {
@@ -175,17 +171,14 @@ fun ExploreShowScreen(
} }
} }
AppModalBottomSheet( AppModalBottomSheet(
show = showGridCountSheet, show = state.sheet == ExploreShowSheet.GridCount,
modifier = Modifier onDismissRequest = { viewModel.onIntent(ExploreShowIntent.DismissSheet) }
.padding(16.dp),
onDismissRequest = { showGridCountSheet = false }
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 16.dp), .padding(bottom = 32.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
@@ -193,49 +186,38 @@ fun ExploreShowScreen(
text = "布局列数", text = "布局列数",
style = LegadoTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold) style = LegadoTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
TextCard( TextCard(
text = "$gridColumnCount", text = "${state.gridCount}",
textStyle = LegadoTheme.typography.titleSmall, textStyle = LegadoTheme.typography.titleSmall,
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
verticalPadding = 4.dp, verticalPadding = 4.dp,
horizontalPadding = 12.dp, horizontalPadding = 12.dp,
cornerRadius = 12.dp cornerRadius = 12.dp
) )
} }
Slider( AppSlider(
value = gridColumnCount.toFloat(), value = state.gridCount.toFloat(),
onValueChange = { onValueChange = {
val col = it.toInt().coerceIn(1, 10) viewModel.onIntent(ExploreShowIntent.SaveGridCount(it.toInt().coerceIn(1, 10)))
viewModel.saveGridCount(col)
}, },
valueRange = 1f..10f, valueRange = 1f..10f,
steps = 8, steps = 8,
modifier = Modifier.padding(horizontal = 20.dp) modifier = Modifier.padding(horizontal = 20.dp)
) )
Spacer(Modifier.height(24.dp)) Spacer(modifier = Modifier.height(32.dp))
OutlinedButton(
onClick = { showGridCountSheet = false },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
AppText("完成")
}
} }
ExploreKindSelectSheet( ExploreKindSelectSheet(
show = showKindSheet, show = state.sheet == ExploreShowSheet.KindSelect,
onDismissRequest = { showKindSheet = false }, onDismissRequest = { viewModel.onIntent(ExploreShowIntent.DismissSheet) },
sourceUrl = sourceUrl, sourceUrl = state.sourceUrl,
onSelected = { selectedKinds -> onSelected = { selectedKinds ->
selectedKinds.firstOrNull()?.let { kind -> selectedKinds.firstOrNull()?.let { kind ->
viewModel.switchExploreUrl(kind) viewModel.onIntent(ExploreShowIntent.SwitchKind(kind))
} }
} }
) )
@@ -245,39 +227,39 @@ fun ExploreShowScreen(
.nestedScroll(scrollBehavior.nestedScrollConnection), .nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
GlassMediumFlexibleTopAppBar( GlassMediumFlexibleTopAppBar(
modifier = Modifier.responsiveHazeEffect( modifier = Modifier.responsiveHazeEffect(state = hazeState),
state = hazeState title = state.selectedKindTitle ?: title,
),
title = selectedTitle ?: title,
navigationIcon = { navigationIcon = {
TopBarNavigationButton(onClick = onBack) TopBarNavigationButton(onClick = onBack)
}, },
actions = { actions = {
Row(
horizontalArrangement = Arrangement.End, AnimatedVisibility(
modifier = Modifier.animateContentSize(tween(300)) visible = isGridMode,
enter = fadeIn(tween(300)),
exit = fadeOut(tween(300))
) { ) {
TopBarActionButton( TopBarActionButton(
onClick = { showKindSheet = true }, onClick = {
imageVector = Icons.Outlined.FilterAlt, viewModel.onIntent(
contentDescription = "分类" ExploreShowIntent.ShowSheet(
ExploreShowSheet.GridCount
)
)
},
imageVector = Icons.AutoMirrored.Outlined.FormatListBulleted,
contentDescription = "列数设置"
) )
AnimatedVisibility(
visible = isGridMode,
enter = fadeIn(tween(300)),
exit = fadeOut(tween(300))
) {
TopBarActionButton(
onClick = { showGridCountSheet = true },
imageVector = Icons.AutoMirrored.Outlined.FormatListBulleted,
contentDescription = "列数设置"
)
}
} }
TopBarActionButton( TopBarActionButton(
onClick = { viewModel.setLayout() }, onClick = { viewModel.onIntent(ExploreShowIntent.ShowSheet(ExploreShowSheet.KindSelect)) },
imageVector = Icons.Outlined.FilterAlt,
contentDescription = "分类"
)
TopBarActionButton(
onClick = { viewModel.onIntent(ExploreShowIntent.ToggleLayout) },
imageVector = if (!isGridMode) Icons.AutoMirrored.Outlined.FormatListBulleted else Icons.Default.GridView, imageVector = if (!isGridMode) Icons.AutoMirrored.Outlined.FormatListBulleted else Icons.Default.GridView,
contentDescription = "切换布局" contentDescription = "切换布局"
) )
@@ -288,8 +270,8 @@ fun ExploreShowScreen(
) { paddingValues -> ) { paddingValues ->
AppPullToRefresh( AppPullToRefresh(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
isRefreshing = isRefreshing, isRefreshing = state.isRefreshing,
onRefresh = { viewModel.loadMore(isRefresh = true) }, onRefresh = { viewModel.onIntent(ExploreShowIntent.Refresh) },
topPadding = paddingValues.calculateTopPadding() topPadding = paddingValues.calculateTopPadding()
) { ) {
Crossfade( Crossfade(
@@ -303,7 +285,7 @@ fun ExploreShowScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.responsiveHazeSource(hazeState), .responsiveHazeSource(hazeState),
columns = GridCells.Fixed(gridColumnCount), columns = GridCells.Fixed(state.gridCount),
contentPadding = PaddingValues( contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding() + 12.dp, top = paddingValues.calculateTopPadding() + 12.dp,
bottom = paddingValues.calculateBottomPadding() + 12.dp, bottom = paddingValues.calculateBottomPadding() + 12.dp,
@@ -324,7 +306,14 @@ fun ExploreShowScreen(
ExploreBookGridItem( ExploreBookGridItem(
book = item.book, book = item.book,
shelfState = item.shelfState, shelfState = item.shelfState,
onClick = { onBookClick(item.book, sharedCoverKey) }, onClick = {
viewModel.onIntent(
ExploreShowIntent.OpenBook(
item.book,
sharedCoverKey
)
)
},
onLongClick = { book, coverKey -> onLongClick = { book, coverKey ->
previewBook = book previewBook = book
previewSharedCoverKey = coverKey previewSharedCoverKey = coverKey
@@ -338,10 +327,10 @@ fun ExploreShowScreen(
item(span = { GridItemSpan(maxLineSpan) }) { item(span = { GridItemSpan(maxLineSpan) }) {
LoadMoreFooter( LoadMoreFooter(
isLoading = isLoading, isLoading = state.isLoading,
errorMsg = errorMsg, errorMsg = state.errorMsg,
isEnd = isBookEnd, isEnd = state.isEnd,
onRetry = viewModel::loadMore onRetry = { viewModel.onIntent(ExploreShowIntent.LoadMore) }
) )
} }
} }
@@ -367,7 +356,14 @@ fun ExploreShowScreen(
ExploreBookItem( ExploreBookItem(
book = item.book, book = item.book,
shelfState = item.shelfState, shelfState = item.shelfState,
onClick = { onBookClick(item.book, sharedCoverKey) }, onClick = {
viewModel.onIntent(
ExploreShowIntent.OpenBook(
item.book,
sharedCoverKey
)
)
},
onLongClick = { book, coverKey -> onLongClick = { book, coverKey ->
previewBook = book previewBook = book
previewSharedCoverKey = coverKey previewSharedCoverKey = coverKey
@@ -381,15 +377,14 @@ fun ExploreShowScreen(
item { item {
LoadMoreFooter( LoadMoreFooter(
isLoading = isLoading, isLoading = state.isLoading,
errorMsg = errorMsg, errorMsg = state.errorMsg,
isEnd = isBookEnd, isEnd = state.isEnd,
onRetry = viewModel::loadMore onRetry = { viewModel.onIntent(ExploreShowIntent.LoadMore) }
) )
} }
} }
} }
} }
} }
} }
@@ -408,7 +403,7 @@ fun ExploreShowScreen(
onBookClick(book, sharedCoverKey) onBookClick(book, sharedCoverKey)
}, },
onAddToShelf = { book -> onAddToShelf = { book ->
viewModel.onAddToShelf(book) viewModel.onIntent(ExploreShowIntent.AddToShelf(book))
}, },
) )
} }
@@ -454,10 +449,9 @@ fun ExploreBookGridItem(
shelfState = shelfState, shelfState = shelfState,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
modifier = modifier, modifier = modifier.padding(4.dp),
sharedTransitionScope = sharedTransitionScope, sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope, animatedVisibilityScope = animatedVisibilityScope,
sharedCoverKey = sharedCoverKey sharedCoverKey = sharedCoverKey
) )
} }
@@ -6,7 +6,6 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.rule.ExploreKind import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.data.repository.ExploreRepository import io.legado.app.data.repository.ExploreRepository
import io.legado.app.domain.model.BookShelfState
import io.legado.app.domain.usecase.AddToBookshelfUseCase import io.legado.app.domain.usecase.AddToBookshelfUseCase
import io.legado.app.domain.usecase.BookShelfKey import io.legado.app.domain.usecase.BookShelfKey
import io.legado.app.domain.usecase.ExploreBooksUseCase import io.legado.app.domain.usecase.ExploreBooksUseCase
@@ -15,43 +14,16 @@ import io.legado.app.domain.usecase.SaveSearchBooksUseCase
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.utils.exploreLayoutGrid import io.legado.app.utils.exploreLayoutGrid
import io.legado.app.utils.stackTraceStr import io.legado.app.utils.stackTraceStr
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import splitties.init.appCtx import splitties.init.appCtx
sealed class BookFilterState(val id: Int) {
data object SHOW_ALL : BookFilterState(0)
data object HIDE_IN_SHELF : BookFilterState(1)
data object HIDE_SAME_NAME_AUTHOR : BookFilterState(2)
data object SHOW_NOT_IN_SHELF_ONLY : BookFilterState(3)
companion object {
fun fromId(id: Int): BookFilterState = when (id) {
1 -> HIDE_IN_SHELF
2 -> HIDE_SAME_NAME_AUTHOR
3 -> SHOW_NOT_IN_SHELF_ONLY
else -> SHOW_ALL
}
}
}
sealed class UiState<out T> {
data object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
data object Empty : UiState<Nothing>()
}
data class ExploreBookItemUi(
val book: SearchBook,
val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF,
)
class ExploreShowViewModel( class ExploreShowViewModel(
private val repository: ExploreRepository, private val repository: ExploreRepository,
private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase, private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase,
@@ -61,72 +33,128 @@ class ExploreShowViewModel(
) : ViewModel() { ) : ViewModel() {
private val _rawBooks = MutableStateFlow<List<SearchBook>>(emptyList()) private val _rawBooks = MutableStateFlow<List<SearchBook>>(emptyList())
private val _bookshelf = MutableStateFlow<Set<BookShelfKey>>(emptySet())
private val _isLoading = MutableStateFlow(false) private val _isLoading = MutableStateFlow(false)
private val _isRefreshing = MutableStateFlow(false) private val _isEnd = MutableStateFlow(false)
private val _errorMsg = MutableStateFlow<String?>(null) private val _errorMsg = MutableStateFlow<String?>(null)
private val _kinds = MutableStateFlow<List<ExploreKind>>(emptyList())
private val _selectedKindTitle = MutableStateFlow<String?>(null)
private var bookSource: BookSource? = null private var bookSource: BookSource? = null
private var sourceUrl: String? = null private var sourceUrl: String? = null
private var exploreUrl: String? = null private var exploreUrl: String? = null
private var page = 1 private var page = 1
private val _isEndStateFlow = MutableStateFlow(false)
private val _bookshelf = MutableStateFlow<Set<BookShelfKey>>(emptySet())
private val _kinds = MutableStateFlow<List<ExploreKind>>(emptyList())
val kinds = _kinds.asStateFlow()
private val _selectedKindTitle = MutableStateFlow<String?>(null)
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()
val isRefreshing = _isRefreshing.asStateFlow() private val _uiState = MutableStateFlow(
ExploreShowUiState(
layoutState = AppConfig.exploreLayoutState,
gridCount = appCtx.exploreLayoutGrid,
)
)
val uiState = _uiState.asStateFlow()
fun saveGridCount(value: Int) { private val _effects = MutableSharedFlow<ExploreShowEffect>(extraBufferCapacity = 16)
appCtx.exploreLayoutGrid = value val effects = _effects.asSharedFlow()
_gridCount.value = value
}
val uiBooks = combine(
_rawBooks,
_bookshelf
) { books, bookshelf ->
books.map { item ->
ExploreBookItemUi(
book = item,
shelfState = resolveBookShelfStateUseCase.execute(
name = item.name,
author = item.author,
url = item.bookUrl,
shelf = bookshelf
)
)
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
val shouldTriggerAutoLoad = combine(
_isLoading,
uiBooks,
_rawBooks,
_isEndStateFlow
) { loading, uiBooks, rawBooks, isEnd ->
!loading && uiBooks.isEmpty() && rawBooks.isNotEmpty() && !isEnd
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
val isLoading = _isLoading.asStateFlow()
val errorMsg = _errorMsg.asStateFlow()
init { init {
viewModelScope.launch { observeBookshelf()
repository.getBookshelfItems().collect { list -> combineUiState()
val keys = list.map { BookShelfKey(it.name, it.author, it.bookUrl) }.toSet() }
_bookshelf.value = keys
fun onIntent(intent: ExploreShowIntent) {
when (intent) {
is ExploreShowIntent.InitData -> initData(intent.sourceUrl, intent.exploreUrl)
ExploreShowIntent.LoadMore -> loadMore()
ExploreShowIntent.Refresh -> loadMore(isRefresh = true)
is ExploreShowIntent.SwitchKind -> switchKind(intent.kind)
ExploreShowIntent.ToggleLayout -> toggleLayout()
is ExploreShowIntent.SaveGridCount -> saveGridCount(intent.count)
is ExploreShowIntent.ShowSheet -> _uiState.update { it.copy(sheet = intent.sheet) }
ExploreShowIntent.DismissSheet -> _uiState.update { it.copy(sheet = ExploreShowSheet.None) }
is ExploreShowIntent.OpenBook -> emitEffect(
ExploreShowEffect.OpenBookInfo(
name = intent.book.name,
author = intent.book.author,
bookUrl = intent.book.bookUrl,
origin = intent.book.origin,
coverPath = intent.book.coverUrl,
sharedCoverKey = intent.sharedCoverKey,
)
)
is ExploreShowIntent.AddToShelf -> viewModelScope.launch {
addToBookshelfUseCase.execute(intent.book)
} }
} }
} }
fun initData(incomingSourceUrl: String?, incomingExploreUrl: String?) { private fun observeBookshelf() {
// 允许 incomingExploreUrl 为空,此时加载书源默认发现页 viewModelScope.launch {
repository.getBookshelfItems().collect { list ->
_bookshelf.value = list.map {
BookShelfKey(it.name, it.author, it.bookUrl)
}.toSet()
}
}
}
private fun combineUiState() {
viewModelScope.launch {
combine(
_rawBooks,
_bookshelf,
_isLoading,
_isEnd,
_errorMsg,
_kinds,
_selectedKindTitle,
) { values ->
@Suppress("UNCHECKED_CAST")
val rawBooks = values[0] as List<SearchBook>
val bookshelf = values[1] as Set<BookShelfKey>
val isLoading = values[2] as Boolean
val isEnd = values[3] as Boolean
val errorMsg = values[4] as String?
val kinds = values[5] as List<ExploreKind>
val selectedKindTitle = values[6] as String?
val books = rawBooks.map { item ->
ExploreBookItemUi(
book = item,
shelfState = resolveBookShelfStateUseCase.execute(
name = item.name,
author = item.author,
url = item.bookUrl,
shelf = bookshelf,
)
)
}
ExploreShowUiState(
sourceUrl = sourceUrl,
books = books.toImmutableList(),
kinds = kinds.toImmutableList(),
selectedKindTitle = selectedKindTitle,
layoutState = _uiState.value.layoutState,
gridCount = _uiState.value.gridCount,
isLoading = isLoading,
isRefreshing = isLoading && page == 1,
isEnd = isEnd,
errorMsg = errorMsg,
sheet = _uiState.value.sheet,
)
}.collect { newState ->
val oldState = _uiState.value
_uiState.value = newState
if (newState.books.isEmpty() && _rawBooks.value.isNotEmpty() && !newState.isEnd && !newState.isLoading) {
loadMore()
}
}
}
}
private fun initData(incomingSourceUrl: String?, incomingExploreUrl: String?) {
if (sourceUrl == incomingSourceUrl && exploreUrl == incomingExploreUrl && bookSource != null) { if (sourceUrl == incomingSourceUrl && exploreUrl == incomingExploreUrl && bookSource != null) {
return return
} }
@@ -135,16 +163,16 @@ class ExploreShowViewModel(
page = 1 page = 1
bookSource = null bookSource = null
_rawBooks.value = emptyList() _rawBooks.value = emptyList()
_isEndStateFlow.value = false _isEnd.value = false
_errorMsg.value = null _errorMsg.value = null
_selectedKindTitle.value = null _selectedKindTitle.value = null
_kinds.value = emptyList()
viewModelScope.launch { viewModelScope.launch {
if (bookSource == null && incomingSourceUrl != null) { if (bookSource == null && incomingSourceUrl != null) {
bookSource = repository.getBookSource(incomingSourceUrl) bookSource = repository.getBookSource(incomingSourceUrl)
} }
// 如果仍然没有发现 URL,且书源已加载,尝试使用书源的默认发现页
if (exploreUrl == null && bookSource != null) { if (exploreUrl == null && bookSource != null) {
loadKinds(incomingSourceUrl!!) loadKinds(incomingSourceUrl!!)
} }
@@ -153,33 +181,36 @@ class ExploreShowViewModel(
} }
} }
fun loadKinds(sourceUrl: String) { private fun loadKinds(sourceUrl: String) {
viewModelScope.launch { viewModelScope.launch {
_kinds.value = repository.getSourceExploreKinds(sourceUrl) _kinds.value = repository.getSourceExploreKinds(sourceUrl)
} }
} }
fun refreshKinds() { private fun switchKind(kind: ExploreKind) {
sourceUrl?.let { loadKinds(it) }
}
fun switchExploreUrl(kind: ExploreKind) {
_selectedKindTitle.value = kind.title _selectedKindTitle.value = kind.title
exploreUrl = kind.url exploreUrl = kind.url
_isEndStateFlow.value = false _isEnd.value = false
loadMore(isRefresh = true) loadMore(isRefresh = true)
} }
fun setLayout() { private fun toggleLayout() {
val newState = if (_layoutState.value == 0) 1 else 0 _uiState.update {
_layoutState.value = newState val newState = if (it.layoutState == 0) 1 else 0
AppConfig.exploreLayoutState = newState AppConfig.exploreLayoutState = newState
it.copy(layoutState = newState)
}
} }
fun loadMore(isRefresh: Boolean = false) { private fun saveGridCount(count: Int) {
appCtx.exploreLayoutGrid = count
_uiState.update { it.copy(gridCount = count) }
}
private fun loadMore(isRefresh: Boolean = false) {
val source = bookSource val source = bookSource
val url = exploreUrl ?: source?.exploreUrl val url = exploreUrl ?: source?.exploreUrl
if (source == null || url == null || _isLoading.value || (_isEndStateFlow.value && !isRefresh)) return if (source == null || url == null || _isLoading.value || (_isEnd.value && !isRefresh)) return
viewModelScope.launch { viewModelScope.launch {
_isLoading.value = true _isLoading.value = true
@@ -187,7 +218,7 @@ class ExploreShowViewModel(
if (isRefresh) { if (isRefresh) {
page = 1 page = 1
_isEndStateFlow.value = false _isEnd.value = false
_rawBooks.value = emptyList() _rawBooks.value = emptyList()
} }
@@ -195,7 +226,7 @@ class ExploreShowViewModel(
exploreBooksUseCase.execute(source.bookSourceUrl, url, args = null, page) exploreBooksUseCase.execute(source.bookSourceUrl, url, args = null, page)
}.onSuccess { result -> }.onSuccess { result ->
if (result.books.isEmpty()) { if (result.books.isEmpty()) {
_isEndStateFlow.value = true _isEnd.value = true
} else { } else {
saveSearchBooksUseCase.save(result.books) saveSearchBooksUseCase.save(result.books)
@@ -207,34 +238,22 @@ class ExploreShowViewModel(
.distinctBy { it.bookUrl } .distinctBy { it.bookUrl }
if (uniqueNewBooks.isEmpty()) { if (uniqueNewBooks.isEmpty()) {
_isEndStateFlow.value = true _isEnd.value = true
} else { } else {
_rawBooks.value = currentList + uniqueNewBooks _rawBooks.value = currentList + uniqueNewBooks
page++ page++
_isEndStateFlow.value = false _isEnd.value = false
} }
} }
}.onFailure {
_errorMsg.value = it.stackTraceStr
} }
.onFailure {
_errorMsg.value = it.stackTraceStr
}
_isLoading.value = false _isLoading.value = false
} }
} }
fun getCurrentBookShelfState(item: SearchBook): BookShelfState { private fun emitEffect(effect: ExploreShowEffect) {
return resolveBookShelfStateUseCase.execute( _effects.tryEmit(effect)
name = item.name,
author = item.author,
url = item.bookUrl,
shelf = _bookshelf.value
)
}
fun onAddToShelf(book: SearchBook) {
viewModelScope.launch {
addToBookshelfUseCase.execute(book)
}
} }
} }
@@ -34,7 +34,7 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.series.MediumPlainButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.image.cover.CoilBookCover import io.legado.app.ui.widget.components.image.cover.CoilBookCover
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -272,11 +272,11 @@ fun GroupDeleteAction(
) { ) {
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
MediumIconButton( MediumPlainButton(
onClick = { onClick = {
showDeleteDialog = true showDeleteDialog = true
}, },
imageVector = Icons.Default.Delete icon = Icons.Default.Delete
) )
AppAlertDialog( AppAlertDialog(
@@ -304,7 +304,7 @@ fun GroupResetCoverAction(
onCoverPathChange: (String?) -> Unit, onCoverPathChange: (String?) -> Unit,
viewModel: GroupViewModel = koinViewModel() viewModel: GroupViewModel = koinViewModel()
) { ) {
MediumIconButton( MediumPlainButton(
onClick = { onClick = {
if (group != null) { if (group != null) {
viewModel.clearCover(group) { viewModel.clearCover(group) {
@@ -315,6 +315,6 @@ fun GroupResetCoverAction(
onCoverPathChange(null) onCoverPathChange(null)
} }
}, },
imageVector = Icons.Default.Restore icon = Icons.Default.Restore
) )
} }
@@ -61,8 +61,8 @@ import io.legado.app.ui.widget.components.AppPullToRefresh
import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.SelectionActions import io.legado.app.ui.widget.components.SelectionActions
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.button.SmallTonalIconButton import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.TextCard import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
@@ -427,9 +427,9 @@ private fun ImportPathNavigationBar(
} }
if (canGoBack) { if (canGoBack) {
SmallTonalIconButton( SmallTonalButton(
onClick = onNavigateBack, onClick = onNavigateBack,
imageVector = Icons.AutoMirrored.Filled.ArrowBack, icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "back" contentDescription = "back"
) )
} }
@@ -511,9 +511,9 @@ private fun ImportBookItem(
if (!item.isDir && !item.isOnBookShelf) { if (!item.isDir && !item.isOnBookShelf) {
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
SmallIconButton( SmallPlainButton(
onClick = onAddToBookshelf, onClick = onAddToBookshelf,
imageVector = if (isSelected) Icons.Default.Check else Icons.Default.AddCircleOutline icon = if (isSelected) Icons.Default.Check else Icons.Default.AddCircleOutline
) )
} }
} }
@@ -76,9 +76,9 @@ import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.SelectionActions import io.legado.app.ui.widget.components.SelectionActions
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.series.MediumPlainButton
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.button.SmallTonalIconButton import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.card.TextCard import io.legado.app.ui.widget.components.card.TextCard
@@ -167,9 +167,9 @@ fun RemoteBookScreen(
}, },
endAction = if (showSheet is RemoteBookSheet.Servers) { endAction = if (showSheet is RemoteBookSheet.Servers) {
{ {
MediumIconButton( MediumPlainButton(
onClick = { showSheet = RemoteBookSheet.ServerConfig(null) }, onClick = { showSheet = RemoteBookSheet.ServerConfig(null) },
imageVector = Icons.Default.Add icon = Icons.Default.Add
) )
} }
} else { } else {
@@ -430,16 +430,16 @@ private fun ServerItem(
{ {
Row { Row {
onEdit?.let { onEdit?.let {
SmallIconButton( SmallPlainButton(
onClick = it, onClick = it,
imageVector = Icons.Default.Edit, icon = Icons.Default.Edit,
contentDescription = "Edit" contentDescription = "Edit"
) )
} }
onDelete?.let { onDelete?.let {
SmallIconButton( SmallPlainButton(
onClick = it, onClick = it,
imageVector = Icons.Default.Delete, icon = Icons.Default.Delete,
contentDescription = "Delete" contentDescription = "Delete"
) )
} }
@@ -602,9 +602,9 @@ private fun PathNavigationBar(
} }
if (canGoBack) { if (canGoBack) {
SmallTonalIconButton( SmallTonalButton(
onClick = onNavigateBack, onClick = onNavigateBack,
imageVector = Icons.AutoMirrored.Filled.ArrowBack, icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "返回上级" contentDescription = "返回上级"
) )
} }
@@ -703,7 +703,7 @@ private fun RemoteBookItem(
if (!book.isDir) { if (!book.isDir) {
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
SmallIconButton( SmallPlainButton(
onClick = { onClick = {
if (book.isOnBookShelf) { if (book.isOnBookShelf) {
onUpdateClick(book) onUpdateClick(book)
@@ -711,7 +711,7 @@ private fun RemoteBookItem(
onAddClick(book) onAddClick(book)
} }
}, },
imageVector = if (book.isOnBookShelf) icon = if (book.isOnBookShelf)
Icons.Outlined.CloudSync Icons.Outlined.CloudSync
else else
Icons.Outlined.AddCircleOutline, Icons.Outlined.AddCircleOutline,
@@ -89,7 +89,7 @@ import io.legado.app.ui.widget.components.AppPullToRefresh
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallTonalIconButton import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.TextCard import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
@@ -1122,9 +1122,9 @@ private fun RelatedBooksBanner(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
SmallTonalIconButton( SmallTonalButton(
onClick = onMoreClick, onClick = onMoreClick,
imageVector = Icons.AutoMirrored.Filled.ArrowForward, icon = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = "more", contentDescription = "more",
) )
} }
@@ -72,8 +72,8 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.series.MediumPlainButton
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.checkBox.AppCheckbox import io.legado.app.ui.widget.components.checkBox.AppCheckbox
@@ -145,15 +145,15 @@ fun GroupSelectSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = stringResource(R.string.group_select), title = stringResource(R.string.group_select),
startAction = { startAction = {
MediumIconButton( MediumPlainButton(
onClick = { showAddGroup = true }, onClick = { showAddGroup = true },
imageVector = Icons.Default.Add icon = Icons.Default.Add
) )
}, },
endAction = { endAction = {
MediumIconButton( MediumPlainButton(
onClick = { onConfirm(selectedGroupId) }, onClick = { onConfirm(selectedGroupId) },
imageVector = Icons.Default.Check icon = Icons.Default.Check
) )
} }
) { ) {
@@ -187,9 +187,9 @@ fun GroupSelectSheet(
) )
}, },
trailingAction = { trailingAction = {
SmallIconButton( SmallPlainButton(
onClick = { editingGroup = group }, onClick = { editingGroup = group },
imageVector = Icons.Default.Edit icon = Icons.Default.Edit
) )
}, },
containerColor = LegadoTheme.colorScheme.surfaceContainerLow containerColor = LegadoTheme.colorScheme.surfaceContainerLow
@@ -236,9 +236,9 @@ fun ChangeCoverSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = stringResource(R.string.change_cover_source), title = stringResource(R.string.change_cover_source),
endAction = { endAction = {
MediumIconButton( MediumPlainButton(
onClick = { viewModel.startOrStopSearch() }, onClick = { viewModel.startOrStopSearch() },
imageVector = if (isSearching) Icons.Default.MoreVert else Icons.Default.Refresh icon = if (isSearching) Icons.Default.MoreVert else Icons.Default.Refresh
) )
} }
) { ) {
@@ -370,9 +370,9 @@ fun ChangeSourceSheet(
startAction = { startAction = {
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Box { Box {
MediumIconButton( MediumPlainButton(
onClick = { showOptionsMenu = true }, onClick = { showOptionsMenu = true },
imageVector = Icons.Default.MoreVert icon = Icons.Default.MoreVert
) )
RoundDropdownMenu( RoundDropdownMenu(
expanded = showOptionsMenu, expanded = showOptionsMenu,
@@ -419,21 +419,21 @@ fun ChangeSourceSheet(
) )
} }
} }
MediumIconButton( MediumPlainButton(
onClick = { showMigrationOptions = true }, onClick = { showMigrationOptions = true },
imageVector = Icons.Outlined.Settings icon = Icons.Outlined.Settings
) )
} }
}, },
endAction = { endAction = {
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
MediumIconButton( MediumPlainButton(
onClick = { viewModel.startOrStopSearch() }, onClick = { viewModel.startOrStopSearch() },
imageVector = if (isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh, icon = if (isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
) )
MediumIconButton( MediumPlainButton(
onClick = { showFilterSheet = true }, onClick = { showFilterSheet = true },
imageVector = Icons.Default.FilterList icon = Icons.Default.FilterList
) )
} }
} }
@@ -484,11 +484,11 @@ fun ChangeSourceSheet(
containerColor = LegadoTheme.colorScheme.onSheetContent, containerColor = LegadoTheme.colorScheme.onSheetContent,
selectedContainerColor = LegadoTheme.colorScheme.primaryContainer.copy(alpha = 0.32f), selectedContainerColor = LegadoTheme.colorScheme.primaryContainer.copy(alpha = 0.32f),
leadingContent = { leadingContent = {
MediumIconButton( MediumPlainButton(
onClick = { onClick = {
viewModel.onBookScoreClick(item) viewModel.onBookScoreClick(item)
}, },
imageVector = Icons.Default.PushPin, icon = Icons.Default.PushPin,
tint = if (bookScore > 0) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.outline, tint = if (bookScore > 0) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.outline,
contentDescription = null contentDescription = null
) )
@@ -59,7 +59,7 @@ import io.legado.app.ui.theme.fadingEdge
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton import io.legado.app.ui.widget.components.button.series.MediumOutlinedButton
import io.legado.app.ui.widget.components.image.cover.CoilBookCover import io.legado.app.ui.widget.components.image.cover.CoilBookCover
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu 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.menuItem.RoundDropdownMenuItem
@@ -156,7 +156,7 @@ fun BookInfoEditContent(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
MediumOutlinedIconButton( MediumOutlinedButton(
onClick = { onClick = {
(context as? BookInfoEditActivity)?.showDialogFragment( (context as? BookInfoEditActivity)?.showDialogFragment(
ChangeCoverDialog( ChangeCoverDialog(
@@ -165,15 +165,15 @@ fun BookInfoEditContent(
) )
) )
}, },
imageVector = Icons.Default.ImageSearch icon = Icons.Default.ImageSearch
) )
MediumOutlinedIconButton( MediumOutlinedButton(
onClick = { selectCover.launch() }, onClick = { selectCover.launch() },
imageVector = Icons.Default.FolderOpen icon = Icons.Default.FolderOpen
) )
MediumOutlinedIconButton( MediumOutlinedButton(
onClick = { viewModel.resetCover() }, onClick = { viewModel.resetCover() },
imageVector = Icons.Default.Replay icon = Icons.Default.Replay
) )
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
@@ -371,19 +371,19 @@ fun KindEditor(
} }
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
MediumOutlinedIconButton( MediumOutlinedButton(
onClick = { onClick = {
onReset() onReset()
}, },
imageVector = Icons.Default.Replay icon = Icons.Default.Replay
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
MediumOutlinedIconButton( MediumOutlinedButton(
onClick = { onClick = {
editingIndex = -1 editingIndex = -1
editText = "" editText = ""
}, },
imageVector = Icons.Default.Add icon = Icons.Default.Add
) )
} }
@@ -82,8 +82,7 @@ import io.legado.app.ui.widget.components.AppFloatingActionButtonMenu
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.FabMenuItem import io.legado.app.ui.widget.components.FabMenuItem
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallTonalIconButton import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.button.SmallTonalTextButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
@@ -768,32 +767,32 @@ private fun BookshelfManageScreen(
} }
) )
} }
SmallTonalIconButton( SmallTonalButton(
onClick = { onClick = {
if (!book.isLocal) { if (!book.isLocal) {
viewModel.dispatch(BookshelfManageScreenIntent.ToggleBookDownload(book)) viewModel.dispatch(BookshelfManageScreenIntent.ToggleBookDownload(book))
} }
}, },
imageVector = if (isDownloading) Icons.Default.Stop else Icons.Default.Download, icon = if (isDownloading) Icons.Default.Stop else Icons.Default.Download,
contentDescription = "download" contentDescription = "download"
) )
SmallTonalIconButton( SmallTonalButton(
onClick = { exportBook(book) }, onClick = { exportBook(book) },
imageVector = Icons.Default.Upload, icon = Icons.Default.Upload,
contentDescription = "upload" contentDescription = "upload"
) )
SmallTonalIconButton( SmallTonalButton(
onClick = { onClick = {
pendingMoveGroupBookUrl = book.bookUrl pendingMoveGroupBookUrl = book.bookUrl
groupPickerCurrentGroupId = book.group.coerceAtLeast(0L) groupPickerCurrentGroupId = book.group.coerceAtLeast(0L)
showGroupSelectSheet = true showGroupSelectSheet = true
}, },
imageVector = Icons.Default.Bookmarks, icon = Icons.Default.Bookmarks,
contentDescription = "group" contentDescription = "group"
) )
SmallTonalIconButton( SmallTonalButton(
onClick = { moreMenuBookUrl = book.bookUrl }, onClick = { moreMenuBookUrl = book.bookUrl },
imageVector = Icons.Default.MoreVert, icon = Icons.Default.MoreVert,
contentDescription = "more" contentDescription = "more"
) )
} }
@@ -1288,12 +1287,12 @@ private fun BookSourcePickerSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = title, title = title,
endAction = { endAction = {
SmallTonalTextButton( SmallTonalButton(
text = stringResource(android.R.string.ok),
imageVector = Icons.Default.PlayArrow,
onClick = { onClick = {
onConfirm(selectedSources.mapNotNull { it.getBookSource() }) onConfirm(selectedSources.mapNotNull { it.getBookSource() })
} },
icon = Icons.Default.PlayArrow,
text = stringResource(android.R.string.ok)
) )
} }
) { ) {
@@ -1402,17 +1401,17 @@ private fun BatchChangePreviewSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = "批量换源预览", title = "批量换源预览",
startAction = { startAction = {
SmallTonalTextButton( SmallTonalButton(
text = "新增全部",
imageVector = Icons.Default.Add,
onClick = onAddAllToShelf, onClick = onAddAllToShelf,
icon = Icons.Default.Add,
text = "新增全部"
) )
}, },
endAction = { endAction = {
SmallTonalTextButton( SmallTonalButton(
text = "迁移全部",
imageVector = Icons.Default.PlayArrow,
onClick = onMigrateAll, onClick = onMigrateAll,
icon = Icons.Default.PlayArrow,
text = "迁移全部"
) )
} }
) { items -> ) { items ->
@@ -1498,30 +1497,30 @@ private fun BatchChangePreviewRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
SmallTonalTextButton( SmallTonalButton(
imageVector = Icons.Default.Search,
onClick = { onManualSearch(item.oldBook) }, onClick = { onManualSearch(item.oldBook) },
icon = Icons.Default.Search
) )
SmallTonalTextButton( SmallTonalButton(
text = "不迁移",
imageVector = Icons.Default.Clear,
onClick = { onSkip(item.oldBook.bookUrl) }, onClick = { onSkip(item.oldBook.bookUrl) },
icon = Icons.Default.Clear,
text = "不迁移"
) )
SmallTonalTextButton( SmallTonalButton(
text = "迁移",
imageVector = Icons.Default.PlayArrow,
onClick = { onMigrate(item.oldBook.bookUrl) }, onClick = { onMigrate(item.oldBook.bookUrl) },
icon = Icons.Default.PlayArrow,
text = "迁移"
) )
SmallTonalTextButton( SmallTonalButton(
text = "新增",
imageVector = Icons.Default.Add,
onClick = { onAddToShelf(item.oldBook.bookUrl) }, onClick = { onAddToShelf(item.oldBook.bookUrl) },
icon = Icons.Default.Add,
text = "新增"
) )
if (item.candidates.size > 1) { if (item.candidates.size > 1) {
SmallTonalTextButton( SmallTonalButton(
text = "查看其他源信息",
imageVector = Icons.Default.Info,
onClick = { onShowOtherSources(item) }, onClick = { onShowOtherSources(item) },
icon = Icons.Default.Info,
text = "查看其他源信息"
) )
} }
} }
@@ -1605,9 +1604,9 @@ private fun OtherSourceOptionsSheet(
isSelected = index == currentItem.selectedCandidateIndex, isSelected = index == currentItem.selectedCandidateIndex,
onToggleSelection = { onSelect(currentItem.oldBook.bookUrl, index) }, onToggleSelection = { onSelect(currentItem.oldBook.bookUrl, index) },
trailingAction = { trailingAction = {
SmallTonalIconButton( SmallTonalButton(
onClick = { onOpenBook(candidate.book) }, onClick = { onOpenBook(candidate.book) },
imageVector = Icons.Default.Info, icon = Icons.Default.Info,
contentDescription = null, contentDescription = null,
) )
}, },
@@ -54,7 +54,7 @@ import io.legado.app.ui.book.readRecord.component.StatsGridCard
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.series.MediumPlainButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.heatmap.HeatmapMode import io.legado.app.ui.widget.components.heatmap.HeatmapMode
import io.legado.app.ui.widget.components.image.cover.CoilBookCover import io.legado.app.ui.widget.components.image.cover.CoilBookCover
@@ -187,9 +187,9 @@ fun DateNavigator(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
MediumIconButton( MediumPlainButton(
onClick = onPrevClick, onClick = onPrevClick,
imageVector = Icons.AutoMirrored.Filled.ArrowLeft icon = Icons.AutoMirrored.Filled.ArrowLeft
) )
AnimatedContent( AnimatedContent(
targetState = referenceDate, targetState = referenceDate,
@@ -224,9 +224,9 @@ fun DateNavigator(
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
) )
} }
MediumIconButton( MediumPlainButton(
onClick = onNextClick, onClick = onNextClick,
imageVector = Icons.AutoMirrored.Filled.ArrowRight icon = Icons.AutoMirrored.Filled.ArrowRight
) )
} }
} }
@@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -27,8 +25,9 @@ import io.legado.app.R
import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.BookSourcePart
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.SearchBar import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.series.MediumPlainButton
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.tabRow.AppTabRow import io.legado.app.ui.widget.components.tabRow.AppTabRow
@@ -68,9 +67,9 @@ fun ScopeSelectSheet(
title = title, title = title,
endAction = onConfirm?.let { endAction = onConfirm?.let {
{ {
MediumIconButton( MediumPlainButton(
onClick = it, onClick = it,
imageVector = Icons.Default.Check icon = AppIcons.Settings
) )
} }
} }
@@ -54,7 +54,7 @@ class SearchActivity : BaseComposeActivity() {
}, },
onOpenSourceManage = { onOpenSourceManage = {
startActivity<BookSourceActivity>() startActivity<BookSourceActivity>()
} },
) )
} }
@@ -5,7 +5,10 @@ import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.entities.SearchKeyword
import io.legado.app.domain.model.BookShelfState import io.legado.app.domain.model.BookShelfState
import io.legado.app.domain.model.MatchMode
import io.legado.app.ui.main.bookshelf.BookShelfItem import io.legado.app.ui.main.bookshelf.BookShelfItem
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Stable @Stable
data class SearchResultItemUi( data class SearchResultItemUi(
@@ -17,17 +20,17 @@ data class SearchResultItemUi(
data class SearchUiState( data class SearchUiState(
val query: String = "", val query: String = "",
val committedQuery: String = "", val committedQuery: String = "",
val results: List<SearchResultItemUi> = emptyList(), val results: ImmutableList<SearchResultItemUi> = persistentListOf(),
val history: List<SearchKeyword> = emptyList(), val history: ImmutableList<SearchKeyword> = persistentListOf(),
val bookshelfHints: List<BookShelfItem> = emptyList(), val bookshelfHints: ImmutableList<BookShelfItem> = persistentListOf(),
val enabledGroups: List<String> = emptyList(), val enabledGroups: ImmutableList<String> = persistentListOf(),
val enabledSources: List<BookSourcePart> = emptyList(), val enabledSources: ImmutableList<BookSourcePart> = persistentListOf(),
val scopeDisplay: String = "", val scopeDisplay: String = "",
val scopeDisplayNames: List<String> = emptyList(), val scopeDisplayNames: ImmutableList<String> = persistentListOf(),
val selectedScopeSourceUrls: Set<String> = emptySet(), val selectedScopeSourceUrls: Set<String> = emptySet(),
val isAllScope: Boolean = true, val isAllScope: Boolean = true,
val isSourceScope: Boolean = false, val isSourceScope: Boolean = false,
val isPrecisionSearch: Boolean = false, val matchMode: MatchMode = MatchMode.DEFAULT,
val isSearching: Boolean = false, val isSearching: Boolean = false,
val isManualStop: Boolean = false, val isManualStop: Boolean = false,
val hasMore: Boolean = true, val hasMore: Boolean = true,
@@ -35,17 +38,24 @@ data class SearchUiState(
val totalSources: Int = 0, val totalSources: Int = 0,
val selectedSourceTypes: Set<Int> = emptySet(), val selectedSourceTypes: Set<Int> = emptySet(),
val showScopeSheet: Boolean = false, val showScopeSheet: Boolean = false,
val showTypeSheet: Boolean = false, val showSettingsSheet: Boolean = false,
val showClearHistoryDialog: Boolean = false, val showClearHistoryDialog: Boolean = false,
val showSuggestions: Boolean = true, val showSuggestions: Boolean = true,
val emptyScopeAction: SearchEmptyScopeAction? = null, val emptyScopeAction: SearchEmptyScopeAction? = null,
val savedScrollIndex: Int = 0, val savedScrollIndex: Int = 0,
val savedScrollOffset: Int = 0, val savedScrollOffset: Int = 0,
val expandedSourceUrl: String? = null,
val expandedSourceName: String? = null,
val expandedSourceBooks: ImmutableList<SearchBook> = persistentListOf(),
val expandedSourceLoading: Boolean = false,
val expandedSourceEnd: Boolean = false,
val expandedSourceError: String? = null,
val expandedSourcePage: Int = 1,
) )
data class SearchEmptyScopeAction( data class SearchEmptyScopeAction(
val scopeDisplay: String, val scopeDisplay: String,
val wasPrecisionSearch: Boolean, val wasMatchMode: MatchMode,
) )
sealed interface SearchIntent { sealed interface SearchIntent {
@@ -60,17 +70,22 @@ sealed interface SearchIntent {
data class UseHistoryKeyword(val keyword: String) : SearchIntent data class UseHistoryKeyword(val keyword: String) : SearchIntent
data class OpenSearchBook(val book: SearchBook, val sharedCoverKey: String?) : SearchIntent data class OpenSearchBook(val book: SearchBook, val sharedCoverKey: String?) : SearchIntent
data class OpenBookshelfBook(val book: BookShelfItem) : SearchIntent data class OpenBookshelfBook(val book: BookShelfItem) : SearchIntent
data class ExpandSource(val sourceUrl: String, val sourceName: String) : SearchIntent
data object DismissExpandedSource : SearchIntent
data object LoadMoreExpandedSource : SearchIntent
data class OpenExpandedSourceBook(val book: SearchBook, val sharedCoverKey: String?) :
SearchIntent
data class DeleteHistory(val item: SearchKeyword) : SearchIntent data class DeleteHistory(val item: SearchKeyword) : SearchIntent
data class SetClearHistoryDialogVisible(val visible: Boolean) : SearchIntent data class SetClearHistoryDialogVisible(val visible: Boolean) : SearchIntent
data object ConfirmClearHistory : SearchIntent data object ConfirmClearHistory : SearchIntent
data class SetScopeSheetVisible(val visible: Boolean) : SearchIntent data class SetScopeSheetVisible(val visible: Boolean) : SearchIntent
data class SetTypeSheetVisible(val visible: Boolean) : SearchIntent data class SetSettingsSheetVisible(val visible: Boolean) : SearchIntent
data class ToggleSourceType(val type: Int) : SearchIntent data class ToggleSourceType(val type: Int) : SearchIntent
data object SelectAllScope : SearchIntent data object SelectAllScope : SearchIntent
data class ToggleScopeGroup(val groupName: String) : SearchIntent data class ToggleScopeGroup(val groupName: String) : SearchIntent
data class ToggleScopeSource(val source: BookSourcePart) : SearchIntent data class ToggleScopeSource(val source: BookSourcePart) : SearchIntent
data class RemoveScopeItem(val scopeName: String) : SearchIntent data class RemoveScopeItem(val scopeName: String) : SearchIntent
data class TogglePrecision(val enabled: Boolean) : SearchIntent data class SetMatchMode(val mode: MatchMode) : SearchIntent
data object ConfirmEmptyScopeAction : SearchIntent data object ConfirmEmptyScopeAction : SearchIntent
data object DismissEmptyScopeAction : SearchIntent data object DismissEmptyScopeAction : SearchIntent
data object OpenSourceManage : SearchIntent data object OpenSourceManage : SearchIntent
@@ -20,8 +20,10 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
import androidx.compose.material.icons.filled.Book import androidx.compose.material.icons.filled.Book
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.Layers import androidx.compose.material.icons.filled.Layers
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Stop import androidx.compose.material.icons.filled.Stop
@@ -54,6 +56,7 @@ import io.legado.app.R
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.entities.SearchKeyword
import io.legado.app.domain.model.BookShelfState import io.legado.app.domain.model.BookShelfState
import io.legado.app.domain.model.MatchMode
import io.legado.app.ui.main.bookCoverSharedElementKey import io.legado.app.ui.main.bookCoverSharedElementKey
import io.legado.app.ui.main.bookshelf.BookShelfItem import io.legado.app.ui.main.bookshelf.BookShelfItem
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
@@ -63,12 +66,12 @@ import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppFloatingActionButton import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.LoadMoreFooter
import io.legado.app.ui.widget.components.SearchBar import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.book.SearchBookListItem import io.legado.app.ui.widget.components.book.SearchBookListItem
import io.legado.app.ui.widget.components.book.SearchBookPreviewSheet import io.legado.app.ui.widget.components.book.SearchBookPreviewSheet
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.button.SmallTextButton
import io.legado.app.ui.widget.components.card.NormalCard import io.legado.app.ui.widget.components.card.NormalCard
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
@@ -76,11 +79,11 @@ import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.topbar.M3GlassScrollBehavior import io.legado.app.ui.widget.components.topbar.M3GlassScrollBehavior
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
import io.legado.app.ui.widget.components.topbar.TopBarAnimatedActionButton import io.legado.app.ui.widget.components.topbar.TopBarAnimatedActionButton
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
@@ -100,6 +103,8 @@ fun SearchScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val searchLayoutMode by viewModel.searchLayoutMode.collectAsStateWithLifecycle()
val isSourceGroupedMode = searchLayoutMode == 1
var previewBook by remember { mutableStateOf<SearchBook?>(null) } var previewBook by remember { mutableStateOf<SearchBook?>(null) }
var previewSharedCoverKey by remember { mutableStateOf<String?>(null) } var previewSharedCoverKey by remember { mutableStateOf<String?>(null) }
val listState = rememberLazyListState() val listState = rememberLazyListState()
@@ -149,13 +154,15 @@ fun SearchScreen(
state.hasMore, state.hasMore,
state.isManualStop, state.isManualStop,
state.showSuggestions, state.showSuggestions,
isSourceGroupedMode,
) { ) {
if ( if (
shouldLoadMore && shouldLoadMore &&
!state.isSearching && !state.isSearching &&
state.hasMore && state.hasMore &&
!state.isManualStop && !state.isManualStop &&
!state.showSuggestions !state.showSuggestions &&
!isSourceGroupedMode
) { ) {
viewModel.onIntent(SearchIntent.LoadMore) viewModel.onIntent(SearchIntent.LoadMore)
} }
@@ -261,32 +268,15 @@ fun SearchScreen(
) )
}, },
actions = { actions = {
TopBarActionButton(
onClick = {
viewModel.onIntent(SearchIntent.OpenSourceManage)
},
imageVector = AppIcons.Settings,
contentDescription = stringResource(R.string.book_source_manage)
)
TopBarAnimatedActionButton( TopBarAnimatedActionButton(
checked = state.isPrecisionSearch, checked = isSourceGroupedMode || state.matchMode == MatchMode.EXACT || state.selectedSourceTypes.isNotEmpty(),
onCheckedChange = { checked ->
viewModel.onIntent(SearchIntent.TogglePrecision(checked))
},
iconChecked = AppIcons.PrecisionSearch,
iconUnchecked = AppIcons.UnPrecisionSearch,
activeText = stringResource(R.string.precision_search),
inactiveText = stringResource(R.string.search),
)
TopBarAnimatedActionButton(
checked = state.selectedSourceTypes.isNotEmpty(),
onCheckedChange = { onCheckedChange = {
viewModel.onIntent(SearchIntent.SetTypeSheetVisible(true)) viewModel.onIntent(SearchIntent.SetSettingsSheetVisible(true))
}, },
iconChecked = Icons.Default.Layers, iconChecked = AppIcons.Settings,
iconUnchecked = Icons.Default.Layers, iconUnchecked = AppIcons.Settings,
activeText = "搜素类型", activeText = stringResource(R.string.setting),
inactiveText = "搜素类型", inactiveText = stringResource(R.string.setting),
) )
TopBarAnimatedActionButton( TopBarAnimatedActionButton(
checked = !state.isAllScope, checked = !state.isAllScope,
@@ -296,7 +286,7 @@ fun SearchScreen(
iconChecked = AppIcons.Filter, iconChecked = AppIcons.Filter,
iconUnchecked = AppIcons.Filter, iconUnchecked = AppIcons.Filter,
activeText = stringResource(R.string.screen), activeText = stringResource(R.string.screen),
inactiveText = stringResource(R.string.screen), inactiveText = stringResource(R.string.screen)
) )
}, },
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
@@ -315,12 +305,13 @@ fun SearchScreen(
placeholder = searchLabel, placeholder = searchLabel,
trailingIcon = { trailingIcon = {
if (queryInput.isNotEmpty()) { if (queryInput.isNotEmpty()) {
TopBarActionButton( SmallPlainButton(
modifier = Modifier.padding(horizontal = 8.dp),
onClick = { onClick = {
queryInput = "" queryInput = ""
viewModel.onIntent(SearchIntent.UpdateQuery("")) viewModel.onIntent(SearchIntent.UpdateQuery(""))
}, },
imageVector = AppIcons.Close, icon = AppIcons.Close,
contentDescription = stringResource(R.string.clear) contentDescription = stringResource(R.string.clear)
) )
} }
@@ -394,52 +385,124 @@ fun SearchScreen(
} }
if (state.results.isNotEmpty()) { if (state.results.isNotEmpty()) {
LazyColumn( val sourceGroupedResults = remember(state.results) {
modifier = Modifier.fillMaxSize(), state.results
state = listState, .groupBy { it.book.origin }
contentPadding = adaptiveContentPaddingOnlyVertical( .map { (origin, books) ->
top = 48.dp, SourceGroup(
bottom = 8.dp origin = origin,
), sourceName = books.firstOrNull()?.book?.originName?.takeIf { it.isNotBlank() }
verticalArrangement = Arrangement.spacedBy(6.dp) ?: origin,
) { items = books
itemsIndexed( )
items = state.results, }
key = { index, item -> "${item.book.origin}:${item.book.bookUrl}:$index" } }
) { index, item ->
val sharedCoverKey = bookCoverSharedElementKey(
item.book.bookUrl,
"search:${item.book.origin}:$index"
)
SearchBookListItem(
book = item.book,
shelfState = item.shelfState,
onClick = {
viewModel.onIntent(
SearchIntent.OpenSearchBook(
item.book,
sharedCoverKey
)
)
},
onLongClick = { book, coverKey ->
previewBook = book
previewSharedCoverKey = coverKey
},
sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope,
sharedCoverKey = sharedCoverKey
)
}
item { AnimatedContent(
SearchResultFooter( targetState = isSourceGroupedMode,
isSearching = state.isSearching, label = "SearchLayoutTransition",
hasMore = state.hasMore, modifier = Modifier.fillMaxSize(),
hasResult = true, ) { isSourceGrouped ->
committedQuery = state.committedQuery, if (isSourceGrouped) {
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMore) }, LazyColumn(
) modifier = Modifier.fillMaxSize(),
contentPadding = adaptiveContentPaddingOnlyVertical(
top = 48.dp,
bottom = 8.dp
),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
sourceGroupedResults.forEachIndexed { groupIndex, group ->
item(key = "header_${group.origin}") {
SearchSourceSection(
sourceName = group.sourceName,
items = group.items,
onClickBook = { book, coverKey ->
viewModel.onIntent(
SearchIntent.OpenSearchBook(
book,
coverKey
)
)
},
onLongClickBook = { book, coverKey ->
previewBook = book
previewSharedCoverKey = coverKey
},
onViewAll = {
viewModel.onIntent(
SearchIntent.ExpandSource(
group.origin,
group.sourceName
)
)
},
sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope,
sourceSectionIndex = groupIndex,
)
}
}
item {
SearchResultFooter(
isSearching = state.isSearching,
hasMore = state.hasMore,
hasResult = true,
committedQuery = state.committedQuery,
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMore) },
)
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = adaptiveContentPaddingOnlyVertical(
top = 48.dp,
bottom = 8.dp
),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(
items = state.results,
key = { index, item -> "${item.book.origin}:${item.book.bookUrl}:$index" }
) { index, item ->
val sharedCoverKey = bookCoverSharedElementKey(
item.book.bookUrl,
"search:${item.book.origin}:$index"
)
SearchBookListItem(
book = item.book,
shelfState = item.shelfState,
onClick = {
viewModel.onIntent(
SearchIntent.OpenSearchBook(
item.book,
sharedCoverKey
)
)
},
onLongClick = { book, coverKey ->
previewBook = book
previewSharedCoverKey = coverKey
},
sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope,
sharedCoverKey = sharedCoverKey
)
}
item {
SearchResultFooter(
isSearching = state.isSearching,
hasMore = state.hasMore,
hasResult = true,
committedQuery = state.committedQuery,
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMore) },
)
}
}
} }
} }
} }
@@ -502,7 +565,7 @@ fun SearchScreen(
}, },
title = stringResource(R.string.draw), title = stringResource(R.string.draw),
textProvider = { textProvider = {
if (wasPrecisionSearch) { if (wasMatchMode == MatchMode.EXACT) {
stringResource(R.string.search_empty_scope_disable_precision, scopeDisplay) stringResource(R.string.search_empty_scope_disable_precision, scopeDisplay)
} else { } else {
stringResource(R.string.search_empty_scope_switch_all, scopeDisplay) stringResource(R.string.search_empty_scope_switch_all, scopeDisplay)
@@ -525,46 +588,101 @@ fun SearchScreen(
sources = state.enabledSources, sources = state.enabledSources,
selectedSources = state.selectedScopeSourceUrls, selectedSources = state.selectedScopeSourceUrls,
onToggleSource = { viewModel.onIntent(SearchIntent.ToggleScopeSource(it)) }, onToggleSource = { viewModel.onIntent(SearchIntent.ToggleScopeSource(it)) },
isSourceScope = state.isSourceScope isSourceScope = state.isSourceScope,
onConfirm = { viewModel.onIntent(SearchIntent.OpenSourceManage) },
) )
AppModalBottomSheet( AppModalBottomSheet(
show = state.showTypeSheet, show = state.showSettingsSheet,
onDismissRequest = { viewModel.onIntent(SearchIntent.SetTypeSheetVisible(false)) }, onDismissRequest = { viewModel.onIntent(SearchIntent.SetSettingsSheetVisible(false)) },
title = "搜素类型", title = stringResource(R.string.setting),
) { ) {
Column { Column(
SelectionItemCard( verticalArrangement = Arrangement.spacedBy(8.dp)
title = stringResource(R.string.all), ) {
isSelected = state.selectedSourceTypes.isEmpty(), CompactDropdownSettingItem(
containerColor = LegadoTheme.colorScheme.onSheetContent, title = stringResource(R.string.layout_mode),
inSelectionMode = true, selectedValue = searchLayoutMode.toString(),
onToggleSelection = { displayEntries = arrayOf(
if (state.selectedSourceTypes.isNotEmpty()) { stringResource(R.string.search_layout_source_grouped),
state.selectedSourceTypes.forEach { stringResource(R.string.search_layout_list)
viewModel.onIntent(SearchIntent.ToggleSourceType(it)) ),
} entryValues = arrayOf("1", "0"),
imageVector = if (isSourceGroupedMode) Icons.Default.GridView else Icons.AutoMirrored.Outlined.FormatListBulleted,
onValueChange = { newValue ->
if (newValue.toInt() != searchLayoutMode) {
viewModel.toggleSearchLayout()
} }
} }
) )
Spacer(modifier = Modifier.height(8.dp)) CompactDropdownSettingItem(
title = stringResource(R.string.precision_search),
selectedValue = state.matchMode.value.toString(),
displayEntries = arrayOf(
stringResource(R.string.precision_search),
stringResource(R.string.search)
),
entryValues = arrayOf(
MatchMode.EXACT.value.toString(),
MatchMode.DEFAULT.value.toString()
),
imageVector = if (state.matchMode == MatchMode.EXACT) AppIcons.PrecisionSearch else AppIcons.UnPrecisionSearch,
onValueChange = { newValue ->
val mode = MatchMode.of(newValue.toInt())
if (mode != state.matchMode) {
viewModel.onIntent(SearchIntent.SetMatchMode(mode))
}
}
)
listOf( Row(
0 to stringResource(R.string.noval), modifier = Modifier
2 to stringResource(R.string.manga), .fillMaxWidth()
1 to stringResource(R.string.audio), .padding(vertical = 12.dp),
).forEach { (type, label) -> verticalAlignment = Alignment.CenterVertically
) {
AppIcon(Icons.Default.Layers, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
AppText(
text = "搜索类型",
style = LegadoTheme.typography.titleSmall
)
}
Column(
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
SelectionItemCard( SelectionItemCard(
title = label, title = stringResource(R.string.all),
isSelected = state.selectedSourceTypes.contains(type), isSelected = state.selectedSourceTypes.isEmpty(),
containerColor = LegadoTheme.colorScheme.onSheetContent, containerColor = LegadoTheme.colorScheme.onSheetContent,
inSelectionMode = true, inSelectionMode = true,
onToggleSelection = { onToggleSelection = {
viewModel.onIntent(SearchIntent.ToggleSourceType(type)) if (state.selectedSourceTypes.isNotEmpty()) {
state.selectedSourceTypes.forEach {
viewModel.onIntent(SearchIntent.ToggleSourceType(it))
}
}
} }
) )
Spacer(modifier = Modifier.height(4.dp))
listOf(
0 to stringResource(R.string.noval),
2 to stringResource(R.string.manga),
1 to stringResource(R.string.audio),
).forEach { (type, label) ->
SelectionItemCard(
title = label,
isSelected = state.selectedSourceTypes.contains(type),
containerColor = LegadoTheme.colorScheme.onSheetContent,
inSelectionMode = true,
onToggleSelection = {
viewModel.onIntent(SearchIntent.ToggleSourceType(type))
}
)
}
} }
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
@@ -588,6 +706,24 @@ fun SearchScreen(
viewModel.onAddToShelf(book) viewModel.onAddToShelf(book)
}, },
) )
ExpandedSourceSheet(
show = state.expandedSourceUrl != null,
sourceName = state.expandedSourceName ?: "",
books = state.expandedSourceBooks,
isLoading = state.expandedSourceLoading,
isEnd = state.expandedSourceEnd,
errorMsg = state.expandedSourceError,
onDismiss = { viewModel.onIntent(SearchIntent.DismissExpandedSource) },
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMoreExpandedSource) },
onBookClick = { book, coverKey ->
viewModel.onIntent(SearchIntent.OpenExpandedSourceBook(book, coverKey))
},
onBookLongClick = { book, coverKey ->
previewBook = book
previewSharedCoverKey = coverKey
},
)
} }
private data class SearchFloatingSummary( private data class SearchFloatingSummary(
@@ -653,10 +789,10 @@ private fun SearchSuggestionPanel(
} }
if (state.history.isNotEmpty()) { if (state.history.isNotEmpty()) {
SmallTextButton( SmallPlainButton(
onClick = onClearHistory, onClick = onClearHistory,
text = stringResource(R.string.clear_all), text = stringResource(R.string.clear_all),
imageVector = Icons.Default.Close icon = Icons.Default.Close
) )
} }
} }
@@ -685,9 +821,9 @@ private fun SearchSuggestionPanel(
title = history.word, title = history.word,
onToggleSelection = { onUseHistory(history.word) }, onToggleSelection = { onUseHistory(history.word) },
trailingAction = { trailingAction = {
SmallIconButton( SmallPlainButton(
onClick = { onDeleteHistory(history) }, onClick = { onDeleteHistory(history) },
imageVector = Icons.Default.Close, icon = Icons.Default.Close,
contentDescription = stringResource(R.string.delete) contentDescription = stringResource(R.string.delete)
) )
} }
@@ -747,3 +883,76 @@ private fun SearchResultFooter(
} }
} }
} }
private data class SourceGroup(
val origin: String,
val sourceName: String,
val items: List<SearchResultItemUi>,
)
@Composable
private fun ExpandedSourceSheet(
show: Boolean,
sourceName: String,
books: List<SearchBook>,
isLoading: Boolean,
isEnd: Boolean,
errorMsg: String?,
onDismiss: () -> Unit,
onLoadMore: () -> Unit,
onBookClick: (SearchBook, String?) -> Unit,
onBookLongClick: ((SearchBook, String?) -> Unit)? = null,
) {
AppModalBottomSheet(
show = show,
onDismissRequest = onDismiss,
title = sourceName,
) {
val listState = rememberLazyListState()
val shouldLoadMore by remember {
derivedStateOf {
val total = listState.layoutInfo.totalItemsCount
val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
total > 0 && last >= total - 3
}
}
LaunchedEffect(shouldLoadMore, isLoading, isEnd) {
if (shouldLoadMore && !isLoading && !isEnd) {
onLoadMore()
}
}
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = adaptiveContentPaddingOnlyVertical(
top = 8.dp,
bottom = 16.dp
),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(
items = books,
key = { it.bookUrl },
) { book ->
SearchBookListItem(
book = book,
shelfState = BookShelfState.NOT_IN_SHELF,
onClick = { onBookClick(book, null) },
onLongClick = onBookLongClick,
)
}
item {
LoadMoreFooter(
isLoading = isLoading,
errorMsg = errorMsg,
isEnd = isEnd,
onRetry = onLoadMore,
)
}
}
}
}
@@ -0,0 +1,111 @@
package io.legado.app.ui.book.search
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.legado.app.data.entities.SearchBook
import io.legado.app.ui.main.bookCoverSharedElementKey
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizonalPadding
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.theme.fadingEdge
import io.legado.app.ui.widget.components.book.SearchBookGridItem
import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.text.AppText
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SearchSourceSection(
sourceName: String,
items: List<SearchResultItemUi>,
onClickBook: (SearchBook, String?) -> Unit,
onLongClickBook: ((SearchBook, String?) -> Unit)? = null,
onViewAll: (() -> Unit)? = null,
modifier: Modifier = Modifier,
sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null,
sourceSectionIndex: Int = 0,
) {
if (items.isEmpty()) return
Column(modifier = modifier) {
Row(
modifier = Modifier
.fillMaxWidth()
.adaptiveHorizontalPadding()
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
AppText(
text = sourceName,
style = LegadoTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
TextCard(
text = "${items.size}",
modifier = Modifier
.padding(start = 12.dp)
)
Spacer(modifier = Modifier.weight(1f))
if (onViewAll != null) {
SmallTonalButton(
onClick = onViewAll,
icon = Icons.AutoMirrored.Filled.ArrowForward
)
}
}
val lazyListState = rememberLazyListState()
LazyRow(
state = lazyListState,
modifier = Modifier
.fillMaxWidth()
.fadingEdge(lazyListState, gradientWidth = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = adaptiveHorizonalPadding(),
) {
itemsIndexed(
items = items,
key = { index, item -> "${item.book.origin}:${item.book.bookUrl}:$index" }
) { index, item ->
val sharedCoverKey = bookCoverSharedElementKey(
item.book.bookUrl,
"search_source:$sourceSectionIndex:$index"
)
SearchBookGridItem(
book = item.book,
shelfState = item.shelfState,
onClick = { onClickBook(item.book, sharedCoverKey) },
onLongClick = onLongClickBook,
modifier = Modifier.width(100.dp),
sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope,
sharedCoverKey = sharedCoverKey,
)
}
}
}
}
@@ -2,50 +2,73 @@ package io.legado.app.ui.book.search
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.local.preferences.LocalPreferencesKeys
import io.legado.app.data.local.preferences.LocalPreferencesRepository
import io.legado.app.data.repository.SearchRepository import io.legado.app.data.repository.SearchRepository
import io.legado.app.domain.model.BookSearchScope import io.legado.app.domain.model.BookSearchScope
import io.legado.app.domain.model.MatchMode
import io.legado.app.domain.usecase.AddToBookshelfUseCase import io.legado.app.domain.usecase.AddToBookshelfUseCase
import io.legado.app.domain.usecase.BookSearchControl import io.legado.app.domain.usecase.BookSearchControl
import io.legado.app.domain.usecase.BookSearchRequest import io.legado.app.domain.usecase.BookSearchRequest
import io.legado.app.domain.usecase.BookShelfKey import io.legado.app.domain.usecase.BookShelfKey
import io.legado.app.domain.usecase.ExploreBooksUseCase
import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
import io.legado.app.domain.usecase.SearchBooksUseCase import io.legado.app.domain.usecase.SearchBooksUseCase
import io.legado.app.domain.usecase.SearchRunEvent import io.legado.app.domain.usecase.SearchRunEvent
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.ui.config.otherConfig.OtherConfig import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.utils.getPrefBoolean import kotlinx.collections.immutable.persistentListOf
import io.legado.app.utils.putPrefBoolean import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import splitties.init.appCtx
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class SearchViewModel( class SearchViewModel(
private val repository: SearchRepository, private val repository: SearchRepository,
private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase, private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase,
private val searchBooksUseCase: SearchBooksUseCase, private val searchBooksUseCase: SearchBooksUseCase,
private val exploreBooksUseCase: ExploreBooksUseCase,
private val addToBookshelfUseCase: AddToBookshelfUseCase, private val addToBookshelfUseCase: AddToBookshelfUseCase,
private val localPreferencesRepository: LocalPreferencesRepository,
) : ViewModel() { ) : ViewModel() {
val searchLayoutMode = localPreferencesRepository
.getPreference(LocalPreferencesKeys.SEARCH_LAYOUT_MODE, 0)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
fun toggleSearchLayout() {
viewModelScope.launch {
val newMode = if (searchLayoutMode.value == 0) 1 else 0
localPreferencesRepository.updatePreference(
LocalPreferencesKeys.SEARCH_LAYOUT_MODE, newMode
)
}
}
private val matchModeFlow = localPreferencesRepository
.getPreference(LocalPreferencesKeys.MATCH_MODE, MatchMode.DEFAULT.value)
.distinctUntilChanged()
.map { MatchMode.of(it) }
private val _uiState = MutableStateFlow( private val _uiState = MutableStateFlow(
SearchUiState( SearchUiState(
isPrecisionSearch = appCtx.getPrefBoolean(PreferKey.precisionSearch),
scopeDisplay = SearchScope(AppConfig.searchScope).display, scopeDisplay = SearchScope(AppConfig.searchScope).display,
scopeDisplayNames = SearchScope(AppConfig.searchScope).displayNames, scopeDisplayNames = SearchScope(AppConfig.searchScope).displayNames.toImmutableList(),
isAllScope = SearchScope(AppConfig.searchScope).isAll(), isAllScope = SearchScope(AppConfig.searchScope).isAll(),
isSourceScope = SearchScope(AppConfig.searchScope).isSource(), isSourceScope = SearchScope(AppConfig.searchScope).isSource(),
) )
@@ -72,6 +95,7 @@ class SearchViewModel(
observeBookshelf() observeBookshelf()
observeQueryHistory() observeQueryHistory()
observeQueryBookshelfHints() observeQueryBookshelfHints()
observeMatchMode()
} }
fun onAddToShelf(book: SearchBook) { fun onAddToShelf(book: SearchBook) {
@@ -153,8 +177,8 @@ class SearchViewModel(
_uiState.update { it.copy(showScopeSheet = intent.visible) } _uiState.update { it.copy(showScopeSheet = intent.visible) }
} }
is SearchIntent.SetTypeSheetVisible -> { is SearchIntent.SetSettingsSheetVisible -> {
_uiState.update { it.copy(showTypeSheet = intent.visible) } _uiState.update { it.copy(showSettingsSheet = intent.visible) }
} }
is SearchIntent.ToggleSourceType -> { is SearchIntent.ToggleSourceType -> {
@@ -184,9 +208,12 @@ class SearchViewModel(
syncScopeState(restartSearch = true, oldScope = oldScope) syncScopeState(restartSearch = true, oldScope = oldScope)
} }
is SearchIntent.TogglePrecision -> { is SearchIntent.SetMatchMode -> {
appCtx.putPrefBoolean(PreferKey.precisionSearch, intent.enabled) viewModelScope.launch {
_uiState.update { it.copy(isPrecisionSearch = intent.enabled) } localPreferencesRepository.updatePreference(
LocalPreferencesKeys.MATCH_MODE, intent.mode.value
)
}
restartCommittedSearchIfNeeded() restartCommittedSearchIfNeeded()
} }
@@ -197,6 +224,61 @@ class SearchViewModel(
SearchIntent.OpenSourceManage -> emitEffect(SearchEffect.OpenSourceManage) SearchIntent.OpenSourceManage -> emitEffect(SearchEffect.OpenSourceManage)
is SearchIntent.ExpandSource -> {
_uiState.update {
it.copy(
expandedSourceUrl = intent.sourceUrl,
expandedSourceName = intent.sourceName,
expandedSourceBooks = persistentListOf(),
expandedSourceLoading = true,
expandedSourceEnd = false,
expandedSourceError = null,
expandedSourcePage = 1,
)
}
loadExpandedSourcePage(intent.sourceUrl, page = 1)
}
SearchIntent.DismissExpandedSource -> {
_uiState.update {
it.copy(
expandedSourceUrl = null,
expandedSourceName = null,
expandedSourceBooks = persistentListOf(),
expandedSourceLoading = false,
expandedSourceEnd = false,
expandedSourceError = null,
expandedSourcePage = 1,
)
}
}
SearchIntent.LoadMoreExpandedSource -> {
val state = _uiState.value
val sourceUrl = state.expandedSourceUrl ?: return
if (state.expandedSourceLoading || state.expandedSourceEnd) return
_uiState.update {
it.copy(
expandedSourceLoading = true,
expandedSourceError = null
)
}
loadExpandedSourcePage(sourceUrl, page = state.expandedSourcePage)
}
is SearchIntent.OpenExpandedSourceBook -> {
emitEffect(
SearchEffect.OpenBookInfo(
name = intent.book.name,
author = intent.book.author,
bookUrl = intent.book.bookUrl,
origin = intent.book.origin,
coverPath = intent.book.coverUrl,
sharedCoverKey = intent.sharedCoverKey,
)
)
}
is SearchIntent.SaveScrollState -> { is SearchIntent.SaveScrollState -> {
_uiState.update { _uiState.update {
it.copy( it.copy(
@@ -233,7 +315,7 @@ class SearchViewModel(
repository.enabledGroups repository.enabledGroups
.catch { emit(emptyList()) } .catch { emit(emptyList()) }
.collect { groups -> .collect { groups ->
_uiState.update { it.copy(enabledGroups = groups) } _uiState.update { it.copy(enabledGroups = groups.toImmutableList()) }
} }
} }
} }
@@ -243,7 +325,7 @@ class SearchViewModel(
repository.enabledSources repository.enabledSources
.catch { emit(emptyList()) } .catch { emit(emptyList()) }
.collect { sources -> .collect { sources ->
_uiState.update { it.copy(enabledSources = sources) } _uiState.update { it.copy(enabledSources = sources.toImmutableList()) }
} }
} }
} }
@@ -255,7 +337,7 @@ class SearchViewModel(
.collect { keys -> .collect { keys ->
bookshelfKeys.value = keys bookshelfKeys.value = keys
_uiState.update { state -> _uiState.update { state ->
state.copy(results = state.results.withShelfState(keys)) state.copy(results = state.results.withShelfState(keys).toImmutableList())
} }
} }
} }
@@ -269,7 +351,7 @@ class SearchViewModel(
.flatMapLatest { repository.searchHistory(it) } .flatMapLatest { repository.searchHistory(it) }
.catch { emit(emptyList()) } .catch { emit(emptyList()) }
.collect { history -> .collect { history ->
_uiState.update { it.copy(history = history) } _uiState.update { it.copy(history = history.toImmutableList()) }
} }
} }
} }
@@ -282,11 +364,19 @@ class SearchViewModel(
.flatMapLatest { repository.searchBookshelf(it) } .flatMapLatest { repository.searchBookshelf(it) }
.catch { emit(emptyList()) } .catch { emit(emptyList()) }
.collect { books -> .collect { books ->
_uiState.update { it.copy(bookshelfHints = books) } _uiState.update { it.copy(bookshelfHints = books.toImmutableList()) }
} }
} }
} }
private fun observeMatchMode() {
viewModelScope.launch {
matchModeFlow.collect { mode ->
_uiState.update { it.copy(matchMode = mode) }
}
}
}
private fun updateQuery(query: String, showSuggestions: Boolean) { private fun updateQuery(query: String, showSuggestions: Boolean) {
val currentState = _uiState.value val currentState = _uiState.value
val isSameQuery = currentState.query == query val isSameQuery = currentState.query == query
@@ -319,7 +409,7 @@ class SearchViewModel(
_uiState.update { _uiState.update {
it.copy( it.copy(
committedQuery = keyword, committedQuery = keyword,
results = emptyList(), results = persistentListOf(),
isManualStop = false, isManualStop = false,
hasMore = true, hasMore = true,
processedSources = 0, processedSources = 0,
@@ -362,7 +452,7 @@ class SearchViewModel(
keyword = keyword, keyword = keyword,
page = page, page = page,
scope = BookSearchScope(searchScope.toString()), scope = BookSearchScope(searchScope.toString()),
precision = _uiState.value.isPrecisionSearch, matchMode = _uiState.value.matchMode,
concurrency = OtherConfig.threadCount, concurrency = OtherConfig.threadCount,
types = _uiState.value.selectedSourceTypes.takeIf { it.isNotEmpty() }, types = _uiState.value.selectedSourceTypes.takeIf { it.isNotEmpty() },
), ),
@@ -395,7 +485,7 @@ class SearchViewModel(
it.copy( it.copy(
results = buildSearchResultItems( results = buildSearchResultItems(
shelf = bookshelfKeys.value, shelf = bookshelfKeys.value,
), ).toImmutableList(),
processedSources = event.processedSources, processedSources = event.processedSources,
totalSources = event.totalSources, totalSources = event.totalSources,
) )
@@ -407,7 +497,7 @@ class SearchViewModel(
val emptyAction = if (searchResultBooks.isEmpty() && event.isEmpty && !searchScope.isAll()) { val emptyAction = if (searchResultBooks.isEmpty() && event.isEmpty && !searchScope.isAll()) {
SearchEmptyScopeAction( SearchEmptyScopeAction(
scopeDisplay = searchScope.display, scopeDisplay = searchScope.display,
wasPrecisionSearch = state.isPrecisionSearch, wasMatchMode = state.matchMode,
) )
} else { } else {
null null
@@ -441,7 +531,7 @@ class SearchViewModel(
it.copy( it.copy(
query = "", query = "",
committedQuery = "", committedQuery = "",
results = emptyList(), results = persistentListOf(),
processedSources = 0, processedSources = 0,
totalSources = 0, totalSources = 0,
isSearching = false, isSearching = false,
@@ -497,9 +587,12 @@ class SearchViewModel(
val action = _uiState.value.emptyScopeAction ?: return val action = _uiState.value.emptyScopeAction ?: return
_uiState.update { it.copy(emptyScopeAction = null) } _uiState.update { it.copy(emptyScopeAction = null) }
if (action.wasPrecisionSearch) { if (action.wasMatchMode == MatchMode.EXACT) {
appCtx.putPrefBoolean(PreferKey.precisionSearch, false) viewModelScope.launch {
_uiState.update { it.copy(isPrecisionSearch = false) } localPreferencesRepository.updatePreference(
LocalPreferencesKeys.MATCH_MODE, MatchMode.DEFAULT.value
)
}
} else { } else {
searchScope.update("") searchScope.update("")
syncScopeState() syncScopeState()
@@ -523,7 +616,7 @@ class SearchViewModel(
_uiState.update { _uiState.update {
it.copy( it.copy(
scopeDisplay = searchScope.display, scopeDisplay = searchScope.display,
scopeDisplayNames = searchScope.displayNames, scopeDisplayNames = searchScope.displayNames.toImmutableList(),
selectedScopeSourceUrls = searchScope.sourceUrls.toSet(), selectedScopeSourceUrls = searchScope.sourceUrls.toSet(),
isAllScope = searchScope.isAll(), isAllScope = searchScope.isAll(),
isSourceScope = searchScope.isSource(), isSourceScope = searchScope.isSource(),
@@ -571,6 +664,37 @@ class SearchViewModel(
} }
} }
private fun loadExpandedSourcePage(sourceUrl: String, page: Int) {
viewModelScope.launch {
val keyword = _uiState.value.committedQuery
try {
val result = exploreBooksUseCase.execute(
sourceUrl = sourceUrl,
moduleUrl = null,
args = null,
page = page,
key = keyword,
)
val newBooks = result.books
_uiState.update {
it.copy(
expandedSourceBooks = (it.expandedSourceBooks + newBooks).toImmutableList(),
expandedSourceLoading = false,
expandedSourceEnd = newBooks.isEmpty(),
expandedSourcePage = page + 1,
)
}
} catch (e: Exception) {
_uiState.update {
it.copy(
expandedSourceLoading = false,
expandedSourceError = e.message ?: "Unknown error",
)
}
}
}
}
private fun emitEffect(effect: SearchEffect) { private fun emitEffect(effect: SearchEffect) {
_effects.tryEmit(effect) _effects.tryEmit(effect)
} }
@@ -34,11 +34,9 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import io.legado.app.ui.widget.components.AppFloatingActionButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.animateFloatingActionButton import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -57,20 +55,23 @@ import androidx.compose.ui.unit.dp
import io.legado.app.data.entities.SearchContentHistory import io.legado.app.data.entities.SearchContentHistory
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.SearchBar import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.button.MediumOutlinedButton import io.legado.app.ui.widget.components.button.series.MediumOutlinedButton
import io.legado.app.ui.widget.components.button.SmallAnimatedActionButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallToggleButton
import io.legado.app.ui.widget.components.topbar.TopBarAnimatedActionButton import io.legado.app.ui.widget.components.button.series.ToggleStyle
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import io.legado.app.ui.widget.components.card.TextCard import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.topbar.TopBarAnimatedActionButton
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@@ -302,14 +303,14 @@ fun SearchHistoryList(
color = LegadoTheme.colorScheme.primary, color = LegadoTheme.colorScheme.primary,
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
SmallAnimatedActionButton( SmallToggleButton(
modifier = Modifier.align(Alignment.CenterEnd), modifier = Modifier.align(Alignment.CenterEnd),
checked = onlyThisBook, checked = onlyThisBook,
onCheckedChange = { onToggleScope() }, onCheckedChange = { onToggleScope() },
style = ToggleStyle.Tonal,
iconChecked = Icons.Default.Book, iconChecked = Icons.Default.Book,
iconUnchecked = Icons.Default.CollectionsBookmark, icon = Icons.Default.CollectionsBookmark,
activeText = "仅本书", text = "仅本书"
inactiveText = "所有记录"
) )
} }
@@ -338,9 +339,9 @@ fun SearchHistoryList(
Icon(Icons.Default.History, contentDescription = null) Icon(Icons.Default.History, contentDescription = null)
}, },
trailingContent = { trailingContent = {
SmallIconButton( SmallPlainButton(
onClick = { onDeleteHistory(item) }, onClick = { onDeleteHistory(item) },
imageVector = Icons.Default.Close, icon = Icons.Default.Close,
contentDescription = "删除" contentDescription = "删除"
) )
}, },
@@ -361,7 +362,7 @@ fun SearchHistoryList(
MediumOutlinedButton( MediumOutlinedButton(
onClick = onClearHistory, onClick = onClearHistory,
modifier = Modifier.fillMaxWidth(0.6f), modifier = Modifier.fillMaxWidth(0.6f),
imageVector = Icons.Outlined.DeleteSweep, icon = Icons.Outlined.DeleteSweep,
text = "清除搜索历史" text = "清除搜索历史"
) )
} }
@@ -95,7 +95,8 @@ import io.legado.app.ui.widget.components.FabMenuItem
import io.legado.app.ui.widget.components.SelectionBottomBar import io.legado.app.ui.widget.components.SelectionBottomBar
import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
import io.legado.app.ui.widget.components.bookmark.BookmarkItem import io.legado.app.ui.widget.components.bookmark.BookmarkItem
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton import io.legado.app.ui.widget.components.button.series.SmallToggleButton
import io.legado.app.ui.widget.components.button.series.ToggleStyle
import io.legado.app.ui.widget.components.card.NormalCard 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.card.TextCard
import io.legado.app.ui.widget.components.divider.PillDivider import io.legado.app.ui.widget.components.divider.PillDivider
@@ -476,10 +477,11 @@ fun TocScreen(
if (pagerState.currentPage == 0 && hasVolumes) { if (pagerState.currentPage == 0 && hasVolumes) {
Box { Box {
SmallOutlinedIconToggleButton( SmallToggleButton(
checked = showVolumeMenu, checked = showVolumeMenu,
onCheckedChange = { showVolumeMenu = it }, onCheckedChange = { showVolumeMenu = it },
imageVector = Icons.AutoMirrored.Filled.FormatListBulleted, style = ToggleStyle.Outlined,
icon = Icons.AutoMirrored.Filled.FormatListBulleted,
contentDescription = stringResource(R.string.volume_management) contentDescription = stringResource(R.string.volume_management)
) )
RoundDropdownMenu( RoundDropdownMenu(
@@ -5,24 +5,15 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -48,7 +39,7 @@ import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.ActionItem import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.DraggableSelectionHandler import io.legado.app.ui.widget.components.DraggableSelectionHandler
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.icon.AppIcons import io.legado.app.ui.widget.components.icon.AppIcons
@@ -59,7 +50,6 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.rules.RuleEditFields import io.legado.app.ui.widget.components.rules.RuleEditFields
import io.legado.app.ui.widget.components.rules.RuleEditSheet import io.legado.app.ui.widget.components.rules.RuleEditSheet
import io.legado.app.ui.widget.components.rules.RuleListScaffold import io.legado.app.ui.widget.components.rules.RuleListScaffold
import io.legado.app.ui.widget.components.text.AppText
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import sh.calvin.reorderable.rememberReorderableLazyListState import sh.calvin.reorderable.rememberReorderableLazyListState
@@ -350,9 +340,9 @@ fun TxtRuleScreen(
onEnabledChange = { enabled -> viewModel.update(item.rule.copy(enable = enabled)) }, onEnabledChange = { enabled -> viewModel.update(item.rule.copy(enable = enabled)) },
onClickEdit = { editingRule = item.rule; showEditSheet = true }, onClickEdit = { editingRule = item.rule; showEditSheet = true },
trailingAction = { trailingAction = {
SmallIconButton( SmallPlainButton(
onClick = { showDeleteRuleDialog = item.rule }, onClick = { showDeleteRuleDialog = item.rule },
imageVector = AppIcons.Delete icon = AppIcons.Delete
) )
} }
) )
@@ -23,7 +23,7 @@ import io.legado.app.R
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.model.BookCover import io.legado.app.model.BookCover
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.series.MediumPlainButton
import io.legado.app.ui.widget.components.checkBox.CheckboxItem import io.legado.app.ui.widget.components.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
@@ -59,8 +59,8 @@ fun CoverRuleConfigSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = stringResource(R.string.cover_rule), title = stringResource(R.string.cover_rule),
startAction = { startAction = {
MediumIconButton( MediumPlainButton(
imageVector = Icons.Default.SettingsBackupRestore, icon = Icons.Default.SettingsBackupRestore,
onClick = { onClick = {
DefaultData.coverRule.let { DefaultData.coverRule.let {
enable = it.enable enable = it.enable
@@ -72,8 +72,8 @@ fun CoverRuleConfigSheet(
) )
}, },
endAction = { endAction = {
MediumIconButton( MediumPlainButton(
imageVector = Icons.Default.Save, icon = Icons.Default.Save,
onClick = { onClick = {
if (searchUrl.isBlank() || coverRule.isBlank()) { if (searchUrl.isBlank() || coverRule.isBlank()) {
appCtx.toastOnUi(R.string.cover_rule_fields_required) appCtx.toastOnUi(R.string.cover_rule_fields_required)
@@ -2,7 +2,6 @@ package io.legado.app.ui.config.otherConfig
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -16,12 +15,8 @@ import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.ContentPaste import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -39,7 +34,7 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.series.MediumPlainButton
import io.legado.app.ui.widget.components.checkBox.CheckboxItem import io.legado.app.ui.widget.components.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu 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.menuItem.RoundDropdownMenuItem
@@ -70,18 +65,18 @@ fun DirectLinkUploadBottomSheet(
show = show, show = show,
title = stringResource(R.string.direct_link_upload_config), title = stringResource(R.string.direct_link_upload_config),
startAction = { startAction = {
MediumIconButton( MediumPlainButton(
onClick = { onClick = {
viewModel.testRule { result -> showTestResult = result } viewModel.testRule { result -> showTestResult = result }
}, },
imageVector = Icons.Default.Checklist icon = Icons.Default.Checklist
) )
}, },
endAction = { endAction = {
Box { Box {
MediumIconButton( MediumPlainButton(
onClick = { showMenu = true }, onClick = { showMenu = true },
imageVector = Icons.Default.MoreVert icon = Icons.Default.MoreVert
) )
RoundDropdownMenu( RoundDropdownMenu(
expanded = showMenu, expanded = showMenu,
@@ -27,7 +27,7 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.SmallTonalIconButton import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.card.NormalCard import io.legado.app.ui.widget.components.card.NormalCard
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
@@ -114,13 +114,13 @@ fun BackgroundImageManageSheet(
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} }
SmallTonalIconButton( SmallTonalButton(
onClick = { viewModel.removeBackground(isDark) }, onClick = { viewModel.removeBackground(isDark) },
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(8.dp) .padding(8.dp)
.size(32.dp), .size(32.dp),
imageVector = Icons.Default.Close icon = Icons.Default.Close
) )
} }
} }
@@ -5,21 +5,19 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.AutoAwesome import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.toMutableStateList
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -30,8 +28,8 @@ import androidx.core.graphics.ColorUtils
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.config.TagColorGenerator import io.legado.app.help.config.TagColorGenerator
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton import io.legado.app.ui.widget.components.button.series.MediumOutlinedButton
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.NormalCard 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.card.TextCard
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
@@ -57,7 +55,7 @@ fun LabelColorManageSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = stringResource(R.string.theme_config_manage_label_colors), title = stringResource(R.string.theme_config_manage_label_colors),
startAction = { startAction = {
MediumOutlinedIconButton( MediumOutlinedButton(
onClick = { onClick = {
val baseColor = if (themeColor != 0) Color(themeColor) else primaryColor val baseColor = if (themeColor != 0) Color(themeColor) else primaryColor
val generatedColors = TagColorGenerator.generateTagColors(baseColor) val generatedColors = TagColorGenerator.generateTagColors(baseColor)
@@ -65,23 +63,25 @@ fun LabelColorManageSheet(
tagColors.addAll(generatedColors) tagColors.addAll(generatedColors)
ThemeConfig.saveCustomTagColors(tagColors) ThemeConfig.saveCustomTagColors(tagColors)
}, },
imageVector = Icons.Default.AutoAwesome icon = Icons.Default.AutoAwesome
) )
}, },
endAction = { endAction = {
MediumOutlinedIconButton( MediumOutlinedButton(
onClick = { onClick = {
tagColors.add(TagColorPair(0, 0)) tagColors.add(TagColorPair(0, 0))
editingIndex = tagColors.size - 1 editingIndex = tagColors.size - 1
editingTextColor = 0 editingTextColor = 0
showColorPicker = true showColorPicker = true
}, },
imageVector = Icons.Default.Add icon = Icons.Default.Add
) )
} }
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp), modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(tagColors.size) { index -> items(tagColors.size) { index ->
@@ -110,20 +110,20 @@ fun LabelColorManageSheet(
Row( Row(
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
SmallIconButton( SmallPlainButton(
onClick = { onClick = {
editingIndex = index editingIndex = index
editingTextColor = colorPair.textColor editingTextColor = colorPair.textColor
showColorPicker = true showColorPicker = true
}, },
imageVector = Icons.Default.Edit icon = Icons.Default.Edit
) )
SmallIconButton( SmallPlainButton(
onClick = { onClick = {
tagColors.removeAt(index) tagColors.removeAt(index)
ThemeConfig.saveCustomTagColors(tagColors) ThemeConfig.saveCustomTagColors(tagColors)
}, },
imageVector = Icons.Default.Delete icon = Icons.Default.Delete
) )
} }
} }
@@ -30,7 +30,7 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.SmallTonalIconButton import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.card.NormalCard import io.legado.app.ui.widget.components.card.NormalCard
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -123,13 +123,13 @@ fun NavIconManageSheet(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Fit contentScale = ContentScale.Fit
) )
SmallTonalIconButton( SmallTonalButton(
onClick = { dest.onSetPath("") }, onClick = { dest.onSetPath("") },
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(4.dp) .padding(4.dp)
.size(24.dp), .size(24.dp),
imageVector = Icons.Default.Close icon = Icons.Default.Close
) )
} }
} else { } else {
@@ -88,7 +88,7 @@ import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SplicedColumnGroup import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
import io.legado.app.ui.widget.components.icon.AppIcons import io.legado.app.ui.widget.components.icon.AppIcons
@@ -222,8 +222,8 @@ fun ThemeConfigScreen(
style = LegadoTheme.typography.labelLargeEmphasized, style = LegadoTheme.typography.labelLargeEmphasized,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
SmallIconButton( SmallPlainButton(
imageVector = AppIcons.Close, icon = AppIcons.Close,
contentDescription = "关闭", contentDescription = "关闭",
onClick = { onClick = {
viewModel.setShowThemeRefactorTip(false) viewModel.setShowThemeRefactorTip(false)
@@ -759,8 +759,8 @@ fun ThemeConfigScreen(
onDismissRequest = { showFontSheet = false }, onDismissRequest = { showFontSheet = false },
title = stringResource(R.string.font_setting), title = stringResource(R.string.font_setting),
startAction = { startAction = {
SmallIconButton( SmallPlainButton(
imageVector = Icons.Default.Delete, icon = Icons.Default.Delete,
contentDescription = stringResource(R.string.clear), contentDescription = stringResource(R.string.clear),
onClick = { onClick = {
ThemeConfig.appFontPath = null ThemeConfig.appFontPath = null
@@ -769,8 +769,8 @@ fun ThemeConfigScreen(
) )
}, },
endAction = { endAction = {
SmallIconButton( SmallPlainButton(
imageVector = Icons.Default.Add, icon = Icons.Default.Add,
contentDescription = stringResource(R.string.select_folder), contentDescription = stringResource(R.string.select_folder),
onClick = { fontFolderLauncher.launch(null) } onClick = { fontFolderLauncher.launch(null) }
) )
@@ -31,7 +31,7 @@ import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.config.ThemeExportData import io.legado.app.help.config.ThemeExportData
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.series.MediumPlainButton
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.CompactClickableSettingItem import io.legado.app.ui.widget.components.settingItem.CompactClickableSettingItem
@@ -60,13 +60,13 @@ fun EditThemeSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = stringResource(R.string.theme_manage_edit_theme), title = stringResource(R.string.theme_manage_edit_theme),
endAction = { endAction = {
MediumIconButton( MediumPlainButton(
onClick = { onClick = {
if (name.isNotBlank()) { if (name.isNotBlank()) {
onSave(name, data) onSave(name, data)
} }
}, },
imageVector = Icons.Default.Done, icon = Icons.Default.Done,
contentDescription = "Save" contentDescription = "Save"
) )
} }
@@ -47,7 +47,7 @@ import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.SplicedColumnGroup import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
@@ -393,17 +393,17 @@ private fun SavedThemeItem(
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
SmallIconButton( SmallPlainButton(
onClick = onEdit, onClick = onEdit,
imageVector = Icons.Default.Edit icon = Icons.Default.Edit
) )
SmallIconButton( SmallPlainButton(
onClick = onExport, onClick = onExport,
imageVector = Icons.Default.Share icon = Icons.Default.Share
) )
SmallIconButton( SmallPlainButton(
onClick = onDelete, onClick = onDelete,
imageVector = Icons.Default.Delete icon = Icons.Default.Delete
) )
} }
} }
@@ -1,168 +0,0 @@
package io.legado.app.ui.dict.rule
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.NoteAdd
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.data.entities.DictRule
import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DictRuleEditSheet(
rule: DictRule?,
onDismissRequest: () -> Unit,
onSave: (DictRule) -> Unit,
onCopy: (DictRule) -> Unit,
onPaste: () -> DictRule?
) {
val scope = rememberCoroutineScope()
var name by remember(rule) { mutableStateOf(rule?.name ?: "") }
var urlRule by remember(rule) { mutableStateOf(rule?.urlRule ?: "") }
var showRule by remember(rule) { mutableStateOf(rule?.showRule ?: "") }
var showMenu by remember { mutableStateOf(false) }
AppModalBottomSheet(
show = true,
onDismissRequest = onDismissRequest
) {
Box(
modifier = Modifier
.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
CenterAlignedTopAppBar(
title = { AppText(stringResource(R.string.dict_rule)) },
navigationIcon = {
IconButton(onClick = onDismissRequest) {
Icon(
Icons.Default.Close,
contentDescription = stringResource(R.string.cancel)
)
}
},
actions = {
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "More")
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
text = { AppText(stringResource(R.string.copy_rule)) },
leadingIcon = { Icon(Icons.AutoMirrored.Filled.NoteAdd, null) },
onClick = {
onCopy(
DictRule(
name,
urlRule,
showRule,
enabled = rule?.enabled ?: true
)
)
showMenu = false
}
)
DropdownMenuItem(
text = { AppText(stringResource(R.string.paste_rule)) },
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
onClick = {
scope.launch {
onPaste()?.let {
name = it.name
urlRule = it.urlRule
showRule = it.showRule
}
}
showMenu = false
}
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
)
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 96.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = name,
onValueChange = { name = it },
label = { AppText(stringResource(R.string.name)) },
singleLine = true
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = urlRule,
onValueChange = { urlRule = it },
label = { AppText(stringResource(R.string.url_rule)) }
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = showRule,
onValueChange = { showRule = it },
label = { AppText(stringResource(R.string.show_rule)) },
minLines = 3
)
}
}
AppFloatingActionButton(
onClick = {
onSave(
rule?.copy(name = name, urlRule = urlRule, showRule = showRule)
?: DictRule(name = name, urlRule = urlRule, showRule = showRule)
)
},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
icon = Icons.Default.Save
)
}
}
}
@@ -10,13 +10,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -41,7 +38,7 @@ import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.ActionItem import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.DraggableSelectionHandler import io.legado.app.ui.widget.components.DraggableSelectionHandler
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.icon.AppIcons import io.legado.app.ui.widget.components.icon.AppIcons
@@ -77,7 +74,6 @@ fun DictRuleScreen(
var showDeleteRuleDialog by remember { mutableStateOf<DictRule?>(null) } var showDeleteRuleDialog by remember { mutableStateOf<DictRule?>(null) }
var showUrlInput by remember { mutableStateOf(false) } var showUrlInput by remember { mutableStateOf(false) }
var showFilePickerSheet by remember { mutableStateOf(false) }
var showImportSheet by remember { mutableStateOf(false) } var showImportSheet by remember { mutableStateOf(false) }
var showExportSheet by remember { mutableStateOf(false) } var showExportSheet by remember { mutableStateOf(false) }
@@ -90,7 +86,6 @@ fun DictRuleScreen(
val clipboardManager = LocalClipboard.current val clipboardManager = LocalClipboard.current
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val importState by viewModel.importState.collectAsStateWithLifecycle() val importState by viewModel.importState.collectAsStateWithLifecycle()
val sheetState = rememberModalBottomSheetState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.events.collect { event -> viewModel.events.collect { event ->
@@ -276,7 +271,7 @@ fun DictRuleScreen(
}), }),
ActionItem( ActionItem(
text = stringResource(R.string.export), text = stringResource(R.string.export),
onClick = { showFilePickerSheet = true }) onClick = { showExportSheet = true })
), ),
onDeleteSelected = { ids -> onDeleteSelected = { ids ->
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -320,9 +315,9 @@ fun DictRuleScreen(
onEnabledChange = { enabled -> viewModel.update(item.rule.copy(enabled = enabled)) }, onEnabledChange = { enabled -> viewModel.update(item.rule.copy(enabled = enabled)) },
onClickEdit = { editingRule = item.rule; showEditSheet = true }, onClickEdit = { editingRule = item.rule; showEditSheet = true },
trailingAction = { trailingAction = {
SmallIconButton( SmallPlainButton(
onClick = { showDeleteRuleDialog = item.rule }, onClick = { showDeleteRuleDialog = item.rule },
imageVector = AppIcons.Delete icon = AppIcons.Delete
) )
} }
) )
@@ -111,7 +111,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
context: Context, context: Context,
exploreName: String? = null, exploreName: String? = null,
sourceUrl: String, sourceUrl: String,
exploreUrl: String? = null exploreUrl: String? = null,
): Intent = MainIntent.createExploreShowIntent(context, exploreName, sourceUrl, exploreUrl) ): Intent = MainIntent.createExploreShowIntent(context, exploreName, sourceUrl, exploreUrl)
} }
@@ -134,7 +134,7 @@ object MainIntent {
context: Context, context: Context,
exploreName: String? = null, exploreName: String? = null,
sourceUrl: String, sourceUrl: String,
exploreUrl: String? = null exploreUrl: String? = null,
): Intent { ): Intent {
return createLauncherIntent(context).apply { return createLauncherIntent(context).apply {
putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_EXPLORE_SHOW) putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_EXPLORE_SHOW)
@@ -19,7 +19,9 @@ import io.legado.app.ui.about.AboutEffect
import io.legado.app.ui.about.AboutScreen import io.legado.app.ui.about.AboutScreen
import io.legado.app.ui.about.AboutViewModel import io.legado.app.ui.about.AboutViewModel
import io.legado.app.ui.book.cache.manage.BookCacheManageRouteScreen import io.legado.app.ui.book.cache.manage.BookCacheManageRouteScreen
import io.legado.app.ui.book.explore.ExploreShowIntent
import io.legado.app.ui.book.explore.ExploreShowScreen import io.legado.app.ui.book.explore.ExploreShowScreen
import io.legado.app.ui.book.explore.ExploreShowViewModel
import io.legado.app.ui.book.import.local.ImportBookScreen import io.legado.app.ui.book.import.local.ImportBookScreen
import io.legado.app.ui.book.import.remote.RemoteBookScreen import io.legado.app.ui.book.import.remote.RemoteBookScreen
import io.legado.app.ui.book.info.BookInfoRouteScreen import io.legado.app.ui.book.info.BookInfoRouteScreen
@@ -39,8 +41,8 @@ import io.legado.app.ui.config.downloadCacheConfig.DownloadCacheConfigScreen
import io.legado.app.ui.config.otherConfig.OtherConfigScreen import io.legado.app.ui.config.otherConfig.OtherConfigScreen
import io.legado.app.ui.config.readConfig.ReadConfigScreen import io.legado.app.ui.config.readConfig.ReadConfigScreen
import io.legado.app.ui.config.themeConfig.ThemeConfigScreen import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
import io.legado.app.ui.config.translation.TranslationConfigScreen
import io.legado.app.ui.config.themeManage.ThemeManageScreen import io.legado.app.ui.config.themeManage.ThemeManageScreen
import io.legado.app.ui.config.translation.TranslationConfigScreen
import io.legado.app.ui.rss.article.MainRouteRssSort import io.legado.app.ui.rss.article.MainRouteRssSort
import io.legado.app.ui.rss.article.RssSortRouteScreen import io.legado.app.ui.rss.article.RssSortRouteScreen
import io.legado.app.ui.rss.favorites.RssFavoritesScreen import io.legado.app.ui.rss.favorites.RssFavoritesScreen
@@ -436,10 +438,17 @@ fun MainActivity.mainEntryProvider(
} }
entry<MainRouteExploreShow> { route -> entry<MainRouteExploreShow> { route ->
val exploreViewModel = koinViewModel<ExploreShowViewModel>()
LaunchedEffect(route.sourceUrl, route.exploreUrl, exploreViewModel) {
exploreViewModel.onIntent(
ExploreShowIntent.InitData(route.sourceUrl, route.exploreUrl)
)
}
ExploreShowScreen( ExploreShowScreen(
viewModel = exploreViewModel,
title = route.title ?: "探索", title = route.title ?: "探索",
sourceUrl = route.sourceUrl,
exploreUrl = route.exploreUrl,
onBack = { onNavigateBack() }, onBack = { onNavigateBack() },
onBookClick = { book, sharedCoverKey -> onBookClick = { book, sharedCoverKey ->
onNavigateToRoute( onNavigateToRoute(
@@ -268,7 +268,7 @@ object MainNavigator {
MainRouteExploreShow( MainRouteExploreShow(
title = intent.getStringExtra(MainIntent.EXTRA_EXPLORE_NAME), title = intent.getStringExtra(MainIntent.EXTRA_EXPLORE_NAME),
sourceUrl = sourceUrl, sourceUrl = sourceUrl,
exploreUrl = intent.getStringExtra(MainIntent.EXTRA_EXPLORE_URL) exploreUrl = intent.getStringExtra(MainIntent.EXTRA_EXPLORE_URL),
) )
} ?: MainRouteHome } ?: MainRouteHome
@@ -114,7 +114,9 @@ import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.SearchBar import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.button.series.SmallToggleButton
import io.legado.app.ui.widget.components.button.series.ToggleStyle
import io.legado.app.ui.widget.components.card.NormalCard 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.card.TextCard
import io.legado.app.ui.widget.components.divider.PillHeaderDivider import io.legado.app.ui.widget.components.divider.PillHeaderDivider
@@ -557,7 +559,7 @@ fun BookshelfScreen(
val showExpandButton by BookshelfConfig.shouldShowExpandButtonState val showExpandButton by BookshelfConfig.shouldShowExpandButtonState
if (showExpandButton) { if (showExpandButton) {
Box(modifier = Modifier) { Box(modifier = Modifier) {
SmallOutlinedIconToggleButton( SmallToggleButton(
checked = showGroupMenu, checked = showGroupMenu,
onCheckedChange = { onCheckedChange = {
if (it) { if (it) {
@@ -566,7 +568,8 @@ fun BookshelfScreen(
viewModel.dismissOverlay() viewModel.dismissOverlay()
} }
}, },
imageVector = Icons.AutoMirrored.Filled.FormatListBulleted, style = ToggleStyle.Outlined,
icon = Icons.AutoMirrored.Filled.FormatListBulleted,
contentDescription = stringResource(R.string.group_manage) contentDescription = stringResource(R.string.group_manage)
) )
RoundDropdownMenu( RoundDropdownMenu(
@@ -837,7 +840,7 @@ fun BookshelfScreen(
if (this != null) Modifier.skipToLookaheadSize() else Modifier if (this != null) Modifier.skipToLookaheadSize() else Modifier
} }
), ),
beyondViewportPageCount = 1, beyondViewportPageCount = 0,
key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it } key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
) { pageIndex -> ) { pageIndex ->
val group = uiState.groups.getOrNull(pageIndex) val group = uiState.groups.getOrNull(pageIndex)
@@ -1058,9 +1061,10 @@ private fun BookshelfTopBar(
}, },
trailingIcon = { trailingIcon = {
if (uiState.searchKey.isNotEmpty()) { if (uiState.searchKey.isNotEmpty()) {
TopBarActionButton( SmallPlainButton(
modifier = Modifier.padding(start = 12.dp),
onClick = onClearSearch, onClick = onClearSearch,
imageVector = AppIcons.Close, icon = AppIcons.Close,
contentDescription = stringResource(R.string.clear) contentDescription = stringResource(R.string.clear)
) )
} }
@@ -41,7 +41,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -196,34 +195,31 @@ class BookshelfViewModel(
}.distinctUntilChanged().flowOn(Dispatchers.Default) }.distinctUntilChanged().flowOn(Dispatchers.Default)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private val allGroupBooksFlow: StateFlow<Map<Long, List<BookUiItem>>> = combine( private val allGroupBooksImmutableFlow: StateFlow<ImmutableMap<Long, ImmutableList<BookUiItem>>> =
groupsFlow, sortConfigFlow combine(groupsFlow, sortConfigFlow) { groups, sortConfig ->
) { groups, sortConfig -> groups to sortConfig
groups to sortConfig }.flatMapLatest { (groups, sortConfig) ->
}.flatMapLatest { (groups, sortConfig) -> if (groups.isEmpty()) {
if (groups.isEmpty()) { flowOf(persistentMapOf())
flowOf(emptyMap()) } else {
} else { val flows = groups.map { group ->
val flows = groups.map { group -> bookRepository.flowBookShelfByGroup(group.groupId).map { books ->
bookRepository.flowBookShelfByGroup(group.groupId).map { books -> group.groupId to bookshelfRepository.sortBooks(
group.groupId to bookshelfRepository.sortBooks( books,
books, group,
group, sortConfig.sort,
sortConfig.sort, sortConfig.sortOrder
sortConfig.sortOrder ).map { it.toUiItem() }.toImmutableList()
).map { it.toUiItem() } }
}
combine(flows) { results ->
results.fold(persistentMapOf<Long, ImmutableList<BookUiItem>>()) { acc, (id, list) ->
acc.put(id, list)
}
} }
} }
combine(flows) { it.toMap() } }.distinctUntilChanged()
} .flowOn(Dispatchers.Default)
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
private val allGroupBooksImmutableFlow: StateFlow<ImmutableMap<Long, ImmutableList<BookUiItem>>> =
allGroupBooksFlow.map { map ->
map.mapValues { it.value.toImmutableList() }.toImmutableMap()
}.flowOn(Dispatchers.Default)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentMapOf()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentMapOf())
private val visibleBooksFlow: Flow<List<BookUiItem>> = combine( private val visibleBooksFlow: Flow<List<BookUiItem>> = combine(
@@ -301,30 +297,21 @@ class BookshelfViewModel(
} }
}.distinctUntilChanged().flowOn(Dispatchers.Default) }.distinctUntilChanged().flowOn(Dispatchers.Default)
private val coreInternalStateFlow = combine( private val internalStateFlow = combine(
groupIdFlow, groupIdFlow,
searchKeyFlow, searchKeyFlow,
searchModeFlow, searchModeFlow,
loadingTextFlow, loadingTextFlow,
updatingBooksFlow combine(updatingBooksFlow, upBooksCountFlow, sortConfigFlow) { a, b, c ->
) { groupId, searchKey, isSearchMode, loadingText, updatingBooks -> Triple(a, b, c)
}
) { groupId, searchKey, isSearchMode, loadingText, (updatingBooks, upBooksCount, sortConfig) ->
InternalState( InternalState(
groupId = groupId, groupId = groupId,
searchKey = searchKey, searchKey = searchKey,
isSearchMode = isSearchMode, isSearchMode = isSearchMode,
loadingText = loadingText, loadingText = loadingText,
updatingBooks = updatingBooks, updatingBooks = updatingBooks,
upBooksCount = 0,
sortConfig = readSortConfig()
)
}
private val internalStateFlow = combine(
coreInternalStateFlow,
upBooksCountFlow,
sortConfigFlow
) { core, upBooksCount, sortConfig ->
core.copy(
upBooksCount = upBooksCount, upBooksCount = upBooksCount,
sortConfig = sortConfig sortConfig = sortConfig
) )
@@ -351,35 +338,29 @@ class BookshelfViewModel(
val pendingSavedBooks: List<BookUiItem>? val pendingSavedBooks: List<BookUiItem>?
) )
private val editStateFlow = combine( private val interactionStateFlow = combine(
activeOverlayFlow, activeOverlayFlow,
isEditModeFlow, isEditModeFlow,
selectedVisibleBookUrlsFlow, selectedVisibleBookUrlsFlow,
isInFolderRootFlow isInFolderRootFlow,
) { activeOverlay, isEditMode, selectedBookUrls, isInFolderRoot -> isRefreshingFlow
EditState(activeOverlay, isEditMode, selectedBookUrls, isInFolderRoot) ) { activeOverlay, isEditMode, selectedBookUrls, isInFolderRoot, isRefreshing ->
}
private data class EditState(
val activeOverlay: BookshelfOverlay?,
val isEditMode: Boolean,
val selectedBookUrls: Set<String>,
val isInFolderRoot: Boolean
)
private val interactionStateFlow = combine(
editStateFlow,
isRefreshingFlow,
bookGroupStyleFlow,
draggingBooksFlow,
pendingSavedBooksFlow
) { editState, isRefreshing, bookGroupStyle, draggingBooks, pendingSavedBooks ->
BookshelfInteractionState( BookshelfInteractionState(
activeOverlay = editState.activeOverlay, activeOverlay = activeOverlay,
isEditMode = editState.isEditMode, isEditMode = isEditMode,
selectedBookUrls = editState.selectedBookUrls, selectedBookUrls = selectedBookUrls,
isInFolderRoot = editState.isInFolderRoot, isInFolderRoot = isInFolderRoot,
isRefreshing = isRefreshing, isRefreshing = isRefreshing,
bookGroupStyle = 0,
draggingBooks = null,
pendingSavedBooks = null
)
}.combine(
combine(bookGroupStyleFlow, draggingBooksFlow, pendingSavedBooksFlow) { a, b, c ->
Triple(a, b, c)
}
) { interaction, (bookGroupStyle, draggingBooks, pendingSavedBooks) ->
interaction.copy(
bookGroupStyle = bookGroupStyle, bookGroupStyle = bookGroupStyle,
draggingBooks = draggingBooks, draggingBooks = draggingBooks,
pendingSavedBooks = pendingSavedBooks pendingSavedBooks = pendingSavedBooks
@@ -390,11 +371,6 @@ class BookshelfViewModel(
GroupPreviewState(persistentMapOf(), persistentMapOf(), 0) GroupPreviewState(persistentMapOf(), persistentMapOf(), 0)
) )
private val allGroupBooksStateFlow =
MutableStateFlow<ImmutableMap<Long, ImmutableList<BookUiItem>>>(
persistentMapOf()
)
private val dataStateFlow = combine( private val dataStateFlow = combine(
booksFlow, booksFlow,
groupsFlow, groupsFlow,
@@ -403,7 +379,7 @@ class BookshelfViewModel(
internalStateFlow internalStateFlow
) { books, groups, allGroups, previews, internal -> ) { books, groups, allGroups, previews, internal ->
BookshelfDataCore(books, groups, allGroups, previews, internal) BookshelfDataCore(books, groups, allGroups, previews, internal)
}.combine(allGroupBooksStateFlow) { core, allGroupBooks -> }.combine(allGroupBooksImmutableFlow) { core, allGroupBooks ->
BookshelfDataState( BookshelfDataState(
books = core.books, books = core.books,
groups = core.groups.map { it.toBookGroupUi() }, groups = core.groups.map { it.toBookGroupUi() },
@@ -529,9 +505,6 @@ class BookshelfViewModel(
viewModelScope.launch { viewModelScope.launch {
groupPreviewsFlow.collect { groupPreviewsStateFlow.value = it } groupPreviewsFlow.collect { groupPreviewsStateFlow.value = it }
} }
viewModelScope.launch {
allGroupBooksImmutableFlow.collect { allGroupBooksStateFlow.value = it }
}
viewModelScope.launch { viewModelScope.launch {
combine(booksFlow, selectedGroupCanReorderFlow) { books, canReorderBooks -> combine(booksFlow, selectedGroupCanReorderFlow) { books, canReorderBooks ->
books to canReorderBooks books to canReorderBooks
@@ -26,12 +26,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.ui.book.group.GroupEditContent
import io.legado.app.ui.book.group.GroupDeleteAction import io.legado.app.ui.book.group.GroupDeleteAction
import io.legado.app.ui.book.group.GroupEditContent
import io.legado.app.ui.book.group.GroupResetCoverAction import io.legado.app.ui.book.group.GroupResetCoverAction
import io.legado.app.ui.book.group.GroupViewModel import io.legado.app.ui.book.group.GroupViewModel
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.utils.move import io.legado.app.utils.move
@@ -95,13 +95,13 @@ fun GroupManageSheet(
}, },
endAction = { endAction = {
if (!isEditing) { if (!isEditing) {
SmallIconButton( SmallPlainButton(
onClick = { onClick = {
editingGroup = null editingGroup = null
coverPath = null coverPath = null
isEditing = true isEditing = true
}, },
imageVector = Icons.Default.Add icon = Icons.Default.Add
) )
} else { } else {
GroupResetCoverAction( GroupResetCoverAction(
@@ -92,6 +92,9 @@ fun ExploreScreen(
val context = LocalContext.current val context = LocalContext.current
val activity = context as? AppCompatActivity val activity = context as? AppCompatActivity
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val listItems by remember(uiState.items, uiState.expandedId, uiState.exploreKinds) {
derivedStateOf { viewModel.buildExploreListItems(uiState) }
}
var sourceToDeleteUrl by rememberSaveable { mutableStateOf<String?>(null) } var sourceToDeleteUrl by rememberSaveable { mutableStateOf<String?>(null) }
val sourceToDelete = remember(sourceToDeleteUrl, uiState.items) { val sourceToDelete = remember(sourceToDeleteUrl, uiState.items) {
uiState.items.firstOrNull { it.bookSourceUrl == sourceToDeleteUrl } uiState.items.firstOrNull { it.bookSourceUrl == sourceToDeleteUrl }
@@ -118,10 +121,10 @@ fun ExploreScreen(
} }
} }
val stickyHeaderSource by remember(uiState.listItems, uiState.items) { val stickyHeaderSource by remember(listItems, uiState.items) {
derivedStateOf { derivedStateOf {
val firstIndex = listState.firstVisibleItemIndex val firstIndex = listState.firstVisibleItemIndex
val item = uiState.listItems.getOrNull(firstIndex) val item = listItems.getOrNull(firstIndex)
if (item is ExploreListItem.KindRow) { if (item is ExploreListItem.KindRow) {
uiState.items.find { it.bookSourceUrl == item.sourceUrl } uiState.items.find { it.bookSourceUrl == item.sourceUrl }
} else { } else {
@@ -178,7 +181,7 @@ fun ExploreScreen(
) )
) { ) {
items( items(
items = uiState.listItems, items = listItems,
key = { it.key } key = { it.key }
) { listItem -> ) { listItem ->
when (listItem) { when (listItem) {
@@ -268,7 +271,7 @@ fun ExploreScreen(
verticalPadding = 8.dp, verticalPadding = 8.dp,
onClick = { onClick = {
scope.launch { scope.launch {
val index = uiState.listItems.indexOfFirst { val index = listItems.indexOfFirst {
it is ExploreListItem.Header && it.source.bookSourceUrl == item.bookSourceUrl it is ExploreListItem.Header && it.source.bookSourceUrl == item.bookSourceUrl
} }
if (index >= 0) listState.animateScrollToItem(index) if (index >= 0) listState.animateScrollToItem(index)
@@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -42,7 +41,6 @@ class ExploreViewModel(
private val _uiState = MutableStateFlow(ExploreUiState()) private val _uiState = MutableStateFlow(ExploreUiState())
val uiState: StateFlow<ExploreUiState> = _uiState val uiState: StateFlow<ExploreUiState> = _uiState
.map { state -> state.copy(listItems = buildExploreListItems(state)) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ExploreUiState()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ExploreUiState())
private val _effects = MutableSharedFlow<ExploreEffect>(extraBufferCapacity = 8) private val _effects = MutableSharedFlow<ExploreEffect>(extraBufferCapacity = 8)
val effects = _effects.asSharedFlow() val effects = _effects.asSharedFlow()
@@ -200,11 +198,10 @@ class ExploreViewModel(
val exploreKinds: ImmutableList<ExploreKind> = persistentListOf(), val exploreKinds: ImmutableList<ExploreKind> = persistentListOf(),
val kindDisplayNames: ImmutableMap<String, String> = persistentMapOf(), val kindDisplayNames: ImmutableMap<String, String> = persistentMapOf(),
val kindValues: ImmutableMap<String, String> = persistentMapOf(), val kindValues: ImmutableMap<String, String> = persistentMapOf(),
val loadingKinds: Boolean = false, val loadingKinds: Boolean = false
val listItems: ImmutableList<ExploreListItem> = persistentListOf()
) : ListUiState<BookSourcePart> ) : ListUiState<BookSourcePart>
private fun buildExploreListItems(state: ExploreUiState): ImmutableList<ExploreListItem> { fun buildExploreListItems(state: ExploreUiState): ImmutableList<ExploreListItem> {
if (state.items.isEmpty()) return persistentListOf() if (state.items.isEmpty()) return persistentListOf()
val expandedId = state.expandedId val expandedId = state.expandedId
val kindRows = if (expandedId != null) { val kindRows = if (expandedId != null) {
@@ -37,7 +37,7 @@ import io.legado.app.ui.main.homepage.manage.SetListPage
import io.legado.app.ui.main.homepage.manage.SourceBrowseDetailPage import io.legado.app.ui.main.homepage.manage.SourceBrowseDetailPage
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu 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.menuItem.RoundDropdownMenuItem
@@ -127,38 +127,38 @@ fun <T> HomepageModuleManageSheet(
}, },
startAction = { startAction = {
if (showCustomSetAddModules) { if (showCustomSetAddModules) {
SmallIconButton( SmallPlainButton(
onClick = { showCustomSetAddModules = false }, onClick = { showCustomSetAddModules = false },
imageVector = Icons.AutoMirrored.Filled.ArrowBack icon = Icons.AutoMirrored.Filled.ArrowBack
) )
} else if (browsingSourceUrl != null || showSourceBrowser) { } else if (browsingSourceUrl != null || showSourceBrowser) {
SmallIconButton( SmallPlainButton(
onClick = { onClick = {
if (browsingDetail) browsingDetail = false if (browsingDetail) browsingDetail = false
else if (showSourceBrowser) showSourceBrowser = false else if (showSourceBrowser) showSourceBrowser = false
else browsingSourceUrl = null else browsingSourceUrl = null
}, },
imageVector = Icons.AutoMirrored.Filled.ArrowBack icon = Icons.AutoMirrored.Filled.ArrowBack
) )
} else if (selectingSetUrl != null) { } else if (selectingSetUrl != null) {
SmallIconButton( SmallPlainButton(
onClick = { selectingSetUrl = null }, onClick = { selectingSetUrl = null },
imageVector = Icons.AutoMirrored.Filled.ArrowBack icon = Icons.AutoMirrored.Filled.ArrowBack
) )
} }
}, },
endAction = { endAction = {
if (browsingDetail && browseTab == 2 && browseModuleType == "buttonGroup" && selectedKindTitles.isNotEmpty()) { if (browsingDetail && browseTab == 2 && browseModuleType == "buttonGroup" && selectedKindTitles.isNotEmpty()) {
SmallIconButton( SmallPlainButton(
onClick = { showAddButtonGroupDialog = true }, onClick = { showAddButtonGroupDialog = true },
imageVector = Icons.Default.Check icon = Icons.Default.Check
) )
} else if ((showSourceBrowser || browsingSourceUrl != null) && !browsingDetail) { } else if ((showSourceBrowser || browsingSourceUrl != null) && !browsingDetail) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
Box { Box {
SmallIconButton( SmallPlainButton(
onClick = { expanded = true }, onClick = { expanded = true },
imageVector = Icons.Default.FilterList icon = Icons.Default.FilterList
) )
RoundDropdownMenu( RoundDropdownMenu(
expanded = expanded, expanded = expanded,
@@ -72,7 +72,7 @@ import io.legado.app.ui.widget.components.LoadMoreFooter
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.book.SearchBookGridItem import io.legado.app.ui.widget.components.book.SearchBookGridItem
import io.legado.app.ui.widget.components.book.SearchBookPreviewSheet import io.legado.app.ui.widget.components.book.SearchBookPreviewSheet
import io.legado.app.ui.widget.components.button.SmallTonalIconButton import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
@@ -82,6 +82,7 @@ import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.topbar.TopBarActionButton import io.legado.app.ui.widget.components.topbar.TopBarActionButton
import io.legado.app.utils.sendToClip import io.legado.app.utils.sendToClip
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@@ -132,7 +133,7 @@ fun HomepageScreen(
} }
LaunchedEffect(viewModel) { LaunchedEffect(viewModel) {
viewModel.effects.collect { effect -> viewModel.effects.collectLatest { effect ->
when (effect) { when (effect) {
is HomepageEffect.NavigateToBookInfo -> is HomepageEffect.NavigateToBookInfo ->
onBookClick( onBookClick(
@@ -717,9 +718,9 @@ private fun ModuleHeader(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
if (onNavigate != null) { if (onNavigate != null) {
SmallTonalIconButton( SmallTonalButton(
onClick = onNavigate, onClick = onNavigate,
imageVector = Icons.AutoMirrored.Filled.ArrowForward icon = Icons.AutoMirrored.Filled.ArrowForward
) )
} }
} }
@@ -338,6 +338,12 @@ class HomepageViewModel(
} }
} }
override fun onCleared() {
super.onCleared()
loadJobs.values.forEach { it.cancel() }
loadJobs.clear()
}
private suspend fun syncModulesFromSource(source: BookSource) { private suspend fun syncModulesFromSource(source: BookSource) {
val json = source.homepageModules ?: return val json = source.homepageModules ?: return
ensureSetForSource(source.bookSourceUrl, source.bookSourceName) ensureSetForSource(source.bookSourceUrl, source.bookSourceName)
@@ -26,7 +26,7 @@ import io.legado.app.ui.main.homepage.HomepageModuleManageUi
import io.legado.app.ui.main.homepage.HomepageViewModel import io.legado.app.ui.main.homepage.HomepageViewModel
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.SecondaryButton import io.legado.app.ui.widget.components.button.SecondaryButton
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.divider.PillDivider import io.legado.app.ui.widget.components.divider.PillDivider
@@ -129,13 +129,13 @@ fun SetDetailPage(
} }
}, },
trailingAction = { trailingAction = {
SmallIconButton( SmallPlainButton(
onClick = { onEditModule(module) }, onClick = { onEditModule(module) },
imageVector = Icons.Default.Edit icon = Icons.Default.Edit
) )
SmallIconButton( SmallPlainButton(
onClick = { onRequestDeleteModule(module.id) }, onClick = { onRequestDeleteModule(module.id) },
imageVector = Icons.Default.Delete icon = Icons.Default.Delete
) )
} }
) )
@@ -160,13 +160,13 @@ fun SetDetailPage(
containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent, containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent,
onEnabledChange = { onToggleModule(module.id, it) }, onEnabledChange = { onToggleModule(module.id, it) },
trailingAction = { trailingAction = {
SmallIconButton( SmallPlainButton(
onClick = { onEditModule(module) }, onClick = { onEditModule(module) },
imageVector = Icons.Default.Edit icon = Icons.Default.Edit
) )
SmallIconButton( SmallPlainButton(
onClick = { onRequestDeleteModule(module.id) }, onClick = { onRequestDeleteModule(module.id) },
imageVector = Icons.Default.Delete icon = Icons.Default.Delete
) )
} }
) )
@@ -22,7 +22,7 @@ import io.legado.app.R
import io.legado.app.ui.main.homepage.HomepageSourceManageUi import io.legado.app.ui.main.homepage.HomepageSourceManageUi
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.SecondaryButton import io.legado.app.ui.widget.components.button.SecondaryButton
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.divider.PillDivider import io.legado.app.ui.widget.components.divider.PillDivider
import io.legado.app.utils.move import io.legado.app.utils.move
@@ -85,13 +85,13 @@ fun SetListPage(
} }
}, },
trailingAction = { trailingAction = {
SmallIconButton( SmallPlainButton(
onClick = { onRenameSet(set.sourceUrl) }, onClick = { onRenameSet(set.sourceUrl) },
imageVector = Icons.Default.DriveFileRenameOutline icon = Icons.Default.DriveFileRenameOutline
) )
SmallIconButton( SmallPlainButton(
onClick = { onDeleteSet(set.sourceUrl) }, onClick = { onDeleteSet(set.sourceUrl) },
imageVector = Icons.Default.Delete icon = Icons.Default.Delete
) )
} }
) )
@@ -32,7 +32,7 @@ import io.legado.app.ui.main.homepage.HomepageModuleManageUi
import io.legado.app.ui.main.homepage.HomepageViewModel import io.legado.app.ui.main.homepage.HomepageViewModel
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.SecondaryButton import io.legado.app.ui.widget.components.button.SecondaryButton
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
@@ -170,13 +170,13 @@ fun SourceBrowseDetailPage(
} }
}, },
trailingAction = { trailingAction = {
SmallIconButton( SmallPlainButton(
onClick = { onEditModule(module) }, onClick = { onEditModule(module) },
imageVector = Icons.Default.Edit icon = Icons.Default.Edit
) )
SmallIconButton( SmallPlainButton(
onClick = { onRequestDeleteModule(module.id) }, onClick = { onRequestDeleteModule(module.id) },
imageVector = Icons.Default.Delete icon = Icons.Default.Delete
) )
} }
) )
@@ -212,13 +212,13 @@ fun SourceBrowseDetailPage(
containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent, containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent,
onEnabledChange = { onToggleModule(module.id, it) }, onEnabledChange = { onToggleModule(module.id, it) },
trailingAction = { trailingAction = {
SmallIconButton( SmallPlainButton(
onClick = { onEditModule(module) }, onClick = { onEditModule(module) },
imageVector = Icons.Default.Edit icon = Icons.Default.Edit
) )
SmallIconButton( SmallPlainButton(
onClick = { onRequestDeleteModule(module.id) }, onClick = { onRequestDeleteModule(module.id) },
imageVector = Icons.Default.Delete icon = Icons.Default.Delete
) )
} }
) )
@@ -334,9 +334,9 @@ fun SourceBrowseDetailPage(
onToggleSelection = { showKindSelect = true }, onToggleSelection = { showKindSelect = true },
trailingAction = { trailingAction = {
if (isButtonGroup && selectedKindTitles.isNotEmpty()) { if (isButtonGroup && selectedKindTitles.isNotEmpty()) {
SmallIconButton( SmallPlainButton(
onClick = { onShowAddButtonGroupDialog() }, onClick = { onShowAddButtonGroupDialog() },
imageVector = Icons.Default.Check icon = Icons.Default.Check
) )
} }
} }
@@ -35,8 +35,6 @@ import androidx.compose.material.icons.filled.Source
import androidx.compose.material.icons.filled.Web import androidx.compose.material.icons.filled.Web
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -54,11 +52,12 @@ import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.ui.theme.adaptiveContentPadding import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SplicedColumnGroup import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.SmallTextButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@@ -81,7 +80,7 @@ fun MyScreen(
GlassMediumFlexibleTopAppBar( GlassMediumFlexibleTopAppBar(
title = stringResource(R.string.my), title = stringResource(R.string.my),
actions = { actions = {
IconButton( TopBarActionButton(
onClick = { onClick = {
onNavigate( onNavigate(
PrefClickEvent.ShowMd( PrefClickEvent.ShowMd(
@@ -89,10 +88,10 @@ fun MyScreen(
path = "appHelp" path = "appHelp"
) )
) )
} },
) {Icon( imageVector = Icons.AutoMirrored.Filled.HelpOutline,
Icons.AutoMirrored.Filled.HelpOutline, null) contentDescription = null
} )
}, },
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
@@ -251,22 +250,22 @@ fun WebServiceSettingBlock(
.padding(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 12.dp), .padding(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
) { ) {
SmallTextButton( SmallPlainButton(
text = stringResource(R.string.copy_url),
imageVector = Icons.Default.ContentCopy,
onClick = { onClick = {
onNavigate(PrefClickEvent.CopyUrl(uiState.webServiceAddress)) onNavigate(PrefClickEvent.CopyUrl(uiState.webServiceAddress))
} },
icon = Icons.Default.ContentCopy,
text = stringResource(R.string.copy_url)
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
SmallTextButton( SmallPlainButton(
text = stringResource(R.string.open_in_browser),
imageVector = Icons.Default.OpenInBrowser,
onClick = { onClick = {
onNavigate(PrefClickEvent.OpenUrl(uiState.webServiceAddress)) onNavigate(PrefClickEvent.OpenUrl(uiState.webServiceAddress))
} },
icon = Icons.Default.OpenInBrowser,
text = stringResource(R.string.open_in_browser)
) )
} }
} }
@@ -5,8 +5,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -19,71 +17,54 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChip
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.ui.res.stringResource
import io.legado.app.R
import io.legado.app.ui.widget.components.AppFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.animateFloatingActionButton import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import io.legado.app.R
import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.MediumIconButton
import io.legado.app.ui.widget.components.button.ToggleChip import io.legado.app.ui.widget.components.button.ToggleChip
import io.legado.app.ui.widget.components.topbar.TopBarActionButton import io.legado.app.ui.widget.components.button.series.MediumPlainButton
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import io.legado.app.ui.widget.components.checkBox.CheckboxItem import io.legado.app.ui.widget.components.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu 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.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@Composable @Composable
@@ -369,9 +350,9 @@ fun GroupSelector(
} }
} }
} }
MediumIconButton( MediumPlainButton(
onClick = onManageClick, onClick = onManageClick,
imageVector = Icons.Default.Settings icon = Icons.Default.Settings
) )
} }
} }
@@ -1,7 +1,6 @@
package io.legado.app.ui.rss.article package io.legado.app.ui.rss.article
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -11,13 +10,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
import androidx.compose.material.icons.automirrored.filled.Login import androidx.compose.material.icons.automirrored.filled.Login
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
@@ -25,7 +22,6 @@ import androidx.compose.material.icons.filled.CleaningServices
import androidx.compose.material.icons.filled.Dataset import androidx.compose.material.icons.filled.Dataset
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Style import androidx.compose.material.icons.filled.Style
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -39,6 +35,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -51,10 +48,9 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton import io.legado.app.ui.widget.components.button.series.SmallToggleButton
import io.legado.app.ui.widget.components.topbar.TopBarActionButton import io.legado.app.ui.widget.components.button.series.ToggleStyle
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.icon.AppIcons import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
@@ -65,6 +61,8 @@ import io.legado.app.ui.widget.components.tabRow.AppTabRow
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -242,10 +240,11 @@ fun RssSortScreen(
if (BookshelfConfig.shouldShowExpandButton) { if (BookshelfConfig.shouldShowExpandButton) {
Box { Box {
SmallOutlinedIconToggleButton( SmallToggleButton(
checked = showGroupMenu, checked = showGroupMenu,
onCheckedChange = { showGroupMenu = it }, onCheckedChange = { showGroupMenu = it },
imageVector = Icons.AutoMirrored.Filled.FormatListBulleted, style = ToggleStyle.Outlined,
icon = Icons.AutoMirrored.Filled.FormatListBulleted,
contentDescription = stringResource(R.string.group_manage) contentDescription = stringResource(R.string.group_manage)
) )
RoundDropdownMenu( RoundDropdownMenu(
@@ -321,9 +320,9 @@ private fun RssReadRecordSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = stringResource(R.string.read_record), title = stringResource(R.string.read_record),
endAction = { endAction = {
SmallIconButton( SmallPlainButton(
onClick = onClear, onClick = onClear,
imageVector = Icons.Default.CleaningServices, icon = Icons.Default.CleaningServices,
contentDescription = stringResource(R.string.clear) contentDescription = stringResource(R.string.clear)
) )
} }
@@ -33,7 +33,7 @@ import io.legado.app.data.entities.RssStar
import io.legado.app.ui.theme.adaptiveContentPadding import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.ActionItem import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.dialog.TextListInputDialog import io.legado.app.ui.widget.components.dialog.TextListInputDialog
import io.legado.app.ui.widget.components.divider.PillDivider import io.legado.app.ui.widget.components.divider.PillDivider
@@ -229,9 +229,9 @@ fun RssFavoritesScreen(
val openAction = { val openAction = {
onOpenRead(rssStar.title, rssStar.origin, rssStar.link, null) onOpenRead(rssStar.title, rssStar.origin, rssStar.link, null)
} }
SmallIconButton( SmallPlainButton(
onClick = openAction, onClick = openAction,
imageVector = Icons.AutoMirrored.Filled.OpenInNew, icon = Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = "Open" contentDescription = "Open"
) )
}, },
@@ -58,7 +58,7 @@ import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon 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.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
@@ -422,9 +422,9 @@ private fun FavoriteEditSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = stringResource(R.string.favorite), title = stringResource(R.string.favorite),
endAction = { endAction = {
SmallIconButton( SmallPlainButton(
onClick = onDelete, onClick = onDelete,
imageVector = Icons.Default.CleaningServices, icon = Icons.Default.CleaningServices,
contentDescription = stringResource(R.string.delete) contentDescription = stringResource(R.string.delete)
) )
} }
@@ -5,7 +5,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -13,13 +12,10 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -45,7 +41,7 @@ import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.DraggableSelectionHandler import io.legado.app.ui.widget.components.DraggableSelectionHandler
import io.legado.app.ui.widget.components.GroupManageBottomSheet import io.legado.app.ui.widget.components.GroupManageBottomSheet
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.dialog.TextListInputDialog import io.legado.app.ui.widget.components.dialog.TextListInputDialog
import io.legado.app.ui.widget.components.divider.PillDivider import io.legado.app.ui.widget.components.divider.PillDivider
@@ -364,9 +360,9 @@ fun RssSourceScreen(
onEnabledChange = { enabled -> viewModel.update(item.source.copy(enabled = enabled)) }, onEnabledChange = { enabled -> viewModel.update(item.source.copy(enabled = enabled)) },
onClickEdit = { onEditSource(item.source) }, onClickEdit = { onEditSource(item.source) },
trailingAction = { trailingAction = {
SmallIconButton( SmallPlainButton(
onClick = { showDeleteRuleDialog = item.source }, onClick = { showDeleteRuleDialog = item.source },
imageVector = Icons.Default.Delete icon = Icons.Default.Delete
) )
} }
) )
@@ -17,7 +17,6 @@ import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.animateFloatingActionButton import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -46,6 +45,7 @@ import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer
import io.legado.app.ui.widget.components.checkBox.CheckboxItem import io.legado.app.ui.widget.components.checkBox.CheckboxItem
@@ -165,9 +165,11 @@ fun RuleSubScreen(
} }
}, },
trailingAction = { trailingAction = {
IconButton(onClick = { showEditDialog = ruleSub }) { SmallPlainButton(
Icon(Icons.Default.Edit, contentDescription = "Edit") onClick = { showEditDialog = ruleSub },
} icon = Icons.Default.Edit,
contentDescription = "Edit"
)
}, },
dropdownContent = { dismiss -> dropdownContent = { dismiss ->
RoundDropdownMenuItem( RoundDropdownMenuItem(
@@ -35,6 +35,14 @@ fun Modifier.adaptiveVerticalPadding(): Modifier {
return this.padding(horizontal = horizontal) return this.padding(horizontal = horizontal)
} }
@Composable
fun adaptiveHorizonalPadding(): PaddingValues {
val horizontal = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 16.dp
return PaddingValues(
horizontal = horizontal
)
}
@Composable @Composable
fun adaptiveContentPaddingOnlyVertical( fun adaptiveContentPaddingOnlyVertical(
top: Dp, top: Dp,
@@ -0,0 +1,41 @@
package io.legado.app.ui.widget.components
import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.Slider as MiuixSlider
@Composable
fun AppSlider(
value: Float,
onValueChange: (Float) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
steps: Int = 0,
onValueChangeFinished: (() -> Unit)? = null,
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
MiuixSlider(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
valueRange = valueRange,
steps = steps,
onValueChangeFinished = onValueChangeFinished
)
} else {
Slider(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
valueRange = valueRange,
steps = steps,
onValueChangeFinished = onValueChangeFinished
)
}
}
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import io.legado.app.ui.widget.components.progressIndicator.AppContainedLoadingIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -24,8 +23,9 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.icon.AppIcons import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.button.SmallTonalTextButton import io.legado.app.ui.widget.components.progressIndicator.AppContainedLoadingIndicator
import io.legado.app.ui.widget.components.text.AnimatedTextLine import io.legado.app.ui.widget.components.text.AnimatedTextLine
@Composable @Composable
@@ -84,10 +84,10 @@ fun EmptyMessage(
if (buttonText != null && onButtonClick != null) { if (buttonText != null && onButtonClick != null) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
SmallTonalTextButton( SmallTonalButton(
onClick = onButtonClick, onClick = onButtonClick,
text = buttonText, text = buttonText,
imageVector = buttonImageVector icon = buttonImageVector
) )
} }
} }
@@ -26,7 +26,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.widget.components.button.SmallTextButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.SettingItem import io.legado.app.ui.widget.components.settingItem.SettingItem
@@ -118,13 +118,13 @@ private fun GroupItem(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
) { ) {
SmallTextButton( SmallPlainButton(
text = stringResource(id = R.string.ok),
imageVector = Icons.Default.Check,
onClick = { onClick = {
onUpdateGroup(group, state.text.toString()) onUpdateGroup(group, state.text.toString())
expanded = false expanded = false
} },
icon = Icons.Default.Check,
text = stringResource(id = R.string.ok)
) )
} }
} }
@@ -15,7 +15,7 @@ import androidx.compose.ui.unit.dp
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonParser import com.google.gson.JsonParser
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
@@ -37,17 +37,17 @@ fun JsonRawEditor(
style = LegadoTheme.typography.labelMediumEmphasized style = LegadoTheme.typography.labelMediumEmphasized
) )
Row { Row {
SmallIconButton( SmallPlainButton(
onClick = { onClick = {
runCatching { runCatching {
val jsonElement = JsonParser.parseString(value) val jsonElement = JsonParser.parseString(value)
onValueChange(GSON.toJson(jsonElement)) onValueChange(GSON.toJson(jsonElement))
} }
}, },
imageVector = Icons.Default.AutoFixHigh, icon = Icons.Default.AutoFixHigh,
contentDescription = "格式化" contentDescription = "格式化"
) )
SmallIconButton( SmallPlainButton(
onClick = { onClick = {
runCatching { runCatching {
val jsonElement = JsonParser.parseString(value) val jsonElement = JsonParser.parseString(value)
@@ -55,7 +55,7 @@ fun JsonRawEditor(
onValueChange(compactGson.toJson(jsonElement)) onValueChange(compactGson.toJson(jsonElement))
} }
}, },
imageVector = Icons.Default.Compress, icon = Icons.Default.Compress,
contentDescription = "压缩" contentDescription = "压缩"
) )
} }
@@ -10,7 +10,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.SmallOutlinedIconButton import io.legado.app.ui.widget.components.button.series.SmallOutlinedButton
import io.legado.app.ui.widget.components.card.TextCard import io.legado.app.ui.widget.components.card.TextCard
@Composable @Composable
@@ -26,12 +26,12 @@ fun ValueStepper(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
SmallOutlinedIconButton( SmallOutlinedButton(
onClick = { onClick = {
val newValue = (value.toInt() - 1).toFloat().coerceIn(valueRange) val newValue = (value.toInt() - 1).toFloat().coerceIn(valueRange)
onValueChange(newValue) onValueChange(newValue)
}, },
imageVector = Icons.Default.Remove icon = Icons.Default.Remove
) )
TextCard( TextCard(
cornerRadius = 8.dp, cornerRadius = 8.dp,
@@ -41,12 +41,12 @@ fun ValueStepper(
backgroundColor = LegadoTheme.colorScheme.surfaceContainer, backgroundColor = LegadoTheme.colorScheme.surfaceContainer,
contentColor = LegadoTheme.colorScheme.onSurface contentColor = LegadoTheme.colorScheme.onSurface
) )
SmallOutlinedIconButton( SmallOutlinedButton(
onClick = { onClick = {
val newValue = (value.toInt() + 1).toFloat().coerceIn(valueRange) val newValue = (value.toInt() + 1).toFloat().coerceIn(valueRange)
onValueChange(newValue) onValueChange(newValue)
}, },
imageVector = Icons.Default.Add icon = Icons.Default.Add
) )
} }
} }
@@ -1,43 +0,0 @@
package io.legado.app.ui.widget.components.button
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.material3.TextButton
import io.legado.app.ui.widget.components.progressIndicator.AppContainedLoadingIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import io.legado.app.ui.widget.components.text.AppText
@Composable
fun AnimatedTextButton(
isLoading: Boolean,
onClick: () -> Unit,
text: String,
modifier: Modifier = Modifier
) {
TextButton(
onClick = onClick,
enabled = !isLoading,
modifier = modifier.animateContentSize()
) {
AnimatedContent(
targetState = isLoading,
contentAlignment = Alignment.Center,
transitionSpec = {
fadeIn(tween(200)) togetherWith fadeOut(tween(200))
},
label = "ButtonLoading"
) { loading ->
if (loading) {
AppContainedLoadingIndicator()
} else {
AppText(text)
}
}
}
}
@@ -1,217 +0,0 @@
package io.legado.app.ui.widget.components.button
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TonalToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
@Composable
fun MediumIconButton(
onClick: () -> Unit,
imageVector: ImageVector,
tint: Color = LegadoTheme.colorScheme.onSurface,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
MiuixIconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled
) {
MiuixIcon(
imageVector = imageVector,
contentDescription = contentDescription,
tint = tint
)
}
} else {
IconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled
) {
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
tint = tint
)
}
}
}
@Composable
fun MediumOutlinedIconButton(
onClick: () -> Unit,
imageVector: ImageVector,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
MiuixIconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
backgroundColor = LegadoTheme.colorScheme.surfaceContainerHigh
) {
MiuixIcon(
imageVector = imageVector,
contentDescription = contentDescription
)
}
} else {
OutlinedIconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
border = ButtonDefaults.outlinedButtonBorder()
) {
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
tint = LegadoTheme.colorScheme.onSurface
)
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MediumAnimatedActionButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
iconChecked: ImageVector,
iconUnchecked: ImageVector,
activeText: String,
inactiveText: String,
modifier: Modifier = Modifier
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
val containerColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainerHigh,
animationSpec = tween(150),
label = "MiuixActionButtonContainer"
)
val contentColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.onPrimaryContainer else MiuixTheme.colorScheme.onSurface,
animationSpec = tween(150),
label = "MiuixActionButtonContent"
)
AnimatedActionButtonCore(
checked = checked,
onCheckedChange = onCheckedChange,
iconChecked = iconChecked,
iconUnchecked = iconUnchecked,
activeText = activeText,
inactiveText = inactiveText,
modifier = modifier,
iconSize = 24.dp,
textStyle = LegadoTheme.typography.labelMedium,
textStartPadding = 8.dp,
contentColor = contentColor,
button = { buttonModifier, onToggle, content ->
MiuixIconButton(
onClick = { onToggle(!checked) },
modifier = buttonModifier,
backgroundColor = containerColor
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 8.dp),
content = content
)
}
},
icon = { imageVector, iconModifier, tint ->
MiuixIcon(
imageVector = imageVector,
contentDescription = null,
modifier = iconModifier,
tint = tint ?: Color.Unspecified
)
},
text = { label, textModifier, style, color ->
MiuixText(
text = label,
color = color ?: Color.Unspecified,
style = style,
modifier = textModifier,
maxLines = 1,
softWrap = false
)
}
)
} else {
AnimatedActionButtonCore(
checked = checked,
onCheckedChange = onCheckedChange,
iconChecked = iconChecked,
iconUnchecked = iconUnchecked,
activeText = activeText,
inactiveText = inactiveText,
modifier = modifier,
iconSize = 24.dp,
textStyle = LegadoTheme.typography.labelMedium,
textStartPadding = 8.dp,
button = { buttonModifier, onToggle, content ->
TonalToggleButton(
checked = checked,
onCheckedChange = onToggle,
modifier = buttonModifier,
contentPadding = PaddingValues(horizontal = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
content = content
)
}
},
icon = { imageVector, iconModifier, _ ->
AnimatedIcon(
imageVector = imageVector,
contentDescription = null,
modifier = iconModifier
)
},
text = { label, textModifier, style, color ->
Text(
text = label,
modifier = textModifier,
style = style,
color = color ?: Color.Unspecified,
maxLines = 1,
softWrap = false
)
}
)
}
}
@@ -1,66 +0,0 @@
package io.legado.app.ui.widget.components.button
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
@Composable
fun MediumOutlinedButton(
onClick: () -> Unit,
imageVector: ImageVector,
text: String,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
MiuixIconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
backgroundColor = LegadoTheme.colorScheme.surfaceContainerHigh
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
MiuixIcon(
imageVector = imageVector,
contentDescription = contentDescription
)
MiuixText(text = text)
}
}
} else {
OutlinedButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
border = ButtonDefaults.outlinedButtonBorder()
) {
Icon(
imageVector = imageVector,
contentDescription = contentDescription
)
Text(
text = text,
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
@@ -1,356 +0,0 @@
package io.legado.app.ui.widget.components.button
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.IconToggleButtonShapes
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.OutlinedIconToggleButton
import androidx.compose.material3.Text
import androidx.compose.material3.TonalToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
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.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
private val SmallMiuixButtonSize = 32.dp
private val SmallMiuixIconSize = 18.dp
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun smallContainerSize() = IconButtonDefaults.extraSmallContainerSize(
IconButtonDefaults.IconButtonWidthOption.Uniform
)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private val smallIconSize: androidx.compose.ui.unit.Dp
get() = IconButtonDefaults.extraSmallIconSize
@Composable
private fun SmallNoMinTouchTarget(content: @Composable () -> Unit) {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
content()
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SmallIconButton(
onClick: () -> Unit,
imageVector: ImageVector,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
MiuixIconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled
) {
MiuixIcon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier.size(16.dp),
)
}
} else {
SmallNoMinTouchTarget {
IconButton(
onClick = onClick,
modifier = modifier.size(smallContainerSize()),
enabled = enabled,
shape = IconButtonDefaults.extraSmallRoundShape,
) {
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize),
)
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SmallOutlinedIconButton(
onClick: () -> Unit,
imageVector: ImageVector,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
MiuixIconButton(
onClick = onClick,
modifier = modifier.size(SmallMiuixButtonSize),
enabled = enabled,
backgroundColor = LegadoTheme.colorScheme.surfaceContainer
) {
MiuixIcon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier.size(SmallMiuixIconSize)
)
}
} else {
SmallNoMinTouchTarget {
OutlinedIconButton(
onClick = onClick,
modifier = modifier.size(smallContainerSize()),
enabled = enabled,
shapes = IconButtonDefaults.shapes(),
border = ButtonDefaults.outlinedButtonBorder()
) {
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize)
)
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SmallTonalIconButton(
onClick: () -> Unit,
imageVector: ImageVector,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
MiuixIconButton(
onClick = onClick,
modifier = modifier.size(SmallMiuixButtonSize),
enabled = enabled,
backgroundColor = LegadoTheme.colorScheme.surfaceContainer
) {
MiuixIcon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier.size(SmallMiuixIconSize)
)
}
} else {
SmallNoMinTouchTarget {
FilledTonalIconButton(
onClick = onClick,
modifier = modifier.size(smallContainerSize()),
enabled = enabled,
shapes = IconButtonDefaults.shapes(),
colors = IconButtonDefaults.filledTonalIconButtonColors()
) {
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize)
)
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SmallOutlinedIconToggleButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
imageVector: ImageVector,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
val containerColor by animateColorAsState(
targetValue = if (checked) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainer,
animationSpec = tween(150),
label = "MiuixToggleContainerColor"
)
val iconTint by animateColorAsState(
targetValue = if (checked) LegadoTheme.colorScheme.onPrimaryContainer else LegadoTheme.colorScheme.onSurfaceVariant,
animationSpec = tween(150),
label = "MiuixToggleIconTint"
)
MiuixIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier.size(SmallMiuixButtonSize),
enabled = enabled,
backgroundColor = containerColor
) {
MiuixIcon(
imageVector = imageVector,
contentDescription = contentDescription,
tint = iconTint,
modifier = Modifier.size(SmallMiuixIconSize)
)
}
} else {
val defaultShape = IconButtonDefaults.extraSmallRoundShape
val pressedShape = IconButtonDefaults.extraSmallPressedShape
val checkedShape = IconButtonDefaults.extraSmallSelectedRoundShape
val toggleShapes = remember(defaultShape, checkedShape) {
IconToggleButtonShapes(
shape = defaultShape,
pressedShape = pressedShape,
checkedShape = checkedShape
)
}
SmallNoMinTouchTarget {
OutlinedIconToggleButton(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier.size(smallContainerSize()),
enabled = enabled,
shapes = toggleShapes
) {
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize),
)
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SmallAnimatedActionButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
iconChecked: ImageVector,
iconUnchecked: ImageVector,
activeText: String,
inactiveText: String,
modifier: Modifier = Modifier
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
val containerColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainer,
animationSpec = tween(150),
label = "MiuixActionButtonContainer"
)
AnimatedActionButtonCore(
checked = checked,
onCheckedChange = onCheckedChange,
iconChecked = iconChecked,
iconUnchecked = iconUnchecked,
activeText = activeText,
inactiveText = inactiveText,
modifier = modifier,
iconSize = 18.dp,
textStyle = LegadoTheme.typography.labelSmall,
textStartPadding = 6.dp,
button = { buttonModifier, onToggle, content ->
MiuixIconButton(
onClick = { onToggle(!checked) },
modifier = buttonModifier,
backgroundColor = containerColor
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
content = content
)
}
},
icon = { imageVector, iconModifier, tint ->
MiuixIcon(
imageVector = imageVector,
contentDescription = null,
modifier = iconModifier,
tint = tint ?: Color.Unspecified
)
},
text = { label, textModifier, style, color ->
MiuixText(
text = label,
style = style,
color = color ?: Color.Unspecified,
modifier = textModifier,
maxLines = 1,
softWrap = false
)
}
)
} else {
SmallNoMinTouchTarget {
AnimatedActionButtonCore(
checked = checked,
onCheckedChange = onCheckedChange,
iconChecked = iconChecked,
iconUnchecked = iconUnchecked,
activeText = activeText,
inactiveText = inactiveText,
modifier = modifier.height(36.dp),
iconSize = 16.dp,
textStyle = LegadoTheme.typography.labelSmall,
textStartPadding = 6.dp,
button = { buttonModifier, onToggle, content ->
TonalToggleButton(
checked = checked,
onCheckedChange = onToggle,
modifier = buttonModifier,
contentPadding = PaddingValues(horizontal = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
content = content
)
}
},
icon = { imageVector, iconModifier, _ ->
AnimatedIcon(
imageVector = imageVector,
contentDescription = null,
modifier = iconModifier
)
},
text = { label, textModifier, style, color ->
Text(
text = label,
style = style,
color = color ?: Color.Unspecified,
modifier = textModifier,
maxLines = 1,
softWrap = false
)
}
)
}
}
}
@@ -1,142 +0,0 @@
package io.legado.app.ui.widget.components.button
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.text.AppText
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
@Composable
fun SmallTextButton(
text: String,
imageVector: ImageVector,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
Card(
onClick = onClick,
modifier = modifier,
showIndication = true,
colors = CardDefaults.defaultColors(
color = LegadoTheme.colorScheme.surfaceVariant,
contentColor = LegadoTheme.colorScheme.onSurfaceVariant
)
) {
Row(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically
) {
MiuixIcon(
imageVector = imageVector,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
AppText(
text = text,
style = LegadoTheme.typography.labelMedium
)
}
}
} else {
TextButton(
onClick = onClick,
modifier = modifier,
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
shape = MaterialTheme.shapes.small
) {
Icon(
imageVector = imageVector,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(4.dp))
AppText(
text = text,
style = LegadoTheme.typography.labelMedium
)
}
}
}
@Composable
fun SmallTonalTextButton(
text: String? = null,
imageVector: ImageVector? = null,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
Card(
onClick = onClick,
modifier = modifier,
showIndication = true,
colors = CardDefaults.defaultColors(
color = LegadoTheme.colorScheme.surfaceContainer,
contentColor = LegadoTheme.colorScheme.onSurfaceVariant
)
) {
Row(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically
) {
if (imageVector != null) {
MiuixIcon(
imageVector = imageVector,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
if (text != null) {
AppText(
text = text,
style = LegadoTheme.typography.labelMedium
)
}
}
}
} else {
FilledTonalButton(
onClick = onClick,
modifier = modifier,
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)
) {
if (imageVector != null) {
Icon(
imageVector = imageVector,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
Spacer(Modifier.width(4.dp))
if (text != null) {
AppText(
text = text,
style = LegadoTheme.typography.labelMedium
)
}
}
}
}
@@ -1,4 +1,4 @@
package io.legado.app.ui.widget.components.button package io.legado.app.ui.widget.components.button.series
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
@@ -1,4 +1,4 @@
package io.legado.app.ui.widget.components.button package io.legado.app.ui.widget.components.button.series
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@@ -0,0 +1,173 @@
package io.legado.app.ui.widget.components.button.series
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TonalToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MediumAnimatedButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
iconChecked: ImageVector? = null,
text: String? = null,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
val containerColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainer,
animationSpec = tween(150),
label = "MiuixAnimatedContainerColor"
)
val contentColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.onPrimaryContainer else MiuixTheme.colorScheme.onSurface,
animationSpec = tween(150),
label = "MiuixAnimatedContentColor"
)
if (text != null) {
AnimatedActionButtonCore(
checked = checked,
onCheckedChange = onCheckedChange,
iconChecked = iconChecked ?: icon!!,
iconUnchecked = icon ?: iconChecked!!,
activeText = text,
inactiveText = text,
modifier = modifier,
iconSize = 24.dp,
textStyle = LegadoTheme.typography.labelMedium,
textStartPadding = 8.dp,
contentColor = contentColor,
button = { buttonModifier, onToggle, content ->
MiuixIconButton(
onClick = { onToggle(!checked) },
modifier = buttonModifier,
backgroundColor = containerColor
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 8.dp),
content = content
)
}
},
icon = { imageVector, iconModifier, tint ->
MiuixIcon(
tint = tint ?: Color.Unspecified,
imageVector = imageVector,
contentDescription = null,
modifier = iconModifier
)
},
text = { label, textModifier, textStyle, color ->
MiuixText(
text = label,
color = color ?: Color.Unspecified,
style = textStyle,
modifier = textModifier,
maxLines = 1,
softWrap = false
)
}
)
} else {
MiuixIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier,
enabled = enabled,
backgroundColor = containerColor
) {
MiuixIcon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = contentDescription,
tint = contentColor
)
}
}
} else {
if (text != null) {
AnimatedActionButtonCore(
checked = checked,
onCheckedChange = onCheckedChange,
iconChecked = iconChecked ?: icon!!,
iconUnchecked = icon ?: iconChecked!!,
activeText = text,
inactiveText = text,
modifier = modifier.height(36.dp),
iconSize = 20.dp,
textStyle = LegadoTheme.typography.labelMedium,
textStartPadding = 8.dp,
button = { buttonModifier, onToggle, content ->
TonalToggleButton(
modifier = buttonModifier,
contentPadding = PaddingValues(horizontal = 8.dp),
checked = checked,
onCheckedChange = onToggle
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
content = content
)
}
},
icon = { imageVector, iconModifier, _ ->
AnimatedIcon(
imageVector = imageVector,
contentDescription = null,
modifier = iconModifier
)
},
text = { label, textModifier, textStyle, color ->
Text(
text = label,
style = textStyle,
color = color ?: Color.Unspecified,
modifier = textModifier,
maxLines = 1,
softWrap = false
)
}
)
} else {
FilledTonalIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier,
enabled = enabled,
) {
Icon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = contentDescription,
)
}
}
}
}
@@ -0,0 +1,78 @@
package io.legado.app.ui.widget.components.button.series
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
@Composable
fun MediumOutlinedButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
text: String? = null,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
if (icon != null && text == null) {
MiuixIconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
backgroundColor = LegadoTheme.colorScheme.surfaceContainerHigh
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription
)
}
} else {
Card(
onClick = onClick,
modifier = modifier,
showIndication = true,
colors = CardDefaults.defaultColors(
color = LegadoTheme.colorScheme.surfaceContainerHigh,
contentColor = LegadoTheme.colorScheme.onSurfaceVariant
)
) {
MediumButtonContent(icon, text, contentDescription)
}
}
} else {
if (icon != null && text == null) {
OutlinedIconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
border = ButtonDefaults.outlinedButtonBorder()
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = LegadoTheme.colorScheme.onSurface
)
}
} else {
OutlinedButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
border = ButtonDefaults.outlinedButtonBorder()
) {
MediumButtonContent(icon, text, contentDescription)
}
}
}
}
@@ -0,0 +1,121 @@
package io.legado.app.ui.widget.components.button.series
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.text.AppText
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
@Composable
internal fun MediumButtonContent(
icon: ImageVector?,
text: String?,
contentDescription: String?
) {
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically
) {
if (icon != null) {
if (isMiuix) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription
)
} else {
Icon(
imageVector = icon,
contentDescription = contentDescription
)
}
}
if (text != null) {
if (isMiuix) {
MiuixText(text = text)
} else {
AppText(text = text)
}
}
}
}
@Composable
fun MediumPlainButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
text: String? = null,
tint: androidx.compose.ui.graphics.Color = LegadoTheme.colorScheme.onSurface,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
if (icon != null && text == null) {
MiuixIconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription,
tint = tint
)
}
} else {
Card(
onClick = onClick,
modifier = modifier,
showIndication = true,
colors = CardDefaults.defaultColors(
color = LegadoTheme.colorScheme.surfaceVariant,
contentColor = LegadoTheme.colorScheme.onSurfaceVariant
)
) {
MediumButtonContent(icon, text, contentDescription)
}
}
} else {
if (icon != null && text == null) {
IconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = tint
)
}
} else {
Card(
onClick = onClick,
modifier = modifier,
showIndication = true,
colors = CardDefaults.defaultColors(
color = LegadoTheme.colorScheme.surfaceVariant,
contentColor = LegadoTheme.colorScheme.onSurfaceVariant
)
) {
MediumButtonContent(icon, text, contentDescription)
}
}
}
}
@@ -0,0 +1,152 @@
package io.legado.app.ui.widget.components.button.series
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MediumToggleButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
style: ToggleStyle = ToggleStyle.Outlined,
icon: ImageVector? = null,
iconChecked: ImageVector? = null,
text: String? = null,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
val containerColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainerHigh,
animationSpec = tween(150),
label = "MiuixToggleContainerColor"
)
val contentColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.onPrimaryContainer else MiuixTheme.colorScheme.onSurface,
animationSpec = tween(150),
label = "MiuixToggleContentColor"
)
if (text != null) {
MiuixIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier,
enabled = enabled,
backgroundColor = containerColor
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 8.dp)
) {
MiuixIcon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = null,
tint = contentColor
)
MiuixText(
text = text,
color = contentColor,
style = LegadoTheme.typography.labelMedium,
modifier = Modifier.padding(start = 8.dp),
maxLines = 1,
softWrap = false
)
}
}
} else {
MiuixIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier,
enabled = enabled,
backgroundColor = containerColor
) {
MiuixIcon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = contentDescription,
tint = contentColor
)
}
}
} else {
if (text != null) {
ToggleButton(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier,
enabled = enabled,
contentPadding = PaddingValues(horizontal = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = null
)
Text(
text = text,
style = LegadoTheme.typography.labelMedium,
modifier = Modifier.padding(start = 8.dp),
maxLines = 1,
softWrap = false
)
}
}
} else {
when (style) {
ToggleStyle.Outlined -> {
FilledTonalButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier,
enabled = enabled,
) {
Icon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = contentDescription,
)
}
}
ToggleStyle.Tonal -> {
FilledTonalIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier,
enabled = enabled,
) {
Icon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = contentDescription,
)
}
}
}
}
}
}
@@ -0,0 +1,74 @@
package io.legado.app.ui.widget.components.button.series
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
@Composable
fun MediumTonalButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
text: String? = null,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
if (icon != null && text == null) {
MiuixIconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
backgroundColor = LegadoTheme.colorScheme.surfaceContainer
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription
)
}
} else {
Card(
onClick = onClick,
modifier = modifier,
showIndication = true,
colors = CardDefaults.defaultColors(
color = LegadoTheme.colorScheme.surfaceContainer,
contentColor = LegadoTheme.colorScheme.onSurfaceVariant
)
) {
MediumButtonContent(icon, text, contentDescription)
}
}
} else {
if (icon != null && text == null) {
FilledTonalIconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled
) {
Icon(
imageVector = icon,
contentDescription = contentDescription
)
}
} else {
FilledTonalButton(
onClick = onClick,
modifier = modifier,
enabled = enabled
) {
MediumButtonContent(icon, text, contentDescription)
}
}
}
}
@@ -0,0 +1,176 @@
package io.legado.app.ui.widget.components.button.series
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.TonalToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SmallAnimatedButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
iconChecked: ImageVector? = null,
text: String? = null,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
val containerColor by animateColorAsState(
targetValue = if (checked) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainer,
animationSpec = tween(150),
label = "MiuixAnimatedContainerColor"
)
val iconTint by animateColorAsState(
targetValue = if (checked) LegadoTheme.colorScheme.onPrimaryContainer else LegadoTheme.colorScheme.onSurfaceVariant,
animationSpec = tween(150),
label = "MiuixAnimatedIconTint"
)
if (text != null) {
AnimatedActionButtonCore(
checked = checked,
onCheckedChange = onCheckedChange,
iconChecked = iconChecked ?: icon!!,
iconUnchecked = icon ?: iconChecked!!,
activeText = text,
inactiveText = text,
modifier = modifier,
iconSize = 18.dp,
textStyle = LegadoTheme.typography.labelSmall,
textStartPadding = 6.dp,
contentColor = iconTint,
button = { buttonModifier, onToggle, content ->
MiuixIconButton(
onClick = { onToggle(!checked) },
modifier = buttonModifier,
backgroundColor = containerColor
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
content = content
)
}
},
icon = { imageVector, iconModifier, tint ->
MiuixIcon(
imageVector = imageVector,
contentDescription = null,
modifier = iconModifier,
tint = tint ?: Color.Unspecified
)
},
text = { label, textModifier, textStyle, color ->
MiuixText(
text = label,
style = textStyle,
color = color ?: Color.Unspecified,
modifier = textModifier,
maxLines = 1,
softWrap = false
)
}
)
} else {
MiuixIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier.size(SmallMiuixButtonSize),
enabled = enabled,
backgroundColor = containerColor
) {
MiuixIcon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = contentDescription,
tint = iconTint,
modifier = Modifier.size(SmallMiuixIconSize)
)
}
}
} else {
SmallNoMinTouchTarget {
if (text != null) {
AnimatedActionButtonCore(
checked = checked,
onCheckedChange = onCheckedChange,
iconChecked = iconChecked ?: icon!!,
iconUnchecked = icon ?: iconChecked!!,
activeText = text,
inactiveText = text,
modifier = modifier.height(36.dp),
iconSize = 16.dp,
textStyle = LegadoTheme.typography.labelSmall,
textStartPadding = 6.dp,
button = { buttonModifier, onToggle, content ->
TonalToggleButton(
checked = checked,
onCheckedChange = onToggle,
modifier = buttonModifier,
contentPadding = PaddingValues(horizontal = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
content = content
)
}
},
icon = { imageVector, iconModifier, _ ->
AnimatedIcon(
imageVector = imageVector,
contentDescription = null,
modifier = iconModifier
)
},
text = { label, textModifier, textStyle, color ->
androidx.compose.material3.Text(
text = label,
style = textStyle,
color = color ?: Color.Unspecified,
modifier = textModifier,
maxLines = 1,
softWrap = false
)
}
)
} else {
FilledTonalIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier.size(smallContainerSize()),
enabled = enabled,
) {
Icon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize),
)
}
}
}
}
}
@@ -0,0 +1,93 @@
package io.legado.app.ui.widget.components.button.series
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SmallOutlinedButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
text: String? = null,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
if (icon != null && text == null) {
MiuixIconButton(
onClick = onClick,
modifier = modifier.size(SmallMiuixButtonSize),
enabled = enabled,
backgroundColor = LegadoTheme.colorScheme.surfaceContainer
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(SmallMiuixIconSize)
)
}
} else {
Card(
onClick = onClick,
modifier = modifier,
showIndication = true,
colors = CardDefaults.defaultColors(
color = LegadoTheme.colorScheme.surfaceContainer,
contentColor = LegadoTheme.colorScheme.onSurfaceVariant
)
) {
SmallButtonContent(icon, text, contentDescription)
}
}
} else {
SmallNoMinTouchTarget {
when {
icon != null && text == null -> {
OutlinedIconButton(
onClick = onClick,
modifier = modifier.size(smallContainerSize()),
enabled = enabled,
shapes = IconButtonDefaults.shapes(),
border = ButtonDefaults.outlinedButtonBorder()
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize)
)
}
}
else -> {
OutlinedButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
border = ButtonDefaults.outlinedButtonBorder()
) {
SmallButtonContent(icon, text, contentDescription)
}
}
}
}
}
}
@@ -0,0 +1,161 @@
package io.legado.app.ui.widget.components.button.series
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.text.AppText
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
internal val SmallMiuixButtonSize = 32.dp
internal val SmallMiuixIconSize = 18.dp
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
internal fun smallContainerSize() = IconButtonDefaults.extraSmallContainerSize(
IconButtonDefaults.IconButtonWidthOption.Uniform
)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
internal val smallIconSize: Dp
get() = IconButtonDefaults.extraSmallIconSize
@Composable
internal fun SmallNoMinTouchTarget(content: @Composable () -> Unit) {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
content()
}
}
@Composable
internal fun SmallButtonContent(
icon: ImageVector?,
text: String?,
contentDescription: String?
) {
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically
) {
if (icon != null) {
if (isMiuix) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(16.dp)
)
} else {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(16.dp)
)
}
}
if (text != null) {
if (isMiuix) {
MiuixText(
text = text,
style = LegadoTheme.typography.labelMedium
)
} else {
AppText(
text = text,
style = LegadoTheme.typography.labelMedium
)
}
}
}
}
@Composable
fun SmallPlainButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
text: String? = null,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
if (icon != null && text == null) {
MiuixIconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(SmallMiuixIconSize),
)
}
} else {
Card(
onClick = onClick,
modifier = modifier,
showIndication = true,
colors = CardDefaults.defaultColors(
color = LegadoTheme.colorScheme.surfaceVariant,
contentColor = LegadoTheme.colorScheme.onSurfaceVariant
)
) {
SmallButtonContent(icon, text, contentDescription)
}
}
} else {
SmallNoMinTouchTarget {
when {
icon != null && text == null -> {
IconButton(
onClick = onClick,
modifier = modifier.size(smallContainerSize()),
enabled = enabled,
shape = IconButtonDefaults.extraSmallRoundShape,
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize),
)
}
}
else -> {
TextButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
shape = MaterialTheme.shapes.small
) {
SmallButtonContent(icon, text, contentDescription)
}
}
}
}
}
}
@@ -0,0 +1,178 @@
package io.legado.app.ui.widget.components.button.series
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.IconToggleButtonShapes
import androidx.compose.material3.OutlinedIconToggleButton
import androidx.compose.material3.Text
import androidx.compose.material3.TonalToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
enum class ToggleStyle { Outlined, Tonal }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SmallToggleButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
style: ToggleStyle = ToggleStyle.Outlined,
icon: ImageVector? = null,
iconChecked: ImageVector? = null,
text: String? = null,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
val containerColor by animateColorAsState(
targetValue = if (checked) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainer,
animationSpec = tween(150),
label = "MiuixToggleContainerColor"
)
val iconTint by animateColorAsState(
targetValue = if (checked) LegadoTheme.colorScheme.onPrimaryContainer else LegadoTheme.colorScheme.onSurfaceVariant,
animationSpec = tween(150),
label = "MiuixToggleIconTint"
)
if (text != null) {
MiuixIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier,
enabled = enabled,
backgroundColor = containerColor
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
) {
MiuixIcon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = iconTint
)
MiuixText(
text = text,
style = LegadoTheme.typography.labelSmall,
color = iconTint,
modifier = Modifier.padding(start = 6.dp),
maxLines = 1,
softWrap = false
)
}
}
} else {
MiuixIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier.size(SmallMiuixButtonSize),
enabled = enabled,
backgroundColor = containerColor
) {
MiuixIcon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = contentDescription,
tint = iconTint,
modifier = Modifier.size(SmallMiuixIconSize)
)
}
}
} else {
SmallNoMinTouchTarget {
if (text != null) {
TonalToggleButton(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier,
enabled = enabled,
contentPadding = PaddingValues(horizontal = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Text(
text = text,
style = LegadoTheme.typography.labelSmall,
modifier = Modifier.padding(start = 6.dp),
maxLines = 1,
softWrap = false
)
}
}
} else {
when (style) {
ToggleStyle.Outlined -> {
val defaultShape = IconButtonDefaults.extraSmallRoundShape
val pressedShape = IconButtonDefaults.extraSmallPressedShape
val checkedShape = IconButtonDefaults.extraSmallSelectedRoundShape
val toggleShapes = remember(defaultShape, checkedShape) {
IconToggleButtonShapes(
shape = defaultShape,
pressedShape = pressedShape,
checkedShape = checkedShape
)
}
OutlinedIconToggleButton(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier.size(smallContainerSize()),
enabled = enabled,
shapes = toggleShapes
) {
Icon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize),
)
}
}
ToggleStyle.Tonal -> {
FilledTonalButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier.size(smallContainerSize()),
enabled = enabled,
) {
Icon(
imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize),
)
}
}
}
}
}
}
}
@@ -0,0 +1,91 @@
package io.legado.app.ui.widget.components.button.series
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SmallTonalButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
text: String? = null,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
if (icon != null && text == null) {
MiuixIconButton(
onClick = onClick,
modifier = modifier.size(SmallMiuixButtonSize),
enabled = enabled,
backgroundColor = LegadoTheme.colorScheme.surfaceContainer
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(SmallMiuixIconSize)
)
}
} else {
Card(
onClick = onClick,
modifier = modifier,
showIndication = true,
colors = CardDefaults.defaultColors(
color = LegadoTheme.colorScheme.surfaceContainer,
contentColor = LegadoTheme.colorScheme.onSurfaceVariant
)
) {
SmallButtonContent(icon, text, contentDescription)
}
}
} else {
SmallNoMinTouchTarget {
when {
icon != null && text == null -> {
FilledTonalIconButton(
onClick = onClick,
modifier = modifier.size(smallContainerSize()),
enabled = enabled,
shapes = IconButtonDefaults.shapes(),
colors = IconButtonDefaults.filledTonalIconButtonColors()
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize)
)
}
}
else -> {
FilledTonalButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)
) {
SmallButtonContent(icon, text, contentDescription)
}
}
}
}
}
}
@@ -16,13 +16,8 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -42,7 +37,7 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.AdaptiveSwitch import io.legado.app.ui.widget.components.AdaptiveSwitch
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.checkBox.AppCheckbox import io.legado.app.ui.widget.components.checkBox.AppCheckbox
import io.legado.app.ui.widget.components.icon.AppIcons import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
@@ -50,7 +45,6 @@ import io.legado.app.ui.widget.components.text.AppText
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.ReorderableLazyListState import sh.calvin.reorderable.ReorderableLazyListState
import top.yukonga.miuix.kmp.basic.BasicComponent import top.yukonga.miuix.kmp.basic.BasicComponent
import top.yukonga.miuix.kmp.theme.MiuixTheme
@Composable @Composable
fun SelectionItemCard( fun SelectionItemCard(
@@ -218,9 +212,9 @@ fun SelectionItemCardContent(
} }
if (onClickEdit != null) { if (onClickEdit != null) {
SmallIconButton( SmallPlainButton(
onClick = onClickEdit, onClick = onClickEdit,
imageVector = AppIcons.Edit, icon = AppIcons.Edit,
contentDescription = "Edit" contentDescription = "Edit"
) )
} }
@@ -231,9 +225,9 @@ fun SelectionItemCardContent(
if (dropdownContent != null) { if (dropdownContent != null) {
Box { Box {
SmallIconButton( SmallPlainButton(
onClick = { showMenu = true }, onClick = { showMenu = true },
imageVector = AppIcons.MoreVert, icon = AppIcons.MoreVert,
contentDescription = "More" contentDescription = "More"
) )
RoundDropdownMenu( RoundDropdownMenu(
@@ -36,8 +36,8 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.MediumAnimatedActionButton import io.legado.app.ui.widget.components.button.series.MediumOutlinedButton
import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton import io.legado.app.ui.widget.components.button.series.MediumToggleButton
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import java.time.LocalDate import java.time.LocalDate
@@ -54,15 +54,14 @@ fun HeatmapCalendarStartAction(
currentMode: HeatmapMode, currentMode: HeatmapMode,
onModeChanged: (HeatmapMode) -> Unit, onModeChanged: (HeatmapMode) -> Unit,
) { ) {
MediumAnimatedActionButton( MediumToggleButton(
checked = currentMode == HeatmapMode.TIME, checked = currentMode == HeatmapMode.TIME,
onCheckedChange = { onCheckedChange = {
onModeChanged(if (it) HeatmapMode.TIME else HeatmapMode.COUNT) onModeChanged(if (it) HeatmapMode.TIME else HeatmapMode.COUNT)
}, },
icon = Icons.Default.FormatListNumbered,
iconChecked = Icons.Default.AccessTime, iconChecked = Icons.Default.AccessTime,
iconUnchecked = Icons.Default.FormatListNumbered, text = "按时长"
activeText = "按时长",
inactiveText = "按次数"
) )
} }
@@ -73,9 +72,9 @@ fun HeatmapCalendarStartAction(
fun HeatmapCalendarEndAction( fun HeatmapCalendarEndAction(
onClearDate: () -> Unit onClearDate: () -> Unit
) { ) {
MediumOutlinedIconButton( MediumOutlinedButton(
onClick = onClearDate, onClick = onClearDate,
imageVector = Icons.Outlined.Delete, icon = Icons.Outlined.Delete,
) )
} }
@@ -44,7 +44,7 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -183,9 +183,9 @@ fun <T> BatchImportDialog(
title = sheetTitle, title = sheetTitle,
startAction = if (isEditing) { startAction = if (isEditing) {
{ {
SmallIconButton( SmallPlainButton(
onClick = { editingIndex = null }, onClick = { editingIndex = null },
imageVector = Icons.AutoMirrored.Filled.ArrowBack, icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "返回" contentDescription = "返回"
) )
} }
@@ -196,9 +196,9 @@ fun <T> BatchImportDialog(
{ {
Row { Row {
topBarActions() topBarActions()
SmallIconButton( SmallPlainButton(
onClick = { onToggleAll(!allSelected) }, onClick = { onToggleAll(!allSelected) },
imageVector = Icons.Default.SelectAll, icon = Icons.Default.SelectAll,
contentDescription = if (allSelected) "全不选" else "全选" contentDescription = if (allSelected) "全不选" else "全选"
) )
} }
@@ -371,9 +371,9 @@ fun ImportItemRow(
modifier = Modifier.padding(end = 4.dp) modifier = Modifier.padding(end = 4.dp)
) )
SmallIconButton( SmallPlainButton(
onClick = onInfoClick, onClick = onInfoClick,
imageVector = Icons.Default.Info, icon = Icons.Default.Info,
contentDescription = "详情" contentDescription = "详情"
) )
} }
@@ -23,7 +23,7 @@ import io.legado.app.R
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.series.MediumPlainButton
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.LogUtils import io.legado.app.utils.LogUtils
@@ -43,12 +43,12 @@ fun AppLogSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = stringResource(R.string.log), title = stringResource(R.string.log),
endAction = { endAction = {
MediumIconButton( MediumPlainButton(
onClick = { onClick = {
AppLog.clear() AppLog.clear()
logs = emptyList() logs = emptyList()
}, },
imageVector = Icons.Default.DeleteSweep icon = Icons.Default.DeleteSweep
) )
} }
) { ) {
@@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -65,7 +63,7 @@ fun RowScope.OptionCard(
.weight(1f) .weight(1f)
.height(100.dp), .height(100.dp),
cornerRadius = 12.dp, cornerRadius = 12.dp,
containerColor = LegadoTheme.colorScheme.surfaceContainerLow, containerColor = LegadoTheme.colorScheme.onSheetContent,
elevation = 2.dp elevation = 2.dp
) { ) {
Column( Column(
@@ -13,15 +13,8 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ContentPaste import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -30,19 +23,16 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppFloatingActionButton import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.series.MediumPlainButton
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu 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.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** /**
@@ -84,17 +74,17 @@ fun <T> RuleEditSheet(
AppModalBottomSheet( AppModalBottomSheet(
title = title, title = title,
startAction = { startAction = {
MediumIconButton( MediumPlainButton(
onClick = onDismissRequest, onClick = onDismissRequest,
imageVector = Icons.Default.Close, icon = Icons.Default.Close,
contentDescription = "Close", contentDescription = "Close",
) )
}, },
endAction = { endAction = {
Box{ Box{
MediumIconButton( MediumPlainButton(
onClick = { showMenu = true }, onClick = { showMenu = true },
imageVector = Icons.Default.MoreVert, icon = Icons.Default.MoreVert,
contentDescription = "More" contentDescription = "More"
) )
RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
@@ -1,9 +1,13 @@
package io.legado.app.ui.widget.components.topbar package io.legado.app.ui.widget.components.topbar
import androidx.compose.foundation.background 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.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -16,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.legado.app.ui.config.themeConfig.ThemeConfig import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LocalHazeState import io.legado.app.ui.theme.LocalHazeState
@@ -81,7 +86,9 @@ fun GlassMediumFlexibleTopAppBar(
} }
val finalModifier = if (hazeState != null) { val finalModifier = if (hazeState != null) {
modifier.background(color = animatedColor).responsiveHazeEffect(state = hazeState) modifier
.background(color = animatedColor)
.responsiveHazeEffect(state = hazeState)
} else { } else {
modifier.background(color = animatedColor) modifier.background(color = animatedColor)
} }
@@ -116,7 +123,7 @@ fun GlassMediumFlexibleTopAppBar(
AdaptiveAnimatedText( AdaptiveAnimatedText(
text = title, text = title,
useCharMode = useCharMode, useCharMode = useCharMode,
maxLines = 1, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
}, },
@@ -126,7 +133,13 @@ fun GlassMediumFlexibleTopAppBar(
} }
}, },
navigationIcon = navigationIcon, navigationIcon = navigationIcon,
actions = actions, actions = {
Box(modifier = Modifier.padding(end = 12.dp)) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) { actions() }
}
},
scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior, scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,
colors = transparentColors colors = transparentColors
) )
@@ -153,7 +166,15 @@ fun GlassMediumFlexibleTopAppBar(
} }
}, },
navigationIcon = navigationIcon, navigationIcon = navigationIcon,
actions = actions, actions = {
Box(modifier = Modifier.padding(end = 12.dp)) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
actions()
}
}
},
scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior, scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,
colors = transparentColors colors = transparentColors
) )
@@ -30,13 +30,13 @@ import io.legado.app.R
import io.legado.app.ui.config.themeConfig.ThemeConfig import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.button.AnimatedActionButtonCore import io.legado.app.ui.widget.components.button.series.AnimatedActionButtonCore
import io.legado.app.ui.widget.components.button.AnimatedIcon import io.legado.app.ui.widget.components.button.series.AnimatedIcon
import io.legado.app.ui.widget.components.icon.AppIcons import io.legado.app.ui.widget.components.icon.AppIcons
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Text as MiuixText
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
@Composable @Composable
private fun TopBarButton( private fun TopBarButton(
@@ -112,7 +112,7 @@ fun TopBarActionButton(
onClick = onClick, onClick = onClick,
imageVector = imageVector, imageVector = imageVector,
contentDescription = contentDescription, contentDescription = contentDescription,
modifier = modifier.padding(end = 12.dp) modifier = modifier
) )
} else { } else {
IconButton( IconButton(
@@ -140,16 +140,10 @@ fun TopBarAnimatedActionButton(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) { if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
val containerColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainerHigh,
animationSpec = tween(150),
label = "MiuixActionButtonContainer"
)
val contentColor by animateColorAsState( val contentColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.onPrimaryContainer else MiuixTheme.colorScheme.onSurface, targetValue = if (checked) MiuixTheme.colorScheme.primary else MiuixTheme.colorScheme.onSurface,
animationSpec = tween(150), animationSpec = tween(150),
label = "MiuixActionButtonContainer" label = "MiuixActionButtonContent"
) )
AnimatedActionButtonCore( AnimatedActionButtonCore(
@@ -167,8 +161,7 @@ fun TopBarAnimatedActionButton(
button = { buttonModifier, onToggle, content -> button = { buttonModifier, onToggle, content ->
MiuixIconButton( MiuixIconButton(
onClick = { onToggle(!checked) }, onClick = { onToggle(!checked) },
modifier = buttonModifier, modifier = buttonModifier
backgroundColor = containerColor
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -1,24 +1,41 @@
package io.legado.app.web.socket package io.legado.app.web.socket
import io.ktor.server.websocket.* import io.ktor.server.websocket.DefaultWebSocketServerSession
import io.ktor.websocket.* import io.ktor.websocket.CloseReason
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PreferKey import io.legado.app.data.local.preferences.LocalPreferencesKeys
import io.legado.app.data.local.preferences.LocalPreferencesRepository
import io.legado.app.domain.model.BookSearchScope import io.legado.app.domain.model.BookSearchScope
import io.legado.app.domain.model.MatchMode
import io.legado.app.domain.usecase.BookSearchControl import io.legado.app.domain.usecase.BookSearchControl
import io.legado.app.domain.usecase.BookSearchRequest import io.legado.app.domain.usecase.BookSearchRequest
import io.legado.app.domain.usecase.SearchBooksUseCase import io.legado.app.domain.usecase.SearchBooksUseCase
import io.legado.app.domain.usecase.SearchRunEvent import io.legado.app.domain.usecase.SearchRunEvent
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.ui.config.otherConfig.OtherConfig import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.utils.* import io.legado.app.utils.GSON
import kotlinx.coroutines.* import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.isJson
import io.legado.app.utils.printOnDebug
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.koin.core.context.GlobalContext import org.koin.core.context.GlobalContext
import splitties.init.appCtx import splitties.init.appCtx
class BookSearchWebSocket(private val session: DefaultWebSocketServerSession) : CoroutineScope by session { class BookSearchWebSocket(private val session: DefaultWebSocketServerSession) : CoroutineScope by session {
private val searchBooksUseCase: SearchBooksUseCase by lazy { GlobalContext.get().get() } private val searchBooksUseCase: SearchBooksUseCase by lazy { GlobalContext.get().get() }
private val localPreferencesRepository: LocalPreferencesRepository by lazy {
GlobalContext.get().get()
}
private val searchControl = BookSearchControl() private val searchControl = BookSearchControl()
private val sentBookUrls = linkedSetOf<String>() private val sentBookUrls = linkedSetOf<String>()
private var searchJob: Job? = null private var searchJob: Job? = null
@@ -66,7 +83,14 @@ class BookSearchWebSocket(private val session: DefaultWebSocketServerSession) :
keyword = key, keyword = key,
page = 1, page = 1,
scope = BookSearchScope(AppConfig.searchScope), scope = BookSearchScope(AppConfig.searchScope),
precision = appCtx.getPrefBoolean(PreferKey.precisionSearch), matchMode = MatchMode.of(
localPreferencesRepository
.getPreference(
LocalPreferencesKeys.MATCH_MODE,
MatchMode.DEFAULT.value
)
.first()
),
concurrency = OtherConfig.threadCount, concurrency = OtherConfig.threadCount,
), ),
searchControl searchControl
+4 -2
View File
@@ -24,7 +24,7 @@
<string name="dialog_setting">去设置</string> <string name="dialog_setting">去设置</string>
<string name="tip_cannot_jump_setting_page">无法跳转至设置界面</string> <string name="tip_cannot_jump_setting_page">无法跳转至设置界面</string>
<string name="manual_input">手动输入</string> <string name="manual_input">输入URL</string>
<string name="preferred_contrast">偏好对比度</string> <string name="preferred_contrast">偏好对比度</string>
<string name="contrast_source_system_summary">来源:系统对比度</string> <string name="contrast_source_system_summary">来源:系统对比度</string>
<string name="contrast_source_manual_summary">来源:手动输入</string> <string name="contrast_source_manual_summary">来源:手动输入</string>
@@ -899,7 +899,7 @@
<string name="body_title">正文标题</string> <string name="body_title">正文标题</string>
<string name="show_hide">显示/隐藏</string> <string name="show_hide">显示/隐藏</string>
<string name="header_footer">页眉与页脚</string> <string name="header_footer">页眉与页脚</string>
<string name="rule_subscription">规则订阅</string> <string name="rule_subscription">规则</string>
<string name="rule_sub_empty_msg">添加大佬们提供的规则导入地址\n添加后点击可导入规则</string> <string name="rule_sub_empty_msg">添加大佬们提供的规则导入地址\n添加后点击可导入规则</string>
<string name="get_book_progress">拉取云端进度</string> <string name="get_book_progress">拉取云端进度</string>
<string name="cover_book_progress">覆盖云端进度</string> <string name="cover_book_progress">覆盖云端进度</string>
@@ -1648,6 +1648,8 @@
<string name="search_source_progress"> · 进度 %1$d/%2$d</string> <string name="search_source_progress"> · 进度 %1$d/%2$d</string>
<string name="search_empty_scope_disable_precision">%1$s分组搜索结果为空,是否关闭精准搜索?</string> <string name="search_empty_scope_disable_precision">%1$s分组搜索结果为空,是否关闭精准搜索?</string>
<string name="search_empty_scope_switch_all">%1$s分组搜索结果为空,是否切换到全部分组?</string> <string name="search_empty_scope_switch_all">%1$s分组搜索结果为空,是否切换到全部分组?</string>
<string name="search_layout_list">列表布局</string>
<string name="search_layout_source_grouped">来源分组</string>
<string name="download_cache_config">下载与缓存</string> <string name="download_cache_config">下载与缓存</string>
<string name="http_cache">HTTP缓存</string> <string name="http_cache">HTTP缓存</string>
<string name="cover_cache">封面缓存</string> <string name="cover_cache">封面缓存</string>
+2
View File
@@ -1684,6 +1684,8 @@
<string name="search_source_progress"> · Progress %1$d/%2$d</string> <string name="search_source_progress"> · Progress %1$d/%2$d</string>
<string name="search_empty_scope_disable_precision">%1$s group search returned no results. Disable precise search?</string> <string name="search_empty_scope_disable_precision">%1$s group search returned no results. Disable precise search?</string>
<string name="search_empty_scope_switch_all">%1$s group search returned no results. Switch to all groups?</string> <string name="search_empty_scope_switch_all">%1$s group search returned no results. Switch to all groups?</string>
<string name="search_layout_list">List layout</string>
<string name="search_layout_source_grouped">Source grouped</string>
<string name="download_cache_config">Download &amp; Cache</string> <string name="download_cache_config">Download &amp; Cache</string>
<string name="http_cache">HTTP Cache</string> <string name="http_cache">HTTP Cache</string>
<string name="cover_cache">Cover Cache</string> <string name="cover_cache">Cover Cache</string>
+3
View File
@@ -0,0 +1,3 @@
node_modules/
.vitepress/dist/
.vitepress/cache/
+168
View File
@@ -0,0 +1,168 @@
import { defineConfig } from 'vitepress'
export default defineConfig({
title: '阅读',
description: 'Legado-with-MD3 使用文档',
base: '/legado-with-MD3/',
head: [
['link', { rel: 'icon', href: '/legado-with-MD3/favicon.ico' }]
],
themeConfig: {
logo: '/logo.png',
siteTitle: 'Legado Docs',
nav: [
{ text: '首页', link: '/' },
{ text: '快速开始', link: '/guide/' },
{ text: '开发文档', link: '/dev/' },
],
sidebar: {
'/guide/': [
{
text: '使用指南',
items: [
{ text: '帮助文档', link: '/guide/' },
{ text: '阅读界面', link: '/guide/reading' },
{ text: '导入源管理', link: '/guide/book-source' },
{ text: '订阅源管理', link: '/guide/rss-source' },
{ text: '替换规则', link: '/guide/replace-rule' },
],
},
{
text: '教程',
items: [
{ text: 'WebDAV 备份教程', link: '/tutorial/webdav-backup' },
{ text: 'WebDAV 书籍同步', link: '/tutorial/webdav-book' },
],
},
],
'/tutorial/': [
{
text: '使用指南',
items: [
{ text: '帮助文档', link: '/guide/' },
{ text: '阅读界面', link: '/guide/reading' },
{ text: '导入源管理', link: '/guide/book-source' },
{ text: '订阅源管理', link: '/guide/rss-source' },
{ text: '替换规则', link: '/guide/replace-rule' },
],
},
{
text: '教程',
items: [
{ text: 'WebDAV 备份教程', link: '/tutorial/webdav-backup' },
{ text: 'WebDAV 书籍同步', link: '/tutorial/webdav-book' },
],
},
],
'/dev/': [
{
text: '入门',
items: [
{ text: '规则语法详解', link: '/dev/syntax' },
{ text: 'URL 参数详解', link: '/dev/url-options' },
{ text: '源字段速查', link: '/dev/source-fields' },
{ text: '源示例', link: '/dev/examples' },
],
},
{
text: '参考',
items: [
{ text: '源规则帮助', link: '/dev/rule' },
{ text: 'JS 变量和函数', link: '/dev/js' },
{ text: 'XPath 路径表达式', link: '/dev/xpath' },
{ text: '正则表达式', link: '/dev/regex' },
],
},
{
text: '配置规范',
items: [
{ text: '请求头配置', link: '/dev/request-headers' },
{ text: '认证与登录', link: '/dev/authentication' },
{ text: '发现 URL 配置', link: '/dev/discovery-url' },
{ text: '首页模块配置', link: '/spec/homepage-modules' },
{ text: '关联书籍配置', link: '/spec/related-books' },
],
},
{
text: '扩展功能',
items: [
{ text: '源调试', link: '/dev/debug' },
{ text: '字典规则', link: '/dev/dict-rule' },
{ text: '在线朗读规则', link: '/dev/tts-rule' },
{ text: 'TXT 目录正则', link: '/dev/txt-toc' },
{ text: 'MIME 类型参考', link: '/spec/mime-types' },
],
},
],
'/spec/': [
{
text: '入门',
items: [
{ text: '规则语法详解', link: '/dev/syntax' },
{ text: 'URL 参数详解', link: '/dev/url-options' },
{ text: '源字段速查', link: '/dev/source-fields' },
{ text: '源示例', link: '/dev/examples' },
],
},
{
text: '参考',
items: [
{ text: '源规则帮助', link: '/dev/rule' },
{ text: 'JS 变量和函数', link: '/dev/js' },
{ text: 'XPath 路径表达式', link: '/dev/xpath' },
{ text: '正则表达式', link: '/dev/regex' },
],
},
{
text: '配置规范',
items: [
{ text: '请求头配置', link: '/dev/request-headers' },
{ text: '认证与登录', link: '/dev/authentication' },
{ text: '发现 URL 配置', link: '/dev/discovery-url' },
{ text: '首页模块配置', link: '/spec/homepage-modules' },
{ text: '关联书籍配置', link: '/spec/related-books' },
],
},
{
text: '扩展功能',
items: [
{ text: '源调试', link: '/dev/debug' },
{ text: '字典规则', link: '/dev/dict-rule' },
{ text: '在线朗读规则', link: '/dev/tts-rule' },
{ text: 'TXT 目录正则', link: '/dev/txt-toc' },
{ text: 'MIME 类型参考', link: '/spec/mime-types' },
],
},
],
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/HapeLee/legado-with-MD3' },
],
footer: {
message: '基于 Apache-2.0 许可发布',
copyright: 'Copyright © 2026 Legado',
},
search: {
provider: 'local',
},
outline: {
level: [2, 3],
label: '页面导航',
},
docFooter: {
prev: '上一页',
next: '下一页',
},
lastUpdated: {
text: '最后更新于',
},
editLink: {
pattern: 'https://github.com/HapeLee/legado-with-MD3/edit/main/docs/:path',
text: '在 GitHub 上编辑此页面',
},
},
lastUpdated: true,
markdown: {
lineNumbers: true,
},
})
+129
View File
@@ -0,0 +1,129 @@
# 认证与登录 (Authentication)
[[toc]]
Legado 支持通过 CookieJar、登录 UI 表单和登录 URL 脚本三种方式实现网站认证。
## 1. CookieJar
启用后自动保存每次响应头中的 `Set-Cookie` 值,适用于需要 session 的网站(如验证码图片)。
在源编辑器中勾选「启用 CookieJar」即可,无需额外配置。
## 2. 登录 UI (Login UI)
通过 JSON 数组定义登录表单,替代内置 WebView 登录方式。
### 字段定义
| 字段 | 类型 | 必须 | 说明 |
|:---------|:---------|:---|:-----------------------|
| `name` | `String` | 是 | 字段名称,显示为输入框标签 |
| `type` | `String` | 是 | 字段类型 |
| `action` | `String` | 否 | 仅 `button` 类型:点击时执行的动作 |
| `style` | `Object` | 否 | Flexbox 布局配置 |
### type 取值
| 值 | 说明 |
|:-----------|:---------------------------------------|
| `text` | 文本输入框 |
| `password` | 密码输入框 |
| `button` | 可点击按钮,`action` 为 URL 时打开浏览器,为函数名时调用 JS |
### JSON 示例
```json
[
{ "name": "telephone", "type": "text" },
{ "name": "password", "type": "password" },
{
"name": "注册",
"type": "button",
"action": "http://www.example.com/register"
},
{
"name": "获取验证码",
"type": "button",
"action": "getVerificationCode()",
"style": {
"layout_flexGrow": 0,
"layout_flexShrink": 1,
"layout_alignSelf": "auto",
"layout_flexBasisPercent": -1,
"layout_wrapBefore": false
}
}
]
```
### style 布局属性
| 属性 | 类型 | 说明 |
|:--------------------------|:----------|:---------------|
| `layout_flexGrow` | `Int` | 弹性增长比例 |
| `layout_flexShrink` | `Int` | 弹性收缩比例 |
| `layout_alignSelf` | `String` | 交叉轴对齐方式 |
| `layout_flexBasisPercent` | `Int` | 基础尺寸百分比,-1 为自动 |
| `layout_wrapBefore` | `Boolean` | 是否强制换行 |
::: tip 版本变更
从版本 20221113 起,按钮支持调用「登录 URL」规则中的函数,必须实现 `login` 函数。
:::
## 3. 登录 URL (Login URL)
可填写登录链接或实现登录逻辑的 JavaScript。配合登录 UI 使用时,需要实现 `login` 函数。
### JS 示例
```js
function login() {
java.log("模拟登录请求");
java.log(source.getLoginInfoMap());
}
function getVerificationCode() {
java.log("登录UI按钮:获取到手机号码" + result.get("telephone"))
}
```
### 获取登录信息
在登录按钮函数和 `login` 函数中,可通过以下方式获取用户输入:
```js
// 登录按钮函数中
result.get("telephone")
// login 函数中
source.getLoginInfo()
source.getLoginInfoMap().get("telephone")
```
### source 登录相关方法
| 方法 | 返回值 | 说明 |
|:-------------------------------|:----------|:----------|
| `login()` | — | 执行登录 |
| `getHeaderMap(hasLoginHeader)` | `Map` | 获取请求头 |
| `getLoginHeader()` | `String?` | 获取登录头字符串 |
| `getLoginHeaderMap()` | `Map?` | 获取登录头 Map |
| `putLoginHeader(header)` | — | 保存登录头 |
| `removeLoginHeader()` | — | 清除登录头 |
| `setVariable(variable)` | — | 设置源变量 |
| `getVariable()` | `String?` | 获取源变量 |
### AnalyzeUrl 函数
以下函数仅在「登录检查 JS」规则中有效:
| 方法 | 返回值 | 说明 |
|:---------------------------------------------------|:--------------|:--------------------|
| `initUrl()` | — | 重新解析 URL |
| `getHeaderMap().putAll(source.getHeaderMap(true))` | — | 重新设置登录头 |
| `getStrResponse(jsStr, sourceRegex)` | `StrResponse` | 返回文本类型的访问结果 |
| `getResponse()` | `Response` | 返回 Response 类型的访问结果 |
## 4. 登录检查 JS (Login Check)
在源的「登录检查 JS」字段中填写 JavaScript,用于判断当前登录状态。返回 `true` 表示已登录,返回其他值会触发重新登录流程。

Some files were not shown because too many files have changed in this diff Show More