Merge pull request #1150 from HapeLee/feature/homepage-modules
Feature/homepage modules
This commit is contained in:
@@ -101,7 +101,8 @@
|
|||||||
"Read(//c/Users/**)",
|
"Read(//c/Users/**)",
|
||||||
"Bash(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\" -Directory -Depth 0)",
|
"Bash(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\" -Directory -Depth 0)",
|
||||||
"Bash(Select-Object Name)",
|
"Bash(Select-Object Name)",
|
||||||
"PowerShell(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\" -Directory -Depth 1 | ForEach-Object { $_.FullName.Replace\\(\"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\\\\\", \"\"\\) })"
|
"PowerShell(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\" -Directory -Depth 1 | ForEach-Object { $_.FullName.Replace\\(\"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\\\\\", \"\"\\) })",
|
||||||
|
"Bash(gh pr *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ object PreferKey {
|
|||||||
const val prevKeys = "prevKeyCodes"
|
const val prevKeys = "prevKeyCodes"
|
||||||
const val nextKeys = "nextKeyCodes"
|
const val nextKeys = "nextKeyCodes"
|
||||||
const val showDiscovery = "showDiscovery"
|
const val showDiscovery = "showDiscovery"
|
||||||
|
const val showHome = "showHome"
|
||||||
const val enableReview = "enableReview"
|
const val enableReview = "enableReview"
|
||||||
const val showRss = "showRss"
|
const val showRss = "showRss"
|
||||||
const val showStatusBar = "showStatusBar"
|
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.ImageDecoderDecoder
|
||||||
import coil.decode.SvgDecoder
|
import coil.decode.SvgDecoder
|
||||||
import io.legado.app.data.AppDatabase
|
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.AppStartupRepository
|
||||||
import io.legado.app.data.repository.BookCacheCleanupRepository
|
import io.legado.app.data.repository.BookCacheCleanupRepository
|
||||||
import io.legado.app.data.repository.BookDomainRepositoryImpl
|
import io.legado.app.data.repository.BookDomainRepositoryImpl
|
||||||
@@ -137,6 +138,7 @@ val appModule = module {
|
|||||||
singleOf(::SearchContentRepository)
|
singleOf(::SearchContentRepository)
|
||||||
singleOf(::RemoteBookRepository)
|
singleOf(::RemoteBookRepository)
|
||||||
singleOf(::SettingsRepository)
|
singleOf(::SettingsRepository)
|
||||||
|
singleOf(::LocalPreferencesRepository)
|
||||||
singleOf(::ExploreBooksUseCase)
|
singleOf(::ExploreBooksUseCase)
|
||||||
singleOf(::ExploreKindUiUseCase)
|
singleOf(::ExploreKindUiUseCase)
|
||||||
singleOf(::SaveSearchBooksUseCase)
|
singleOf(::SaveSearchBooksUseCase)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package io.legado.app.domain.usecase
|
|||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.data.repository.BookSourceRepository
|
import io.legado.app.data.repository.BookSourceRepository
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.model.webBook.WebBook
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class ExploreBooksUseCase(
|
class ExploreBooksUseCase(
|
||||||
private val bookSourceRepository: BookSourceRepository,
|
private val bookSourceRepository: BookSourceRepository,
|
||||||
@@ -20,7 +22,7 @@ class ExploreBooksUseCase(
|
|||||||
moduleUrl: String?,
|
moduleUrl: String?,
|
||||||
args: String?,
|
args: String?,
|
||||||
page: Int = 1
|
page: Int = 1
|
||||||
): ExploreResult {
|
): ExploreResult = withContext(Dispatchers.IO) {
|
||||||
val base = bookSourceRepository.getBookSource(sourceUrl)
|
val base = bookSourceRepository.getBookSource(sourceUrl)
|
||||||
?: throw SourceNotFound(sourceUrl)
|
?: throw SourceNotFound(sourceUrl)
|
||||||
val source = args?.let { base.copy().also { s -> s.setVariable(it) } } ?: base
|
val source = args?.let { base.copy().also { s -> s.setVariable(it) } } ?: base
|
||||||
@@ -33,14 +35,14 @@ class ExploreBooksUseCase(
|
|||||||
throw InvalidUrl(resolvedUrl)
|
throw InvalidUrl(resolvedUrl)
|
||||||
}
|
}
|
||||||
val books = WebBook.exploreBookSuspend(source, resolvedUrl, page)
|
val books = WebBook.exploreBookSuspend(source, resolvedUrl, page)
|
||||||
return ExploreResult(resolvedUrl, books)
|
ExploreResult(resolvedUrl, books)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun executeForRanking(
|
suspend fun executeForRanking(
|
||||||
sourceUrl: String,
|
sourceUrl: String,
|
||||||
moduleUrl: String?,
|
moduleUrl: String?,
|
||||||
args: String?
|
args: String?
|
||||||
): List<SearchBook> {
|
): List<SearchBook> = withContext(Dispatchers.IO) {
|
||||||
val result = execute(sourceUrl, moduleUrl, args)
|
val result = execute(sourceUrl, moduleUrl, args)
|
||||||
var books = result.books
|
var books = result.books
|
||||||
var page = 1
|
var page = 1
|
||||||
@@ -50,7 +52,7 @@ class ExploreBooksUseCase(
|
|||||||
WebBook.exploreBookSuspend(
|
WebBook.exploreBookSuspend(
|
||||||
bookSourceRepository.getBookSource(sourceUrl)
|
bookSourceRepository.getBookSource(sourceUrl)
|
||||||
?.let { s -> args?.let { s.copy().also { x -> x.setVariable(it) } } ?: s }
|
?.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,
|
result.resolvedUrl,
|
||||||
page,
|
page,
|
||||||
)
|
)
|
||||||
@@ -60,7 +62,7 @@ class ExploreBooksUseCase(
|
|||||||
if (next.isEmpty()) break
|
if (next.isEmpty()) break
|
||||||
books = (books + next)
|
books = (books + next)
|
||||||
}
|
}
|
||||||
return books.take(MAX_RANKING_BOOKS)
|
books.take(MAX_RANKING_BOOKS)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ExploreResult(val resolvedUrl: String, val books: List<SearchBook>)
|
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? {
|
private suspend fun evalUiJs(jsStr: String, sourceUrl: String, infoMap: InfoMap): String? =
|
||||||
val source = getOrLoadBookSource(sourceUrl) ?: return null
|
withContext(Dispatchers.IO) {
|
||||||
return runScriptWithContext {
|
val source = getOrLoadBookSource(sourceUrl) ?: return@withContext null
|
||||||
|
runScriptWithContext {
|
||||||
source.evalJS(jsStr) {
|
source.evalJS(jsStr) {
|
||||||
put("infoMap", infoMap)
|
put("infoMap", infoMap)
|
||||||
}?.toString()
|
}?.toString()
|
||||||
|
|||||||
@@ -306,6 +306,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
val showDiscovery: Boolean
|
val showDiscovery: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true)
|
get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true)
|
||||||
|
|
||||||
|
val showHome: Boolean
|
||||||
|
get() = appCtx.getPrefBoolean(PreferKey.showHome, true)
|
||||||
|
|
||||||
val showRSS: Boolean
|
val showRSS: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.showRss, true)
|
get() = appCtx.getPrefBoolean(PreferKey.showRss, true)
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,8 @@ package io.legado.app.help.config
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.google.gson.GsonBuilder
|
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.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.inputStream
|
|
||||||
import io.legado.app.utils.outputStream
|
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -175,6 +171,7 @@ object ThemeImportExport {
|
|||||||
customTagColorsJson = ThemeConfig.customTagColorsJson,
|
customTagColorsJson = ThemeConfig.customTagColorsJson,
|
||||||
|
|
||||||
// 主界面设置
|
// 主界面设置
|
||||||
|
showHome = ThemeConfig.showHome,
|
||||||
showDiscovery = ThemeConfig.showDiscovery,
|
showDiscovery = ThemeConfig.showDiscovery,
|
||||||
showRss = ThemeConfig.showRss,
|
showRss = ThemeConfig.showRss,
|
||||||
showStatusBar = ThemeConfig.showStatusBar,
|
showStatusBar = ThemeConfig.showStatusBar,
|
||||||
@@ -258,6 +255,7 @@ object ThemeImportExport {
|
|||||||
ThemeConfig.customTagColorsJson = data.customTagColorsJson
|
ThemeConfig.customTagColorsJson = data.customTagColorsJson
|
||||||
|
|
||||||
// 主界面设置
|
// 主界面设置
|
||||||
|
ThemeConfig.showHome = data.showHome
|
||||||
ThemeConfig.showDiscovery = data.showDiscovery
|
ThemeConfig.showDiscovery = data.showDiscovery
|
||||||
ThemeConfig.showRss = data.showRss
|
ThemeConfig.showRss = data.showRss
|
||||||
ThemeConfig.showStatusBar = data.showStatusBar
|
ThemeConfig.showStatusBar = data.showStatusBar
|
||||||
@@ -394,6 +392,7 @@ data class ThemeExportData(
|
|||||||
val customTagColorsJson: String? = null,
|
val customTagColorsJson: String? = null,
|
||||||
|
|
||||||
// 主界面设置
|
// 主界面设置
|
||||||
|
val showHome: Boolean = true,
|
||||||
val showDiscovery: Boolean = true,
|
val showDiscovery: Boolean = true,
|
||||||
val showRss: Boolean = true,
|
val showRss: Boolean = true,
|
||||||
val showStatusBar: Boolean = true,
|
val showStatusBar: Boolean = true,
|
||||||
|
|||||||
@@ -86,7 +86,9 @@ class AddToBookshelfDialog() : BaseDialogFragment(R.layout.dialog_add_to_bookshe
|
|||||||
context = requireContext(),
|
context = requireContext(),
|
||||||
name = it.name,
|
name = it.name,
|
||||||
author = it.author,
|
author = it.author,
|
||||||
bookUrl = it.bookUrl
|
bookUrl = it.bookUrl,
|
||||||
|
origin = it.origin,
|
||||||
|
coverPath = it.coverUrl
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
dismiss()
|
dismiss()
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package io.legado.app.ui.book.explore
|
package io.legado.app.ui.book.explore
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.AnimatedVisibilityScope
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
@@ -13,8 +11,6 @@ import androidx.compose.animation.core.tween
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
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.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
||||||
@@ -52,9 +48,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.chrisbanes.haze.HazeState
|
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.theme.responsiveHazeSource
|
||||||
import io.legado.app.ui.widget.components.AppPullToRefresh
|
import io.legado.app.ui.widget.components.AppPullToRefresh
|
||||||
import io.legado.app.ui.widget.components.AppScaffold
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
import io.legado.app.ui.widget.components.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.SearchBookGridItem
|
||||||
import io.legado.app.ui.widget.components.book.SearchBookListItem
|
import io.legado.app.ui.widget.components.book.SearchBookListItem
|
||||||
import io.legado.app.ui.widget.components.button.AnimatedTextButton
|
|
||||||
import io.legado.app.ui.widget.components.card.TextCard
|
import io.legado.app.ui.widget.components.card.TextCard
|
||||||
import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem
|
import io.legado.app.ui.widget.components.explore.ExploreKindSelectSheet
|
||||||
import io.legado.app.ui.widget.components.explore.calculateExploreKindRows
|
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
@@ -98,7 +90,7 @@ fun ExploreShowScreen(
|
|||||||
sourceUrl: String?,
|
sourceUrl: String?,
|
||||||
exploreUrl: String?,
|
exploreUrl: String?,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onBookClick: (SearchBook) -> Unit,
|
onBookClick: (SearchBook, String?) -> Unit,
|
||||||
viewModel: ExploreShowViewModel = koinViewModel(),
|
viewModel: ExploreShowViewModel = koinViewModel(),
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
@@ -111,7 +103,6 @@ fun ExploreShowScreen(
|
|||||||
val books by viewModel.uiBooks.collectAsState()
|
val books by viewModel.uiBooks.collectAsState()
|
||||||
val isBookEnd by viewModel.isEnd.collectAsState()
|
val isBookEnd by viewModel.isEnd.collectAsState()
|
||||||
val shouldTriggerAutoLoad by viewModel.shouldTriggerAutoLoad.collectAsState()
|
val shouldTriggerAutoLoad by viewModel.shouldTriggerAutoLoad.collectAsState()
|
||||||
val kinds by viewModel.kinds.collectAsState()
|
|
||||||
val isLoading by viewModel.isLoading.collectAsState()
|
val isLoading by viewModel.isLoading.collectAsState()
|
||||||
val errorMsg by viewModel.errorMsg.collectAsState()
|
val errorMsg by viewModel.errorMsg.collectAsState()
|
||||||
val filterState by viewModel.filterState.collectAsState()
|
val filterState by viewModel.filterState.collectAsState()
|
||||||
@@ -125,8 +116,6 @@ fun ExploreShowScreen(
|
|||||||
var showGridCountSheet by remember { mutableStateOf(false) }
|
var showGridCountSheet by remember { mutableStateOf(false) }
|
||||||
val gridColumnCount by viewModel.gridCount.collectAsState()
|
val gridColumnCount by viewModel.gridCount.collectAsState()
|
||||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||||
val context = LocalContext.current
|
|
||||||
val activity = context as? AppCompatActivity
|
|
||||||
val exploreKindUseCase: ExploreKindUiUseCase = koinInject()
|
val exploreKindUseCase: ExploreKindUiUseCase = koinInject()
|
||||||
|
|
||||||
LaunchedEffect(sourceUrl) {
|
LaunchedEffect(sourceUrl) {
|
||||||
@@ -233,73 +222,16 @@ fun ExploreShowScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
AppModalBottomSheet(
|
ExploreKindSelectSheet(
|
||||||
show = showKindSheet,
|
show = showKindSheet,
|
||||||
onDismissRequest = { showKindSheet = false }
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,
|
sourceUrl = sourceUrl,
|
||||||
activity = activity,
|
onSelected = { selectedKinds ->
|
||||||
onOpenUrl = { url ->
|
selectedKinds.firstOrNull()?.let { kind ->
|
||||||
showKindSheet = false
|
viewModel.switchExploreUrl(kind)
|
||||||
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(
|
AppScaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -433,17 +365,22 @@ fun ExploreShowScreen(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
items(
|
itemsIndexed(
|
||||||
items = books,
|
items = books,
|
||||||
key = { it.book.bookUrl }
|
key = { index, item -> "${item.book.bookUrl}:$index" }
|
||||||
) { item ->
|
) { index, item ->
|
||||||
|
val sharedCoverKey = bookCoverSharedElementKey(
|
||||||
|
item.book.bookUrl,
|
||||||
|
"explore:grid:$index"
|
||||||
|
)
|
||||||
ExploreBookGridItem(
|
ExploreBookGridItem(
|
||||||
book = item.book,
|
book = item.book,
|
||||||
shelfState = item.shelfState,
|
shelfState = item.shelfState,
|
||||||
onClick = { onBookClick(item.book) },
|
onClick = { onBookClick(item.book, sharedCoverKey) },
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKey = sharedCoverKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,17 +404,22 @@ fun ExploreShowScreen(
|
|||||||
bottom = paddingValues.calculateBottomPadding() + 16.dp
|
bottom = paddingValues.calculateBottomPadding() + 16.dp
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
items(
|
itemsIndexed(
|
||||||
items = books,
|
items = books,
|
||||||
key = { it.book.bookUrl }
|
key = { index, item -> "${item.book.bookUrl}:$index" }
|
||||||
) { item ->
|
) { index, item ->
|
||||||
|
val sharedCoverKey = bookCoverSharedElementKey(
|
||||||
|
item.book.bookUrl,
|
||||||
|
"explore:list:$index"
|
||||||
|
)
|
||||||
ExploreBookItem(
|
ExploreBookItem(
|
||||||
book = item.book,
|
book = item.book,
|
||||||
shelfState = item.shelfState,
|
shelfState = item.shelfState,
|
||||||
onClick = { onBookClick(item.book) },
|
onClick = { onBookClick(item.book, sharedCoverKey) },
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKey = sharedCoverKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,6 +448,7 @@ fun ExploreBookItem(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
sharedCoverKey: String? = null,
|
||||||
) {
|
) {
|
||||||
SearchBookListItem(
|
SearchBookListItem(
|
||||||
book = book,
|
book = book,
|
||||||
@@ -514,7 +457,7 @@ fun ExploreBookItem(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
sharedCoverKey = sharedCoverKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,6 +470,7 @@ fun ExploreBookGridItem(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
sharedCoverKey: String? = null,
|
||||||
) {
|
) {
|
||||||
SearchBookGridItem(
|
SearchBookGridItem(
|
||||||
book = book,
|
book = book,
|
||||||
@@ -535,60 +479,7 @@ fun ExploreBookGridItem(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
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.domain.usecase.SaveSearchBooksUseCase
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.utils.exploreLayoutGrid
|
import io.legado.app.utils.exploreLayoutGrid
|
||||||
|
import io.legado.app.utils.stackTraceStr
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -222,7 +223,7 @@ class ExploreShowViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
_errorMsg.value = it.localizedMessage
|
_errorMsg.value = it.stackTraceStr
|
||||||
}
|
}
|
||||||
|
|
||||||
_isLoading.value = false
|
_isLoading.value = false
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
@Composable
|
@Composable
|
||||||
fun BookInfoRouteScreen(
|
fun BookInfoRouteScreen(
|
||||||
bookUrl: String,
|
bookUrl: String,
|
||||||
|
name: String? = null,
|
||||||
|
author: String? = null,
|
||||||
|
origin: String? = null,
|
||||||
|
coverPath: String? = null,
|
||||||
viewModel: BookInfoViewModel,
|
viewModel: BookInfoViewModel,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onFinish: (resultCode: Int?, afterTransition: Boolean) -> Unit,
|
onFinish: (resultCode: Int?, afterTransition: Boolean) -> Unit,
|
||||||
@@ -80,8 +84,14 @@ fun BookInfoRouteScreen(
|
|||||||
viewModel.onReaderResult(it.resultCode)
|
viewModel.onReaderResult(it.resultCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(bookUrl, viewModel) {
|
LaunchedEffect(bookUrl, name, author, origin, coverPath, viewModel) {
|
||||||
viewModel.initData(bookUrl)
|
viewModel.initData(
|
||||||
|
bookUrl = bookUrl,
|
||||||
|
name = name,
|
||||||
|
author = author,
|
||||||
|
origin = origin,
|
||||||
|
coverPath = coverPath
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(viewModel) {
|
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.localBook.LocalBook
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.model.webBook.WebBook
|
||||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
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.ui.widget.components.image.cover.buildCoverImageRequest
|
||||||
import io.legado.app.utils.ArchiveUtils
|
import io.legado.app.utils.ArchiveUtils
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
@@ -113,12 +114,37 @@ class BookInfoViewModel(
|
|||||||
private var readRecordObserveJob: Job? = null
|
private var readRecordObserveJob: Job? = null
|
||||||
|
|
||||||
fun initData(intent: Intent) {
|
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
|
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()
|
currentChapterList = emptyList()
|
||||||
currentWebFiles = emptyList()
|
currentWebFiles = emptyList()
|
||||||
currentKindLabels = emptyList()
|
currentKindLabels = emptyList()
|
||||||
@@ -128,24 +154,32 @@ class BookInfoViewModel(
|
|||||||
bookSource = null
|
bookSource = null
|
||||||
chapterChanged = false
|
chapterChanged = false
|
||||||
clearReadRecordObserve()
|
clearReadRecordObserve()
|
||||||
_uiState.value = BookInfoUiState()
|
syncUiState()
|
||||||
execute {
|
execute {
|
||||||
val book = appDb.bookDao.getBook(bookUrl)?.let {
|
val dbBook = appDb.bookDao.getBook(bookUrl)
|
||||||
inBookshelf = !it.isNotShelf
|
if (dbBook != null) {
|
||||||
it
|
inBookshelf = !dbBook.isNotShelf
|
||||||
} ?: appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()?.let {
|
dbBook
|
||||||
|
} else {
|
||||||
|
val searchBook = appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()
|
||||||
|
if (searchBook != null) {
|
||||||
inBookshelf = false
|
inBookshelf = false
|
||||||
it
|
searchBook
|
||||||
} ?: throw NoStackTraceException("未找到书籍")
|
} else {
|
||||||
|
currentBook ?: throw NoStackTraceException("未找到书籍")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onSuccess { book ->
|
||||||
|
// 如果从数据库/搜索中拿到的书没有封面,但我们有传入的封面,则保留传入的封面
|
||||||
|
if (book.coverUrl.isNullOrBlank() && !coverPath.isNullOrBlank()) {
|
||||||
|
book.coverUrl = coverPath
|
||||||
|
}
|
||||||
val source = if (book.isLocal) {
|
val source = if (book.isLocal) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
appDb.bookSourceDao.getBookSource(book.origin)
|
appDb.bookSourceDao.getBookSource(book.origin)
|
||||||
}
|
}
|
||||||
book to source
|
upBook(book, source)
|
||||||
}.onSuccess {
|
|
||||||
upBook(it.first, it.second)
|
|
||||||
}.onError {
|
}.onError {
|
||||||
context.toastOnUi(it.localizedMessage ?: "未找到书籍")
|
context.toastOnUi(it.localizedMessage ?: "未找到书籍")
|
||||||
emitEffect(BookInfoEffect.Finish(afterTransition = true))
|
emitEffect(BookInfoEffect.Finish(afterTransition = true))
|
||||||
|
|||||||
@@ -40,13 +40,15 @@ class SearchActivity : BaseComposeActivity() {
|
|||||||
SearchScreen(
|
SearchScreen(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onBack = { finish() },
|
onBack = { finish() },
|
||||||
onOpenBookInfo = { name, author, bookUrl ->
|
onOpenBookInfo = { name, author, bookUrl, origin, coverPath, _ ->
|
||||||
startActivity(
|
startActivity(
|
||||||
MainActivity.createBookInfoIntent(
|
MainActivity.createBookInfoIntent(
|
||||||
context = this,
|
context = this,
|
||||||
name = name,
|
name = name,
|
||||||
author = author,
|
author = author,
|
||||||
bookUrl = bookUrl
|
bookUrl = bookUrl,
|
||||||
|
origin = origin,
|
||||||
|
coverPath = coverPath
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ sealed interface SearchIntent {
|
|||||||
data object PauseEngine : SearchIntent
|
data object PauseEngine : SearchIntent
|
||||||
data object ResumeEngine : SearchIntent
|
data object ResumeEngine : SearchIntent
|
||||||
data class UseHistoryKeyword(val keyword: String) : 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 OpenBookshelfBook(val book: BookShelfItem) : SearchIntent
|
||||||
data class DeleteHistory(val item: SearchKeyword) : SearchIntent
|
data class DeleteHistory(val item: SearchKeyword) : SearchIntent
|
||||||
data class SetClearHistoryDialogVisible(val visible: Boolean) : SearchIntent
|
data class SetClearHistoryDialogVisible(val visible: Boolean) : SearchIntent
|
||||||
@@ -82,6 +82,9 @@ sealed interface SearchEffect {
|
|||||||
val name: String,
|
val name: String,
|
||||||
val author: String,
|
val author: String,
|
||||||
val bookUrl: String,
|
val bookUrl: String,
|
||||||
|
val origin: String? = null,
|
||||||
|
val coverPath: String? = null,
|
||||||
|
val sharedCoverKey: String?,
|
||||||
) : SearchEffect
|
) : SearchEffect
|
||||||
|
|
||||||
data object OpenSourceManage : SearchEffect
|
data object OpenSourceManage : SearchEffect
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||||||
fun SearchScreen(
|
fun SearchScreen(
|
||||||
viewModel: SearchViewModel,
|
viewModel: SearchViewModel,
|
||||||
onBack: () -> Unit,
|
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,
|
onOpenSourceManage: () -> Unit,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
@@ -160,7 +160,14 @@ fun SearchScreen(
|
|||||||
viewModel.effects.collect { effect ->
|
viewModel.effects.collect { effect ->
|
||||||
when (effect) {
|
when (effect) {
|
||||||
is SearchEffect.OpenBookInfo -> {
|
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()
|
SearchEffect.OpenSourceManage -> onOpenSourceManage()
|
||||||
@@ -394,16 +401,25 @@ fun SearchScreen(
|
|||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = state.results,
|
items = state.results,
|
||||||
key = { index, item -> "${item.book.origin}:${item.book.bookUrl}:$index" }
|
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(
|
SearchBookListItem(
|
||||||
book = item.book,
|
book = item.book,
|
||||||
shelfState = item.shelfState,
|
shelfState = item.shelfState,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onIntent(SearchIntent.OpenSearchBook(item.book))
|
viewModel.onIntent(
|
||||||
|
SearchIntent.OpenSearchBook(
|
||||||
|
item.book,
|
||||||
|
sharedCoverKey
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
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.domain.usecase.SearchRunEvent
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||||
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
|
||||||
import io.legado.app.utils.getPrefBoolean
|
import io.legado.app.utils.getPrefBoolean
|
||||||
import io.legado.app.utils.putPrefBoolean
|
import io.legado.app.utils.putPrefBoolean
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -107,6 +106,9 @@ class SearchViewModel(
|
|||||||
name = intent.book.name,
|
name = intent.book.name,
|
||||||
author = intent.book.author,
|
author = intent.book.author,
|
||||||
bookUrl = intent.book.bookUrl,
|
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,
|
name = intent.book.name,
|
||||||
author = intent.book.author,
|
author = intent.book.author,
|
||||||
bookUrl = intent.book.bookUrl,
|
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 showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
|
||||||
|
|
||||||
|
var showHome by prefDelegate(PreferKey.showHome, true)
|
||||||
|
|
||||||
var showRss by prefDelegate(PreferKey.showRss, true)
|
var showRss by prefDelegate(PreferKey.showRss, true)
|
||||||
|
|
||||||
var showStatusBar by prefDelegate(PreferKey.showStatusBar, 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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -47,7 +46,6 @@ import androidx.compose.material3.ButtonGroupDefaults
|
|||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -57,7 +55,6 @@ import androidx.compose.material3.ToggleButtonDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
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.role
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.constraintlayout.compose.ConstraintLayout
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
import com.google.android.material.color.DynamicColors
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.google.android.material.color.DynamicColorsOptions
|
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.AppContextWrapper
|
import io.legado.app.base.AppContextWrapper
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.help.LauncherIconHelp
|
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.AppConfig
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.OldThemeConfig
|
||||||
import io.legado.app.lib.theme.ThemeStore
|
import io.legado.app.help.loadFontFiles
|
||||||
import io.legado.app.lib.theme.primaryColor
|
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.ThemeEngine
|
import io.legado.app.ui.theme.ThemeEngine
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||||
import io.legado.app.ui.widget.components.AppScaffold
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
import io.legado.app.ui.widget.components.AppTextField
|
|
||||||
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
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.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.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.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.ClickableSettingItem
|
||||||
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
||||||
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
|
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.text.AppText
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||||
import io.legado.app.utils.FileDoc
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||||
import io.legado.app.utils.getPrefString
|
import io.legado.app.utils.getPrefString
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
import io.legado.app.utils.putPrefString
|
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.takePersistablePermissionSafely
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import org.koin.androidx.compose.koinViewModel
|
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)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -138,16 +129,16 @@ fun ThemeConfigScreen(
|
|||||||
var showBorderColorPicker by remember { mutableStateOf(false) }
|
var showBorderColorPicker by remember { mutableStateOf(false) }
|
||||||
var showNavIconSheet by remember { mutableStateOf(false) }
|
var showNavIconSheet by remember { mutableStateOf(false) }
|
||||||
var showFontSheet by remember { mutableStateOf(false) }
|
var showFontSheet by remember { mutableStateOf(false) }
|
||||||
var fontItems by remember { mutableStateOf<List<FileDoc>>(emptyList()) }
|
val showThemeRefactorTip by viewModel.showThemeRefactorTip.collectAsStateWithLifecycle()
|
||||||
var fontFolderUri by remember { mutableStateOf<Uri?>(null) }
|
|
||||||
|
|
||||||
fun loadFonts() {
|
var fontFolderUri by remember {
|
||||||
fontItems = loadFontFiles(context, fontFolderUri)
|
mutableStateOf(
|
||||||
|
context.getPrefString(PreferKey.fontFolder)?.let { Uri.parse(it) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
remember {
|
|
||||||
val saved = context.getPrefString(PreferKey.fontFolder)
|
val fontItems = remember(fontFolderUri) {
|
||||||
if (!saved.isNullOrEmpty()) fontFolderUri = Uri.parse(saved)
|
loadFontFiles(context, fontFolderUri)
|
||||||
loadFonts()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val fontFolderLauncher = rememberLauncherForActivityResult(
|
val fontFolderLauncher = rememberLauncherForActivityResult(
|
||||||
@@ -157,7 +148,6 @@ fun ThemeConfigScreen(
|
|||||||
fontFolderUri = uri
|
fontFolderUri = uri
|
||||||
uri.takePersistablePermissionSafely(context, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
uri.takePersistablePermissionSafely(context, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
context.putPrefString(PreferKey.fontFolder, uri.toString())
|
context.putPrefString(PreferKey.fontFolder, uri.toString())
|
||||||
loadFonts()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,16 +208,28 @@ fun ThemeConfigScreen(
|
|||||||
themeItems.zip(themeValues).toList()
|
themeItems.zip(themeValues).toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMiuixEngine) {
|
AnimatedVisibility(visible = showThemeRefactorTip) {
|
||||||
MiuixCard(
|
GlassCard(
|
||||||
cornerRadius = 16.dp,
|
cornerRadius = 16.dp,
|
||||||
insideMargin = PaddingValues(16.dp),
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
colors = MiuixCardDefaults.defaultColors(
|
|
||||||
color = MiuixTheme.colorScheme.primaryVariant,
|
|
||||||
contentColor = MiuixTheme.colorScheme.onPrimary
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
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)) {
|
SplicedColumnGroup(title = stringResource(R.string.main_activity)) {
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_home),
|
||||||
|
checked = ThemeConfig.showHome,
|
||||||
|
onCheckedChange = { ThemeConfig.showHome = it }
|
||||||
|
)
|
||||||
SwitchSettingItem(
|
SwitchSettingItem(
|
||||||
title = stringResource(R.string.show_discovery),
|
title = stringResource(R.string.show_discovery),
|
||||||
checked = ThemeConfig.showDiscovery,
|
checked = ThemeConfig.showDiscovery,
|
||||||
@@ -648,7 +655,11 @@ fun ThemeConfigScreen(
|
|||||||
.size(28.dp)
|
.size(28.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color(ThemeConfig.itemDividerColor))
|
.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 = {
|
content = {
|
||||||
if (fontItems.isEmpty()) {
|
if (fontItems.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth().height(120.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(120.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -789,7 +802,9 @@ fun ThemeConfigScreen(
|
|||||||
fontItems.forEach { fontDoc ->
|
fontItems.forEach { fontDoc ->
|
||||||
item {
|
item {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth().height(100.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(100.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
ThemeConfig.appFontPath = fontDoc.uri.toString()
|
ThemeConfig.appFontPath = fontDoc.uri.toString()
|
||||||
showFontSheet = false
|
showFontSheet = false
|
||||||
|
|||||||
@@ -2,19 +2,40 @@ package io.legado.app.ui.config.themeConfig
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.constant.PreferKey
|
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.FileDoc
|
||||||
import io.legado.app.utils.FileUtils
|
import io.legado.app.utils.FileUtils
|
||||||
import io.legado.app.utils.MD5Utils
|
import io.legado.app.utils.MD5Utils
|
||||||
import io.legado.app.utils.externalFiles
|
import io.legado.app.utils.externalFiles
|
||||||
import io.legado.app.utils.inputStream
|
import io.legado.app.utils.inputStream
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
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.R
|
||||||
import io.legado.app.help.config.ThemeExportData
|
import io.legado.app.help.config.ThemeExportData
|
||||||
import io.legado.app.ui.widget.components.AppTextField
|
import io.legado.app.ui.widget.components.AppTextField
|
||||||
import io.legado.app.ui.widget.components.SearchBar
|
|
||||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
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.dialog.ColorPickerSheet
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
@@ -164,6 +163,11 @@ fun EditThemeSheet(
|
|||||||
|
|
||||||
// Interface layout
|
// Interface layout
|
||||||
SectionTitle(stringResource(R.string.theme_manage_section_layout))
|
SectionTitle(stringResource(R.string.theme_manage_section_layout))
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "首页",
|
||||||
|
checked = data.showHome,
|
||||||
|
onCheckedChange = { data = data.copy(showHome = it) }
|
||||||
|
)
|
||||||
CompactSwitchSettingItem(
|
CompactSwitchSettingItem(
|
||||||
title = stringResource(R.string.theme_manage_show_discovery),
|
title = stringResource(R.string.theme_manage_show_discovery),
|
||||||
checked = data.showDiscovery,
|
checked = data.showDiscovery,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
package io.legado.app.ui.main
|
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,
|
context: Context,
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
author: String? = null,
|
author: String? = null,
|
||||||
bookUrl: String
|
bookUrl: String,
|
||||||
): Intent = MainIntent.createBookInfoIntent(context, name, author, bookUrl)
|
origin: String? = null,
|
||||||
|
coverPath: String? = null
|
||||||
|
): Intent =
|
||||||
|
MainIntent.createBookInfoIntent(context, name, author, bookUrl, origin, coverPath)
|
||||||
|
|
||||||
fun createExploreShowIntent(
|
fun createExploreShowIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ object MainIntent {
|
|||||||
const val EXTRA_BOOK_NAME = "name"
|
const val EXTRA_BOOK_NAME = "name"
|
||||||
const val EXTRA_BOOK_AUTHOR = "author"
|
const val EXTRA_BOOK_AUTHOR = "author"
|
||||||
const val EXTRA_BOOK_URL = "bookUrl"
|
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_EXPLORE_NAME = "exploreName"
|
||||||
const val EXTRA_SOURCE_URL = "sourceUrl"
|
const val EXTRA_SOURCE_URL = "sourceUrl"
|
||||||
const val EXTRA_EXPLORE_URL = "exploreUrl"
|
const val EXTRA_EXPLORE_URL = "exploreUrl"
|
||||||
@@ -114,13 +116,17 @@ object MainIntent {
|
|||||||
context: Context,
|
context: Context,
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
author: String? = null,
|
author: String? = null,
|
||||||
bookUrl: String
|
bookUrl: String,
|
||||||
|
origin: String? = null,
|
||||||
|
coverPath: String? = null
|
||||||
): Intent {
|
): Intent {
|
||||||
return createLauncherIntent(context).apply {
|
return createLauncherIntent(context).apply {
|
||||||
putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_BOOK_INFO)
|
putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_BOOK_INFO)
|
||||||
putExtra(EXTRA_BOOK_NAME, name)
|
putExtra(EXTRA_BOOK_NAME, name)
|
||||||
putExtra(EXTRA_BOOK_AUTHOR, author)
|
putExtra(EXTRA_BOOK_AUTHOR, author)
|
||||||
putExtra(EXTRA_BOOK_URL, bookUrl)
|
putExtra(EXTRA_BOOK_URL, bookUrl)
|
||||||
|
putExtra(EXTRA_BOOK_ORIGIN, origin)
|
||||||
|
putExtra(EXTRA_BOOK_COVER, coverPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,12 +90,15 @@ fun MainActivity.mainEntryProvider(
|
|||||||
onNavigateToBookCacheManage = {
|
onNavigateToBookCacheManage = {
|
||||||
onNavigateToRoute(MainRouteBookCacheManage)
|
onNavigateToRoute(MainRouteBookCacheManage)
|
||||||
},
|
},
|
||||||
onNavigateToBookInfo = { name, author, bookUrl ->
|
onNavigateToBookInfo = { name, author, bookUrl, origin, coverPath, sharedCoverKey ->
|
||||||
onNavigateToRoute(
|
onNavigateToRoute(
|
||||||
MainRouteBookInfo(
|
MainRouteBookInfo(
|
||||||
name = name,
|
name = name,
|
||||||
author = author,
|
author = author,
|
||||||
bookUrl = bookUrl
|
bookUrl = bookUrl,
|
||||||
|
origin = origin,
|
||||||
|
coverPath = coverPath,
|
||||||
|
sharedCoverKey = sharedCoverKey
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -246,12 +249,15 @@ fun MainActivity.mainEntryProvider(
|
|||||||
searchViewModel.onIntent(SearchIntent.ClearSearchResults)
|
searchViewModel.onIntent(SearchIntent.ClearSearchResults)
|
||||||
onNavigateBack()
|
onNavigateBack()
|
||||||
},
|
},
|
||||||
onOpenBookInfo = { name, author, bookUrl ->
|
onOpenBookInfo = { name, author, bookUrl, origin, coverPath, sharedCoverKey ->
|
||||||
onNavigateToRoute(
|
onNavigateToRoute(
|
||||||
MainRouteBookInfo(
|
MainRouteBookInfo(
|
||||||
name = name,
|
name = name,
|
||||||
author = author,
|
author = author,
|
||||||
bookUrl = bookUrl
|
bookUrl = bookUrl,
|
||||||
|
origin = origin,
|
||||||
|
coverPath = coverPath,
|
||||||
|
sharedCoverKey = sharedCoverKey
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -395,9 +401,13 @@ fun MainActivity.mainEntryProvider(
|
|||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
) { route ->
|
) { route ->
|
||||||
val bookInfoViewModel = koinViewModel<BookInfoViewModel>()
|
val bookInfoViewModel = koinViewModel<BookInfoViewModel>(key = route.bookUrl)
|
||||||
BookInfoRouteScreen(
|
BookInfoRouteScreen(
|
||||||
bookUrl = route.bookUrl,
|
bookUrl = route.bookUrl,
|
||||||
|
name = route.name,
|
||||||
|
author = route.author,
|
||||||
|
origin = route.origin,
|
||||||
|
coverPath = route.coverPath,
|
||||||
viewModel = bookInfoViewModel,
|
viewModel = bookInfoViewModel,
|
||||||
onBack = { onNavigateBack() },
|
onBack = { onNavigateBack() },
|
||||||
onFinish = { _, _ -> onNavigateBack() },
|
onFinish = { _, _ -> onNavigateBack() },
|
||||||
@@ -406,7 +416,7 @@ fun MainActivity.mainEntryProvider(
|
|||||||
},
|
},
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||||
sharedCoverKey = bookCoverSharedElementKey(route.bookUrl),
|
sharedCoverKey = route.sharedCoverKey ?: bookCoverSharedElementKey(route.bookUrl),
|
||||||
onRegisterVariableSetter = { setter ->
|
onRegisterVariableSetter = { setter ->
|
||||||
onRegisterVariableSetter(setter)
|
onRegisterVariableSetter(setter)
|
||||||
}
|
}
|
||||||
@@ -419,12 +429,15 @@ fun MainActivity.mainEntryProvider(
|
|||||||
sourceUrl = route.sourceUrl,
|
sourceUrl = route.sourceUrl,
|
||||||
exploreUrl = route.exploreUrl,
|
exploreUrl = route.exploreUrl,
|
||||||
onBack = { onNavigateBack() },
|
onBack = { onNavigateBack() },
|
||||||
onBookClick = { book ->
|
onBookClick = { book, sharedCoverKey ->
|
||||||
onNavigateToRoute(
|
onNavigateToRoute(
|
||||||
MainRouteBookInfo(
|
MainRouteBookInfo(
|
||||||
name = book.name,
|
name = book.name,
|
||||||
author = book.author,
|
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 name: String?,
|
||||||
val author: String?,
|
val author: String?,
|
||||||
val bookUrl: String,
|
val bookUrl: String,
|
||||||
|
val origin: String? = null,
|
||||||
|
val coverPath: String? = null,
|
||||||
|
val sharedCoverKey: String? = null,
|
||||||
) : MainRoute
|
) : MainRoute
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -254,7 +254,9 @@ object MainNavigator {
|
|||||||
MainRouteBookInfo(
|
MainRouteBookInfo(
|
||||||
name = intent.getStringExtra(MainIntent.EXTRA_BOOK_NAME),
|
name = intent.getStringExtra(MainIntent.EXTRA_BOOK_NAME),
|
||||||
author = intent.getStringExtra(MainIntent.EXTRA_BOOK_AUTHOR),
|
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
|
} ?: MainRouteHome
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ fun MainScreen(
|
|||||||
onNavigateToLocalImport: () -> Unit,
|
onNavigateToLocalImport: () -> Unit,
|
||||||
onNavigateToCache: (Long) -> Unit,
|
onNavigateToCache: (Long) -> Unit,
|
||||||
onNavigateToBookCacheManage: () -> 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,
|
onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
||||||
onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
|
onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
|
||||||
onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: 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
|
val destination = destinations.getOrNull(page) ?: return@HorizontalPager
|
||||||
when (destination) {
|
when (destination) {
|
||||||
MainDestination.Home -> HomepageScreen(
|
MainDestination.Home -> HomepageScreen(
|
||||||
onBookClick = { name, author, bookUrl ->
|
onBookClick = { name, author, bookUrl, origin, coverPath, sharedCoverKey ->
|
||||||
onNavigateToBookInfo(name ?: "", author ?: "", bookUrl)
|
onNavigateToBookInfo(
|
||||||
|
name ?: "",
|
||||||
|
author ?: "",
|
||||||
|
bookUrl,
|
||||||
|
origin,
|
||||||
|
coverPath,
|
||||||
|
sharedCoverKey
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onModuleHeaderClick = { title, sourceUrl, exploreUrl ->
|
onModuleHeaderClick = { title, sourceUrl, exploreUrl ->
|
||||||
onNavigateToExploreShow(title, sourceUrl, exploreUrl)
|
onNavigateToExploreShow(title, sourceUrl, exploreUrl)
|
||||||
@@ -382,8 +389,15 @@ fun MainScreen(
|
|||||||
onBookClick = { book ->
|
onBookClick = { book ->
|
||||||
context.startActivityForBook(book)
|
context.startActivityForBook(book)
|
||||||
},
|
},
|
||||||
onBookLongClick = { book ->
|
onBookLongClick = { book, sharedCoverKey ->
|
||||||
onNavigateToBookInfo(book.name, book.author, book.bookUrl)
|
onNavigateToBookInfo(
|
||||||
|
book.name,
|
||||||
|
book.author,
|
||||||
|
book.bookUrl,
|
||||||
|
book.origin,
|
||||||
|
book.getDisplayCover(),
|
||||||
|
sharedCoverKey
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onNavigateToSearch = { query -> onNavigateToSearch(query) },
|
onNavigateToSearch = { query -> onNavigateToSearch(query) },
|
||||||
onNavigateToRemoteImport = onNavigateToRemoteImport,
|
onNavigateToRemoteImport = onNavigateToRemoteImport,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class MainViewModel(
|
|||||||
private val prefs = context.defaultSharedPreferences
|
private val prefs = context.defaultSharedPreferences
|
||||||
private val mainPreferenceKeys = setOf(
|
private val mainPreferenceKeys = setOf(
|
||||||
PreferKey.showDiscovery,
|
PreferKey.showDiscovery,
|
||||||
|
PreferKey.showHome,
|
||||||
PreferKey.showRss,
|
PreferKey.showRss,
|
||||||
PreferKey.showBottomView,
|
PreferKey.showBottomView,
|
||||||
PreferKey.useFloatingBottomBar,
|
PreferKey.useFloatingBottomBar,
|
||||||
@@ -152,10 +153,12 @@ private const val NAV_EXTENDED_KEY = "navExtended"
|
|||||||
|
|
||||||
private fun MainViewModel.readMainUiState(): MainUiState {
|
private fun MainViewModel.readMainUiState(): MainUiState {
|
||||||
val showDiscovery = context.getPrefBoolean(PreferKey.showDiscovery, true)
|
val showDiscovery = context.getPrefBoolean(PreferKey.showDiscovery, true)
|
||||||
|
val showHome = context.getPrefBoolean(PreferKey.showHome, true)
|
||||||
val showRss = context.getPrefBoolean(PreferKey.showRss, true)
|
val showRss = context.getPrefBoolean(PreferKey.showRss, true)
|
||||||
val destinations = MainDestination.mainDestinations.filter {
|
val destinations = MainDestination.mainDestinations.filter {
|
||||||
when (it) {
|
when (it) {
|
||||||
MainDestination.Explore -> showDiscovery
|
MainDestination.Explore -> showDiscovery
|
||||||
|
MainDestination.Home -> showHome
|
||||||
MainDestination.Rss -> showRss
|
MainDestination.Rss -> showRss
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ import sh.calvin.reorderable.rememberReorderableLazyGridState
|
|||||||
fun BookshelfScreen(
|
fun BookshelfScreen(
|
||||||
viewModel: BookshelfViewModel = koinViewModel(),
|
viewModel: BookshelfViewModel = koinViewModel(),
|
||||||
onBookClick: (BookShelfItem) -> Unit,
|
onBookClick: (BookShelfItem) -> Unit,
|
||||||
onBookLongClick: (BookShelfItem) -> Unit,
|
onBookLongClick: (book: BookShelfItem, sharedCoverKey: String?) -> Unit,
|
||||||
onNavigateToSearch: (String) -> Unit,
|
onNavigateToSearch: (String) -> Unit,
|
||||||
onNavigateToRemoteImport: () -> Unit,
|
onNavigateToRemoteImport: () -> Unit,
|
||||||
onNavigateToLocalImport: () -> Unit,
|
onNavigateToLocalImport: () -> Unit,
|
||||||
@@ -1211,7 +1211,7 @@ fun BookshelfPage(
|
|||||||
onDragFinished: () -> Unit,
|
onDragFinished: () -> Unit,
|
||||||
onGlobalSearch: () -> Unit,
|
onGlobalSearch: () -> Unit,
|
||||||
onBookClick: (BookShelfItem) -> Unit,
|
onBookClick: (BookShelfItem) -> Unit,
|
||||||
onBookLongClick: (BookShelfItem) -> Unit,
|
onBookLongClick: (BookShelfItem, String?) -> Unit,
|
||||||
isCurrentPage: Boolean = true,
|
isCurrentPage: Boolean = true,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
@@ -1327,6 +1327,14 @@ fun BookshelfPage(
|
|||||||
) {
|
) {
|
||||||
items(displayBooks, key = { it.book.bookUrl }) { bookUi ->
|
items(displayBooks, key = { it.book.bookUrl }) { bookUi ->
|
||||||
val isSelected = selectedBookUrls.contains(bookUi.book.bookUrl)
|
val isSelected = selectedBookUrls.contains(bookUi.book.bookUrl)
|
||||||
|
val sharedCoverKey = if (isCurrentPage) {
|
||||||
|
bookCoverSharedElementKey(
|
||||||
|
bookUi.book.bookUrl,
|
||||||
|
"bookshelf:${uiState.selectedGroupId}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
ReorderableItem(
|
ReorderableItem(
|
||||||
state = reorderableState,
|
state = reorderableState,
|
||||||
key = bookUi.book.bookUrl,
|
key = bookUi.book.bookUrl,
|
||||||
@@ -1366,7 +1374,7 @@ fun BookshelfPage(
|
|||||||
searchKey = uiState.searchKey,
|
searchKey = uiState.searchKey,
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
sharedCoverKey = if (isCurrentPage) bookCoverSharedElementKey(bookUi.book.bookUrl) else null,
|
sharedCoverKey = sharedCoverKey,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.isEditMode) {
|
if (uiState.isEditMode) {
|
||||||
onToggleBookSelection(bookUi)
|
onToggleBookSelection(bookUi)
|
||||||
@@ -1381,7 +1389,7 @@ fun BookshelfPage(
|
|||||||
if (uiState.isEditMode) {
|
if (uiState.isEditMode) {
|
||||||
onToggleBookSelection(bookUi)
|
onToggleBookSelection(bookUi)
|
||||||
} else {
|
} else {
|
||||||
onBookLongClick(bookUi.book)
|
onBookLongClick(bookUi.book, sharedCoverKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -151,7 +152,8 @@ fun ExploreScreen(
|
|||||||
onClick = { viewModel.setGroup(group); dismiss() }
|
onClick = { viewModel.setGroup(group); dismiss() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
contentWindowInsets = WindowInsets(0)
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
if (uiState.items.isEmpty()) {
|
if (uiState.items.isEmpty()) {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ sealed interface HomepageEffect {
|
|||||||
val name: String?,
|
val name: String?,
|
||||||
val author: String?,
|
val author: String?,
|
||||||
val bookUrl: String,
|
val bookUrl: String,
|
||||||
|
val origin: String? = null,
|
||||||
|
val coverPath: String? = null,
|
||||||
|
val sharedCoverKey: String?,
|
||||||
) : HomepageEffect
|
) : HomepageEffect
|
||||||
|
|
||||||
data class NavigateToExploreShow(
|
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.alert.AppAlertDialog
|
||||||
import io.legado.app.ui.widget.components.button.SecondaryButton
|
import io.legado.app.ui.widget.components.button.SecondaryButton
|
||||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
import io.legado.app.ui.widget.components.button.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.ReorderableSelectionItem
|
||||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||||
import io.legado.app.ui.widget.components.divider.PillDivider
|
import io.legado.app.ui.widget.components.divider.PillDivider
|
||||||
|
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.icon.AppIcon
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
@@ -84,6 +87,7 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
onGetAllModulesGroupedBySource: () -> Map<String, List<HomepageModuleManageUi>> = { emptyMap() },
|
onGetAllModulesGroupedBySource: () -> Map<String, List<HomepageModuleManageUi>> = { emptyMap() },
|
||||||
onGetSourceName: (String) -> String = { it },
|
onGetSourceName: (String) -> String = { it },
|
||||||
onAssignModuleToCustomSet: (String, String?) -> Unit = { _, _ -> },
|
onAssignModuleToCustomSet: (String, String?) -> Unit = { _, _ -> },
|
||||||
|
onSyncSourceModules: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
var selectingSetUrl by remember(data != null) { mutableStateOf<String?>(null) }
|
var selectingSetUrl by remember(data != null) { mutableStateOf<String?>(null) }
|
||||||
var browsingSourceUrl by remember(data != null) { mutableStateOf<String?>(null) }
|
var browsingSourceUrl by remember(data != null) { mutableStateOf<String?>(null) }
|
||||||
@@ -102,6 +106,7 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
var browseModuleType by remember(data != null) { mutableStateOf("card") }
|
var browseModuleType by remember(data != null) { mutableStateOf("card") }
|
||||||
var selectedKindTitles by remember(data != null) { mutableStateOf<Set<String>>(emptySet()) }
|
var selectedKindTitles by remember(data != null) { mutableStateOf<Set<String>>(emptySet()) }
|
||||||
var showCustomSetAddModules by remember(data != null) { mutableStateOf(false) }
|
var showCustomSetAddModules by remember(data != null) { mutableStateOf(false) }
|
||||||
|
var showKindSelect by remember(data != null) { mutableStateOf(false) }
|
||||||
var showAddButtonGroupDialog by remember(data != null) { mutableStateOf(false) }
|
var showAddButtonGroupDialog by remember(data != null) { mutableStateOf(false) }
|
||||||
val defaultQuickActionsTitle = stringResource(R.string.homepage_quick_actions)
|
val defaultQuickActionsTitle = stringResource(R.string.homepage_quick_actions)
|
||||||
var tempButtonGroupTitle by remember(data != null) { mutableStateOf(defaultQuickActionsTitle) }
|
var tempButtonGroupTitle by remember(data != null) { mutableStateOf(defaultQuickActionsTitle) }
|
||||||
@@ -204,18 +209,27 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
val setUrl = selectingSetUrl
|
val setUrl = selectingSetUrl
|
||||||
val browseUrl = browsingSourceUrl
|
val browseUrl = browsingSourceUrl
|
||||||
val isBrowsing = showSourceBrowser || browseUrl != null
|
val isBrowsing = showSourceBrowser || browseUrl != null
|
||||||
|
|
||||||
|
LaunchedEffect(browseUrl) {
|
||||||
|
browseUrl?.let { onSyncSourceModules(it) }
|
||||||
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
browseUrl != null && browsingDetail -> {
|
browseUrl != null && browsingDetail -> {
|
||||||
// 三级:浏览书源的模块列表(已加入 / 书源模块 / 发现)
|
// 三级:浏览书源的模块列表(已加入 / 书源模块 / 发现)
|
||||||
val displaySetUrl =
|
val displaySetUrl =
|
||||||
selectingSetUrl ?: HomepageViewModel.customSetUrl("src_$browseUrl")
|
selectingSetUrl ?: HomepageViewModel.customSetUrl("src_$browseUrl")
|
||||||
val currentSetId = HomepageViewModel.customSetIdFromUrl(displaySetUrl)
|
val currentSetId = HomepageViewModel.customSetIdFromUrl(displaySetUrl)
|
||||||
val joinedModules = onGetModulesInSet(displaySetUrl)
|
val joinedModules = remember(displaySetUrl, sets, browseSources) {
|
||||||
|
onGetModulesInSet(displaySetUrl).distinctBy { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
val standardModules =
|
val standardModules = remember(joinedModules) {
|
||||||
joinedModules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
joinedModules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
||||||
val infiniteModules =
|
}
|
||||||
|
val infiniteModules = remember(joinedModules) {
|
||||||
joinedModules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
joinedModules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
||||||
|
}
|
||||||
|
|
||||||
val joinedKeys = joinedModules.map { it.moduleKey }.toSet()
|
val joinedKeys = joinedModules.map { it.moduleKey }.toSet()
|
||||||
val sourceModules = onGetSourceModules(browseUrl, currentSetId)
|
val sourceModules = onGetSourceModules(browseUrl, currentSetId)
|
||||||
@@ -243,9 +257,9 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
AppText(stringResource(R.string.homepage_no_joined_modules))
|
AppText(stringResource(R.string.homepage_no_joined_modules))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var listData by remember(displaySetUrl) {
|
var listData by remember(displaySetUrl, standardModules) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
standardModules
|
standardModules.distinctBy { it.id }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
@@ -264,7 +278,7 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
||||||
if (!reorderableState.isAnyItemDragging) {
|
if (!reorderableState.isAnyItemDragging) {
|
||||||
val orderedIds =
|
val orderedIds =
|
||||||
listData.map { it.id } + infiniteModules.map { it.id }
|
(listData.map { it.id } + infiniteModules.map { it.id }).distinct()
|
||||||
if (orderedIds != joinedModules.map { it.id }) {
|
if (orderedIds != joinedModules.map { it.id }) {
|
||||||
onReorderModules(orderedIds)
|
onReorderModules(orderedIds)
|
||||||
}
|
}
|
||||||
@@ -310,8 +324,7 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
onClick = { deleteConfirmId = module.id },
|
onClick = { deleteConfirmId = module.id },
|
||||||
imageVector = Icons.Default.Delete
|
imageVector = Icons.Default.Delete
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,8 +366,7 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
onClick = { deleteConfirmId = module.id },
|
onClick = { deleteConfirmId = module.id },
|
||||||
imageVector = Icons.Default.Delete
|
imageVector = Icons.Default.Delete
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,8 +410,7 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
sourceUrl = browseUrl,
|
sourceUrl = browseUrl,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -408,11 +419,15 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
|
|
||||||
2 -> {
|
2 -> {
|
||||||
val isButtonGroup = browseModuleType == "buttonGroup"
|
val isButtonGroup = browseModuleType == "buttonGroup"
|
||||||
val selectableKinds = exploreKinds
|
|
||||||
Column {
|
Column {
|
||||||
val typeList = remember {
|
val typeList = remember {
|
||||||
HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown }
|
HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GlassCard(
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
cornerRadius = 12.dp
|
||||||
|
) {
|
||||||
CompactDropdownSettingItem(
|
CompactDropdownSettingItem(
|
||||||
title = stringResource(R.string.homepage_module_type),
|
title = stringResource(R.string.homepage_module_type),
|
||||||
selectedValue = browseModuleType,
|
selectedValue = browseModuleType,
|
||||||
@@ -422,76 +437,58 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
browseModuleType = it; selectedKindTitles = emptySet()
|
browseModuleType = it; selectedKindTitles = emptySet()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (selectableKinds.isEmpty()) {
|
}
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(24.dp),
|
SelectionItemCard(
|
||||||
contentAlignment = Alignment.Center
|
title = stringResource(R.string.homepage_select_from_kinds),
|
||||||
) {
|
subtitle = if (isButtonGroup) {
|
||||||
AppText(
|
if (selectedKindTitles.isEmpty()) stringResource(R.string.homepage_select_multiple_kinds)
|
||||||
stringResource(R.string.homepage_source_no_discover),
|
else stringResource(
|
||||||
color = LegadoTheme.colorScheme.onSurfaceVariant
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
AppText(
|
|
||||||
stringResource(R.string.homepage_select_items),
|
|
||||||
style = LegadoTheme.typography.labelMedium,
|
|
||||||
modifier = Modifier.padding(
|
|
||||||
horizontal = 16.dp,
|
|
||||||
vertical = 4.dp
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
LazyColumn(
|
ExploreKindSelectSheet(
|
||||||
modifier = Modifier
|
show = showKindSelect,
|
||||||
.fillMaxWidth()
|
onDismissRequest = { showKindSelect = false },
|
||||||
.weight(1f),
|
sourceUrl = browseUrl,
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
multiple = isButtonGroup,
|
||||||
) {
|
initialSelectedTitles = selectedKindTitles.toList(),
|
||||||
items(
|
onSelected = { kinds ->
|
||||||
selectableKinds.distinctBy { it.first + it.second },
|
|
||||||
key = { it.first + it.second }) { (kindTitle, kindUrl) ->
|
|
||||||
if (isButtonGroup) {
|
if (isButtonGroup) {
|
||||||
val isSelected = kindTitle in selectedKindTitles
|
selectedKindTitles = kinds.map { it.title }.toSet()
|
||||||
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 {
|
} else {
|
||||||
val isJoined = joinedKeys.contains(kindTitle)
|
kinds.firstOrNull()?.let { kind ->
|
||||||
SelectionItemCard(
|
addDialogPrefill = AddDialogPrefill(
|
||||||
title = kindTitle,
|
title = kind.title,
|
||||||
subtitle = kindUrl.take(60) + if (isJoined) stringResource(
|
url = kind.url ?: "",
|
||||||
R.string.homepage_status_joined
|
type = browseModuleType
|
||||||
) else "",
|
|
||||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
|
||||||
isSelected = isJoined,
|
|
||||||
inSelectionMode = true,
|
|
||||||
onToggleSelection = {
|
|
||||||
if (!isJoined) addDialogPrefill =
|
|
||||||
AddDialogPrefill(
|
|
||||||
kindTitle,
|
|
||||||
kindUrl,
|
|
||||||
browseModuleType
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
PillDivider(
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
text = stringResource(R.string.homepage_manual_add),
|
text = stringResource(R.string.homepage_manual_add),
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -526,7 +523,7 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
items(modules, key = { it.sourceUrl + it.moduleKey }) { module ->
|
items(modules, key = { it.id }) { module ->
|
||||||
val instanceIdInCurrentSet = joinedInCurrent[module.moduleKey]
|
val instanceIdInCurrentSet = joinedInCurrent[module.moduleKey]
|
||||||
val inCurrentSet = instanceIdInCurrentSet != null
|
val inCurrentSet = instanceIdInCurrentSet != null
|
||||||
SelectionItemCard(
|
SelectionItemCard(
|
||||||
@@ -544,8 +541,7 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
joinedInCurrent =
|
joinedInCurrent =
|
||||||
joinedInCurrent + (module.moduleKey to "temp_${module.id}")
|
joinedInCurrent + (module.moduleKey to "temp_${module.id}")
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -554,11 +550,14 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
|
|
||||||
isBrowsing -> {
|
isBrowsing -> {
|
||||||
// 二级:浏览书源列表
|
// 二级:浏览书源列表
|
||||||
|
val sources = remember(filteredBrowseSources) {
|
||||||
|
filteredBrowseSources.distinctBy { it.sourceUrl }
|
||||||
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
items(filteredBrowseSources, key = { it.sourceUrl }) { source ->
|
items(sources, key = { it.sourceUrl }) { source ->
|
||||||
val moduleCount = onGetSourceModules(source.sourceUrl, null).size
|
val moduleCount = onGetSourceModules(source.sourceUrl, null).size
|
||||||
SelectionItemCard(
|
SelectionItemCard(
|
||||||
title = source.sourceName,
|
title = source.sourceName,
|
||||||
@@ -567,8 +566,7 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
onToggleSelection = {
|
onToggleSelection = {
|
||||||
browsingSourceUrl = source.sourceUrl
|
browsingSourceUrl = source.sourceUrl
|
||||||
browsingDetail = true
|
browsingDetail = true
|
||||||
},
|
}
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -577,12 +575,16 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
setUrl != null && HomepageViewModel.isCustomSetUrl(setUrl) -> {
|
setUrl != null && HomepageViewModel.isCustomSetUrl(setUrl) -> {
|
||||||
// 二级:集详情
|
// 二级:集详情
|
||||||
val setId = HomepageViewModel.customSetIdFromUrl(setUrl)
|
val setId = HomepageViewModel.customSetIdFromUrl(setUrl)
|
||||||
val modules = onGetModulesInSet(setUrl)
|
val modules = remember(setUrl, sets) {
|
||||||
|
onGetModulesInSet(setUrl).distinctBy { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
val standardModules =
|
val standardModules = remember(modules) {
|
||||||
modules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
modules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
||||||
val infiniteModules =
|
}
|
||||||
|
val infiniteModules = remember(modules) {
|
||||||
modules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
modules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
||||||
|
}
|
||||||
|
|
||||||
if (modules.isEmpty()) {
|
if (modules.isEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
@@ -605,7 +607,9 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var listData by remember(setUrl) { mutableStateOf(standardModules) }
|
var listData by remember(setUrl, standardModules) {
|
||||||
|
mutableStateOf(standardModules)
|
||||||
|
}
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
|
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
|
||||||
listData = listData.toMutableList().apply {
|
listData = listData.toMutableList().apply {
|
||||||
@@ -617,11 +621,12 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
|
|
||||||
LaunchedEffect(standardModules) {
|
LaunchedEffect(standardModules) {
|
||||||
if (!reorderableState.isAnyItemDragging) listData =
|
if (!reorderableState.isAnyItemDragging) listData =
|
||||||
standardModules.distinctBy { it.id }
|
standardModules
|
||||||
}
|
}
|
||||||
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
||||||
if (!reorderableState.isAnyItemDragging) {
|
if (!reorderableState.isAnyItemDragging) {
|
||||||
val orderedIds = listData.map { it.id } + infiniteModules.map { it.id }
|
val orderedIds =
|
||||||
|
(listData.map { it.id } + infiniteModules.map { it.id }).distinct()
|
||||||
if (orderedIds != modules.map { it.id }) onReorderModules(orderedIds)
|
if (orderedIds != modules.map { it.id }) onReorderModules(orderedIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -662,8 +667,7 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
onClick = { deleteConfirmId = module.id },
|
onClick = { deleteConfirmId = module.id },
|
||||||
imageVector = Icons.Default.Delete
|
imageVector = Icons.Default.Delete
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -694,8 +698,7 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
onClick = { deleteConfirmId = module.id },
|
onClick = { deleteConfirmId = module.id },
|
||||||
imageVector = Icons.Default.Delete
|
imageVector = Icons.Default.Delete
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -720,7 +723,9 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
// 一级:集列表
|
// 一级:集列表
|
||||||
var localSets by remember(data != null) { mutableStateOf(sets) }
|
var localSets by remember(data != null, sets) {
|
||||||
|
mutableStateOf(sets.distinctBy { it.sourceUrl })
|
||||||
|
}
|
||||||
val setsListState = rememberLazyListState()
|
val setsListState = rememberLazyListState()
|
||||||
val setsReorderableState =
|
val setsReorderableState =
|
||||||
rememberReorderableLazyListState(setsListState) { from, to ->
|
rememberReorderableLazyListState(setsListState) { from, to ->
|
||||||
@@ -770,8 +775,12 @@ fun <T> HomepageModuleManageSheet(
|
|||||||
onClick = { deleteSetConfirmId = set.sourceUrl },
|
onClick = { deleteSetConfirmId = set.sourceUrl },
|
||||||
imageVector = Icons.Default.Delete
|
imageVector = Icons.Default.Delete
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
PillDivider(
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item(key = "create_set") {
|
item(key = "create_set") {
|
||||||
@@ -1001,23 +1010,30 @@ fun <T> AddCustomModuleDialog(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(400.dp)
|
.height(400.dp)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
AppTextField(
|
AppTextField(
|
||||||
value = title,
|
value = title,
|
||||||
onValueChange = { title = it },
|
onValueChange = { title = it },
|
||||||
|
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
label = stringResource(R.string.homepage_title_label),
|
label = stringResource(R.string.homepage_title_label),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
AppTextField(
|
AppTextField(
|
||||||
value = url,
|
value = url,
|
||||||
onValueChange = { url = it },
|
onValueChange = { url = it },
|
||||||
|
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
label = "URL",
|
label = "URL",
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
val typeList = remember {
|
val typeList = remember {
|
||||||
HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown }
|
HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GlassCard(
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent
|
||||||
|
) {
|
||||||
DropdownListSettingItem(
|
DropdownListSettingItem(
|
||||||
title = stringResource(R.string.homepage_type_label),
|
title = stringResource(R.string.homepage_type_label),
|
||||||
selectedValue = type,
|
selectedValue = type,
|
||||||
@@ -1025,17 +1041,20 @@ fun <T> AddCustomModuleDialog(
|
|||||||
entryValues = typeList.map { it.key }.toTypedArray(),
|
entryValues = typeList.map { it.key }.toTypedArray(),
|
||||||
onValueChange = { type = it }
|
onValueChange = { type = it }
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AppTextField(
|
AppTextField(
|
||||||
value = args,
|
value = args,
|
||||||
onValueChange = { args = it },
|
onValueChange = { args = it },
|
||||||
|
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
label = "Args (JSON)",
|
label = "Args (JSON)",
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
AppText(
|
|
||||||
text = stringResource(R.string.homepage_layout_config_label),
|
PillHeaderDivider(
|
||||||
style = LegadoTheme.typography.labelMedium,
|
title = stringResource(R.string.homepage_layout_config_label)
|
||||||
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (hasVisualizableKeys) {
|
if (hasVisualizableKeys) {
|
||||||
JsonConfigEditor(
|
JsonConfigEditor(
|
||||||
jsonString = layoutConfig,
|
jsonString = layoutConfig,
|
||||||
|
|||||||
@@ -10,27 +10,28 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.LazyStaggeredGridState
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
|
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.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.filled.GridView
|
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.filled.Settings
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.GridRankingModule
|
||||||
import io.legado.app.ui.main.homepage.modules.RankingModule
|
import io.legado.app.ui.main.homepage.modules.RankingModule
|
||||||
import io.legado.app.ui.main.homepage.modules.WaterfallItem
|
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.AppPullToRefresh
|
||||||
import io.legado.app.ui.widget.components.AppScaffold
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
|
import io.legado.app.ui.widget.components.LoadMoreFooter
|
||||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||||
import io.legado.app.ui.widget.components.book.SearchBookGridItem
|
import io.legado.app.ui.widget.components.book.SearchBookGridItem
|
||||||
import io.legado.app.ui.widget.components.button.SecondaryButton
|
|
||||||
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
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.progressIndicator.AppCircularProgressIndicator
|
||||||
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
@@ -84,7 +87,7 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun HomepageScreen(
|
fun HomepageScreen(
|
||||||
viewModel: HomepageViewModel = koinViewModel(),
|
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,
|
onModuleHeaderClick: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
@@ -137,7 +140,14 @@ fun HomepageScreen(
|
|||||||
viewModel.effects.collect { effect ->
|
viewModel.effects.collect { effect ->
|
||||||
when (effect) {
|
when (effect) {
|
||||||
is HomepageEffect.NavigateToBookInfo ->
|
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 ->
|
is HomepageEffect.NavigateToExploreShow ->
|
||||||
onModuleHeaderClick(effect.title, effect.sourceUrl, effect.exploreUrl)
|
onModuleHeaderClick(effect.title, effect.sourceUrl, effect.exploreUrl)
|
||||||
@@ -238,6 +248,7 @@ fun HomepageScreen(
|
|||||||
data = errorMsg,
|
data = errorMsg,
|
||||||
onDismissRequest = { errorMsg = null },
|
onDismissRequest = { errorMsg = null },
|
||||||
title = stringResource(R.string.homepage_module_error),
|
title = stringResource(R.string.homepage_module_error),
|
||||||
|
text = errorMsg,
|
||||||
confirmText = stringResource(R.string.copy_text),
|
confirmText = stringResource(R.string.copy_text),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
context.sendToClip(it)
|
context.sendToClip(it)
|
||||||
@@ -255,6 +266,7 @@ fun HomepageScreen(
|
|||||||
onToggleSet = { url, isEnabled -> viewModel.toggleSourceFilter(url, isEnabled) },
|
onToggleSet = { url, isEnabled -> viewModel.toggleSourceFilter(url, isEnabled) },
|
||||||
onGetModulesInSet = { viewModel.getJoinedModules(it) },
|
onGetModulesInSet = { viewModel.getJoinedModules(it) },
|
||||||
onGetSourceModules = { url, setId -> viewModel.getSourceModules(url, setId) },
|
onGetSourceModules = { url, setId -> viewModel.getSourceModules(url, setId) },
|
||||||
|
onSyncSourceModules = { viewModel.syncSourceModules(it) },
|
||||||
onToggleModule = { id, visible -> viewModel.setModuleVisible(id, visible) },
|
onToggleModule = { id, visible -> viewModel.setModuleVisible(id, visible) },
|
||||||
onJoinModule = { sourceUrl, targetSetId, def ->
|
onJoinModule = { sourceUrl, targetSetId, def ->
|
||||||
viewModel.joinModule(
|
viewModel.joinModule(
|
||||||
@@ -355,12 +367,14 @@ private fun ModuleList(
|
|||||||
item(key = "header_${moduleUi.globalId}", span = StaggeredGridItemSpan.FullLine) {
|
item(key = "header_${moduleUi.globalId}", span = StaggeredGridItemSpan.FullLine) {
|
||||||
ModuleHeader(
|
ModuleHeader(
|
||||||
title = moduleUi.title,
|
title = moduleUi.title,
|
||||||
onNavigate = {
|
onNavigate = if (moduleUi.type == HomepageModuleType.ButtonGroup) null else {
|
||||||
|
{
|
||||||
viewModel.onModuleHeaderClick(
|
viewModel.onModuleHeaderClick(
|
||||||
moduleUi.sourceUrl,
|
moduleUi.sourceUrl,
|
||||||
moduleUi.exploreUrl,
|
moduleUi.exploreUrl,
|
||||||
moduleUi.title,
|
moduleUi.title,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -390,27 +404,78 @@ private fun ModuleList(
|
|||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
.height(80.dp)
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
.clickable { onErrorClick(state.message) },
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
) {
|
||||||
|
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(
|
AppText(
|
||||||
text = state.message,
|
text = state.message,
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = LegadoTheme.colorScheme.error,
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
}
|
||||||
SecondaryButton(
|
|
||||||
text = stringResource(R.string.retry),
|
HorizontalDivider(
|
||||||
onClick = {
|
color = LegadoTheme.colorScheme.error.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
viewModel.retryModule(moduleUi.globalId)
|
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 +500,63 @@ private fun ModuleList(
|
|||||||
val config = moduleUi.config
|
val config = moduleUi.config
|
||||||
when (moduleUi.type) {
|
when (moduleUi.type) {
|
||||||
HomepageModuleType.Waterfall -> {
|
HomepageModuleType.Waterfall -> {
|
||||||
items(
|
itemsIndexed(
|
||||||
state.books,
|
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(
|
WaterfallItem(
|
||||||
book = book,
|
book = book,
|
||||||
onClick = { viewModel.onBookClick(book) },
|
onClick = { viewModel.onBookClick(book, sharedCoverKey) },
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKey = sharedCoverKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.hasMore) {
|
|
||||||
item(
|
item(
|
||||||
key = "wf_more_${moduleUi.globalId}",
|
key = "wf_more_${moduleUi.globalId}",
|
||||||
span = StaggeredGridItemSpan.FullLine
|
span = StaggeredGridItemSpan.FullLine
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(state.books.size) {
|
LoadMoreFooter(
|
||||||
viewModel.loadMoreModule(moduleUi.globalId)
|
isLoading = state.isLoadingMore,
|
||||||
}
|
errorMsg = null,
|
||||||
Box(
|
isEnd = !state.hasMore,
|
||||||
modifier = Modifier
|
onRetry = { viewModel.loadMoreModule(moduleUi.globalId) }
|
||||||
.fillMaxWidth()
|
)
|
||||||
.padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
AppCircularProgressIndicator(modifier = Modifier.size(24.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HomepageModuleType.InfiniteGrid -> {
|
HomepageModuleType.InfiniteGrid -> {
|
||||||
items(
|
itemsIndexed(
|
||||||
state.books,
|
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(
|
SearchBookGridItem(
|
||||||
book = book,
|
book = book,
|
||||||
shelfState = io.legado.app.domain.model.BookShelfState.NOT_IN_SHELF,
|
shelfState = io.legado.app.domain.model.BookShelfState.NOT_IN_SHELF,
|
||||||
onClick = { viewModel.onBookClick(book) },
|
onClick = { viewModel.onBookClick(book, sharedCoverKey) },
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
sharedCoverKey = sharedCoverKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.hasMore) {
|
|
||||||
item(
|
item(
|
||||||
key = "inf_grid_more_${moduleUi.globalId}",
|
key = "inf_grid_more_${moduleUi.globalId}",
|
||||||
span = StaggeredGridItemSpan.FullLine
|
span = StaggeredGridItemSpan.FullLine
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(state.books.size) {
|
LoadMoreFooter(
|
||||||
viewModel.loadMoreModule(moduleUi.globalId)
|
isLoading = state.isLoadingMore,
|
||||||
}
|
errorMsg = null,
|
||||||
Box(
|
isEnd = !state.hasMore,
|
||||||
modifier = Modifier
|
onRetry = { viewModel.loadMoreModule(moduleUi.globalId) }
|
||||||
.fillMaxWidth()
|
)
|
||||||
.padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
AppCircularProgressIndicator(modifier = Modifier.size(24.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,12 +569,15 @@ private fun ModuleList(
|
|||||||
) {
|
) {
|
||||||
GridModule(
|
GridModule(
|
||||||
books = state.books,
|
books = state.books,
|
||||||
onClick = { viewModel.onBookClick(it) },
|
onClick = { book, sharedCoverKey ->
|
||||||
|
viewModel.onBookClick(book, sharedCoverKey)
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
columns = columns,
|
columns = columns,
|
||||||
maxRows = rows,
|
maxRows = rows,
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,10 +591,13 @@ private fun ModuleList(
|
|||||||
) {
|
) {
|
||||||
BannerModule(
|
BannerModule(
|
||||||
books = state.books,
|
books = state.books,
|
||||||
onClick = { viewModel.onBookClick(it) },
|
onClick = { book, sharedCoverKey ->
|
||||||
|
viewModel.onBookClick(book, sharedCoverKey)
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKeySourceId = "home:${moduleUi.globalId}:banner",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -543,10 +609,13 @@ private fun ModuleList(
|
|||||||
) {
|
) {
|
||||||
RankingModule(
|
RankingModule(
|
||||||
books = state.books,
|
books = state.books,
|
||||||
onClick = { viewModel.onBookClick(it) },
|
onClick = { book, sharedCoverKey ->
|
||||||
|
viewModel.onBookClick(book, sharedCoverKey)
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKeySourceId = "home:${moduleUi.globalId}:ranking",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,11 +627,14 @@ private fun ModuleList(
|
|||||||
) {
|
) {
|
||||||
GridRankingModule(
|
GridRankingModule(
|
||||||
books = state.books,
|
books = state.books,
|
||||||
onClick = { viewModel.onBookClick(it) },
|
onClick = { book, sharedCoverKey ->
|
||||||
|
viewModel.onBookClick(book, sharedCoverKey)
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
rows = config["layout_rows"]?.toIntOrNull() ?: 4,
|
rows = config["layout_rows"]?.toIntOrNull() ?: 4,
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid-ranking",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -574,10 +646,13 @@ private fun ModuleList(
|
|||||||
) {
|
) {
|
||||||
CardModule(
|
CardModule(
|
||||||
books = state.books,
|
books = state.books,
|
||||||
onClick = { viewModel.onBookClick(it) },
|
onClick = { book, sharedCoverKey ->
|
||||||
|
viewModel.onBookClick(book, sharedCoverKey)
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKeySourceId = "home:${moduleUi.globalId}:card",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -596,7 +671,7 @@ private fun ModuleList(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ModuleHeader(
|
private fun ModuleHeader(
|
||||||
title: String,
|
title: String,
|
||||||
onNavigate: () -> Unit,
|
onNavigate: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -612,9 +687,11 @@ private fun ModuleHeader(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
|
if (onNavigate != null) {
|
||||||
SmallTonalIconButton(
|
SmallTonalIconButton(
|
||||||
onClick = onNavigate,
|
onClick = onNavigate,
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowForward
|
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.help.source.exploreKinds
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.fromJsonArray
|
import io.legado.app.utils.fromJsonArray
|
||||||
|
import io.legado.app.utils.stackTraceStr
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -34,6 +35,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class HomepageViewModel(
|
class HomepageViewModel(
|
||||||
application: Application,
|
application: Application,
|
||||||
@@ -79,7 +81,7 @@ class HomepageViewModel(
|
|||||||
private val _effects = MutableSharedFlow<HomepageEffect>(extraBufferCapacity = 8)
|
private val _effects = MutableSharedFlow<HomepageEffect>(extraBufferCapacity = 8)
|
||||||
val effects = _effects.asSharedFlow()
|
val effects = _effects.asSharedFlow()
|
||||||
|
|
||||||
private val loadJobs = mutableMapOf<String, Job>()
|
private val loadJobs = ConcurrentHashMap<String, Job>()
|
||||||
private val initModulesSyncFlow = bookSourceRepository.flowHomepageModules()
|
private val initModulesSyncFlow = bookSourceRepository.flowHomepageModules()
|
||||||
private val exploreSourcesFlow = bookSourceRepository.flowExploreSources()
|
private val exploreSourcesFlow = bookSourceRepository.flowExploreSources()
|
||||||
|
|
||||||
@@ -215,28 +217,10 @@ class HomepageViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sync: 只处理有 homepageModules 的书源
|
// 清理 _pendingUserModules 中已入库的条目
|
||||||
viewModelScope.launch {
|
|
||||||
initModulesSyncFlow.collect { sources ->
|
|
||||||
sources.forEach { source -> syncModulesFromSource(source) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cache: 所有启用发现的书源(包括无 homepageModules 的)
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
exploreSourcesFlow.collect { sources ->
|
exploreSourcesFlow.collect { sources ->
|
||||||
_bookSourcesCache.value = sources.associateBy { it.bookSourceUrl }
|
_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 +351,7 @@ class HomepageViewModel(
|
|||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
_moduleContentStates.update {
|
_moduleContentStates.update {
|
||||||
it + (module.id to ModuleLoadState.Error(
|
it + (module.id to ModuleLoadState.Error(
|
||||||
e.message ?: "Unknown error"
|
e.stackTraceStr
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,7 +388,7 @@ class HomepageViewModel(
|
|||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
_moduleContentStates.update {
|
_moduleContentStates.update {
|
||||||
it + (module.id to ModuleLoadState.Error(
|
it + (module.id to ModuleLoadState.Error(
|
||||||
e.message ?: "Unknown error"
|
e.stackTraceStr
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,7 +438,7 @@ class HomepageViewModel(
|
|||||||
HomepageEffect.ShowSnackbar(
|
HomepageEffect.ShowSnackbar(
|
||||||
getApplication<Application>().getString(
|
getApplication<Application>().getString(
|
||||||
R.string.homepage_load_more_failed,
|
R.string.homepage_load_more_failed,
|
||||||
e.message ?: ""
|
e.stackTraceStr
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -477,6 +461,13 @@ class HomepageViewModel(
|
|||||||
_isRefreshing.value = true
|
_isRefreshing.value = true
|
||||||
loadJobs.values.forEach { it.cancel() }
|
loadJobs.values.forEach { it.cancel() }
|
||||||
loadJobs.clear()
|
loadJobs.clear()
|
||||||
|
|
||||||
|
// 刷新时同步当前已启用模块所属书源的定义
|
||||||
|
val activeSourceUrls = uiState.value.modules.map { it.sourceUrl }.distinct()
|
||||||
|
activeSourceUrls.forEach { url ->
|
||||||
|
resolveBookSource(url)?.let { syncModulesFromSource(it) }
|
||||||
|
}
|
||||||
|
|
||||||
_moduleContentStates.value = emptyMap()
|
_moduleContentStates.value = emptyMap()
|
||||||
uiState.map { it.modules }.first { modules ->
|
uiState.map { it.modules }.first { modules ->
|
||||||
modules.all { it.state !is ModuleLoadState.Loading }
|
modules.all { it.state !is ModuleLoadState.Loading }
|
||||||
@@ -698,12 +689,19 @@ class HomepageViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun syncSourceModules(sourceUrl: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
resolveBookSource(sourceUrl)?.let { syncModulesFromSource(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 「书源模块」tab:仅 JSON,纯参考 */
|
/** 「书源模块」tab:仅 JSON,纯参考 */
|
||||||
fun getSourceModules(
|
fun getSourceModules(
|
||||||
sourceUrl: String,
|
sourceUrl: String,
|
||||||
targetSetId: String? = null
|
targetSetId: String? = null
|
||||||
): List<HomepageModuleManageUi> {
|
): List<HomepageModuleManageUi> {
|
||||||
val source = resolveBookSource(sourceUrl) ?: return emptyList()
|
val source = resolveBookSource(sourceUrl) ?: return emptyList()
|
||||||
|
|
||||||
val json = source.homepageModules ?: return emptyList()
|
val json = source.homepageModules ?: return emptyList()
|
||||||
val jsonDefs = parseBookSourceModules(source, json)
|
val jsonDefs = parseBookSourceModules(source, json)
|
||||||
|
|
||||||
@@ -850,10 +848,19 @@ class HomepageViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBookClick(book: SearchBook) {
|
fun onBookClick(book: SearchBook, sharedCoverKey: String?) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
saveSearchBooksUseCase.save(book)
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
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.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -23,10 +23,11 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
@Composable
|
@Composable
|
||||||
fun BannerModule(
|
fun BannerModule(
|
||||||
books: ImmutableList<SearchBook>,
|
books: ImmutableList<SearchBook>,
|
||||||
onClick: (SearchBook) -> Unit,
|
onClick: (SearchBook, String?) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
sharedCoverKeySourceId: String? = null,
|
||||||
) {
|
) {
|
||||||
if (books.isEmpty()) return
|
if (books.isEmpty()) return
|
||||||
|
|
||||||
@@ -38,7 +39,11 @@ fun BannerModule(
|
|||||||
.fadingEdge(lazyListState, gradientWidth = 16.dp),
|
.fadingEdge(lazyListState, gradientWidth = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.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(
|
CoilBookCover(
|
||||||
name = book.name,
|
name = book.name,
|
||||||
author = book.author,
|
author = book.author,
|
||||||
@@ -47,10 +52,10 @@ fun BannerModule(
|
|||||||
sourceOrigin = book.origin,
|
sourceOrigin = book.origin,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(96.dp)
|
.width(96.dp)
|
||||||
.clickable { onClick(book) },
|
.clickable { onClick(book, sharedCoverKey) },
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
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.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.foundation.basicMarquee
|
import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.runtime.Composable
|
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.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -26,14 +22,14 @@ 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.data.entities.rule.ExploreKind
|
||||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
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.main.homepage.HomepageViewModel
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
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.image.sourceIcon.SourceIcon
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -51,10 +47,7 @@ fun ButtonGroupModule(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val activity = context as? AppCompatActivity
|
val activity = context as? AppCompatActivity
|
||||||
val useCase: ExploreKindUiUseCase = koinInject()
|
val useCase: ExploreKindUiUseCase = koinInject()
|
||||||
val scope = rememberCoroutineScope()
|
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||||
val infoMap = remember(sourceUrl) {
|
|
||||||
sourceUrl.takeIf { it.isNotBlank() }?.let { getExploreInfoMap(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析图标映射表和默认图标
|
// 解析图标映射表和默认图标
|
||||||
val (iconMap, defaultIcon) = remember(layoutConfig) {
|
val (iconMap, defaultIcon) = remember(layoutConfig) {
|
||||||
@@ -90,43 +83,28 @@ fun ButtonGroupModule(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
rowKinds.forEach { kind ->
|
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 buttonIcon = iconMap[kind.title] ?: defaultIcon
|
||||||
val hasIcon = !buttonIcon.isNullOrBlank()
|
val hasIcon = !buttonIcon.isNullOrBlank()
|
||||||
|
|
||||||
GlassCard(
|
ExploreKindMultiTypeItem(
|
||||||
onClick = {
|
kind = kind,
|
||||||
when (kind.type) {
|
|
||||||
ExploreKind.Type.url -> {
|
|
||||||
kind.url?.takeIf { it.isNotBlank() }?.let {
|
|
||||||
viewModel.onKindUrlClick(sourceUrl, it, kind.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExploreKind.Type.button -> {
|
|
||||||
scope.launch {
|
|
||||||
useCase.executeAction(
|
|
||||||
action = kind.action,
|
|
||||||
title = kind.title,
|
|
||||||
sourceUrl = sourceUrl,
|
sourceUrl = sourceUrl,
|
||||||
infoMap = infoMap,
|
|
||||||
activity = activity,
|
activity = activity,
|
||||||
|
onOpenUrl = { url ->
|
||||||
|
viewModel.onKindUrlClick(sourceUrl, url, kind.title)
|
||||||
|
},
|
||||||
onRefreshKinds = {
|
onRefreshKinds = {
|
||||||
viewModel.refreshButtonGroup(globalId)
|
viewModel.refreshButtonGroup(globalId)
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
useCase = useCase,
|
||||||
|
isMiuix = isMiuix,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
content = { displayName, isSelected, onClick, trailingIcon ->
|
||||||
|
GlassCard(
|
||||||
|
onClick = onClick,
|
||||||
cornerRadius = 8.dp,
|
cornerRadius = 8.dp,
|
||||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
|
containerColor = if (isSelected) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainerLow,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -137,7 +115,7 @@ fun ButtonGroupModule(
|
|||||||
) {
|
) {
|
||||||
if (hasIcon) {
|
if (hasIcon) {
|
||||||
SourceIcon(
|
SourceIcon(
|
||||||
path = buttonIcon,
|
path = buttonIcon!!,
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
placeholderIcon = {
|
placeholderIcon = {
|
||||||
|
|
||||||
@@ -146,6 +124,10 @@ fun ButtonGroupModule(
|
|||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
AppText(
|
AppText(
|
||||||
text = displayName,
|
text = displayName,
|
||||||
style = LegadoTheme.typography.labelSmallEmphasized,
|
style = LegadoTheme.typography.labelSmallEmphasized,
|
||||||
@@ -156,9 +138,22 @@ fun ButtonGroupModule(
|
|||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
.basicMarquee()
|
.basicMarquee()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (trailingIcon != null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(end = 2.dp)
|
||||||
|
) {
|
||||||
|
trailingIcon()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (rowKinds.size < actualColumns) {
|
if (rowKinds.size < actualColumns) {
|
||||||
repeat(actualColumns - rowKinds.size) {
|
repeat(actualColumns - rowKinds.size) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
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.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -35,10 +35,11 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
@Composable
|
@Composable
|
||||||
fun CardModule(
|
fun CardModule(
|
||||||
books: ImmutableList<SearchBook>,
|
books: ImmutableList<SearchBook>,
|
||||||
onClick: (SearchBook) -> Unit,
|
onClick: (SearchBook, String?) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
sharedCoverKeySourceId: String? = null,
|
||||||
) {
|
) {
|
||||||
if (books.isEmpty()) return
|
if (books.isEmpty()) return
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
@@ -49,13 +50,17 @@ fun CardModule(
|
|||||||
.fadingEdge(lazyListState, gradientWidth = 8.dp),
|
.fadingEdge(lazyListState, gradientWidth = 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(120.dp)
|
.width(120.dp)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.background(LegadoTheme.colorScheme.surfaceContainerLow)
|
.background(LegadoTheme.colorScheme.surfaceContainerLow)
|
||||||
.clickable { onClick(book) }
|
.clickable { onClick(book, sharedCoverKey) }
|
||||||
) {
|
) {
|
||||||
CoilBookCover(
|
CoilBookCover(
|
||||||
name = book.name,
|
name = book.name,
|
||||||
@@ -67,19 +72,20 @@ fun CardModule(
|
|||||||
.wrapContentWidth(),
|
.wrapContentWidth(),
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
sharedCoverKey = sharedCoverKey
|
||||||
)
|
)
|
||||||
|
|
||||||
AppText(
|
AppText(
|
||||||
text = book.name,
|
text = book.name,
|
||||||
style = LegadoTheme.typography.labelLargeEmphasized,
|
style = LegadoTheme.typography.labelLargeEmphasized,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
|
minLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
start = 8.dp,
|
start = 8.dp,
|
||||||
end = 8.dp,
|
end = 8.dp,
|
||||||
top = 8.dp,
|
top = 8.dp,
|
||||||
bottom = 2.dp
|
bottom = 8.dp
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,7 +94,7 @@ fun CardModule(
|
|||||||
if (intro != null) {
|
if (intro != null) {
|
||||||
AppText(
|
AppText(
|
||||||
text = intro,
|
text = intro,
|
||||||
style = LegadoTheme.typography.bodySmall,
|
style = LegadoTheme.typography.labelSmallEmphasized,
|
||||||
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
@Composable
|
@Composable
|
||||||
fun GridModule(
|
fun GridModule(
|
||||||
books: ImmutableList<SearchBook>,
|
books: ImmutableList<SearchBook>,
|
||||||
onClick: (SearchBook) -> Unit,
|
onClick: (SearchBook, String?) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
columns: Int = 3,
|
columns: Int = 3,
|
||||||
maxRows: Int? = null,
|
maxRows: Int? = null,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
sharedCoverKeySourceId: String? = null,
|
||||||
) {
|
) {
|
||||||
if (books.isEmpty()) return
|
if (books.isEmpty()) return
|
||||||
var rows = books.toList().chunked(columns)
|
var rows = books.toList().chunked(columns)
|
||||||
@@ -37,20 +38,25 @@ fun GridModule(
|
|||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
for (row in rows) {
|
for ((rowIndex, row) in rows.withIndex()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
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(
|
SearchBookGridItem(
|
||||||
book = book,
|
book = book,
|
||||||
shelfState = BookShelfState.NOT_IN_SHELF,
|
shelfState = BookShelfState.NOT_IN_SHELF,
|
||||||
onClick = { onClick(book) },
|
onClick = { onClick(book, sharedCoverKey) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
sharedCoverKey = sharedCoverKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
repeat(columns - row.size) { Spacer(Modifier.weight(1f)) }
|
repeat(columns - row.size) { Spacer(Modifier.weight(1f)) }
|
||||||
|
|||||||
@@ -37,11 +37,12 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
@Composable
|
@Composable
|
||||||
fun GridRankingModule(
|
fun GridRankingModule(
|
||||||
books: ImmutableList<SearchBook>,
|
books: ImmutableList<SearchBook>,
|
||||||
onClick: (SearchBook) -> Unit,
|
onClick: (SearchBook, String?) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
rows: Int = 4,
|
rows: Int = 4,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
sharedCoverKeySourceId: String? = null,
|
||||||
) {
|
) {
|
||||||
if (books.isEmpty()) return
|
if (books.isEmpty()) return
|
||||||
// 限制最多显示 20 项
|
// 限制最多显示 20 项
|
||||||
@@ -70,13 +71,19 @@ fun GridRankingModule(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 12.dp, horizontal = 12.dp)
|
.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(
|
GridRankingItem(
|
||||||
rank = pages.flatten().indexOf(book) + 1,
|
rank = pages.flatten().indexOf(book) + 1,
|
||||||
book = book,
|
book = book,
|
||||||
onClick = { onClick(book) },
|
onClick = { onClick(book, sharedCoverKey) },
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKey = sharedCoverKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// 占位逻辑
|
// 占位逻辑
|
||||||
@@ -96,6 +103,7 @@ private fun GridRankingItem(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
sharedCoverKey: String? = null,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -114,7 +122,7 @@ private fun GridRankingItem(
|
|||||||
modifier = Modifier.width(48.dp),
|
modifier = Modifier.width(48.dp),
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
sharedCoverKey = sharedCoverKey
|
||||||
)
|
)
|
||||||
|
|
||||||
// 2. 排名
|
// 2. 排名
|
||||||
|
|||||||
@@ -45,10 +45,11 @@ private const val MAX_COUNT = 20
|
|||||||
@Composable
|
@Composable
|
||||||
fun RankingModule(
|
fun RankingModule(
|
||||||
books: ImmutableList<SearchBook>,
|
books: ImmutableList<SearchBook>,
|
||||||
onClick: (SearchBook) -> Unit,
|
onClick: (SearchBook, String?) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
sharedCoverKeySourceId: String? = null,
|
||||||
) {
|
) {
|
||||||
var visibleCount by rememberSaveable { mutableIntStateOf(INITIAL_COUNT) }
|
var visibleCount by rememberSaveable { mutableIntStateOf(INITIAL_COUNT) }
|
||||||
val displayBooks = books.take(visibleCount)
|
val displayBooks = books.take(visibleCount)
|
||||||
@@ -72,6 +73,10 @@ fun RankingModule(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKey = bookCoverSharedElementKey(
|
||||||
|
book.bookUrl,
|
||||||
|
sharedCoverKeySourceId?.let { "$it:$index" }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,14 +119,15 @@ fun RankingModule(
|
|||||||
private fun RankingItem(
|
private fun RankingItem(
|
||||||
rank: Int,
|
rank: Int,
|
||||||
book: SearchBook,
|
book: SearchBook,
|
||||||
onClick: (SearchBook) -> Unit,
|
onClick: (SearchBook, String?) -> Unit,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
sharedCoverKey: String? = null,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onClick(book) }
|
.clickable { onClick(book, sharedCoverKey) }
|
||||||
.padding(vertical = 4.dp, horizontal = 4.dp),
|
.padding(vertical = 4.dp, horizontal = 4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
@@ -144,7 +150,7 @@ private fun RankingItem(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.ui.main.bookCoverSharedElementKey
|
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.widget.components.book.SearchBookTagChip
|
import io.legado.app.ui.widget.components.book.SearchBookTagChip
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
@@ -36,6 +35,7 @@ fun WaterfallItem(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
sharedCoverKey: String? = null,
|
||||||
) {
|
) {
|
||||||
GlassCard(
|
GlassCard(
|
||||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow
|
containerColor = LegadoTheme.colorScheme.surfaceContainerLow
|
||||||
@@ -55,7 +55,7 @@ fun WaterfallItem(
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
sharedCoverKey = sharedCoverKey
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -165,7 +166,8 @@ fun RssScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
contentWindowInsets = WindowInsets(0)
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(minSize = 72.dp),
|
columns = GridCells.Adaptive(minSize = 72.dp),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package io.legado.app.ui.rss.article
|
package io.legado.app.ui.rss.article
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.LegadoTheme
|
||||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||||
import io.legado.app.ui.widget.components.AppPullToRefresh
|
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.card.GlassCard
|
||||||
import io.legado.app.ui.widget.components.image.cover.buildCoverImageRequest
|
import io.legado.app.ui.widget.components.image.cover.buildCoverImageRequest
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
@@ -163,7 +162,9 @@ fun RssArticlesPage(
|
|||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
LoadMoreFooter(
|
LoadMoreFooter(
|
||||||
state = loadState,
|
isLoading = loadState.isRefreshing || loadState.isLoadingMore,
|
||||||
|
errorMsg = loadState.errorMessage,
|
||||||
|
isEnd = !loadState.hasMore,
|
||||||
onRetry = { rssSource?.let(viewModel::loadMore) }
|
onRetry = { rssSource?.let(viewModel::loadMore) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -198,7 +199,9 @@ fun RssArticlesPage(
|
|||||||
}
|
}
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
LoadMoreFooter(
|
LoadMoreFooter(
|
||||||
state = loadState,
|
isLoading = loadState.isRefreshing || loadState.isLoadingMore,
|
||||||
|
errorMsg = loadState.errorMessage,
|
||||||
|
isEnd = !loadState.hasMore,
|
||||||
onRetry = { rssSource?.let(viewModel::loadMore) }
|
onRetry = { rssSource?.let(viewModel::loadMore) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -233,7 +236,9 @@ fun RssArticlesPage(
|
|||||||
}
|
}
|
||||||
item(span = StaggeredGridItemSpan.FullLine) {
|
item(span = StaggeredGridItemSpan.FullLine) {
|
||||||
LoadMoreFooter(
|
LoadMoreFooter(
|
||||||
state = loadState,
|
isLoading = loadState.isRefreshing || loadState.isLoadingMore,
|
||||||
|
errorMsg = loadState.errorMessage,
|
||||||
|
isEnd = !loadState.hasMore,
|
||||||
onRetry = { rssSource?.let(viewModel::loadMore) }
|
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
|
@Composable
|
||||||
private fun RssArticleItem(
|
private fun RssArticleItem(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.legado.app.ui.theme
|
|||||||
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import top.yukonga.miuix.kmp.theme.TextStyles
|
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
|
if (fontFamily == null) return this
|
||||||
return copy(
|
return copy(
|
||||||
headlineLarge = headlineLarge.copy(fontFamily = fontFamily),
|
headlineLarge = headlineLarge.copy(fontFamily = fontFamily),
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fun AppFloatingActionButton(
|
|||||||
MiuixIcon(
|
MiuixIcon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = tooltipText,
|
contentDescription = tooltipText,
|
||||||
tint = Color.White
|
tint = containerColor
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -89,7 +89,8 @@ fun AppFloatingActionButton(
|
|||||||
MiuixFloatingActionButton(
|
MiuixFloatingActionButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
content = fabContent
|
content = fabContent,
|
||||||
|
containerColor = LegadoTheme.colorScheme.surfaceContainer
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if (tooltipText != null) {
|
if (tooltipText != null) {
|
||||||
|
|||||||
@@ -8,18 +8,13 @@ import androidx.compose.foundation.layout.heightIn
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||||
import androidx.compose.material.icons.filled.Compress
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
@@ -39,8 +34,7 @@ fun JsonRawEditor(
|
|||||||
) {
|
) {
|
||||||
AppText(
|
AppText(
|
||||||
text = label,
|
text = label,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = LegadoTheme.typography.labelMediumEmphasized
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
)
|
||||||
Row {
|
Row {
|
||||||
SmallIconButton(
|
SmallIconButton(
|
||||||
@@ -67,21 +61,13 @@ fun JsonRawEditor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField(
|
AppTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 150.dp, max = 400.dp),
|
.heightIn(min = 150.dp, max = 400.dp),
|
||||||
textStyle = TextStyle(
|
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize
|
|
||||||
),
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = Color.Transparent,
|
|
||||||
unfocusedContainerColor = Color.Transparent,
|
|
||||||
disabledContainerColor = Color.Transparent,
|
|
||||||
),
|
|
||||||
maxLines = 1000
|
maxLines = 1000
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
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.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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.AlertDialogDefaults
|
import androidx.compose.material3.AlertDialogDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
@@ -98,13 +101,17 @@ fun AppAlertDialog(
|
|||||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||||
title = title?.let { { Text(text = it) } },
|
title = title?.let { { Text(text = it) } },
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
|
SelectionContainer {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
modifier = Modifier.padding(bottom = if (content != null) 16.dp else 0.dp)
|
modifier = Modifier.padding(bottom = if (content != null) 16.dp else 0.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@@ -161,16 +168,17 @@ fun <T> AppAlertDialog(
|
|||||||
val currentData = cachedData
|
val currentData = cachedData
|
||||||
if (currentData != null) {
|
if (currentData != null) {
|
||||||
val currentText = text ?: textProvider?.invoke(currentData)
|
val currentText = text ?: textProvider?.invoke(currentData)
|
||||||
var cachedText by remember { mutableStateOf(currentText) }
|
var lastValidText by remember { mutableStateOf(currentText) }
|
||||||
|
|
||||||
if (currentText != null) {
|
if (currentText != null) {
|
||||||
cachedText = currentText
|
lastValidText = currentText
|
||||||
}
|
}
|
||||||
|
|
||||||
AppAlertDialog(
|
AppAlertDialog(
|
||||||
show = data != null,
|
show = data != null,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = title,
|
title = title,
|
||||||
text = currentText ?: cachedText,
|
text = currentText ?: lastValidText,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
confirmText = confirmText,
|
confirmText = confirmText,
|
||||||
onConfirm = onConfirm?.let { { it(currentData) } },
|
onConfirm = onConfirm?.let { { it(currentData) } },
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ fun SmallTextButton(
|
|||||||
@Composable
|
@Composable
|
||||||
fun SmallTonalTextButton(
|
fun SmallTonalTextButton(
|
||||||
text: String? = null,
|
text: String? = null,
|
||||||
imageVector: ImageVector,
|
imageVector: ImageVector? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -102,11 +102,13 @@ fun SmallTonalTextButton(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
|
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
if (imageVector != null) {
|
||||||
MiuixIcon(
|
MiuixIcon(
|
||||||
imageVector = imageVector,
|
imageVector = imageVector,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
AppText(
|
AppText(
|
||||||
text = text,
|
text = text,
|
||||||
@@ -121,11 +123,13 @@ fun SmallTonalTextButton(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)
|
||||||
) {
|
) {
|
||||||
|
if (imageVector != null) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = imageVector,
|
imageVector = imageVector,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
AppText(
|
AppText(
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ package io.legado.app.ui.widget.components.explore
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Alignment
|
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 androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.data.entities.rule.ExploreKind
|
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.theme.LegadoTheme
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExploreKindItem(
|
fun ExploreKindItem(
|
||||||
@@ -31,6 +27,7 @@ fun ExploreKindItem(
|
|||||||
isMiuix: Boolean,
|
isMiuix: Boolean,
|
||||||
backgroundColor: androidx.compose.ui.graphics.Color = LegadoTheme.colorScheme.surfaceContainer,
|
backgroundColor: androidx.compose.ui.graphics.Color = LegadoTheme.colorScheme.surfaceContainer,
|
||||||
displayText: String = kind.title,
|
displayText: String = kind.title,
|
||||||
|
isSelected: Boolean = false,
|
||||||
trailingIcon: (@Composable () -> Unit)? = null
|
trailingIcon: (@Composable () -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
@@ -38,31 +35,45 @@ fun ExploreKindItem(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
val cornerRadius = 12.dp
|
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) {
|
if (isClickable) {
|
||||||
GlassCard(
|
GlassCard(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
cornerRadius = cornerRadius,
|
cornerRadius = cornerRadius,
|
||||||
containerColor = backgroundColor,
|
containerColor = containerColor,
|
||||||
contentColor = LegadoTheme.colorScheme.onSurface,
|
contentColor = contentColor,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
KindText(
|
KindText(
|
||||||
text = displayText,
|
text = displayText,
|
||||||
isClickable = true,
|
isClickable = true,
|
||||||
|
contentColor = contentColor,
|
||||||
trailingIcon = trailingIcon
|
trailingIcon = trailingIcon
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlassCard(
|
GlassCard(
|
||||||
cornerRadius = cornerRadius,
|
cornerRadius = cornerRadius,
|
||||||
containerColor = backgroundColor,
|
containerColor = containerColor,
|
||||||
contentColor = LegadoTheme.colorScheme.primary,
|
contentColor = contentColor,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
KindText(
|
KindText(
|
||||||
text = displayText,
|
text = displayText,
|
||||||
isClickable = false,
|
isClickable = false,
|
||||||
|
contentColor = contentColor,
|
||||||
trailingIcon = trailingIcon
|
trailingIcon = trailingIcon
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -75,6 +86,7 @@ fun ExploreKindItem(
|
|||||||
private fun KindText(
|
private fun KindText(
|
||||||
text: String,
|
text: String,
|
||||||
isClickable: Boolean,
|
isClickable: Boolean,
|
||||||
|
contentColor: androidx.compose.ui.graphics.Color = LegadoTheme.colorScheme.onSurface,
|
||||||
trailingIcon: (@Composable () -> Unit)? = null
|
trailingIcon: (@Composable () -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
@@ -84,7 +96,7 @@ private fun KindText(
|
|||||||
) {
|
) {
|
||||||
AppText(
|
AppText(
|
||||||
text = text,
|
text = text,
|
||||||
color = if (isClickable) LegadoTheme.colorScheme.onSurface else LegadoTheme.colorScheme.primary,
|
color = contentColor,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(end = if (trailingIcon == null) 0.dp else 18.dp),
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+231
-175
@@ -1,14 +1,9 @@
|
|||||||
package io.legado.app.ui.widget.components.explore
|
package io.legado.app.ui.widget.components.explore
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
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.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.data.entities.rule.ExploreKind
|
import io.legado.app.data.entities.rule.ExploreKind
|
||||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
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.theme.LegadoTheme
|
||||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
|
||||||
import io.legado.app.ui.widget.dialog.TextDialog
|
import io.legado.app.ui.widget.dialog.TextDialog
|
||||||
import io.legado.app.utils.showDialogFragment
|
import io.legado.app.utils.showDialogFragment
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -56,126 +45,204 @@ fun ExploreKindMultiTypeItem(
|
|||||||
isMiuix: Boolean,
|
isMiuix: Boolean,
|
||||||
displayNameOverride: String? = null,
|
displayNameOverride: String? = null,
|
||||||
valueOverride: String? = null,
|
valueOverride: String? = null,
|
||||||
|
isSelected: Boolean = false,
|
||||||
onValueChange: ((String) -> Unit)? = null,
|
onValueChange: ((String) -> Unit)? = null,
|
||||||
onRunAction: (() -> 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 state = rememberExploreKindItemState(kind, sourceUrl, useCase, activity, onRefreshKinds)
|
||||||
val infoMap = remember(sourceUrl, useCase) {
|
state.ResolveDisplayName(displayNameOverride)
|
||||||
if (useCase == null) null else sourceUrl?.takeIf { it.isNotBlank() }?.let(::getExploreInfoMap)
|
|
||||||
}
|
|
||||||
var displayName by remember(sourceUrl, kind.title, kind.viewName) { mutableStateOf(kind.title) }
|
|
||||||
|
|
||||||
LaunchedEffect(displayNameOverride, sourceUrl, kind.title, kind.viewName, useCase) {
|
val trailingIcon = rememberTrailingIcon(kind.type, isSelected)
|
||||||
displayName = displayNameOverride
|
|
||||||
?: useCase?.resolveDisplayName(kind, sourceUrl, infoMap)
|
|
||||||
?: kind.title
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runAction(action: String?) {
|
if (onClick != null) {
|
||||||
if (action.isNullOrBlank()) return
|
if (content != null) {
|
||||||
if (onRunAction != null) {
|
content(state.displayName, isSelected, onClick, trailingIcon)
|
||||||
onRunAction()
|
|
||||||
} else {
|
} else {
|
||||||
val useCase = useCase ?: return
|
ExploreKindItem(
|
||||||
scope.launch(IO) {
|
kind = kind,
|
||||||
useCase.executeAction(
|
isClickable = true,
|
||||||
action = action,
|
onClick = onClick,
|
||||||
title = kind.title,
|
modifier = modifier,
|
||||||
sourceUrl = sourceUrl,
|
backgroundColor = backgroundColor,
|
||||||
infoMap = infoMap,
|
isMiuix = isMiuix,
|
||||||
activity = activity,
|
displayText = state.displayName,
|
||||||
onRefreshKinds = onRefreshKinds
|
isSelected = isSelected,
|
||||||
|
trailingIcon = trailingIcon
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
return
|
||||||
}
|
|
||||||
|
|
||||||
fun updateValue(value: String) {
|
|
||||||
if (onValueChange != null) {
|
|
||||||
onValueChange(value)
|
|
||||||
} else {
|
|
||||||
infoMap?.let {
|
|
||||||
it[kind.title] = value
|
|
||||||
it.saveNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
when (kind.type) {
|
when (kind.type) {
|
||||||
ExploreKind.Type.url -> {
|
ExploreKind.Type.url -> {
|
||||||
val url = kind.url?.takeIf { it.isNotBlank() }
|
val url = kind.url?.takeIf { it.isNotBlank() }
|
||||||
ExploreKindItem(
|
val internalOnClick = {
|
||||||
kind = kind,
|
if (!url.isNullOrBlank()) {
|
||||||
isClickable = !url.isNullOrBlank(),
|
|
||||||
onClick = {
|
|
||||||
if (url.isNullOrBlank()) return@ExploreKindItem
|
|
||||||
if (kind.title.startsWith("ERROR:")) {
|
if (kind.title.startsWith("ERROR:")) {
|
||||||
activity?.showDialogFragment(TextDialog("ERROR", url))
|
activity?.showDialogFragment(TextDialog("ERROR", url))
|
||||||
} else {
|
} else {
|
||||||
onOpenUrl(url)
|
onOpenUrl(url)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
if (content != null) {
|
||||||
|
content(state.displayName, isSelected, internalOnClick, trailingIcon)
|
||||||
|
} else {
|
||||||
|
ExploreKindItem(
|
||||||
|
kind = kind,
|
||||||
|
isClickable = !url.isNullOrBlank(),
|
||||||
|
onClick = internalOnClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
isMiuix = isMiuix,
|
isMiuix = isMiuix,
|
||||||
displayText = displayName
|
displayText = state.displayName,
|
||||||
|
isSelected = isSelected
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ExploreKind.Type.button -> {
|
ExploreKind.Type.button -> {
|
||||||
|
val internalOnClick = {
|
||||||
|
if (onRunAction != null) onRunAction()
|
||||||
|
else state.executeAction(kind.action)
|
||||||
|
}
|
||||||
|
if (content != null) {
|
||||||
|
content(state.displayName, isSelected, internalOnClick, trailingIcon)
|
||||||
|
} else {
|
||||||
ExploreKindItem(
|
ExploreKindItem(
|
||||||
kind = kind,
|
kind = kind,
|
||||||
isClickable = !kind.action.isNullOrBlank(),
|
isClickable = !kind.action.isNullOrBlank(),
|
||||||
onClick = { runAction(kind.action) },
|
onClick = internalOnClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
isMiuix = isMiuix,
|
isMiuix = isMiuix,
|
||||||
displayText = displayName,
|
displayText = state.displayName,
|
||||||
trailingIcon = {
|
isSelected = isSelected,
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
trailingIcon = trailingIcon
|
||||||
AppIcon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.height(14.dp),
|
|
||||||
tint = LegadoTheme.colorScheme.outlineVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ExploreKind.Type.text -> {
|
ExploreKind.Type.text -> {
|
||||||
|
if (content != null) {
|
||||||
|
content(state.displayName, isSelected, {}, null)
|
||||||
|
} else {
|
||||||
|
TextTypeItem(
|
||||||
|
kind,
|
||||||
|
sourceUrl,
|
||||||
|
state,
|
||||||
|
valueOverride,
|
||||||
|
onValueChange,
|
||||||
|
modifier,
|
||||||
|
backgroundColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExploreKind.Type.toggle -> {
|
||||||
|
ToggleTypeItem(
|
||||||
|
kind,
|
||||||
|
sourceUrl,
|
||||||
|
state,
|
||||||
|
valueOverride,
|
||||||
|
onValueChange,
|
||||||
|
isSelected,
|
||||||
|
modifier,
|
||||||
|
backgroundColor,
|
||||||
|
isMiuix,
|
||||||
|
trailingIcon,
|
||||||
|
content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ExploreKind.Type.select -> {
|
||||||
|
SelectTypeItem(
|
||||||
|
kind,
|
||||||
|
sourceUrl,
|
||||||
|
state,
|
||||||
|
valueOverride,
|
||||||
|
onValueChange,
|
||||||
|
isSelected,
|
||||||
|
modifier,
|
||||||
|
backgroundColor,
|
||||||
|
isMiuix,
|
||||||
|
trailingIcon,
|
||||||
|
content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
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 TextTypeItem(
|
||||||
|
kind: ExploreKind,
|
||||||
|
sourceUrl: String?,
|
||||||
|
state: ExploreKindItemState,
|
||||||
|
valueOverride: String?,
|
||||||
|
onValueChange: ((String) -> Unit)?,
|
||||||
|
modifier: Modifier,
|
||||||
|
backgroundColor: Color
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
var value by remember(sourceUrl, kind.title) {
|
var value by remember(sourceUrl, kind.title) {
|
||||||
mutableStateOf(valueOverride ?: infoMap?.get(kind.title).orEmpty())
|
mutableStateOf(valueOverride ?: state.infoMap?.get(kind.title).orEmpty())
|
||||||
}
|
}
|
||||||
LaunchedEffect(valueOverride) {
|
LaunchedEffect(valueOverride) {
|
||||||
if (valueOverride != null) {
|
if (valueOverride != null) value = valueOverride
|
||||||
value = valueOverride
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var actionJob by remember(sourceUrl, kind.title) { mutableStateOf<Job?>(null) }
|
var actionJob by remember(sourceUrl, kind.title) { mutableStateOf<Job?>(null) }
|
||||||
ExploreKindCompactTextField(
|
ExploreKindCompactTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = { newValue ->
|
onValueChange = { newValue ->
|
||||||
value = newValue
|
value = newValue
|
||||||
updateValue(newValue)
|
state.updateValue(newValue, onValueChange)
|
||||||
if (!kind.action.isNullOrBlank()) {
|
if (!kind.action.isNullOrBlank()) {
|
||||||
actionJob?.cancel()
|
actionJob?.cancel()
|
||||||
actionJob = scope.launch {
|
actionJob = scope.launch {
|
||||||
delay(600)
|
delay(600)
|
||||||
runAction(kind.action)
|
state.executeAction(kind.action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
placeholder = displayName,
|
placeholder = state.displayName,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor
|
||||||
isMiuix = isMiuix
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ExploreKind.Type.toggle -> {
|
@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) {
|
val chars = remember(kind.chars) {
|
||||||
kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
|
kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
|
||||||
}
|
}
|
||||||
@@ -183,88 +250,94 @@ fun ExploreKindMultiTypeItem(
|
|||||||
var char by remember(sourceUrl, kind.title, kind.default, kind.chars) {
|
var char by remember(sourceUrl, kind.title, kind.default, kind.chars) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
valueOverride
|
valueOverride
|
||||||
?: infoMap?.get(kind.title)
|
?: state.infoMap?.get(kind.title)
|
||||||
?.takeUnless { it.isEmpty() }
|
?.takeUnless { it.isEmpty() }
|
||||||
?: (kind.default ?: chars.first()).also {
|
?: (kind.default ?: chars.first()).also {
|
||||||
infoMap?.let { map ->
|
state.updateValue(it, onValueChange)
|
||||||
map[kind.title] = it
|
|
||||||
map.saveNow()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LaunchedEffect(valueOverride) {
|
LaunchedEffect(valueOverride) {
|
||||||
if (valueOverride != null) {
|
if (valueOverride != null) char = valueOverride
|
||||||
char = valueOverride
|
|
||||||
}
|
}
|
||||||
}
|
val text = if (left) "$char${state.displayName}" else "${state.displayName}$char"
|
||||||
val text = if (left) "$char$displayName" else "$displayName$char"
|
val internalOnClick = {
|
||||||
ExploreKindItem(
|
|
||||||
kind = kind,
|
|
||||||
isClickable = true,
|
|
||||||
onClick = {
|
|
||||||
val currentIndex = chars.indexOf(char)
|
val currentIndex = chars.indexOf(char)
|
||||||
val nextIndex = if (currentIndex < 0) 0 else (currentIndex + 1) % chars.size
|
val nextIndex = if (currentIndex < 0) 0 else (currentIndex + 1) % chars.size
|
||||||
char = chars.getOrElse(nextIndex) { "" }
|
char = chars.getOrElse(nextIndex) { "" }
|
||||||
updateValue(char)
|
state.updateValue(char, onValueChange)
|
||||||
runAction(kind.action)
|
state.executeAction(kind.action)
|
||||||
},
|
}
|
||||||
|
|
||||||
|
if (content != null) {
|
||||||
|
content(text, isSelected, internalOnClick, trailingIcon)
|
||||||
|
} else {
|
||||||
|
ExploreKindItem(
|
||||||
|
kind = kind,
|
||||||
|
isClickable = true,
|
||||||
|
onClick = internalOnClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
isMiuix = isMiuix,
|
isMiuix = isMiuix,
|
||||||
displayText = text,
|
displayText = text,
|
||||||
trailingIcon = {
|
isSelected = isSelected,
|
||||||
AppIcon(
|
trailingIcon = trailingIcon
|
||||||
imageVector = Icons.Default.Refresh,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.height(14.dp),
|
|
||||||
tint = LegadoTheme.colorScheme.outlineVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ExploreKind.Type.select -> {
|
@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) {
|
val chars = remember(kind.chars) {
|
||||||
kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
|
kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
|
||||||
}
|
}
|
||||||
var selected by remember(sourceUrl, kind.title, kind.default, kind.chars) {
|
var selected by remember(sourceUrl, kind.title, kind.default, kind.chars) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
valueOverride
|
valueOverride
|
||||||
?: infoMap?.get(kind.title)
|
?: state.infoMap?.get(kind.title)
|
||||||
?.takeUnless { it.isEmpty() }
|
?.takeUnless { it.isEmpty() }
|
||||||
?: (kind.default ?: chars.first()).also {
|
?: (kind.default ?: chars.first()).also {
|
||||||
infoMap?.let { map ->
|
state.updateValue(it, onValueChange)
|
||||||
map[kind.title] = it
|
|
||||||
map.saveNow()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LaunchedEffect(valueOverride) {
|
LaunchedEffect(valueOverride) {
|
||||||
if (valueOverride != null) {
|
if (valueOverride != null) selected = valueOverride
|
||||||
selected = valueOverride
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var showSelector by remember(sourceUrl, kind.title) { mutableStateOf(false) }
|
var showSelector by remember(sourceUrl, kind.title) { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
|
val internalOnClick = { showSelector = true }
|
||||||
|
val displayText = "${state.displayName} $selected"
|
||||||
|
|
||||||
|
if (content != null) {
|
||||||
|
content(displayText, isSelected, internalOnClick, trailingIcon)
|
||||||
|
} else {
|
||||||
ExploreKindItem(
|
ExploreKindItem(
|
||||||
kind = kind,
|
kind = kind,
|
||||||
isClickable = chars.isNotEmpty(),
|
isClickable = chars.isNotEmpty(),
|
||||||
onClick = { showSelector = true },
|
onClick = internalOnClick,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
isMiuix = isMiuix,
|
isMiuix = isMiuix,
|
||||||
displayText = "$displayName $selected",
|
displayText = displayText,
|
||||||
trailingIcon = {
|
isSelected = isSelected,
|
||||||
AppIcon(
|
trailingIcon = trailingIcon
|
||||||
imageVector = Icons.Default.UnfoldMore,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.height(14.dp),
|
|
||||||
tint = LegadoTheme.colorScheme.outlineVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
RoundDropdownMenu(
|
RoundDropdownMenu(
|
||||||
expanded = showSelector,
|
expanded = showSelector,
|
||||||
onDismissRequest = { showSelector = false }
|
onDismissRequest = { showSelector = false }
|
||||||
@@ -276,75 +349,58 @@ fun ExploreKindMultiTypeItem(
|
|||||||
showSelector = false
|
showSelector = false
|
||||||
if (selected != option) {
|
if (selected != option) {
|
||||||
selected = option
|
selected = option
|
||||||
updateValue(option)
|
state.updateValue(option, onValueChange)
|
||||||
runAction(kind.action)
|
state.executeAction(kind.action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
ExploreKindItem(
|
|
||||||
kind = kind,
|
|
||||||
isClickable = false,
|
|
||||||
onClick = {},
|
|
||||||
modifier = modifier,
|
|
||||||
backgroundColor = backgroundColor,
|
|
||||||
isMiuix = isMiuix,
|
|
||||||
displayText = displayName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExploreKindCompactTextField(
|
private fun rememberTrailingIcon(type: String, isSelected: Boolean): @Composable (() -> Unit)? {
|
||||||
value: String,
|
return remember(type, isSelected) {
|
||||||
onValueChange: (String) -> Unit,
|
when (type) {
|
||||||
placeholder: String,
|
ExploreKind.Type.button -> {
|
||||||
modifier: Modifier = Modifier,
|
{
|
||||||
backgroundColor: Color = LegadoTheme.colorScheme.surfaceContainer,
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
isMiuix: Boolean
|
AppIcon(
|
||||||
) {
|
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
contentDescription = null,
|
||||||
val shape = RoundedCornerShape(10.dp)
|
modifier = Modifier.height(14.dp),
|
||||||
|
tint = if (isSelected) LegadoTheme.colorScheme.onPrimaryContainer.copy(
|
||||||
|
alpha = 0.7f
|
||||||
|
) else LegadoTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BasicTextField(
|
ExploreKind.Type.toggle -> {
|
||||||
value = value,
|
{
|
||||||
onValueChange = onValueChange,
|
AppIcon(
|
||||||
singleLine = true,
|
imageVector = Icons.Default.Refresh,
|
||||||
textStyle = LegadoTheme.typography.bodySmall.copy(color = LegadoTheme.colorScheme.onSurface),
|
contentDescription = null,
|
||||||
cursorBrush = SolidColor(LegadoTheme.colorScheme.primary),
|
modifier = Modifier.height(14.dp),
|
||||||
interactionSource = interactionSource,
|
tint = if (isSelected) LegadoTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) else LegadoTheme.colorScheme.outlineVariant
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,8 +7,10 @@ import android.text.StaticLayout
|
|||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.compose.animation.AnimatedVisibilityScope
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
|
import androidx.compose.animation.EnterExitState
|
||||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
import androidx.compose.animation.SharedTransitionScope
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -25,6 +27,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -47,6 +50,9 @@ import io.legado.app.ui.theme.LegadoTheme
|
|||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import io.legado.app.model.BookCover as BookCoverModel
|
import io.legado.app.model.BookCover as BookCoverModel
|
||||||
|
|
||||||
|
private const val SharedCoverRadiusCacheMaxSize = 256
|
||||||
|
private val sharedCoverRadiusCache = mutableStateMapOf<String, Dp>()
|
||||||
|
|
||||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CoilBookCover(
|
fun CoilBookCover(
|
||||||
@@ -81,6 +87,14 @@ fun CoilBookCover(
|
|||||||
mutableStateOf(sharedCoverKey != null && finalPath != null)
|
mutableStateOf(sharedCoverKey != null && finalPath != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val transitionRadius = rememberSharedCoverTransitionRadius(
|
||||||
|
sharedCoverKey = sharedCoverKey,
|
||||||
|
radius = radius,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope
|
||||||
|
)
|
||||||
|
val shape = remember(transitionRadius) { RoundedCornerShape(transitionRadius) }
|
||||||
|
|
||||||
|
key(path, sharedCoverKey) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.aspectRatio(5f / 7f)
|
.aspectRatio(5f / 7f)
|
||||||
@@ -90,26 +104,26 @@ fun CoilBookCover(
|
|||||||
Modifier.sharedElement(
|
Modifier.sharedElement(
|
||||||
sharedContentState = rememberSharedContentState(sharedCoverKey),
|
sharedContentState = rememberSharedContentState(sharedCoverKey),
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
renderInOverlayDuringTransition = true
|
clipInOverlayDuringTransition = OverlayClip(shape)
|
||||||
)
|
)
|
||||||
} else Modifier
|
} else Modifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
if (CoverConfig.coverShowShadow) {
|
if (CoverConfig.coverShowShadow) {
|
||||||
Modifier.shadow(4.dp, RoundedCornerShape(radius))
|
Modifier.shadow(4.dp, shape)
|
||||||
} else Modifier
|
} else Modifier
|
||||||
)
|
)
|
||||||
.background(
|
.background(
|
||||||
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
||||||
LegadoTheme.colorScheme.surfaceContainerLow
|
LegadoTheme.colorScheme.surfaceContainerLow
|
||||||
} else Color.Transparent,
|
} else Color.Transparent,
|
||||||
RoundedCornerShape(radius)
|
shape
|
||||||
)
|
)
|
||||||
.clip(RoundedCornerShape(radius))
|
.clip(shape)
|
||||||
) {
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
if (hasCustomDefault && !isOnlineCoverLoaded) {
|
if (hasCustomDefault && !isOnlineCoverLoaded) {
|
||||||
key(randomPath) {
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = buildCoverImageRequest(
|
model = buildCoverImageRequest(
|
||||||
context = context,
|
context = context,
|
||||||
@@ -124,13 +138,10 @@ fun CoilBookCover(
|
|||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(RoundedCornerShape(radius))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (finalPath != null) {
|
if (finalPath != null) {
|
||||||
key(finalPath) {
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = buildCoverImageRequest(
|
model = buildCoverImageRequest(
|
||||||
context = context,
|
context = context,
|
||||||
@@ -143,9 +154,7 @@ fun CoilBookCover(
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
imageLoader = koinInject(),
|
imageLoader = koinInject(),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
|
||||||
.clip(RoundedCornerShape(4.dp)),
|
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
isOnlineCoverLoaded = true
|
isOnlineCoverLoaded = true
|
||||||
onLoadFinish?.invoke()
|
onLoadFinish?.invoke()
|
||||||
@@ -155,12 +164,12 @@ fun CoilBookCover(
|
|||||||
onLoadFinish?.invoke()
|
onLoadFinish?.invoke()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
onLoadFinish?.invoke()
|
onLoadFinish?.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showLoadingPlaceholder && !isOnlineCoverLoaded) {
|
if (showLoadingPlaceholder && !isOnlineCoverLoaded) {
|
||||||
if (!hasCustomDefault) {
|
if (!hasCustomDefault) {
|
||||||
@@ -180,6 +189,48 @@ fun CoilBookCover(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun rememberSharedCoverTransitionRadius(
|
||||||
|
sharedCoverKey: String?,
|
||||||
|
radius: Dp,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope?
|
||||||
|
): Dp {
|
||||||
|
if (sharedCoverKey == null || animatedVisibilityScope == null) {
|
||||||
|
return radius
|
||||||
|
}
|
||||||
|
|
||||||
|
val transition = animatedVisibilityScope.transition
|
||||||
|
val startRadius = sharedCoverRadiusCache[sharedCoverKey] ?: radius
|
||||||
|
val animatedRadiusValue by transition.animateFloat(
|
||||||
|
label = "book-cover-corner-radius"
|
||||||
|
) { state ->
|
||||||
|
if (state == EnterExitState.Visible) radius.value else startRadius.value
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(
|
||||||
|
sharedCoverKey,
|
||||||
|
radius,
|
||||||
|
transition.currentState,
|
||||||
|
transition.targetState
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
transition.currentState == EnterExitState.Visible &&
|
||||||
|
transition.targetState == EnterExitState.Visible
|
||||||
|
) {
|
||||||
|
sharedCoverRadiusCache[sharedCoverKey] = radius
|
||||||
|
if (sharedCoverRadiusCache.size > SharedCoverRadiusCacheMaxSize) {
|
||||||
|
sharedCoverRadiusCache.keys
|
||||||
|
.firstOrNull { it != sharedCoverKey }
|
||||||
|
?.let(sharedCoverRadiusCache::remove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return animatedRadiusValue.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -16,6 +17,7 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
|
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
|
||||||
|
import androidx.compose.material3.ScaffoldDefaults
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.animateFloatingActionButton
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
@@ -69,6 +71,7 @@ fun <T> ListScaffold(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||||
|
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
|
||||||
content: @Composable (PaddingValues) -> Unit
|
content: @Composable (PaddingValues) -> Unit
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
@@ -102,7 +105,8 @@ fun <T> ListScaffold(
|
|||||||
bottomContent = bottomContent
|
bottomContent = bottomContent
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = floatingActionButton
|
floatingActionButton = floatingActionButton,
|
||||||
|
contentWindowInsets = contentWindowInsets
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
|
|||||||
@@ -952,6 +952,7 @@
|
|||||||
<string name="hide_when_status_bar_show">状态栏显示时隐藏</string>
|
<string name="hide_when_status_bar_show">状态栏显示时隐藏</string>
|
||||||
<string name="reverse_toc">反转目录</string>
|
<string name="reverse_toc">反转目录</string>
|
||||||
<string name="show_discovery">显示发现</string>
|
<string name="show_discovery">显示发现</string>
|
||||||
|
<string name="show_home">显示首页</string>
|
||||||
<string name="style">样式</string>
|
<string name="style">样式</string>
|
||||||
<string name="group_style">分组样式</string>
|
<string name="group_style">分组样式</string>
|
||||||
<string name="export_file_name">导出文件名</string>
|
<string name="export_file_name">导出文件名</string>
|
||||||
@@ -1710,6 +1711,11 @@
|
|||||||
<string name="homepage_delete_custom_set_confirm">确定要删除该集及其包含的所有模块副本吗?</string>
|
<string name="homepage_delete_custom_set_confirm">确定要删除该集及其包含的所有模块副本吗?</string>
|
||||||
<string name="homepage_remove_module">移除模块</string>
|
<string name="homepage_remove_module">移除模块</string>
|
||||||
<string name="homepage_remove_module_confirm">确定要从当前集中移除该模块吗?</string>
|
<string name="homepage_remove_module_confirm">确定要从当前集中移除该模块吗?</string>
|
||||||
|
<string name="homepage_select_from_kinds">从发现分类选择</string>
|
||||||
|
<string name="homepage_select_multiple_kinds">选择多个分类</string>
|
||||||
|
<string name="homepage_n_selected">已选择 %1$d 个</string>
|
||||||
|
<string name="homepage_select_one_kind">选择一个分类</string>
|
||||||
|
<string name="or">或</string>
|
||||||
<string name="homepage_add_button_group">添加按钮组</string>
|
<string name="homepage_add_button_group">添加按钮组</string>
|
||||||
<string name="homepage_module_title_label">模块标题</string>
|
<string name="homepage_module_title_label">模块标题</string>
|
||||||
<string name="homepage_custom_title">自定义标题</string>
|
<string name="homepage_custom_title">自定义标题</string>
|
||||||
|
|||||||
@@ -890,6 +890,7 @@
|
|||||||
<string name="diy_edit_source_group">輸入自訂源分組名稱</string>
|
<string name="diy_edit_source_group">輸入自訂源分組名稱</string>
|
||||||
<string name="reverse_toc">反轉目錄</string>
|
<string name="reverse_toc">反轉目錄</string>
|
||||||
<string name="show_discovery">顯示發現</string>
|
<string name="show_discovery">顯示發現</string>
|
||||||
|
<string name="show_home">顯示首頁</string>
|
||||||
<string name="style">樣式</string>
|
<string name="style">樣式</string>
|
||||||
<string name="group_style">分組樣式</string>
|
<string name="group_style">分組樣式</string>
|
||||||
<string name="export_file_name">導出文件名</string>
|
<string name="export_file_name">導出文件名</string>
|
||||||
|
|||||||
@@ -893,6 +893,7 @@
|
|||||||
<string name="hide_when_status_bar_show">狀態欄顯示時隱藏</string>
|
<string name="hide_when_status_bar_show">狀態欄顯示時隱藏</string>
|
||||||
<string name="reverse_toc">反轉目錄</string>
|
<string name="reverse_toc">反轉目錄</string>
|
||||||
<string name="show_discovery">顯示發現</string>
|
<string name="show_discovery">顯示發現</string>
|
||||||
|
<string name="show_home">顯示首頁</string>
|
||||||
<string name="style">樣式</string>
|
<string name="style">樣式</string>
|
||||||
<string name="group_style">分組樣式</string>
|
<string name="group_style">分組樣式</string>
|
||||||
<string name="export_file_name">匯出檔案名</string>
|
<string name="export_file_name">匯出檔案名</string>
|
||||||
|
|||||||
@@ -982,6 +982,7 @@
|
|||||||
<string name="hide_when_status_bar_show">Hide when status bar show</string>
|
<string name="hide_when_status_bar_show">Hide when status bar show</string>
|
||||||
<string name="reverse_toc">Reverse toc</string>
|
<string name="reverse_toc">Reverse toc</string>
|
||||||
<string name="show_discovery">Show Discovery</string>
|
<string name="show_discovery">Show Discovery</string>
|
||||||
|
<string name="show_home">Show Homepage</string>
|
||||||
<string name="style">Style</string>
|
<string name="style">Style</string>
|
||||||
<string name="group_style">Group style</string>
|
<string name="group_style">Group style</string>
|
||||||
<string name="export_file_name">Export file name</string>
|
<string name="export_file_name">Export file name</string>
|
||||||
@@ -1716,6 +1717,11 @@
|
|||||||
<string name="homepage_delete_custom_set_confirm">Are you sure you want to delete this set and all its module copies?</string>
|
<string name="homepage_delete_custom_set_confirm">Are you sure you want to delete this set and all its module copies?</string>
|
||||||
<string name="homepage_remove_module">Remove Module</string>
|
<string name="homepage_remove_module">Remove Module</string>
|
||||||
<string name="homepage_remove_module_confirm">Are you sure you want to remove this module from the current set?</string>
|
<string name="homepage_remove_module_confirm">Are you sure you want to remove this module from the current set?</string>
|
||||||
|
<string name="homepage_select_from_kinds">Select from Kinds</string>
|
||||||
|
<string name="homepage_select_multiple_kinds">Select multiple kinds</string>
|
||||||
|
<string name="homepage_n_selected">%1$d selected</string>
|
||||||
|
<string name="homepage_select_one_kind">Select one kind</string>
|
||||||
|
<string name="or">OR</string>
|
||||||
<string name="homepage_add_button_group">Add Button Group</string>
|
<string name="homepage_add_button_group">Add Button Group</string>
|
||||||
<string name="homepage_module_title_label">Module Title</string>
|
<string name="homepage_module_title_label">Module Title</string>
|
||||||
<string name="homepage_custom_title">Custom Title</string>
|
<string name="homepage_custom_title">Custom Title</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user