优化
This commit is contained in:
@@ -45,6 +45,7 @@ object PreferKey {
|
||||
const val prevKeys = "prevKeyCodes"
|
||||
const val nextKeys = "nextKeyCodes"
|
||||
const val showDiscovery = "showDiscovery"
|
||||
const val showHome = "showHome"
|
||||
const val enableReview = "enableReview"
|
||||
const val showRss = "showRss"
|
||||
const val showStatusBar = "showStatusBar"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.legado.app.data.local.preferences
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
|
||||
val Context.localDataStore: DataStore<Preferences> by preferencesDataStore(name = "local_ui_status")
|
||||
|
||||
object LocalPreferencesKeys {
|
||||
val SHOW_THEME_REFACTOR_TIP = booleanPreferencesKey("show_theme_refactor_tip")
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.legado.app.data.local.preferences
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.IOException
|
||||
|
||||
class LocalPreferencesRepository(private val context: Context) {
|
||||
|
||||
private val dataStore = context.localDataStore
|
||||
|
||||
fun <T> getPreference(key: Preferences.Key<T>, defaultValue: T): Flow<T> {
|
||||
return dataStore.data
|
||||
.catch { exception ->
|
||||
if (exception is IOException) {
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
.map { preferences ->
|
||||
preferences[key] ?: defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.SvgDecoder
|
||||
import io.legado.app.data.AppDatabase
|
||||
import io.legado.app.data.local.preferences.LocalPreferencesRepository
|
||||
import io.legado.app.data.repository.AppStartupRepository
|
||||
import io.legado.app.data.repository.BookCacheCleanupRepository
|
||||
import io.legado.app.data.repository.BookDomainRepositoryImpl
|
||||
@@ -137,6 +138,7 @@ val appModule = module {
|
||||
singleOf(::SearchContentRepository)
|
||||
singleOf(::RemoteBookRepository)
|
||||
singleOf(::SettingsRepository)
|
||||
singleOf(::LocalPreferencesRepository)
|
||||
singleOf(::ExploreBooksUseCase)
|
||||
singleOf(::ExploreKindUiUseCase)
|
||||
singleOf(::SaveSearchBooksUseCase)
|
||||
|
||||
@@ -3,6 +3,8 @@ package io.legado.app.domain.usecase
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.data.repository.BookSourceRepository
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ExploreBooksUseCase(
|
||||
private val bookSourceRepository: BookSourceRepository,
|
||||
@@ -20,7 +22,7 @@ class ExploreBooksUseCase(
|
||||
moduleUrl: String?,
|
||||
args: String?,
|
||||
page: Int = 1
|
||||
): ExploreResult {
|
||||
): ExploreResult = withContext(Dispatchers.IO) {
|
||||
val base = bookSourceRepository.getBookSource(sourceUrl)
|
||||
?: throw SourceNotFound(sourceUrl)
|
||||
val source = args?.let { base.copy().also { s -> s.setVariable(it) } } ?: base
|
||||
@@ -33,14 +35,14 @@ class ExploreBooksUseCase(
|
||||
throw InvalidUrl(resolvedUrl)
|
||||
}
|
||||
val books = WebBook.exploreBookSuspend(source, resolvedUrl, page)
|
||||
return ExploreResult(resolvedUrl, books)
|
||||
ExploreResult(resolvedUrl, books)
|
||||
}
|
||||
|
||||
suspend fun executeForRanking(
|
||||
sourceUrl: String,
|
||||
moduleUrl: String?,
|
||||
args: String?
|
||||
): List<SearchBook> {
|
||||
): List<SearchBook> = withContext(Dispatchers.IO) {
|
||||
val result = execute(sourceUrl, moduleUrl, args)
|
||||
var books = result.books
|
||||
var page = 1
|
||||
@@ -50,7 +52,7 @@ class ExploreBooksUseCase(
|
||||
WebBook.exploreBookSuspend(
|
||||
bookSourceRepository.getBookSource(sourceUrl)
|
||||
?.let { s -> args?.let { s.copy().also { x -> x.setVariable(it) } } ?: s }
|
||||
?: return books.take(MAX_RANKING_BOOKS),
|
||||
?: return@withContext books.take(MAX_RANKING_BOOKS),
|
||||
result.resolvedUrl,
|
||||
page,
|
||||
)
|
||||
@@ -60,7 +62,7 @@ class ExploreBooksUseCase(
|
||||
if (next.isEmpty()) break
|
||||
books = (books + next)
|
||||
}
|
||||
return books.take(MAX_RANKING_BOOKS)
|
||||
books.take(MAX_RANKING_BOOKS)
|
||||
}
|
||||
|
||||
data class ExploreResult(val resolvedUrl: String, val books: List<SearchBook>)
|
||||
|
||||
@@ -99,9 +99,10 @@ class ExploreKindUiUseCase(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun evalUiJs(jsStr: String, sourceUrl: String, infoMap: InfoMap): String? {
|
||||
val source = getOrLoadBookSource(sourceUrl) ?: return null
|
||||
return runScriptWithContext {
|
||||
private suspend fun evalUiJs(jsStr: String, sourceUrl: String, infoMap: InfoMap): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val source = getOrLoadBookSource(sourceUrl) ?: return@withContext null
|
||||
runScriptWithContext {
|
||||
source.evalJS(jsStr) {
|
||||
put("infoMap", infoMap)
|
||||
}?.toString()
|
||||
|
||||
@@ -306,6 +306,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
val showDiscovery: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true)
|
||||
|
||||
val showHome: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.showHome, true)
|
||||
|
||||
val showRSS: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.showRss, true)
|
||||
|
||||
|
||||
@@ -3,12 +3,8 @@ package io.legado.app.help.config
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.inputStream
|
||||
import io.legado.app.utils.outputStream
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
|
||||
@@ -175,6 +171,7 @@ object ThemeImportExport {
|
||||
customTagColorsJson = ThemeConfig.customTagColorsJson,
|
||||
|
||||
// 主界面设置
|
||||
showHome = ThemeConfig.showHome,
|
||||
showDiscovery = ThemeConfig.showDiscovery,
|
||||
showRss = ThemeConfig.showRss,
|
||||
showStatusBar = ThemeConfig.showStatusBar,
|
||||
@@ -258,6 +255,7 @@ object ThemeImportExport {
|
||||
ThemeConfig.customTagColorsJson = data.customTagColorsJson
|
||||
|
||||
// 主界面设置
|
||||
ThemeConfig.showHome = data.showHome
|
||||
ThemeConfig.showDiscovery = data.showDiscovery
|
||||
ThemeConfig.showRss = data.showRss
|
||||
ThemeConfig.showStatusBar = data.showStatusBar
|
||||
@@ -394,6 +392,7 @@ data class ThemeExportData(
|
||||
val customTagColorsJson: String? = null,
|
||||
|
||||
// 主界面设置
|
||||
val showHome: Boolean = true,
|
||||
val showDiscovery: Boolean = true,
|
||||
val showRss: Boolean = true,
|
||||
val showStatusBar: Boolean = true,
|
||||
|
||||
@@ -86,7 +86,9 @@ class AddToBookshelfDialog() : BaseDialogFragment(R.layout.dialog_add_to_bookshe
|
||||
context = requireContext(),
|
||||
name = it.name,
|
||||
author = it.author,
|
||||
bookUrl = it.bookUrl
|
||||
bookUrl = it.bookUrl,
|
||||
origin = it.origin,
|
||||
coverPath = it.coverUrl
|
||||
)
|
||||
)
|
||||
dismiss()
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package io.legado.app.ui.book.explore
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.Crossfade
|
||||
@@ -13,8 +11,6 @@ import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -27,9 +23,9 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
||||
@@ -52,9 +48,7 @@ import androidx.compose.runtime.remember
|
||||
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.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
@@ -69,13 +63,11 @@ import io.legado.app.ui.theme.responsiveHazeEffect
|
||||
import io.legado.app.ui.theme.responsiveHazeSource
|
||||
import io.legado.app.ui.widget.components.AppPullToRefresh
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.SearchBar
|
||||
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.SearchBookListItem
|
||||
import io.legado.app.ui.widget.components.button.AnimatedTextButton
|
||||
import io.legado.app.ui.widget.components.card.TextCard
|
||||
import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem
|
||||
import io.legado.app.ui.widget.components.explore.calculateExploreKindRows
|
||||
import io.legado.app.ui.widget.components.explore.ExploreKindSelectSheet
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
@@ -98,7 +90,7 @@ fun ExploreShowScreen(
|
||||
sourceUrl: String?,
|
||||
exploreUrl: String?,
|
||||
onBack: () -> Unit,
|
||||
onBookClick: (SearchBook) -> Unit,
|
||||
onBookClick: (SearchBook, String?) -> Unit,
|
||||
viewModel: ExploreShowViewModel = koinViewModel(),
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
@@ -111,7 +103,6 @@ fun ExploreShowScreen(
|
||||
val books by viewModel.uiBooks.collectAsState()
|
||||
val isBookEnd by viewModel.isEnd.collectAsState()
|
||||
val shouldTriggerAutoLoad by viewModel.shouldTriggerAutoLoad.collectAsState()
|
||||
val kinds by viewModel.kinds.collectAsState()
|
||||
val isLoading by viewModel.isLoading.collectAsState()
|
||||
val errorMsg by viewModel.errorMsg.collectAsState()
|
||||
val filterState by viewModel.filterState.collectAsState()
|
||||
@@ -125,8 +116,6 @@ fun ExploreShowScreen(
|
||||
var showGridCountSheet by remember { mutableStateOf(false) }
|
||||
val gridColumnCount by viewModel.gridCount.collectAsState()
|
||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||
val context = LocalContext.current
|
||||
val activity = context as? AppCompatActivity
|
||||
val exploreKindUseCase: ExploreKindUiUseCase = koinInject()
|
||||
|
||||
LaunchedEffect(sourceUrl) {
|
||||
@@ -233,73 +222,16 @@ fun ExploreShowScreen(
|
||||
}
|
||||
|
||||
|
||||
AppModalBottomSheet(
|
||||
ExploreKindSelectSheet(
|
||||
show = showKindSheet,
|
||||
onDismissRequest = { showKindSheet = false }
|
||||
) {
|
||||
|
||||
var kindQuery by remember { mutableStateOf("") }
|
||||
|
||||
SearchBar(
|
||||
query = kindQuery,
|
||||
backgroundColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.5f),
|
||||
onQueryChange = { kindQuery = it },
|
||||
placeholder = "选择或搜索分类",
|
||||
)
|
||||
|
||||
val filteredKinds = remember(kindQuery, kinds) {
|
||||
if (kindQuery.isBlank()) kinds
|
||||
else kinds.filter { kind ->
|
||||
kind.title.contains(kindQuery, ignoreCase = true) ||
|
||||
(kind.url?.contains(kindQuery, ignoreCase = true) == true)
|
||||
onDismissRequest = { showKindSheet = false },
|
||||
sourceUrl = sourceUrl,
|
||||
onSelected = { selectedKinds ->
|
||||
selectedKinds.firstOrNull()?.let { kind ->
|
||||
viewModel.switchExploreUrl(kind)
|
||||
}
|
||||
}
|
||||
val kindRows = remember(filteredKinds) {
|
||||
calculateExploreKindRows(filteredKinds, 6)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(vertical = 16.dp),
|
||||
modifier = Modifier.weight(1f, fill = false)
|
||||
) {
|
||||
items(kindRows) { rowItems ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
rowItems.forEach { (kind, span) ->
|
||||
ExploreKindMultiTypeItem(
|
||||
modifier = Modifier
|
||||
.weight(span.toFloat())
|
||||
.animateItem(),
|
||||
kind = kind,
|
||||
sourceUrl = sourceUrl,
|
||||
activity = activity,
|
||||
onOpenUrl = { url ->
|
||||
showKindSheet = false
|
||||
viewModel.switchExploreUrl(kind.copy(url = url))
|
||||
},
|
||||
onRefreshKinds = viewModel::refreshKinds,
|
||||
backgroundColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.5f),
|
||||
isMiuix = isMiuix,
|
||||
useCase = exploreKindUseCase
|
||||
)
|
||||
}
|
||||
|
||||
val totalSpan = rowItems.sumOf { it.second }
|
||||
if (totalSpan < 6) {
|
||||
Spacer(
|
||||
modifier = Modifier.weight((6 - totalSpan).toFloat())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
AppScaffold(
|
||||
modifier = Modifier
|
||||
@@ -433,17 +365,22 @@ fun ExploreShowScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(
|
||||
itemsIndexed(
|
||||
items = books,
|
||||
key = { it.book.bookUrl }
|
||||
) { item ->
|
||||
key = { index, item -> "${item.book.bookUrl}:$index" }
|
||||
) { index, item ->
|
||||
val sharedCoverKey = bookCoverSharedElementKey(
|
||||
item.book.bookUrl,
|
||||
"explore:grid:$index"
|
||||
)
|
||||
ExploreBookGridItem(
|
||||
book = item.book,
|
||||
shelfState = item.shelfState,
|
||||
onClick = { onBookClick(item.book) },
|
||||
onClick = { onBookClick(item.book, sharedCoverKey) },
|
||||
modifier = Modifier.animateItem(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -467,17 +404,22 @@ fun ExploreShowScreen(
|
||||
bottom = paddingValues.calculateBottomPadding() + 16.dp
|
||||
)
|
||||
) {
|
||||
items(
|
||||
itemsIndexed(
|
||||
items = books,
|
||||
key = { it.book.bookUrl }
|
||||
) { item ->
|
||||
key = { index, item -> "${item.book.bookUrl}:$index" }
|
||||
) { index, item ->
|
||||
val sharedCoverKey = bookCoverSharedElementKey(
|
||||
item.book.bookUrl,
|
||||
"explore:list:$index"
|
||||
)
|
||||
ExploreBookItem(
|
||||
book = item.book,
|
||||
shelfState = item.shelfState,
|
||||
onClick = { onBookClick(item.book) },
|
||||
onClick = { onBookClick(item.book, sharedCoverKey) },
|
||||
modifier = Modifier.animateItem(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -506,6 +448,7 @@ fun ExploreBookItem(
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
) {
|
||||
SearchBookListItem(
|
||||
book = book,
|
||||
@@ -514,7 +457,7 @@ fun ExploreBookItem(
|
||||
modifier = modifier,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
}
|
||||
|
||||
@@ -527,6 +470,7 @@ fun ExploreBookGridItem(
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
) {
|
||||
SearchBookGridItem(
|
||||
book = book,
|
||||
@@ -535,60 +479,7 @@ fun ExploreBookGridItem(
|
||||
modifier = modifier,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun LoadMoreFooter(
|
||||
isLoading: Boolean,
|
||||
errorMsg: String?,
|
||||
isEnd: Boolean,
|
||||
onRetry: () -> Unit
|
||||
) {
|
||||
|
||||
LaunchedEffect(isLoading, errorMsg, isEnd) {
|
||||
if (!isLoading && errorMsg == null && !isEnd) {
|
||||
onRetry()
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
|
||||
AnimatedContent(
|
||||
targetState = when {
|
||||
isLoading -> "加载中…"
|
||||
errorMsg != null -> "加载失败: $errorMsg"
|
||||
isEnd -> "已经到底了~"
|
||||
else -> "我爱你"
|
||||
},
|
||||
label = "FooterTextChange"
|
||||
) { text ->
|
||||
AppText(
|
||||
text = text,
|
||||
color = when {
|
||||
errorMsg != null -> Color.Red
|
||||
else -> Color.Gray
|
||||
},
|
||||
style = LegadoTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
AnimatedTextButton(
|
||||
isLoading = isLoading,
|
||||
onClick = onRetry,
|
||||
text = if (errorMsg != null) "重试" else "再试一次",
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
|
||||
import io.legado.app.domain.usecase.SaveSearchBooksUseCase
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.utils.exploreLayoutGrid
|
||||
import io.legado.app.utils.stackTraceStr
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -222,7 +223,7 @@ class ExploreShowViewModel(
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
_errorMsg.value = it.localizedMessage
|
||||
_errorMsg.value = it.stackTraceStr
|
||||
}
|
||||
|
||||
_isLoading.value = false
|
||||
|
||||
@@ -40,6 +40,10 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
@Composable
|
||||
fun BookInfoRouteScreen(
|
||||
bookUrl: String,
|
||||
name: String? = null,
|
||||
author: String? = null,
|
||||
origin: String? = null,
|
||||
coverPath: String? = null,
|
||||
viewModel: BookInfoViewModel,
|
||||
onBack: () -> Unit,
|
||||
onFinish: (resultCode: Int?, afterTransition: Boolean) -> Unit,
|
||||
@@ -80,8 +84,14 @@ fun BookInfoRouteScreen(
|
||||
viewModel.onReaderResult(it.resultCode)
|
||||
}
|
||||
|
||||
LaunchedEffect(bookUrl, viewModel) {
|
||||
viewModel.initData(bookUrl)
|
||||
LaunchedEffect(bookUrl, name, author, origin, coverPath, viewModel) {
|
||||
viewModel.initData(
|
||||
bookUrl = bookUrl,
|
||||
name = name,
|
||||
author = author,
|
||||
origin = origin,
|
||||
coverPath = coverPath
|
||||
)
|
||||
}
|
||||
|
||||
DisposableEffect(viewModel) {
|
||||
|
||||
@@ -50,6 +50,7 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||
import io.legado.app.ui.main.MainIntent
|
||||
import io.legado.app.ui.widget.components.image.cover.buildCoverImageRequest
|
||||
import io.legado.app.utils.ArchiveUtils
|
||||
import io.legado.app.utils.GSON
|
||||
@@ -113,12 +114,37 @@ class BookInfoViewModel(
|
||||
private var readRecordObserveJob: Job? = null
|
||||
|
||||
fun initData(intent: Intent) {
|
||||
initData(intent.getStringExtra("bookUrl") ?: "")
|
||||
initData(
|
||||
bookUrl = intent.getStringExtra(MainIntent.EXTRA_BOOK_URL) ?: "",
|
||||
name = intent.getStringExtra(MainIntent.EXTRA_BOOK_NAME),
|
||||
author = intent.getStringExtra(MainIntent.EXTRA_BOOK_AUTHOR),
|
||||
origin = intent.getStringExtra(MainIntent.EXTRA_BOOK_ORIGIN),
|
||||
coverPath = intent.getStringExtra(MainIntent.EXTRA_BOOK_COVER)
|
||||
)
|
||||
}
|
||||
|
||||
fun initData(bookUrl: String) {
|
||||
fun initData(
|
||||
bookUrl: String,
|
||||
name: String? = null,
|
||||
author: String? = null,
|
||||
origin: String? = null,
|
||||
coverPath: String? = null
|
||||
) {
|
||||
if (currentBook?.bookUrl == bookUrl) return
|
||||
currentBook = null
|
||||
_uiState.value = BookInfoUiState() // 立即重置 UI 状态
|
||||
currentBook = if (!name.isNullOrBlank() && !author.isNullOrBlank()) {
|
||||
Book(
|
||||
bookUrl = bookUrl,
|
||||
name = name,
|
||||
author = author,
|
||||
origin = origin ?: BookType.localTag,
|
||||
coverUrl = coverPath
|
||||
).apply {
|
||||
addType(BookType.notShelf)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
currentChapterList = emptyList()
|
||||
currentWebFiles = emptyList()
|
||||
currentKindLabels = emptyList()
|
||||
@@ -128,24 +154,32 @@ class BookInfoViewModel(
|
||||
bookSource = null
|
||||
chapterChanged = false
|
||||
clearReadRecordObserve()
|
||||
_uiState.value = BookInfoUiState()
|
||||
syncUiState()
|
||||
execute {
|
||||
val book = appDb.bookDao.getBook(bookUrl)?.let {
|
||||
inBookshelf = !it.isNotShelf
|
||||
it
|
||||
} ?: appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()?.let {
|
||||
inBookshelf = false
|
||||
it
|
||||
} ?: throw NoStackTraceException("未找到书籍")
|
||||
|
||||
val dbBook = appDb.bookDao.getBook(bookUrl)
|
||||
if (dbBook != null) {
|
||||
inBookshelf = !dbBook.isNotShelf
|
||||
dbBook
|
||||
} else {
|
||||
val searchBook = appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()
|
||||
if (searchBook != null) {
|
||||
inBookshelf = false
|
||||
searchBook
|
||||
} else {
|
||||
currentBook ?: throw NoStackTraceException("未找到书籍")
|
||||
}
|
||||
}
|
||||
}.onSuccess { book ->
|
||||
// 如果从数据库/搜索中拿到的书没有封面,但我们有传入的封面,则保留传入的封面
|
||||
if (book.coverUrl.isNullOrBlank() && !coverPath.isNullOrBlank()) {
|
||||
book.coverUrl = coverPath
|
||||
}
|
||||
val source = if (book.isLocal) {
|
||||
null
|
||||
} else {
|
||||
appDb.bookSourceDao.getBookSource(book.origin)
|
||||
}
|
||||
book to source
|
||||
}.onSuccess {
|
||||
upBook(it.first, it.second)
|
||||
upBook(book, source)
|
||||
}.onError {
|
||||
context.toastOnUi(it.localizedMessage ?: "未找到书籍")
|
||||
emitEffect(BookInfoEffect.Finish(afterTransition = true))
|
||||
|
||||
@@ -40,13 +40,15 @@ class SearchActivity : BaseComposeActivity() {
|
||||
SearchScreen(
|
||||
viewModel = viewModel,
|
||||
onBack = { finish() },
|
||||
onOpenBookInfo = { name, author, bookUrl ->
|
||||
onOpenBookInfo = { name, author, bookUrl, origin, coverPath, _ ->
|
||||
startActivity(
|
||||
MainActivity.createBookInfoIntent(
|
||||
context = this,
|
||||
name = name,
|
||||
author = author,
|
||||
bookUrl = bookUrl
|
||||
bookUrl = bookUrl,
|
||||
origin = origin,
|
||||
coverPath = coverPath
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
@@ -58,7 +58,7 @@ sealed interface SearchIntent {
|
||||
data object PauseEngine : SearchIntent
|
||||
data object ResumeEngine : SearchIntent
|
||||
data class UseHistoryKeyword(val keyword: String) : SearchIntent
|
||||
data class OpenSearchBook(val book: SearchBook) : SearchIntent
|
||||
data class OpenSearchBook(val book: SearchBook, val sharedCoverKey: String?) : SearchIntent
|
||||
data class OpenBookshelfBook(val book: BookShelfItem) : SearchIntent
|
||||
data class DeleteHistory(val item: SearchKeyword) : SearchIntent
|
||||
data class SetClearHistoryDialogVisible(val visible: Boolean) : SearchIntent
|
||||
@@ -82,6 +82,9 @@ sealed interface SearchEffect {
|
||||
val name: String,
|
||||
val author: String,
|
||||
val bookUrl: String,
|
||||
val origin: String? = null,
|
||||
val coverPath: String? = null,
|
||||
val sharedCoverKey: String?,
|
||||
) : SearchEffect
|
||||
|
||||
data object OpenSourceManage : SearchEffect
|
||||
|
||||
@@ -90,7 +90,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
fun SearchScreen(
|
||||
viewModel: SearchViewModel,
|
||||
onBack: () -> Unit,
|
||||
onOpenBookInfo: (name: String, author: String, bookUrl: String) -> Unit,
|
||||
onOpenBookInfo: (name: String, author: String, bookUrl: String, origin: String?, coverPath: String?, sharedCoverKey: String?) -> Unit,
|
||||
onOpenSourceManage: () -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
@@ -160,7 +160,14 @@ fun SearchScreen(
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is SearchEffect.OpenBookInfo -> {
|
||||
onOpenBookInfo(effect.name, effect.author, effect.bookUrl)
|
||||
onOpenBookInfo(
|
||||
effect.name,
|
||||
effect.author,
|
||||
effect.bookUrl,
|
||||
effect.origin,
|
||||
effect.coverPath,
|
||||
effect.sharedCoverKey
|
||||
)
|
||||
}
|
||||
|
||||
SearchEffect.OpenSourceManage -> onOpenSourceManage()
|
||||
@@ -394,16 +401,25 @@ fun SearchScreen(
|
||||
itemsIndexed(
|
||||
items = state.results,
|
||||
key = { index, item -> "${item.book.origin}:${item.book.bookUrl}:$index" }
|
||||
) { _, item ->
|
||||
) { 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))
|
||||
viewModel.onIntent(
|
||||
SearchIntent.OpenSearchBook(
|
||||
item.book,
|
||||
sharedCoverKey
|
||||
)
|
||||
)
|
||||
},
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(item.book.bookUrl)
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import io.legado.app.domain.usecase.SearchBooksUseCase
|
||||
import io.legado.app.domain.usecase.SearchRunEvent
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.putPrefBoolean
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -107,6 +106,9 @@ class SearchViewModel(
|
||||
name = intent.book.name,
|
||||
author = intent.book.author,
|
||||
bookUrl = intent.book.bookUrl,
|
||||
origin = intent.book.origin,
|
||||
coverPath = intent.book.coverUrl,
|
||||
sharedCoverKey = intent.sharedCoverKey,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -117,6 +119,9 @@ class SearchViewModel(
|
||||
name = intent.book.name,
|
||||
author = intent.book.author,
|
||||
bookUrl = intent.book.bookUrl,
|
||||
origin = intent.book.origin,
|
||||
coverPath = intent.book.getDisplayCover(),
|
||||
sharedCoverKey = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -141,6 +141,8 @@ object ThemeConfig {
|
||||
|
||||
var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
|
||||
|
||||
var showHome by prefDelegate(PreferKey.showHome, true)
|
||||
|
||||
var showRss by prefDelegate(PreferKey.showRss, true)
|
||||
|
||||
var showStatusBar by prefDelegate(PreferKey.showStatusBar, true)
|
||||
|
||||
@@ -20,7 +20,6 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -47,7 +46,6 @@ import androidx.compose.material3.ButtonGroupDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -57,7 +55,6 @@ import androidx.compose.material3.ToggleButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -73,31 +70,29 @@ import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.color.DynamicColorsOptions
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.AppContextWrapper
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.help.LauncherIconHelp
|
||||
import io.legado.app.help.loadFontFiles
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.OldThemeConfig
|
||||
import io.legado.app.lib.theme.ThemeStore
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.help.loadFontFiles
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeEngine
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
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.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
|
||||
@@ -105,7 +100,7 @@ import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||
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.GlassTopAppBarDefaults
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||
import io.legado.app.utils.getPrefString
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.putPrefString
|
||||
@@ -113,10 +108,6 @@ import io.legado.app.utils.restart
|
||||
import io.legado.app.utils.takePersistablePermissionSafely
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import top.yukonga.miuix.kmp.basic.Card as MiuixCard
|
||||
import top.yukonga.miuix.kmp.basic.CardDefaults as MiuixCardDefaults
|
||||
import top.yukonga.miuix.kmp.basic.Text as MiuixText
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -138,16 +129,16 @@ fun ThemeConfigScreen(
|
||||
var showBorderColorPicker by remember { mutableStateOf(false) }
|
||||
var showNavIconSheet by remember { mutableStateOf(false) }
|
||||
var showFontSheet by remember { mutableStateOf(false) }
|
||||
var fontItems by remember { mutableStateOf<List<FileDoc>>(emptyList()) }
|
||||
var fontFolderUri by remember { mutableStateOf<Uri?>(null) }
|
||||
val showThemeRefactorTip by viewModel.showThemeRefactorTip.collectAsStateWithLifecycle()
|
||||
|
||||
fun loadFonts() {
|
||||
fontItems = loadFontFiles(context, fontFolderUri)
|
||||
var fontFolderUri by remember {
|
||||
mutableStateOf(
|
||||
context.getPrefString(PreferKey.fontFolder)?.let { Uri.parse(it) }
|
||||
)
|
||||
}
|
||||
remember {
|
||||
val saved = context.getPrefString(PreferKey.fontFolder)
|
||||
if (!saved.isNullOrEmpty()) fontFolderUri = Uri.parse(saved)
|
||||
loadFonts()
|
||||
|
||||
val fontItems = remember(fontFolderUri) {
|
||||
loadFontFiles(context, fontFolderUri)
|
||||
}
|
||||
|
||||
val fontFolderLauncher = rememberLauncherForActivityResult(
|
||||
@@ -157,7 +148,6 @@ fun ThemeConfigScreen(
|
||||
fontFolderUri = uri
|
||||
uri.takePersistablePermissionSafely(context, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
context.putPrefString(PreferKey.fontFolder, uri.toString())
|
||||
loadFonts()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,16 +208,28 @@ fun ThemeConfigScreen(
|
||||
themeItems.zip(themeValues).toList()
|
||||
}
|
||||
|
||||
if (isMiuixEngine) {
|
||||
MiuixCard(
|
||||
AnimatedVisibility(visible = showThemeRefactorTip) {
|
||||
GlassCard(
|
||||
cornerRadius = 16.dp,
|
||||
insideMargin = PaddingValues(16.dp),
|
||||
colors = MiuixCardDefaults.defaultColors(
|
||||
color = MiuixTheme.colorScheme.primaryVariant,
|
||||
contentColor = MiuixTheme.colorScheme.onPrimary
|
||||
)
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
MiuixText(stringResource(R.string.theme_config_miuix_experimental_warning))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
AppText(
|
||||
text = "仍有部分界面未用Compose重构,这些界面会与大部分界面有较大差异。",
|
||||
style = LegadoTheme.typography.labelLargeEmphasized,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
SmallIconButton(
|
||||
imageVector = AppIcons.Close,
|
||||
contentDescription = "关闭",
|
||||
onClick = {
|
||||
viewModel.setShowThemeRefactorTip(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +378,11 @@ fun ThemeConfigScreen(
|
||||
}
|
||||
|
||||
SplicedColumnGroup(title = stringResource(R.string.main_activity)) {
|
||||
SwitchSettingItem(
|
||||
title = stringResource(R.string.show_home),
|
||||
checked = ThemeConfig.showHome,
|
||||
onCheckedChange = { ThemeConfig.showHome = it }
|
||||
)
|
||||
SwitchSettingItem(
|
||||
title = stringResource(R.string.show_discovery),
|
||||
checked = ThemeConfig.showDiscovery,
|
||||
@@ -648,7 +655,11 @@ fun ThemeConfigScreen(
|
||||
.size(28.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(ThemeConfig.itemDividerColor))
|
||||
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.outlineVariant,
|
||||
CircleShape
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -771,7 +782,9 @@ fun ThemeConfigScreen(
|
||||
content = {
|
||||
if (fontItems.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().height(120.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
@@ -789,7 +802,9 @@ fun ThemeConfigScreen(
|
||||
fontItems.forEach { fontDoc ->
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().height(100.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
onClick = {
|
||||
ThemeConfig.appFontPath = fontDoc.uri.toString()
|
||||
showFontSheet = false
|
||||
|
||||
@@ -2,19 +2,40 @@ package io.legado.app.ui.config.themeConfig
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.utils.FileDoc
|
||||
import io.legado.app.utils.FileUtils
|
||||
import io.legado.app.utils.MD5Utils
|
||||
import io.legado.app.utils.externalFiles
|
||||
import io.legado.app.utils.inputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class ThemeConfigViewModel : ViewModel() {
|
||||
class ThemeConfigViewModel(
|
||||
private val localPreferencesRepository: LocalPreferencesRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val showThemeRefactorTip = localPreferencesRepository
|
||||
.getPreference(LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP, true)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)
|
||||
|
||||
fun setShowThemeRefactorTip(show: Boolean) {
|
||||
viewModelScope.launch {
|
||||
localPreferencesRepository.updatePreference(
|
||||
LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP,
|
||||
show
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置背景图片
|
||||
|
||||
@@ -31,7 +31,6 @@ import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.help.config.ThemeExportData
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.SearchBar
|
||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
@@ -164,6 +163,11 @@ fun EditThemeSheet(
|
||||
|
||||
// Interface layout
|
||||
SectionTitle(stringResource(R.string.theme_manage_section_layout))
|
||||
CompactSwitchSettingItem(
|
||||
title = "首页",
|
||||
checked = data.showHome,
|
||||
onCheckedChange = { data = data.copy(showHome = it) }
|
||||
)
|
||||
CompactSwitchSettingItem(
|
||||
title = stringResource(R.string.theme_manage_show_discovery),
|
||||
checked = data.showDiscovery,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
package io.legado.app.ui.main
|
||||
|
||||
fun bookCoverSharedElementKey(bookUrl: String): String = "book-cover:$bookUrl"
|
||||
fun bookCoverSharedElementKey(bookUrl: String, sourceId: String? = null): String {
|
||||
val source = sourceId?.takeIf { it.isNotBlank() } ?: return "book-cover:$bookUrl"
|
||||
return "book-cover:$source:$bookUrl"
|
||||
}
|
||||
|
||||
@@ -100,8 +100,11 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
context: Context,
|
||||
name: String? = null,
|
||||
author: String? = null,
|
||||
bookUrl: String
|
||||
): Intent = MainIntent.createBookInfoIntent(context, name, author, bookUrl)
|
||||
bookUrl: String,
|
||||
origin: String? = null,
|
||||
coverPath: String? = null
|
||||
): Intent =
|
||||
MainIntent.createBookInfoIntent(context, name, author, bookUrl, origin, coverPath)
|
||||
|
||||
fun createExploreShowIntent(
|
||||
context: Context,
|
||||
|
||||
@@ -12,6 +12,8 @@ object MainIntent {
|
||||
const val EXTRA_BOOK_NAME = "name"
|
||||
const val EXTRA_BOOK_AUTHOR = "author"
|
||||
const val EXTRA_BOOK_URL = "bookUrl"
|
||||
const val EXTRA_BOOK_ORIGIN = "origin"
|
||||
const val EXTRA_BOOK_COVER = "coverPath"
|
||||
const val EXTRA_EXPLORE_NAME = "exploreName"
|
||||
const val EXTRA_SOURCE_URL = "sourceUrl"
|
||||
const val EXTRA_EXPLORE_URL = "exploreUrl"
|
||||
@@ -114,13 +116,17 @@ object MainIntent {
|
||||
context: Context,
|
||||
name: String? = null,
|
||||
author: String? = null,
|
||||
bookUrl: String
|
||||
bookUrl: String,
|
||||
origin: String? = null,
|
||||
coverPath: String? = null
|
||||
): Intent {
|
||||
return createLauncherIntent(context).apply {
|
||||
putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_BOOK_INFO)
|
||||
putExtra(EXTRA_BOOK_NAME, name)
|
||||
putExtra(EXTRA_BOOK_AUTHOR, author)
|
||||
putExtra(EXTRA_BOOK_URL, bookUrl)
|
||||
putExtra(EXTRA_BOOK_ORIGIN, origin)
|
||||
putExtra(EXTRA_BOOK_COVER, coverPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,12 +90,15 @@ fun MainActivity.mainEntryProvider(
|
||||
onNavigateToBookCacheManage = {
|
||||
onNavigateToRoute(MainRouteBookCacheManage)
|
||||
},
|
||||
onNavigateToBookInfo = { name, author, bookUrl ->
|
||||
onNavigateToBookInfo = { name, author, bookUrl, origin, coverPath, sharedCoverKey ->
|
||||
onNavigateToRoute(
|
||||
MainRouteBookInfo(
|
||||
name = name,
|
||||
author = author,
|
||||
bookUrl = bookUrl
|
||||
bookUrl = bookUrl,
|
||||
origin = origin,
|
||||
coverPath = coverPath,
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -246,12 +249,15 @@ fun MainActivity.mainEntryProvider(
|
||||
searchViewModel.onIntent(SearchIntent.ClearSearchResults)
|
||||
onNavigateBack()
|
||||
},
|
||||
onOpenBookInfo = { name, author, bookUrl ->
|
||||
onOpenBookInfo = { name, author, bookUrl, origin, coverPath, sharedCoverKey ->
|
||||
onNavigateToRoute(
|
||||
MainRouteBookInfo(
|
||||
name = name,
|
||||
author = author,
|
||||
bookUrl = bookUrl
|
||||
bookUrl = bookUrl,
|
||||
origin = origin,
|
||||
coverPath = coverPath,
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -395,9 +401,13 @@ fun MainActivity.mainEntryProvider(
|
||||
} else null
|
||||
}
|
||||
) { route ->
|
||||
val bookInfoViewModel = koinViewModel<BookInfoViewModel>()
|
||||
val bookInfoViewModel = koinViewModel<BookInfoViewModel>(key = route.bookUrl)
|
||||
BookInfoRouteScreen(
|
||||
bookUrl = route.bookUrl,
|
||||
name = route.name,
|
||||
author = route.author,
|
||||
origin = route.origin,
|
||||
coverPath = route.coverPath,
|
||||
viewModel = bookInfoViewModel,
|
||||
onBack = { onNavigateBack() },
|
||||
onFinish = { _, _ -> onNavigateBack() },
|
||||
@@ -406,7 +416,7 @@ fun MainActivity.mainEntryProvider(
|
||||
},
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||
sharedCoverKey = bookCoverSharedElementKey(route.bookUrl),
|
||||
sharedCoverKey = route.sharedCoverKey ?: bookCoverSharedElementKey(route.bookUrl),
|
||||
onRegisterVariableSetter = { setter ->
|
||||
onRegisterVariableSetter(setter)
|
||||
}
|
||||
@@ -419,12 +429,15 @@ fun MainActivity.mainEntryProvider(
|
||||
sourceUrl = route.sourceUrl,
|
||||
exploreUrl = route.exploreUrl,
|
||||
onBack = { onNavigateBack() },
|
||||
onBookClick = { book ->
|
||||
onBookClick = { book, sharedCoverKey ->
|
||||
onNavigateToRoute(
|
||||
MainRouteBookInfo(
|
||||
name = book.name,
|
||||
author = book.author,
|
||||
bookUrl = book.bookUrl
|
||||
bookUrl = book.bookUrl,
|
||||
origin = book.origin,
|
||||
coverPath = book.coverUrl,
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
@@ -65,6 +65,9 @@ data class MainRouteBookInfo(
|
||||
val name: String?,
|
||||
val author: String?,
|
||||
val bookUrl: String,
|
||||
val origin: String? = null,
|
||||
val coverPath: String? = null,
|
||||
val sharedCoverKey: String? = null,
|
||||
) : MainRoute
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -254,7 +254,9 @@ object MainNavigator {
|
||||
MainRouteBookInfo(
|
||||
name = intent.getStringExtra(MainIntent.EXTRA_BOOK_NAME),
|
||||
author = intent.getStringExtra(MainIntent.EXTRA_BOOK_AUTHOR),
|
||||
bookUrl = bookUrl
|
||||
bookUrl = bookUrl,
|
||||
origin = intent.getStringExtra(MainIntent.EXTRA_BOOK_ORIGIN),
|
||||
coverPath = intent.getStringExtra(MainIntent.EXTRA_BOOK_COVER)
|
||||
)
|
||||
} ?: MainRouteHome
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ fun MainScreen(
|
||||
onNavigateToLocalImport: () -> Unit,
|
||||
onNavigateToCache: (Long) -> Unit,
|
||||
onNavigateToBookCacheManage: () -> Unit,
|
||||
onNavigateToBookInfo: (name: String, author: String, bookUrl: String) -> Unit,
|
||||
onNavigateToBookInfo: (name: String, author: String, bookUrl: String, origin: String?, coverPath: String?, sharedCoverKey: String?) -> Unit,
|
||||
onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
||||
onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
|
||||
onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
|
||||
@@ -368,8 +368,15 @@ fun MainScreen(
|
||||
val destination = destinations.getOrNull(page) ?: return@HorizontalPager
|
||||
when (destination) {
|
||||
MainDestination.Home -> HomepageScreen(
|
||||
onBookClick = { name, author, bookUrl ->
|
||||
onNavigateToBookInfo(name ?: "", author ?: "", bookUrl)
|
||||
onBookClick = { name, author, bookUrl, origin, coverPath, sharedCoverKey ->
|
||||
onNavigateToBookInfo(
|
||||
name ?: "",
|
||||
author ?: "",
|
||||
bookUrl,
|
||||
origin,
|
||||
coverPath,
|
||||
sharedCoverKey
|
||||
)
|
||||
},
|
||||
onModuleHeaderClick = { title, sourceUrl, exploreUrl ->
|
||||
onNavigateToExploreShow(title, sourceUrl, exploreUrl)
|
||||
@@ -382,8 +389,15 @@ fun MainScreen(
|
||||
onBookClick = { book ->
|
||||
context.startActivityForBook(book)
|
||||
},
|
||||
onBookLongClick = { book ->
|
||||
onNavigateToBookInfo(book.name, book.author, book.bookUrl)
|
||||
onBookLongClick = { book, sharedCoverKey ->
|
||||
onNavigateToBookInfo(
|
||||
book.name,
|
||||
book.author,
|
||||
book.bookUrl,
|
||||
book.origin,
|
||||
book.getDisplayCover(),
|
||||
sharedCoverKey
|
||||
)
|
||||
},
|
||||
onNavigateToSearch = { query -> onNavigateToSearch(query) },
|
||||
onNavigateToRemoteImport = onNavigateToRemoteImport,
|
||||
|
||||
@@ -31,6 +31,7 @@ class MainViewModel(
|
||||
private val prefs = context.defaultSharedPreferences
|
||||
private val mainPreferenceKeys = setOf(
|
||||
PreferKey.showDiscovery,
|
||||
PreferKey.showHome,
|
||||
PreferKey.showRss,
|
||||
PreferKey.showBottomView,
|
||||
PreferKey.useFloatingBottomBar,
|
||||
@@ -152,10 +153,12 @@ private const val NAV_EXTENDED_KEY = "navExtended"
|
||||
|
||||
private fun MainViewModel.readMainUiState(): MainUiState {
|
||||
val showDiscovery = context.getPrefBoolean(PreferKey.showDiscovery, true)
|
||||
val showHome = context.getPrefBoolean(PreferKey.showHome, true)
|
||||
val showRss = context.getPrefBoolean(PreferKey.showRss, true)
|
||||
val destinations = MainDestination.mainDestinations.filter {
|
||||
when (it) {
|
||||
MainDestination.Explore -> showDiscovery
|
||||
MainDestination.Home -> showHome
|
||||
MainDestination.Rss -> showRss
|
||||
else -> true
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ import sh.calvin.reorderable.rememberReorderableLazyGridState
|
||||
fun BookshelfScreen(
|
||||
viewModel: BookshelfViewModel = koinViewModel(),
|
||||
onBookClick: (BookShelfItem) -> Unit,
|
||||
onBookLongClick: (BookShelfItem) -> Unit,
|
||||
onBookLongClick: (book: BookShelfItem, sharedCoverKey: String?) -> Unit,
|
||||
onNavigateToSearch: (String) -> Unit,
|
||||
onNavigateToRemoteImport: () -> Unit,
|
||||
onNavigateToLocalImport: () -> Unit,
|
||||
@@ -1211,7 +1211,7 @@ fun BookshelfPage(
|
||||
onDragFinished: () -> Unit,
|
||||
onGlobalSearch: () -> Unit,
|
||||
onBookClick: (BookShelfItem) -> Unit,
|
||||
onBookLongClick: (BookShelfItem) -> Unit,
|
||||
onBookLongClick: (BookShelfItem, String?) -> Unit,
|
||||
isCurrentPage: Boolean = true,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
@@ -1327,6 +1327,14 @@ fun BookshelfPage(
|
||||
) {
|
||||
items(displayBooks, key = { it.book.bookUrl }) { bookUi ->
|
||||
val isSelected = selectedBookUrls.contains(bookUi.book.bookUrl)
|
||||
val sharedCoverKey = if (isCurrentPage) {
|
||||
bookCoverSharedElementKey(
|
||||
bookUi.book.bookUrl,
|
||||
"bookshelf:${uiState.selectedGroupId}"
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
ReorderableItem(
|
||||
state = reorderableState,
|
||||
key = bookUi.book.bookUrl,
|
||||
@@ -1366,7 +1374,7 @@ fun BookshelfPage(
|
||||
searchKey = uiState.searchKey,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = if (isCurrentPage) bookCoverSharedElementKey(bookUi.book.bookUrl) else null,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
onClick = {
|
||||
if (uiState.isEditMode) {
|
||||
onToggleBookSelection(bookUi)
|
||||
@@ -1381,7 +1389,7 @@ fun BookshelfPage(
|
||||
if (uiState.isEditMode) {
|
||||
onToggleBookSelection(bookUi)
|
||||
} else {
|
||||
onBookLongClick(bookUi.book)
|
||||
onBookLongClick(bookUi.book, sharedCoverKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ sealed interface HomepageEffect {
|
||||
val name: String?,
|
||||
val author: String?,
|
||||
val bookUrl: String,
|
||||
val origin: String? = null,
|
||||
val coverPath: String? = null,
|
||||
val sharedCoverKey: String?,
|
||||
) : HomepageEffect
|
||||
|
||||
data class NavigateToExploreShow(
|
||||
|
||||
@@ -43,9 +43,12 @@ import io.legado.app.ui.widget.components.JsonRawEditor
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
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.card.GlassCard
|
||||
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.divider.PillDivider
|
||||
import io.legado.app.ui.widget.components.divider.PillHeaderDivider
|
||||
import io.legado.app.ui.widget.components.explore.ExploreKindSelectSheet
|
||||
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.RoundDropdownMenuItem
|
||||
@@ -102,6 +105,7 @@ fun <T> HomepageModuleManageSheet(
|
||||
var browseModuleType by remember(data != null) { mutableStateOf("card") }
|
||||
var selectedKindTitles by remember(data != null) { mutableStateOf<Set<String>>(emptySet()) }
|
||||
var showCustomSetAddModules by remember(data != null) { mutableStateOf(false) }
|
||||
var showKindSelect by remember(data != null) { mutableStateOf(false) }
|
||||
var showAddButtonGroupDialog by remember(data != null) { mutableStateOf(false) }
|
||||
val defaultQuickActionsTitle = stringResource(R.string.homepage_quick_actions)
|
||||
var tempButtonGroupTitle by remember(data != null) { mutableStateOf(defaultQuickActionsTitle) }
|
||||
@@ -310,8 +314,7 @@ fun <T> HomepageModuleManageSheet(
|
||||
onClick = { deleteConfirmId = module.id },
|
||||
imageVector = Icons.Default.Delete
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -353,8 +356,7 @@ fun <T> HomepageModuleManageSheet(
|
||||
onClick = { deleteConfirmId = module.id },
|
||||
imageVector = Icons.Default.Delete
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -398,8 +400,7 @@ fun <T> HomepageModuleManageSheet(
|
||||
sourceUrl = browseUrl,
|
||||
)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -408,90 +409,76 @@ fun <T> HomepageModuleManageSheet(
|
||||
|
||||
2 -> {
|
||||
val isButtonGroup = browseModuleType == "buttonGroup"
|
||||
val selectableKinds = exploreKinds
|
||||
Column {
|
||||
val typeList = remember {
|
||||
HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown }
|
||||
}
|
||||
CompactDropdownSettingItem(
|
||||
title = stringResource(R.string.homepage_module_type),
|
||||
selectedValue = browseModuleType,
|
||||
displayEntries = typeList.map { it.title }.toTypedArray(),
|
||||
entryValues = typeList.map { it.key }.toTypedArray(),
|
||||
onValueChange = {
|
||||
browseModuleType = it; selectedKindTitles = emptySet()
|
||||
|
||||
GlassCard(
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
cornerRadius = 12.dp
|
||||
) {
|
||||
CompactDropdownSettingItem(
|
||||
title = stringResource(R.string.homepage_module_type),
|
||||
selectedValue = browseModuleType,
|
||||
displayEntries = typeList.map { it.title }.toTypedArray(),
|
||||
entryValues = typeList.map { it.key }.toTypedArray(),
|
||||
onValueChange = {
|
||||
browseModuleType = it; selectedKindTitles = emptySet()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
SelectionItemCard(
|
||||
title = stringResource(R.string.homepage_select_from_kinds),
|
||||
subtitle = if (isButtonGroup) {
|
||||
if (selectedKindTitles.isEmpty()) stringResource(R.string.homepage_select_multiple_kinds)
|
||||
else stringResource(
|
||||
R.string.homepage_n_selected,
|
||||
selectedKindTitles.size
|
||||
)
|
||||
} else {
|
||||
stringResource(R.string.homepage_select_one_kind)
|
||||
},
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
onToggleSelection = { showKindSelect = true },
|
||||
trailingAction = {
|
||||
if (isButtonGroup && selectedKindTitles.isNotEmpty()) {
|
||||
SmallIconButton(
|
||||
onClick = { showAddButtonGroupDialog = true },
|
||||
imageVector = Icons.Default.Check
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (selectableKinds.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AppText(
|
||||
stringResource(R.string.homepage_source_no_discover),
|
||||
color = LegadoTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
AppText(
|
||||
stringResource(R.string.homepage_select_items),
|
||||
style = LegadoTheme.typography.labelMedium,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 4.dp
|
||||
)
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(
|
||||
selectableKinds.distinctBy { it.first + it.second },
|
||||
key = { it.first + it.second }) { (kindTitle, kindUrl) ->
|
||||
if (isButtonGroup) {
|
||||
val isSelected = kindTitle in selectedKindTitles
|
||||
SelectionItemCard(
|
||||
title = kindTitle,
|
||||
subtitle = kindUrl.take(60),
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
isSelected = isSelected,
|
||||
inSelectionMode = true,
|
||||
onToggleSelection = {
|
||||
selectedKindTitles =
|
||||
if (isSelected) selectedKindTitles - kindTitle
|
||||
else selectedKindTitles + kindTitle
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
} else {
|
||||
val isJoined = joinedKeys.contains(kindTitle)
|
||||
SelectionItemCard(
|
||||
title = kindTitle,
|
||||
subtitle = kindUrl.take(60) + if (isJoined) stringResource(
|
||||
R.string.homepage_status_joined
|
||||
) else "",
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
isSelected = isJoined,
|
||||
inSelectionMode = true,
|
||||
onToggleSelection = {
|
||||
if (!isJoined) addDialogPrefill =
|
||||
AddDialogPrefill(
|
||||
kindTitle,
|
||||
kindUrl,
|
||||
browseModuleType
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
|
||||
ExploreKindSelectSheet(
|
||||
show = showKindSelect,
|
||||
onDismissRequest = { showKindSelect = false },
|
||||
sourceUrl = browseUrl,
|
||||
multiple = isButtonGroup,
|
||||
initialSelectedTitles = selectedKindTitles.toList(),
|
||||
onSelected = { kinds ->
|
||||
if (isButtonGroup) {
|
||||
selectedKindTitles = kinds.map { it.title }.toSet()
|
||||
} else {
|
||||
kinds.firstOrNull()?.let { kind ->
|
||||
addDialogPrefill = AddDialogPrefill(
|
||||
title = kind.title,
|
||||
url = kind.url ?: "",
|
||||
type = browseModuleType
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
)
|
||||
|
||||
PillDivider(
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.homepage_manual_add),
|
||||
onClick = {
|
||||
@@ -544,8 +531,7 @@ fun <T> HomepageModuleManageSheet(
|
||||
joinedInCurrent =
|
||||
joinedInCurrent + (module.moduleKey to "temp_${module.id}")
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -567,8 +553,7 @@ fun <T> HomepageModuleManageSheet(
|
||||
onToggleSelection = {
|
||||
browsingSourceUrl = source.sourceUrl
|
||||
browsingDetail = true
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -662,8 +647,7 @@ fun <T> HomepageModuleManageSheet(
|
||||
onClick = { deleteConfirmId = module.id },
|
||||
imageVector = Icons.Default.Delete
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -694,8 +678,7 @@ fun <T> HomepageModuleManageSheet(
|
||||
onClick = { deleteConfirmId = module.id },
|
||||
imageVector = Icons.Default.Delete
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -770,8 +753,12 @@ fun <T> HomepageModuleManageSheet(
|
||||
onClick = { deleteSetConfirmId = set.sourceUrl },
|
||||
imageVector = Icons.Default.Delete
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
PillDivider(
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
item(key = "create_set") {
|
||||
@@ -1001,41 +988,51 @@ fun <T> AddCustomModuleDialog(
|
||||
.fillMaxWidth()
|
||||
.height(400.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
AppTextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
label = stringResource(R.string.homepage_title_label),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
AppTextField(
|
||||
value = url,
|
||||
onValueChange = { url = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
label = "URL",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
val typeList = remember {
|
||||
HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown }
|
||||
}
|
||||
DropdownListSettingItem(
|
||||
title = stringResource(R.string.homepage_type_label),
|
||||
selectedValue = type,
|
||||
displayEntries = typeList.map { it.title }.toTypedArray(),
|
||||
entryValues = typeList.map { it.key }.toTypedArray(),
|
||||
onValueChange = { type = it }
|
||||
)
|
||||
|
||||
GlassCard(
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent
|
||||
) {
|
||||
DropdownListSettingItem(
|
||||
title = stringResource(R.string.homepage_type_label),
|
||||
selectedValue = type,
|
||||
displayEntries = typeList.map { it.title }.toTypedArray(),
|
||||
entryValues = typeList.map { it.key }.toTypedArray(),
|
||||
onValueChange = { type = it }
|
||||
)
|
||||
}
|
||||
|
||||
AppTextField(
|
||||
value = args,
|
||||
onValueChange = { args = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
label = "Args (JSON)",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
AppText(
|
||||
text = stringResource(R.string.homepage_layout_config_label),
|
||||
style = LegadoTheme.typography.labelMedium,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp)
|
||||
|
||||
PillHeaderDivider(
|
||||
title = stringResource(R.string.homepage_layout_config_label)
|
||||
)
|
||||
|
||||
if (hasVisualizableKeys) {
|
||||
JsonConfigEditor(
|
||||
jsonString = layoutConfig,
|
||||
|
||||
@@ -10,27 +10,28 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.items
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.GridView
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -46,7 +47,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -60,12 +60,15 @@ import io.legado.app.ui.main.homepage.modules.GridModule
|
||||
import io.legado.app.ui.main.homepage.modules.GridRankingModule
|
||||
import io.legado.app.ui.main.homepage.modules.RankingModule
|
||||
import io.legado.app.ui.main.homepage.modules.WaterfallItem
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppPullToRefresh
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.LoadMoreFooter
|
||||
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.button.SecondaryButton
|
||||
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
||||
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.progressIndicator.AppCircularProgressIndicator
|
||||
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
@@ -84,7 +87,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
@Composable
|
||||
fun HomepageScreen(
|
||||
viewModel: HomepageViewModel = koinViewModel(),
|
||||
onBookClick: (name: String?, author: String?, bookUrl: String) -> Unit,
|
||||
onBookClick: (name: String?, author: String?, bookUrl: String, origin: String?, coverPath: String?, sharedCoverKey: String?) -> Unit,
|
||||
onModuleHeaderClick: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
@@ -137,7 +140,14 @@ fun HomepageScreen(
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is HomepageEffect.NavigateToBookInfo ->
|
||||
onBookClick(effect.name, effect.author, effect.bookUrl)
|
||||
onBookClick(
|
||||
effect.name,
|
||||
effect.author,
|
||||
effect.bookUrl,
|
||||
effect.origin,
|
||||
effect.coverPath,
|
||||
effect.sharedCoverKey
|
||||
)
|
||||
|
||||
is HomepageEffect.NavigateToExploreShow ->
|
||||
onModuleHeaderClick(effect.title, effect.sourceUrl, effect.exploreUrl)
|
||||
@@ -238,6 +248,7 @@ fun HomepageScreen(
|
||||
data = errorMsg,
|
||||
onDismissRequest = { errorMsg = null },
|
||||
title = stringResource(R.string.homepage_module_error),
|
||||
text = errorMsg,
|
||||
confirmText = stringResource(R.string.copy_text),
|
||||
onConfirm = {
|
||||
context.sendToClip(it)
|
||||
@@ -355,12 +366,14 @@ private fun ModuleList(
|
||||
item(key = "header_${moduleUi.globalId}", span = StaggeredGridItemSpan.FullLine) {
|
||||
ModuleHeader(
|
||||
title = moduleUi.title,
|
||||
onNavigate = {
|
||||
viewModel.onModuleHeaderClick(
|
||||
moduleUi.sourceUrl,
|
||||
moduleUi.exploreUrl,
|
||||
moduleUi.title,
|
||||
)
|
||||
onNavigate = if (moduleUi.type == HomepageModuleType.ButtonGroup) null else {
|
||||
{
|
||||
viewModel.onModuleHeaderClick(
|
||||
moduleUi.sourceUrl,
|
||||
moduleUi.exploreUrl,
|
||||
moduleUi.title,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -390,27 +403,78 @@ private fun ModuleList(
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(80.dp)
|
||||
.clickable { onErrorClick(state.message) },
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
AppText(
|
||||
text = state.message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.retry),
|
||||
onClick = {
|
||||
viewModel.retryModule(moduleUi.globalId)
|
||||
GlassCard(
|
||||
onClick = { onErrorClick(state.message) },
|
||||
containerColor = LegadoTheme.colorScheme.errorContainer.copy(
|
||||
alpha = 0.6f
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 16.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
|
||||
AppIcon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.error
|
||||
)
|
||||
|
||||
AppText(
|
||||
text = state.message,
|
||||
color = LegadoTheme.colorScheme.error,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = LegadoTheme.colorScheme.error.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
viewModel.retryModule(moduleUi.globalId)
|
||||
}
|
||||
.padding(vertical = 10.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.error
|
||||
)
|
||||
|
||||
AppText(
|
||||
text = "重试",
|
||||
color = LegadoTheme.colorScheme.error,
|
||||
style = LegadoTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,68 +499,63 @@ private fun ModuleList(
|
||||
val config = moduleUi.config
|
||||
when (moduleUi.type) {
|
||||
HomepageModuleType.Waterfall -> {
|
||||
items(
|
||||
itemsIndexed(
|
||||
state.books,
|
||||
key = { "wf_${moduleUi.globalId}_${it.bookUrl}" }) { book ->
|
||||
key = { index, book -> "wf_${moduleUi.globalId}_${book.bookUrl}_$index" }) { index, book ->
|
||||
val sharedCoverKey = bookCoverSharedElementKey(
|
||||
book.bookUrl,
|
||||
"home:${moduleUi.globalId}:waterfall:$index"
|
||||
)
|
||||
WaterfallItem(
|
||||
book = book,
|
||||
onClick = { viewModel.onBookClick(book) },
|
||||
onClick = { viewModel.onBookClick(book, sharedCoverKey) },
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
}
|
||||
|
||||
if (state.hasMore) {
|
||||
item(
|
||||
key = "wf_more_${moduleUi.globalId}",
|
||||
span = StaggeredGridItemSpan.FullLine
|
||||
) {
|
||||
LaunchedEffect(state.books.size) {
|
||||
viewModel.loadMoreModule(moduleUi.globalId)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AppCircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
item(
|
||||
key = "wf_more_${moduleUi.globalId}",
|
||||
span = StaggeredGridItemSpan.FullLine
|
||||
) {
|
||||
LoadMoreFooter(
|
||||
isLoading = state.isLoadingMore,
|
||||
errorMsg = null,
|
||||
isEnd = !state.hasMore,
|
||||
onRetry = { viewModel.loadMoreModule(moduleUi.globalId) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HomepageModuleType.InfiniteGrid -> {
|
||||
items(
|
||||
itemsIndexed(
|
||||
state.books,
|
||||
key = { "inf_grid_${moduleUi.globalId}_${it.bookUrl}" }) { book ->
|
||||
key = { index, book -> "inf_grid_${moduleUi.globalId}_${book.bookUrl}_$index" }) { index, book ->
|
||||
val sharedCoverKey = bookCoverSharedElementKey(
|
||||
book.bookUrl,
|
||||
"home:${moduleUi.globalId}:infinite:$index"
|
||||
)
|
||||
SearchBookGridItem(
|
||||
book = book,
|
||||
shelfState = io.legado.app.domain.model.BookShelfState.NOT_IN_SHELF,
|
||||
onClick = { viewModel.onBookClick(book) },
|
||||
onClick = { viewModel.onBookClick(book, sharedCoverKey) },
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
}
|
||||
|
||||
if (state.hasMore) {
|
||||
item(
|
||||
key = "inf_grid_more_${moduleUi.globalId}",
|
||||
span = StaggeredGridItemSpan.FullLine
|
||||
) {
|
||||
LaunchedEffect(state.books.size) {
|
||||
viewModel.loadMoreModule(moduleUi.globalId)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AppCircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
item(
|
||||
key = "inf_grid_more_${moduleUi.globalId}",
|
||||
span = StaggeredGridItemSpan.FullLine
|
||||
) {
|
||||
LoadMoreFooter(
|
||||
isLoading = state.isLoadingMore,
|
||||
errorMsg = null,
|
||||
isEnd = !state.hasMore,
|
||||
onRetry = { viewModel.loadMoreModule(moduleUi.globalId) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,12 +568,15 @@ private fun ModuleList(
|
||||
) {
|
||||
GridModule(
|
||||
books = state.books,
|
||||
onClick = { viewModel.onBookClick(it) },
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
columns = columns,
|
||||
maxRows = rows,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -528,10 +590,13 @@ private fun ModuleList(
|
||||
) {
|
||||
BannerModule(
|
||||
books = state.books,
|
||||
onClick = { viewModel.onBookClick(it) },
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:banner",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -543,10 +608,13 @@ private fun ModuleList(
|
||||
) {
|
||||
RankingModule(
|
||||
books = state.books,
|
||||
onClick = { viewModel.onBookClick(it) },
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:ranking",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -558,11 +626,14 @@ private fun ModuleList(
|
||||
) {
|
||||
GridRankingModule(
|
||||
books = state.books,
|
||||
onClick = { viewModel.onBookClick(it) },
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
rows = config["layout_rows"]?.toIntOrNull() ?: 4,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid-ranking",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -574,10 +645,13 @@ private fun ModuleList(
|
||||
) {
|
||||
CardModule(
|
||||
books = state.books,
|
||||
onClick = { viewModel.onBookClick(it) },
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:card",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -596,7 +670,7 @@ private fun ModuleList(
|
||||
@Composable
|
||||
private fun ModuleHeader(
|
||||
title: String,
|
||||
onNavigate: () -> Unit,
|
||||
onNavigate: (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -612,9 +686,11 @@ private fun ModuleHeader(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
SmallTonalIconButton(
|
||||
onClick = onNavigate,
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowForward
|
||||
)
|
||||
if (onNavigate != null) {
|
||||
SmallTonalIconButton(
|
||||
onClick = onNavigate,
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowForward
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import io.legado.app.domain.usecase.SaveSearchBooksUseCase
|
||||
import io.legado.app.help.source.exploreKinds
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonArray
|
||||
import io.legado.app.utils.stackTraceStr
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -215,28 +216,10 @@ class HomepageViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
// sync: 只处理有 homepageModules 的书源
|
||||
viewModelScope.launch {
|
||||
initModulesSyncFlow.collect { sources ->
|
||||
sources.forEach { source -> syncModulesFromSource(source) }
|
||||
}
|
||||
}
|
||||
|
||||
// cache: 所有启用发现的书源(包括无 homepageModules 的)
|
||||
// 清理 _pendingUserModules 中已入库的条目
|
||||
viewModelScope.launch {
|
||||
exploreSourcesFlow.collect { sources ->
|
||||
_bookSourcesCache.value = sources.associateBy { it.bookSourceUrl }
|
||||
val kindsCache = mutableMapOf<String, List<Pair<String, String>>>()
|
||||
for (source in sources) {
|
||||
kindsCache[source.bookSourceUrl] = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
source.exploreKinds().map { it.title to (it.url ?: "") }
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
_exploreKindsCache.value = kindsCache
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +350,7 @@ class HomepageViewModel(
|
||||
}.onFailure { e ->
|
||||
_moduleContentStates.update {
|
||||
it + (module.id to ModuleLoadState.Error(
|
||||
e.message ?: "Unknown error"
|
||||
e.stackTraceStr
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -404,7 +387,7 @@ class HomepageViewModel(
|
||||
}.onFailure { e ->
|
||||
_moduleContentStates.update {
|
||||
it + (module.id to ModuleLoadState.Error(
|
||||
e.message ?: "Unknown error"
|
||||
e.stackTraceStr
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -454,7 +437,7 @@ class HomepageViewModel(
|
||||
HomepageEffect.ShowSnackbar(
|
||||
getApplication<Application>().getString(
|
||||
R.string.homepage_load_more_failed,
|
||||
e.message ?: ""
|
||||
e.stackTraceStr
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -477,6 +460,13 @@ class HomepageViewModel(
|
||||
_isRefreshing.value = true
|
||||
loadJobs.values.forEach { it.cancel() }
|
||||
loadJobs.clear()
|
||||
|
||||
// 刷新时同步当前已启用模块所属书源的定义
|
||||
val activeSourceUrls = uiState.value.modules.map { it.sourceUrl }.distinct()
|
||||
activeSourceUrls.forEach { url ->
|
||||
resolveBookSource(url)?.let { syncModulesFromSource(it) }
|
||||
}
|
||||
|
||||
_moduleContentStates.value = emptyMap()
|
||||
uiState.map { it.modules }.first { modules ->
|
||||
modules.all { it.state !is ModuleLoadState.Loading }
|
||||
@@ -704,6 +694,12 @@ class HomepageViewModel(
|
||||
targetSetId: String? = null
|
||||
): List<HomepageModuleManageUi> {
|
||||
val source = resolveBookSource(sourceUrl) ?: return emptyList()
|
||||
|
||||
// 按需同步:只有进入该源的管理页才同步其 JSON 定义
|
||||
viewModelScope.launch {
|
||||
syncModulesFromSource(source)
|
||||
}
|
||||
|
||||
val json = source.homepageModules ?: return emptyList()
|
||||
val jsonDefs = parseBookSourceModules(source, json)
|
||||
|
||||
@@ -850,10 +846,19 @@ class HomepageViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun onBookClick(book: SearchBook) {
|
||||
fun onBookClick(book: SearchBook, sharedCoverKey: String?) {
|
||||
viewModelScope.launch {
|
||||
saveSearchBooksUseCase.save(book)
|
||||
_effects.emit(HomepageEffect.NavigateToBookInfo(book.name, book.author, book.bookUrl))
|
||||
_effects.emit(
|
||||
HomepageEffect.NavigateToBookInfo(
|
||||
name = book.name,
|
||||
author = book.author,
|
||||
bookUrl = book.bookUrl,
|
||||
origin = book.origin,
|
||||
coverPath = book.coverUrl,
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -909,4 +914,4 @@ private data class HomepageUiFlags(
|
||||
val isRefreshing: Boolean,
|
||||
val isManageMode: Boolean,
|
||||
val isConfigMode: Boolean
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -23,10 +23,11 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
@Composable
|
||||
fun BannerModule(
|
||||
books: ImmutableList<SearchBook>,
|
||||
onClick: (SearchBook) -> Unit,
|
||||
onClick: (SearchBook, String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKeySourceId: String? = null,
|
||||
) {
|
||||
if (books.isEmpty()) return
|
||||
|
||||
@@ -38,7 +39,11 @@ fun BannerModule(
|
||||
.fadingEdge(lazyListState, gradientWidth = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(books) { book ->
|
||||
itemsIndexed(books, key = { index, book -> "${book.bookUrl}:$index" }) { index, book ->
|
||||
val sharedCoverKey = bookCoverSharedElementKey(
|
||||
book.bookUrl,
|
||||
sharedCoverKeySourceId?.let { "$it:$index" }
|
||||
)
|
||||
CoilBookCover(
|
||||
name = book.name,
|
||||
author = book.author,
|
||||
@@ -47,10 +52,10 @@ fun BannerModule(
|
||||
sourceOrigin = book.origin,
|
||||
modifier = Modifier
|
||||
.width(96.dp)
|
||||
.clickable { onClick(book) },
|
||||
.clickable { onClick(book, sharedCoverKey) },
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.legado.app.ui.main.homepage.modules
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -12,12 +13,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.platform.LocalContext
|
||||
@@ -26,14 +22,14 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.data.entities.rule.ExploreKind
|
||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
||||
import io.legado.app.help.source.getExploreInfoMap
|
||||
import io.legado.app.ui.main.homepage.HomepageViewModel
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem
|
||||
import io.legado.app.ui.widget.components.image.sourceIcon.SourceIcon
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.utils.GSON
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
@@ -51,10 +47,7 @@ fun ButtonGroupModule(
|
||||
val context = LocalContext.current
|
||||
val activity = context as? AppCompatActivity
|
||||
val useCase: ExploreKindUiUseCase = koinInject()
|
||||
val scope = rememberCoroutineScope()
|
||||
val infoMap = remember(sourceUrl) {
|
||||
sourceUrl.takeIf { it.isNotBlank() }?.let { getExploreInfoMap(it) }
|
||||
}
|
||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||
|
||||
// 解析图标映射表和默认图标
|
||||
val (iconMap, defaultIcon) = remember(layoutConfig) {
|
||||
@@ -90,74 +83,76 @@ fun ButtonGroupModule(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
rowKinds.forEach { kind ->
|
||||
var displayName by remember(kind.title) { mutableStateOf(kind.title) }
|
||||
|
||||
LaunchedEffect(kind, sourceUrl, infoMap) {
|
||||
displayName = useCase.resolveDisplayName(kind, sourceUrl, infoMap)
|
||||
}
|
||||
|
||||
val buttonIcon = iconMap[kind.title] ?: defaultIcon
|
||||
val hasIcon = !buttonIcon.isNullOrBlank()
|
||||
|
||||
GlassCard(
|
||||
onClick = {
|
||||
when (kind.type) {
|
||||
ExploreKind.Type.url -> {
|
||||
kind.url?.takeIf { it.isNotBlank() }?.let {
|
||||
viewModel.onKindUrlClick(sourceUrl, it, kind.title)
|
||||
}
|
||||
}
|
||||
ExploreKindMultiTypeItem(
|
||||
kind = kind,
|
||||
sourceUrl = sourceUrl,
|
||||
activity = activity,
|
||||
onOpenUrl = { url ->
|
||||
viewModel.onKindUrlClick(sourceUrl, url, kind.title)
|
||||
},
|
||||
onRefreshKinds = {
|
||||
viewModel.refreshButtonGroup(globalId)
|
||||
},
|
||||
useCase = useCase,
|
||||
isMiuix = isMiuix,
|
||||
modifier = Modifier.weight(1f),
|
||||
content = { displayName, isSelected, onClick, trailingIcon ->
|
||||
GlassCard(
|
||||
onClick = onClick,
|
||||
cornerRadius = 8.dp,
|
||||
containerColor = if (isSelected) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainerLow,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||
) {
|
||||
if (hasIcon) {
|
||||
SourceIcon(
|
||||
path = buttonIcon!!,
|
||||
modifier = Modifier.size(20.dp),
|
||||
placeholderIcon = {
|
||||
|
||||
ExploreKind.Type.button -> {
|
||||
scope.launch {
|
||||
useCase.executeAction(
|
||||
action = kind.action,
|
||||
title = kind.title,
|
||||
sourceUrl = sourceUrl,
|
||||
infoMap = infoMap,
|
||||
activity = activity,
|
||||
onRefreshKinds = {
|
||||
viewModel.refreshButtonGroup(globalId)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AppText(
|
||||
text = displayName,
|
||||
style = LegadoTheme.typography.labelSmallEmphasized,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.basicMarquee()
|
||||
)
|
||||
|
||||
if (trailingIcon != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(end = 2.dp)
|
||||
) {
|
||||
trailingIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
cornerRadius = 8.dp,
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||
) {
|
||||
if (hasIcon) {
|
||||
SourceIcon(
|
||||
path = buttonIcon,
|
||||
modifier = Modifier.size(20.dp),
|
||||
placeholderIcon = {
|
||||
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
|
||||
AppText(
|
||||
text = displayName,
|
||||
style = LegadoTheme.typography.labelSmallEmphasized,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.basicMarquee()
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (rowKinds.size < actualColumns) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -35,10 +35,11 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
@Composable
|
||||
fun CardModule(
|
||||
books: ImmutableList<SearchBook>,
|
||||
onClick: (SearchBook) -> Unit,
|
||||
onClick: (SearchBook, String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKeySourceId: String? = null,
|
||||
) {
|
||||
if (books.isEmpty()) return
|
||||
val lazyListState = rememberLazyListState()
|
||||
@@ -49,13 +50,17 @@ fun CardModule(
|
||||
.fadingEdge(lazyListState, gradientWidth = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(books, key = { it.bookUrl }) { book ->
|
||||
itemsIndexed(books, key = { index, book -> "${book.bookUrl}:$index" }) { index, book ->
|
||||
val sharedCoverKey = bookCoverSharedElementKey(
|
||||
book.bookUrl,
|
||||
sharedCoverKeySourceId?.let { "$it:$index" }
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(120.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(LegadoTheme.colorScheme.surfaceContainerLow)
|
||||
.clickable { onClick(book) }
|
||||
.clickable { onClick(book, sharedCoverKey) }
|
||||
) {
|
||||
CoilBookCover(
|
||||
name = book.name,
|
||||
@@ -67,19 +72,20 @@ fun CardModule(
|
||||
.wrapContentWidth(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
|
||||
AppText(
|
||||
text = book.name,
|
||||
style = LegadoTheme.typography.labelLargeEmphasized,
|
||||
maxLines = 2,
|
||||
minLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
top = 8.dp,
|
||||
bottom = 2.dp
|
||||
bottom = 8.dp
|
||||
),
|
||||
)
|
||||
|
||||
@@ -88,7 +94,7 @@ fun CardModule(
|
||||
if (intro != null) {
|
||||
AppText(
|
||||
text = intro,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
style = LegadoTheme.typography.labelSmallEmphasized,
|
||||
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
||||
@@ -21,12 +21,13 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
@Composable
|
||||
fun GridModule(
|
||||
books: ImmutableList<SearchBook>,
|
||||
onClick: (SearchBook) -> Unit,
|
||||
onClick: (SearchBook, String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
columns: Int = 3,
|
||||
maxRows: Int? = null,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKeySourceId: String? = null,
|
||||
) {
|
||||
if (books.isEmpty()) return
|
||||
var rows = books.toList().chunked(columns)
|
||||
@@ -37,20 +38,25 @@ fun GridModule(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
for (row in rows) {
|
||||
for ((rowIndex, row) in rows.withIndex()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
for (book in row) {
|
||||
for ((columnIndex, book) in row.withIndex()) {
|
||||
val itemIndex = rowIndex * columns + columnIndex
|
||||
val sharedCoverKey = bookCoverSharedElementKey(
|
||||
book.bookUrl,
|
||||
sharedCoverKeySourceId?.let { "$it:$itemIndex" }
|
||||
)
|
||||
SearchBookGridItem(
|
||||
book = book,
|
||||
shelfState = BookShelfState.NOT_IN_SHELF,
|
||||
onClick = { onClick(book) },
|
||||
onClick = { onClick(book, sharedCoverKey) },
|
||||
modifier = Modifier.weight(1f),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
}
|
||||
repeat(columns - row.size) { Spacer(Modifier.weight(1f)) }
|
||||
|
||||
@@ -37,11 +37,12 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
@Composable
|
||||
fun GridRankingModule(
|
||||
books: ImmutableList<SearchBook>,
|
||||
onClick: (SearchBook) -> Unit,
|
||||
onClick: (SearchBook, String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
rows: Int = 4,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKeySourceId: String? = null,
|
||||
) {
|
||||
if (books.isEmpty()) return
|
||||
// 限制最多显示 20 项
|
||||
@@ -70,13 +71,19 @@ fun GridRankingModule(
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp, horizontal = 12.dp)
|
||||
) {
|
||||
for (book in page) {
|
||||
for ((rowIndex, book) in page.withIndex()) {
|
||||
val itemIndex = pageIndex * rows + rowIndex
|
||||
val sharedCoverKey = bookCoverSharedElementKey(
|
||||
book.bookUrl,
|
||||
sharedCoverKeySourceId?.let { "$it:$itemIndex" }
|
||||
)
|
||||
GridRankingItem(
|
||||
rank = pages.flatten().indexOf(book) + 1,
|
||||
book = book,
|
||||
onClick = { onClick(book) },
|
||||
onClick = { onClick(book, sharedCoverKey) },
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
}
|
||||
// 占位逻辑
|
||||
@@ -96,6 +103,7 @@ private fun GridRankingItem(
|
||||
onClick: () -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -114,7 +122,7 @@ private fun GridRankingItem(
|
||||
modifier = Modifier.width(48.dp),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
|
||||
// 2. 排名
|
||||
|
||||
@@ -45,10 +45,11 @@ private const val MAX_COUNT = 20
|
||||
@Composable
|
||||
fun RankingModule(
|
||||
books: ImmutableList<SearchBook>,
|
||||
onClick: (SearchBook) -> Unit,
|
||||
onClick: (SearchBook, String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKeySourceId: String? = null,
|
||||
) {
|
||||
var visibleCount by rememberSaveable { mutableIntStateOf(INITIAL_COUNT) }
|
||||
val displayBooks = books.take(visibleCount)
|
||||
@@ -72,6 +73,10 @@ fun RankingModule(
|
||||
onClick = onClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(
|
||||
book.bookUrl,
|
||||
sharedCoverKeySourceId?.let { "$it:$index" }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -114,14 +119,15 @@ fun RankingModule(
|
||||
private fun RankingItem(
|
||||
rank: Int,
|
||||
book: SearchBook,
|
||||
onClick: (SearchBook) -> Unit,
|
||||
onClick: (SearchBook, String?) -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick(book) }
|
||||
.clickable { onClick(book, sharedCoverKey) }
|
||||
.padding(vertical = 4.dp, horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@@ -144,7 +150,7 @@ private fun RankingItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ 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.widget.components.book.SearchBookTagChip
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
@@ -36,6 +35,7 @@ fun WaterfallItem(
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
) {
|
||||
GlassCard(
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow
|
||||
@@ -55,7 +55,7 @@ fun WaterfallItem(
|
||||
.fillMaxWidth(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.legado.app.ui.rss.article
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -59,7 +58,7 @@ import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.widget.components.AppPullToRefresh
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.LoadMoreFooter
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.image.cover.buildCoverImageRequest
|
||||
import io.legado.app.utils.toastOnUi
|
||||
@@ -163,7 +162,9 @@ fun RssArticlesPage(
|
||||
}
|
||||
item {
|
||||
LoadMoreFooter(
|
||||
state = loadState,
|
||||
isLoading = loadState.isRefreshing || loadState.isLoadingMore,
|
||||
errorMsg = loadState.errorMessage,
|
||||
isEnd = !loadState.hasMore,
|
||||
onRetry = { rssSource?.let(viewModel::loadMore) }
|
||||
)
|
||||
}
|
||||
@@ -198,7 +199,9 @@ fun RssArticlesPage(
|
||||
}
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
LoadMoreFooter(
|
||||
state = loadState,
|
||||
isLoading = loadState.isRefreshing || loadState.isLoadingMore,
|
||||
errorMsg = loadState.errorMessage,
|
||||
isEnd = !loadState.hasMore,
|
||||
onRetry = { rssSource?.let(viewModel::loadMore) }
|
||||
)
|
||||
}
|
||||
@@ -233,7 +236,9 @@ fun RssArticlesPage(
|
||||
}
|
||||
item(span = StaggeredGridItemSpan.FullLine) {
|
||||
LoadMoreFooter(
|
||||
state = loadState,
|
||||
isLoading = loadState.isRefreshing || loadState.isLoadingMore,
|
||||
errorMsg = loadState.errorMessage,
|
||||
isEnd = !loadState.hasMore,
|
||||
onRetry = { rssSource?.let(viewModel::loadMore) }
|
||||
)
|
||||
}
|
||||
@@ -297,35 +302,6 @@ private fun StaggeredLoadMoreDetector(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadMoreFooter(
|
||||
state: RssArticlesLoadState,
|
||||
onRetry: () -> Unit
|
||||
) {
|
||||
val text = when {
|
||||
state.isRefreshing || state.isLoadingMore -> "加载中..."
|
||||
!state.hasMore -> "没有更多了"
|
||||
state.errorMessage != null -> "加载失败,点击重试"
|
||||
else -> "上拉加载更多"
|
||||
}
|
||||
val contentModifier = if (state.errorMessage != null) {
|
||||
Modifier.clickable(onClick = onRetry)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
EmptyMessage(
|
||||
message = text,
|
||||
isLoading = state.isRefreshing || state.isLoadingMore,
|
||||
modifier = contentModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RssArticleItem(
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.legado.app.ui.theme
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import top.yukonga.miuix.kmp.theme.TextStyles
|
||||
@@ -66,7 +67,7 @@ fun Typography.toLegadoTypography(): LegadoTypography {
|
||||
)
|
||||
}
|
||||
|
||||
fun LegadoTypography.withFont(fontFamily: androidx.compose.ui.text.font.FontFamily?): LegadoTypography {
|
||||
fun LegadoTypography.withFont(fontFamily: FontFamily?): LegadoTypography {
|
||||
if (fontFamily == null) return this
|
||||
return copy(
|
||||
headlineLarge = headlineLarge.copy(fontFamily = fontFamily),
|
||||
|
||||
@@ -71,7 +71,7 @@ fun AppFloatingActionButton(
|
||||
MiuixIcon(
|
||||
imageVector = icon,
|
||||
contentDescription = tooltipText,
|
||||
tint = Color.White
|
||||
tint = containerColor
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
@@ -89,7 +89,8 @@ fun AppFloatingActionButton(
|
||||
MiuixFloatingActionButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
content = fabContent
|
||||
content = fabContent,
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainer
|
||||
)
|
||||
} else {
|
||||
if (tooltipText != null) {
|
||||
|
||||
@@ -8,18 +8,13 @@ import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||
import androidx.compose.material.icons.filled.Compress
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonParser
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.utils.GSON
|
||||
@@ -39,8 +34,7 @@ fun JsonRawEditor(
|
||||
) {
|
||||
AppText(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
style = LegadoTheme.typography.labelMediumEmphasized
|
||||
)
|
||||
Row {
|
||||
SmallIconButton(
|
||||
@@ -67,21 +61,13 @@ fun JsonRawEditor(
|
||||
}
|
||||
}
|
||||
|
||||
TextField(
|
||||
AppTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 150.dp, max = 400.dp),
|
||||
textStyle = TextStyle(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize
|
||||
),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
),
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
maxLines = 1000
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.AnimatedTextButton
|
||||
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.progressIndicator.AppContainedLoadingIndicator
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.utils.sendToClip
|
||||
|
||||
@Composable
|
||||
fun LoadMoreFooter(
|
||||
isLoading: Boolean,
|
||||
errorMsg: String?,
|
||||
isEnd: Boolean,
|
||||
onRetry: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showFullError by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(isLoading, errorMsg, isEnd) {
|
||||
if (!isLoading && errorMsg == null && !isEnd) {
|
||||
onRetry()
|
||||
}
|
||||
}
|
||||
|
||||
AppAlertDialog(
|
||||
data = showFullError,
|
||||
onDismissRequest = { showFullError = null },
|
||||
title = "错误详情",
|
||||
textProvider = { this },
|
||||
confirmText = "复制",
|
||||
onConfirm = { error ->
|
||||
context.sendToClip(error)
|
||||
showFullError = null
|
||||
},
|
||||
dismissText = "关闭",
|
||||
onDismiss = { showFullError = null }
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.adaptiveHorizontalPadding(vertical = 8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
AnimatedContent(
|
||||
targetState = Triple(isLoading, errorMsg, isEnd),
|
||||
label = "LoadMoreFooter"
|
||||
) { (loading, error, end) ->
|
||||
|
||||
when {
|
||||
error != null -> {
|
||||
|
||||
GlassCard(
|
||||
onClick = { showFullError = error },
|
||||
containerColor = LegadoTheme.colorScheme.errorContainer.copy(alpha = 0.6f),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
|
||||
// 信息区域
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
all = 16.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
|
||||
AppIcon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.error
|
||||
)
|
||||
|
||||
AppText(
|
||||
text = error,
|
||||
color = LegadoTheme.colorScheme.error,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = LegadoTheme.colorScheme.outlineVariant.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
// 操作区域
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onRetry)
|
||||
.padding(vertical = 10.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.error
|
||||
)
|
||||
|
||||
AppText(
|
||||
text = "重新加载",
|
||||
color = LegadoTheme.colorScheme.error,
|
||||
style = LegadoTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loading -> {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
|
||||
AppContainedLoadingIndicator()
|
||||
|
||||
AppText(
|
||||
text = "正在加载更多内容…",
|
||||
color = LegadoTheme.colorScheme.outline,
|
||||
style = LegadoTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
end -> {
|
||||
GlassCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainer,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
all = 16.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
|
||||
AppIcon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
AppText(
|
||||
text = "已经到底了~",
|
||||
color = LegadoTheme.colorScheme.onSurface,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
GlassCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainer,
|
||||
onClick = onRetry
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
all = 16.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
|
||||
AppIcon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
AppText(
|
||||
text = "加载更多",
|
||||
color = LegadoTheme.colorScheme.onSurface,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedTextButton(
|
||||
isLoading = false,
|
||||
onClick = onRetry,
|
||||
text = "尝试加载更多"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
@@ -98,12 +101,16 @@ fun AppAlertDialog(
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
title = title?.let { { Text(text = it) } },
|
||||
text = {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
if (text != null) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(bottom = if (content != null) 16.dp else 0.dp)
|
||||
)
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(bottom = if (content != null) 16.dp else 0.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (content != null) {
|
||||
content()
|
||||
@@ -161,16 +168,17 @@ fun <T> AppAlertDialog(
|
||||
val currentData = cachedData
|
||||
if (currentData != null) {
|
||||
val currentText = text ?: textProvider?.invoke(currentData)
|
||||
var cachedText by remember { mutableStateOf(currentText) }
|
||||
var lastValidText by remember { mutableStateOf(currentText) }
|
||||
|
||||
if (currentText != null) {
|
||||
cachedText = currentText
|
||||
lastValidText = currentText
|
||||
}
|
||||
|
||||
AppAlertDialog(
|
||||
show = data != null,
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = title,
|
||||
text = currentText ?: cachedText,
|
||||
text = currentText ?: lastValidText,
|
||||
modifier = modifier,
|
||||
confirmText = confirmText,
|
||||
onConfirm = onConfirm?.let { { it(currentData) } },
|
||||
|
||||
@@ -82,7 +82,7 @@ fun SmallTextButton(
|
||||
@Composable
|
||||
fun SmallTonalTextButton(
|
||||
text: String? = null,
|
||||
imageVector: ImageVector,
|
||||
imageVector: ImageVector? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
@@ -102,11 +102,13 @@ fun SmallTonalTextButton(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
if (imageVector != null) {
|
||||
MiuixIcon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
if (text != null) {
|
||||
AppText(
|
||||
text = text,
|
||||
@@ -121,11 +123,13 @@ fun SmallTonalTextButton(
|
||||
modifier = modifier,
|
||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
if (imageVector != null) {
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
if (text != null) {
|
||||
AppText(
|
||||
|
||||
@@ -3,10 +3,8 @@ package io.legado.app.ui.widget.components.explore
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -16,11 +14,9 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.data.entities.rule.ExploreKind
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
|
||||
@Composable
|
||||
fun ExploreKindItem(
|
||||
@@ -31,6 +27,7 @@ fun ExploreKindItem(
|
||||
isMiuix: Boolean,
|
||||
backgroundColor: androidx.compose.ui.graphics.Color = LegadoTheme.colorScheme.surfaceContainer,
|
||||
displayText: String = kind.title,
|
||||
isSelected: Boolean = false,
|
||||
trailingIcon: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
@@ -38,31 +35,45 @@ fun ExploreKindItem(
|
||||
) {
|
||||
|
||||
val cornerRadius = 12.dp
|
||||
val containerColor = if (isSelected) {
|
||||
LegadoTheme.colorScheme.primaryContainer
|
||||
} else {
|
||||
backgroundColor
|
||||
}
|
||||
val contentColor = if (isSelected) {
|
||||
LegadoTheme.colorScheme.onPrimaryContainer
|
||||
} else if (isClickable) {
|
||||
LegadoTheme.colorScheme.onSurface
|
||||
} else {
|
||||
LegadoTheme.colorScheme.primary
|
||||
}
|
||||
|
||||
if (isClickable) {
|
||||
GlassCard(
|
||||
onClick = onClick,
|
||||
cornerRadius = cornerRadius,
|
||||
containerColor = backgroundColor,
|
||||
contentColor = LegadoTheme.colorScheme.onSurface,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
modifier = modifier,
|
||||
) {
|
||||
KindText(
|
||||
text = displayText,
|
||||
isClickable = true,
|
||||
contentColor = contentColor,
|
||||
trailingIcon = trailingIcon
|
||||
)
|
||||
}
|
||||
} else {
|
||||
GlassCard(
|
||||
cornerRadius = cornerRadius,
|
||||
containerColor = backgroundColor,
|
||||
contentColor = LegadoTheme.colorScheme.primary,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
modifier = modifier,
|
||||
) {
|
||||
KindText(
|
||||
text = displayText,
|
||||
isClickable = false,
|
||||
contentColor = contentColor,
|
||||
trailingIcon = trailingIcon
|
||||
)
|
||||
}
|
||||
@@ -75,6 +86,7 @@ fun ExploreKindItem(
|
||||
private fun KindText(
|
||||
text: String,
|
||||
isClickable: Boolean,
|
||||
contentColor: androidx.compose.ui.graphics.Color = LegadoTheme.colorScheme.onSurface,
|
||||
trailingIcon: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
Box(
|
||||
@@ -84,7 +96,7 @@ private fun KindText(
|
||||
) {
|
||||
AppText(
|
||||
text = text,
|
||||
color = if (isClickable) LegadoTheme.colorScheme.onSurface else LegadoTheme.colorScheme.primary,
|
||||
color = contentColor,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = if (trailingIcon == null) 0.dp else 18.dp),
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package io.legado.app.ui.widget.components.explore
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
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 io.legado.app.data.entities.rule.ExploreKind
|
||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
||||
import io.legado.app.help.source.getExploreInfoMap
|
||||
import io.legado.app.utils.InfoMap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 封装 ExploreKind 的业务状态与交互逻辑
|
||||
*/
|
||||
@Stable
|
||||
class ExploreKindItemState(
|
||||
val kind: ExploreKind,
|
||||
val sourceUrl: String?,
|
||||
private val useCase: ExploreKindUiUseCase?,
|
||||
private val scope: CoroutineScope,
|
||||
private val activity: AppCompatActivity?,
|
||||
private val onRefreshKinds: () -> Unit
|
||||
) {
|
||||
val infoMap: InfoMap? = if (useCase == null) null else sourceUrl?.takeIf { it.isNotBlank() }
|
||||
?.let(::getExploreInfoMap)
|
||||
var displayName by mutableStateOf(kind.title)
|
||||
internal set
|
||||
|
||||
fun executeAction(action: String?) {
|
||||
if (action.isNullOrBlank()) return
|
||||
val useCase = useCase ?: return
|
||||
scope.launch(IO) {
|
||||
useCase.executeAction(
|
||||
action = action,
|
||||
title = kind.title,
|
||||
sourceUrl = sourceUrl,
|
||||
infoMap = infoMap,
|
||||
activity = activity,
|
||||
onRefreshKinds = onRefreshKinds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateValue(value: String, onValueChange: ((String) -> Unit)?) {
|
||||
if (onValueChange != null) {
|
||||
onValueChange(value)
|
||||
} else {
|
||||
infoMap?.let {
|
||||
it[kind.title] = value
|
||||
it.saveNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResolveDisplayName(override: String?) {
|
||||
LaunchedEffect(override, sourceUrl, kind.title, kind.viewName, useCase) {
|
||||
displayName = override
|
||||
?: useCase?.resolveDisplayName(kind, sourceUrl, infoMap)
|
||||
?: kind.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberExploreKindItemState(
|
||||
kind: ExploreKind,
|
||||
sourceUrl: String?,
|
||||
useCase: ExploreKindUiUseCase?,
|
||||
activity: AppCompatActivity?,
|
||||
onRefreshKinds: () -> Unit
|
||||
): ExploreKindItemState {
|
||||
val scope = rememberCoroutineScope()
|
||||
return remember(kind, sourceUrl, useCase, activity) {
|
||||
ExploreKindItemState(kind, sourceUrl, useCase, scope, activity, onRefreshKinds)
|
||||
}
|
||||
}
|
||||
+314
-258
@@ -1,14 +1,9 @@
|
||||
package io.legado.app.ui.widget.components.explore
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
@@ -22,24 +17,18 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.data.entities.rule.ExploreKind
|
||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
||||
import io.legado.app.help.source.getExploreInfoMap
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
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.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.ui.widget.dialog.TextDialog
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -56,295 +45,362 @@ fun ExploreKindMultiTypeItem(
|
||||
isMiuix: Boolean,
|
||||
displayNameOverride: String? = null,
|
||||
valueOverride: String? = null,
|
||||
isSelected: Boolean = false,
|
||||
onValueChange: ((String) -> Unit)? = null,
|
||||
onRunAction: (() -> Unit)? = null,
|
||||
useCase: ExploreKindUiUseCase? = null
|
||||
useCase: ExploreKindUiUseCase? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)? = null
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val infoMap = remember(sourceUrl, useCase) {
|
||||
if (useCase == null) null else sourceUrl?.takeIf { it.isNotBlank() }?.let(::getExploreInfoMap)
|
||||
}
|
||||
var displayName by remember(sourceUrl, kind.title, kind.viewName) { mutableStateOf(kind.title) }
|
||||
val state = rememberExploreKindItemState(kind, sourceUrl, useCase, activity, onRefreshKinds)
|
||||
state.ResolveDisplayName(displayNameOverride)
|
||||
|
||||
LaunchedEffect(displayNameOverride, sourceUrl, kind.title, kind.viewName, useCase) {
|
||||
displayName = displayNameOverride
|
||||
?: useCase?.resolveDisplayName(kind, sourceUrl, infoMap)
|
||||
?: kind.title
|
||||
}
|
||||
val trailingIcon = rememberTrailingIcon(kind.type, isSelected)
|
||||
|
||||
fun runAction(action: String?) {
|
||||
if (action.isNullOrBlank()) return
|
||||
if (onRunAction != null) {
|
||||
onRunAction()
|
||||
if (onClick != null) {
|
||||
if (content != null) {
|
||||
content(state.displayName, isSelected, onClick, trailingIcon)
|
||||
} else {
|
||||
val useCase = useCase ?: return
|
||||
scope.launch(IO) {
|
||||
useCase.executeAction(
|
||||
action = action,
|
||||
title = kind.title,
|
||||
sourceUrl = sourceUrl,
|
||||
infoMap = infoMap,
|
||||
activity = activity,
|
||||
onRefreshKinds = onRefreshKinds
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateValue(value: String) {
|
||||
if (onValueChange != null) {
|
||||
onValueChange(value)
|
||||
} else {
|
||||
infoMap?.let {
|
||||
it[kind.title] = value
|
||||
it.saveNow()
|
||||
}
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = true,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix,
|
||||
displayText = state.displayName,
|
||||
isSelected = isSelected,
|
||||
trailingIcon = trailingIcon
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
when (kind.type) {
|
||||
ExploreKind.Type.url -> {
|
||||
val url = kind.url?.takeIf { it.isNotBlank() }
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = !url.isNullOrBlank(),
|
||||
onClick = {
|
||||
if (url.isNullOrBlank()) return@ExploreKindItem
|
||||
val internalOnClick = {
|
||||
if (!url.isNullOrBlank()) {
|
||||
if (kind.title.startsWith("ERROR:")) {
|
||||
activity?.showDialogFragment(TextDialog("ERROR", url))
|
||||
} else {
|
||||
onOpenUrl(url)
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix,
|
||||
displayText = displayName
|
||||
)
|
||||
}
|
||||
}
|
||||
if (content != null) {
|
||||
content(state.displayName, isSelected, internalOnClick, trailingIcon)
|
||||
} else {
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = !url.isNullOrBlank(),
|
||||
onClick = internalOnClick,
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix,
|
||||
displayText = state.displayName,
|
||||
isSelected = isSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExploreKind.Type.button -> {
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = !kind.action.isNullOrBlank(),
|
||||
onClick = { runAction(kind.action) },
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix,
|
||||
displayText = displayName,
|
||||
trailingIcon = {
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
AppIcon(
|
||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(14.dp),
|
||||
tint = LegadoTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
val internalOnClick = {
|
||||
if (onRunAction != null) onRunAction()
|
||||
else state.executeAction(kind.action)
|
||||
}
|
||||
if (content != null) {
|
||||
content(state.displayName, isSelected, internalOnClick, trailingIcon)
|
||||
} else {
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = !kind.action.isNullOrBlank(),
|
||||
onClick = internalOnClick,
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix,
|
||||
displayText = state.displayName,
|
||||
isSelected = isSelected,
|
||||
trailingIcon = trailingIcon
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExploreKind.Type.text -> {
|
||||
var value by remember(sourceUrl, kind.title) {
|
||||
mutableStateOf(valueOverride ?: infoMap?.get(kind.title).orEmpty())
|
||||
if (content != null) {
|
||||
content(state.displayName, isSelected, {}, null)
|
||||
} else {
|
||||
TextTypeItem(
|
||||
kind,
|
||||
sourceUrl,
|
||||
state,
|
||||
valueOverride,
|
||||
onValueChange,
|
||||
modifier,
|
||||
backgroundColor
|
||||
)
|
||||
}
|
||||
LaunchedEffect(valueOverride) {
|
||||
if (valueOverride != null) {
|
||||
value = valueOverride
|
||||
}
|
||||
}
|
||||
var actionJob by remember(sourceUrl, kind.title) { mutableStateOf<Job?>(null) }
|
||||
ExploreKindCompactTextField(
|
||||
value = value,
|
||||
onValueChange = { newValue ->
|
||||
value = newValue
|
||||
updateValue(newValue)
|
||||
if (!kind.action.isNullOrBlank()) {
|
||||
actionJob?.cancel()
|
||||
actionJob = scope.launch {
|
||||
delay(600)
|
||||
runAction(kind.action)
|
||||
}
|
||||
}
|
||||
},
|
||||
placeholder = displayName,
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix
|
||||
)
|
||||
}
|
||||
|
||||
ExploreKind.Type.toggle -> {
|
||||
val chars = remember(kind.chars) {
|
||||
kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
|
||||
}
|
||||
val left = kind.style().layout_justifySelf != "right"
|
||||
var char by remember(sourceUrl, kind.title, kind.default, kind.chars) {
|
||||
mutableStateOf(
|
||||
valueOverride
|
||||
?: infoMap?.get(kind.title)
|
||||
?.takeUnless { it.isEmpty() }
|
||||
?: (kind.default ?: chars.first()).also {
|
||||
infoMap?.let { map ->
|
||||
map[kind.title] = it
|
||||
map.saveNow()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
LaunchedEffect(valueOverride) {
|
||||
if (valueOverride != null) {
|
||||
char = valueOverride
|
||||
}
|
||||
}
|
||||
val text = if (left) "$char$displayName" else "$displayName$char"
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = true,
|
||||
onClick = {
|
||||
val currentIndex = chars.indexOf(char)
|
||||
val nextIndex = if (currentIndex < 0) 0 else (currentIndex + 1) % chars.size
|
||||
char = chars.getOrElse(nextIndex) { "" }
|
||||
updateValue(char)
|
||||
runAction(kind.action)
|
||||
},
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix,
|
||||
displayText = text,
|
||||
trailingIcon = {
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(14.dp),
|
||||
tint = LegadoTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
ToggleTypeItem(
|
||||
kind,
|
||||
sourceUrl,
|
||||
state,
|
||||
valueOverride,
|
||||
onValueChange,
|
||||
isSelected,
|
||||
modifier,
|
||||
backgroundColor,
|
||||
isMiuix,
|
||||
trailingIcon,
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
ExploreKind.Type.select -> {
|
||||
val chars = remember(kind.chars) {
|
||||
kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
|
||||
}
|
||||
var selected by remember(sourceUrl, kind.title, kind.default, kind.chars) {
|
||||
mutableStateOf(
|
||||
valueOverride
|
||||
?: infoMap?.get(kind.title)
|
||||
?.takeUnless { it.isEmpty() }
|
||||
?: (kind.default ?: chars.first()).also {
|
||||
infoMap?.let { map ->
|
||||
map[kind.title] = it
|
||||
map.saveNow()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
LaunchedEffect(valueOverride) {
|
||||
if (valueOverride != null) {
|
||||
selected = valueOverride
|
||||
}
|
||||
}
|
||||
var showSelector by remember(sourceUrl, kind.title) { mutableStateOf(false) }
|
||||
Box(modifier = modifier) {
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = chars.isNotEmpty(),
|
||||
onClick = { showSelector = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix,
|
||||
displayText = "$displayName $selected",
|
||||
trailingIcon = {
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.UnfoldMore,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(14.dp),
|
||||
tint = LegadoTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
RoundDropdownMenu(
|
||||
expanded = showSelector,
|
||||
onDismissRequest = { showSelector = false }
|
||||
) {
|
||||
chars.forEach { option ->
|
||||
RoundDropdownMenuItem(
|
||||
text = option,
|
||||
onClick = {
|
||||
showSelector = false
|
||||
if (selected != option) {
|
||||
selected = option
|
||||
updateValue(option)
|
||||
runAction(kind.action)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SelectTypeItem(
|
||||
kind,
|
||||
sourceUrl,
|
||||
state,
|
||||
valueOverride,
|
||||
onValueChange,
|
||||
isSelected,
|
||||
modifier,
|
||||
backgroundColor,
|
||||
isMiuix,
|
||||
trailingIcon,
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = false,
|
||||
onClick = {},
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix,
|
||||
displayText = displayName
|
||||
)
|
||||
if (content != null) {
|
||||
content(state.displayName, isSelected, {}, null)
|
||||
} else {
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = false,
|
||||
onClick = {},
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix,
|
||||
displayText = state.displayName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExploreKindCompactTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
placeholder: String,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = LegadoTheme.colorScheme.surfaceContainer,
|
||||
isMiuix: Boolean
|
||||
private fun TextTypeItem(
|
||||
kind: ExploreKind,
|
||||
sourceUrl: String?,
|
||||
state: ExploreKindItemState,
|
||||
valueOverride: String?,
|
||||
onValueChange: ((String) -> Unit)?,
|
||||
modifier: Modifier,
|
||||
backgroundColor: Color
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val shape = RoundedCornerShape(10.dp)
|
||||
|
||||
BasicTextField(
|
||||
val scope = rememberCoroutineScope()
|
||||
var value by remember(sourceUrl, kind.title) {
|
||||
mutableStateOf(valueOverride ?: state.infoMap?.get(kind.title).orEmpty())
|
||||
}
|
||||
LaunchedEffect(valueOverride) {
|
||||
if (valueOverride != null) value = valueOverride
|
||||
}
|
||||
var actionJob by remember(sourceUrl, kind.title) { mutableStateOf<Job?>(null) }
|
||||
ExploreKindCompactTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = true,
|
||||
textStyle = LegadoTheme.typography.bodySmall.copy(color = LegadoTheme.colorScheme.onSurface),
|
||||
cursorBrush = SolidColor(LegadoTheme.colorScheme.primary),
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier
|
||||
.height(34.dp)
|
||||
.clip(shape)
|
||||
.background(backgroundColor),
|
||||
decorationBox = { innerTextField ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(34.dp)
|
||||
.padding(horizontal = 10.dp),
|
||||
contentAlignment = androidx.compose.ui.Alignment.CenterStart
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
AppText(
|
||||
text = placeholder,
|
||||
color = LegadoTheme.colorScheme.outline,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
innerTextField()
|
||||
onValueChange = { newValue ->
|
||||
value = newValue
|
||||
state.updateValue(newValue, onValueChange)
|
||||
if (!kind.action.isNullOrBlank()) {
|
||||
actionJob?.cancel()
|
||||
actionJob = scope.launch {
|
||||
delay(600)
|
||||
state.executeAction(kind.action)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
placeholder = state.displayName,
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleTypeItem(
|
||||
kind: ExploreKind,
|
||||
sourceUrl: String?,
|
||||
state: ExploreKindItemState,
|
||||
valueOverride: String?,
|
||||
onValueChange: ((String) -> Unit)?,
|
||||
isSelected: Boolean,
|
||||
modifier: Modifier,
|
||||
backgroundColor: Color,
|
||||
isMiuix: Boolean,
|
||||
trailingIcon: @Composable (() -> Unit)?,
|
||||
content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)?
|
||||
) {
|
||||
val chars = remember(kind.chars) {
|
||||
kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
|
||||
}
|
||||
val left = kind.style().layout_justifySelf != "right"
|
||||
var char by remember(sourceUrl, kind.title, kind.default, kind.chars) {
|
||||
mutableStateOf(
|
||||
valueOverride
|
||||
?: state.infoMap?.get(kind.title)
|
||||
?.takeUnless { it.isEmpty() }
|
||||
?: (kind.default ?: chars.first()).also {
|
||||
state.updateValue(it, onValueChange)
|
||||
}
|
||||
)
|
||||
}
|
||||
LaunchedEffect(valueOverride) {
|
||||
if (valueOverride != null) char = valueOverride
|
||||
}
|
||||
val text = if (left) "$char${state.displayName}" else "${state.displayName}$char"
|
||||
val internalOnClick = {
|
||||
val currentIndex = chars.indexOf(char)
|
||||
val nextIndex = if (currentIndex < 0) 0 else (currentIndex + 1) % chars.size
|
||||
char = chars.getOrElse(nextIndex) { "" }
|
||||
state.updateValue(char, onValueChange)
|
||||
state.executeAction(kind.action)
|
||||
}
|
||||
|
||||
if (content != null) {
|
||||
content(text, isSelected, internalOnClick, trailingIcon)
|
||||
} else {
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = true,
|
||||
onClick = internalOnClick,
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix,
|
||||
displayText = text,
|
||||
isSelected = isSelected,
|
||||
trailingIcon = trailingIcon
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectTypeItem(
|
||||
kind: ExploreKind,
|
||||
sourceUrl: String?,
|
||||
state: ExploreKindItemState,
|
||||
valueOverride: String?,
|
||||
onValueChange: ((String) -> Unit)?,
|
||||
isSelected: Boolean,
|
||||
modifier: Modifier,
|
||||
backgroundColor: Color,
|
||||
isMiuix: Boolean,
|
||||
trailingIcon: @Composable (() -> Unit)?,
|
||||
content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)?
|
||||
) {
|
||||
val chars = remember(kind.chars) {
|
||||
kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
|
||||
}
|
||||
var selected by remember(sourceUrl, kind.title, kind.default, kind.chars) {
|
||||
mutableStateOf(
|
||||
valueOverride
|
||||
?: state.infoMap?.get(kind.title)
|
||||
?.takeUnless { it.isEmpty() }
|
||||
?: (kind.default ?: chars.first()).also {
|
||||
state.updateValue(it, onValueChange)
|
||||
}
|
||||
)
|
||||
}
|
||||
LaunchedEffect(valueOverride) {
|
||||
if (valueOverride != null) selected = valueOverride
|
||||
}
|
||||
var showSelector by remember(sourceUrl, kind.title) { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
val internalOnClick = { showSelector = true }
|
||||
val displayText = "${state.displayName} $selected"
|
||||
|
||||
if (content != null) {
|
||||
content(displayText, isSelected, internalOnClick, trailingIcon)
|
||||
} else {
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = chars.isNotEmpty(),
|
||||
onClick = internalOnClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = backgroundColor,
|
||||
isMiuix = isMiuix,
|
||||
displayText = displayText,
|
||||
isSelected = isSelected,
|
||||
trailingIcon = trailingIcon
|
||||
)
|
||||
}
|
||||
|
||||
RoundDropdownMenu(
|
||||
expanded = showSelector,
|
||||
onDismissRequest = { showSelector = false }
|
||||
) {
|
||||
chars.forEach { option ->
|
||||
RoundDropdownMenuItem(
|
||||
text = option,
|
||||
onClick = {
|
||||
showSelector = false
|
||||
if (selected != option) {
|
||||
selected = option
|
||||
state.updateValue(option, onValueChange)
|
||||
state.executeAction(kind.action)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberTrailingIcon(type: String, isSelected: Boolean): @Composable (() -> Unit)? {
|
||||
return remember(type, isSelected) {
|
||||
when (type) {
|
||||
ExploreKind.Type.button -> {
|
||||
{
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
AppIcon(
|
||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(14.dp),
|
||||
tint = if (isSelected) LegadoTheme.colorScheme.onPrimaryContainer.copy(
|
||||
alpha = 0.7f
|
||||
) else LegadoTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExploreKind.Type.toggle -> {
|
||||
{
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(14.dp),
|
||||
tint = if (isSelected) LegadoTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) else LegadoTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExploreKind.Type.select -> {
|
||||
{
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.UnfoldMore,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(14.dp),
|
||||
tint = if (isSelected) LegadoTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) else LegadoTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package io.legado.app.ui.widget.components.explore
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.data.entities.rule.ExploreKind
|
||||
import io.legado.app.data.repository.ExploreRepository
|
||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.widget.components.SearchBar
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ExploreKindSelectSheet(
|
||||
show: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
sourceUrl: String?,
|
||||
onSelected: (List<ExploreKind>) -> Unit,
|
||||
multiple: Boolean = false,
|
||||
initialSelectedTitles: List<String> = emptyList(),
|
||||
repository: ExploreRepository = koinInject(),
|
||||
useCase: ExploreKindUiUseCase = koinInject()
|
||||
) {
|
||||
var kinds by remember { mutableStateOf<List<ExploreKind>>(emptyList()) }
|
||||
var selectedTitles by remember(initialSelectedTitles, show) {
|
||||
mutableStateOf(initialSelectedTitles.toSet())
|
||||
}
|
||||
var query by remember { mutableStateOf("") }
|
||||
val context = LocalContext.current
|
||||
val activity = context as? AppCompatActivity
|
||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||
|
||||
LaunchedEffect(show, sourceUrl) {
|
||||
if (show && !sourceUrl.isNullOrBlank()) {
|
||||
kinds = repository.getSourceExploreKinds(sourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
val filteredKinds = remember(query, kinds) {
|
||||
if (query.isBlank()) kinds
|
||||
else kinds.filter { kind ->
|
||||
kind.title.contains(query, ignoreCase = true) ||
|
||||
(kind.url?.contains(query, ignoreCase = true) == true)
|
||||
}
|
||||
}
|
||||
val kindRows = remember(filteredKinds) {
|
||||
calculateExploreKindRows(filteredKinds, 6)
|
||||
}
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest,
|
||||
endAction = {
|
||||
if (multiple && selectedTitles.isNotEmpty()) {
|
||||
TopBarActionButton(
|
||||
onClick = {
|
||||
val selectedKinds = kinds.filter { it.title in selectedTitles }
|
||||
onSelected(selectedKinds)
|
||||
onDismissRequest()
|
||||
},
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Confirm"
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Column {
|
||||
SearchBar(
|
||||
query = query,
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
onQueryChange = { query = it },
|
||||
placeholder = "选择或搜索分类",
|
||||
autoFocus = false
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(vertical = 16.dp),
|
||||
modifier = Modifier.weight(1f, fill = false)
|
||||
) {
|
||||
items(kindRows) { rowItems ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
rowItems.forEach { (kind, span) ->
|
||||
val isSelected = kind.title in selectedTitles
|
||||
ExploreKindMultiTypeItem(
|
||||
modifier = Modifier
|
||||
.weight(span.toFloat())
|
||||
.animateItem(),
|
||||
kind = kind,
|
||||
sourceUrl = sourceUrl,
|
||||
activity = activity,
|
||||
onOpenUrl = { url ->
|
||||
if (!multiple) {
|
||||
onSelected(listOf(kind.copy(url = url)))
|
||||
onDismissRequest()
|
||||
}
|
||||
},
|
||||
isSelected = isSelected,
|
||||
onClick = {
|
||||
if (multiple) {
|
||||
selectedTitles = if (isSelected) {
|
||||
selectedTitles - kind.title
|
||||
} else {
|
||||
selectedTitles + kind.title
|
||||
}
|
||||
} else {
|
||||
onSelected(listOf(kind))
|
||||
onDismissRequest()
|
||||
}
|
||||
},
|
||||
backgroundColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.5f),
|
||||
isMiuix = isMiuix,
|
||||
useCase = useCase
|
||||
)
|
||||
}
|
||||
|
||||
val totalSpan = rowItems.sumOf { it.second }
|
||||
if (totalSpan < 6) {
|
||||
Spacer(
|
||||
modifier = Modifier.weight((6 - totalSpan).toFloat())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package io.legado.app.ui.widget.components.explore
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
|
||||
@Composable
|
||||
fun ExploreKindCompactTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
placeholder: String,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = LegadoTheme.colorScheme.surfaceContainer,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val shape = RoundedCornerShape(10.dp)
|
||||
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = true,
|
||||
textStyle = LegadoTheme.typography.bodySmall.copy(color = LegadoTheme.colorScheme.onSurface),
|
||||
cursorBrush = SolidColor(LegadoTheme.colorScheme.primary),
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier
|
||||
.height(34.dp)
|
||||
.clip(shape)
|
||||
.background(backgroundColor),
|
||||
decorationBox = { innerTextField ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(34.dp)
|
||||
.padding(horizontal = 10.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
AppText(
|
||||
text = placeholder,
|
||||
color = LegadoTheme.colorScheme.outline,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user