优化
This commit is contained in:
16 files changed
+1486
-1164
No files matched your search
@@ -101,7 +101,8 @@
|
|||||||
"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 *)"
|
"Bash(gh pr *)",
|
||||||
|
"PowerShell(Get-Content *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,8 +8,9 @@ import androidx.compose.foundation.border
|
|||||||
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.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
@@ -63,9 +64,8 @@ fun BookshelfConfigSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.animateContentSize()
|
.animateContentSize(),
|
||||||
.padding(bottom = 32.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
) {
|
||||||
CompactDropdownSettingItem(
|
CompactDropdownSettingItem(
|
||||||
title = stringResource(R.string.group_style),
|
title = stringResource(R.string.group_style),
|
||||||
@@ -506,6 +506,9 @@ fun BookshelfConfigSheet(
|
|||||||
steps = 100,
|
steps = 100,
|
||||||
onValueChange = { BookshelfConfig.bookshelfRefreshingLimit = it.toInt() }
|
onValueChange = { BookshelfConfig.bookshelfRefreshingLimit = it.toInt() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LabelColorManageSheet(
|
LabelColorManageSheet(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
|
|||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.data.entities.rule.ExploreKind
|
import io.legado.app.data.entities.rule.ExploreKind
|
||||||
import io.legado.app.domain.model.HomepageModuleType
|
import io.legado.app.domain.model.HomepageModuleType
|
||||||
|
import io.legado.app.domain.model.ModuleDef
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
@@ -13,6 +14,36 @@ data class HomepageUiState(
|
|||||||
val isManageMode: Boolean = false,
|
val isManageMode: Boolean = false,
|
||||||
val isConfigMode: Boolean = false,
|
val isConfigMode: Boolean = false,
|
||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
|
val manageState: HomepageManageUiState = HomepageManageUiState()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
data class HomepageManageUiState(
|
||||||
|
val sets: ImmutableList<HomepageSourceManageUi> = persistentListOf(),
|
||||||
|
val browseSources: ImmutableList<HomepageSourceManageUi> = persistentListOf(),
|
||||||
|
val allJoinedModules: ImmutableList<HomepageModuleManageUi> = persistentListOf(),
|
||||||
|
val sourceNames: Map<String, String> = emptyMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
data class HomepageManageActions(
|
||||||
|
val onToggleSet: (String, Boolean) -> Unit = { _, _ -> },
|
||||||
|
val onGetSourceModules: (String, String?) -> List<HomepageModuleManageUi> = { _, _ -> emptyList() },
|
||||||
|
val onSyncSourceModules: (String) -> Unit = {},
|
||||||
|
val onToggleModule: (String, Boolean) -> Unit = { _, _ -> },
|
||||||
|
val onJoinModule: (String, String?, ModuleDef) -> Unit = { _, _, _ -> },
|
||||||
|
val onAddCustomModule: (String, String?, ModuleDef) -> Unit = { _, _, _ -> },
|
||||||
|
val onAddButtonGroupFromKinds: (String, String?, String, List<String>) -> Unit = { _, _, _, _ -> },
|
||||||
|
val onGetExploreKinds: (String) -> List<Pair<String, String>> = { emptyList() },
|
||||||
|
val onUpdateModule: (String, ModuleDef) -> Unit = { _, _ -> },
|
||||||
|
val onDeleteModule: (String) -> Unit = {},
|
||||||
|
val onReorderModules: (List<String>) -> Unit = {},
|
||||||
|
val onReorderSets: (List<String>) -> Unit = {},
|
||||||
|
val onSetCustomSetTitle: (String, String?) -> Unit = { _, _ -> },
|
||||||
|
val onCreateCustomSet: (String) -> Unit = {},
|
||||||
|
val onRenameCustomSet: (String, String) -> Unit = { _, _ -> },
|
||||||
|
val onDeleteCustomSet: (String) -> Unit = {},
|
||||||
|
val onAssignModuleToCustomSet: (String, String?) -> Unit = { _, _ -> },
|
||||||
)
|
)
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -93,15 +93,16 @@ fun HomepageScreen(
|
|||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val allSets by viewModel.setsFlow.collectAsStateWithLifecycle()
|
// Removed allSets and browseSources as they are now part of uiState.manageState
|
||||||
val browseSources by viewModel.browseSourcesFlow.collectAsStateWithLifecycle()
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var errorMsg by remember { mutableStateOf<String?>(null) }
|
var errorMsg by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
val layoutMode = HomepageConfig.homepageLayoutModeState.value
|
val layoutMode = HomepageConfig.homepageLayoutModeState.value
|
||||||
|
|
||||||
val selectedSets = remember(allSets) { allSets.filter { it.isSelected } }
|
val selectedSets = remember(uiState.manageState.sets) {
|
||||||
|
uiState.manageState.sets.filter { it.isSelected }
|
||||||
|
}
|
||||||
val pagerState = rememberPagerState(pageCount = {
|
val pagerState = rememberPagerState(pageCount = {
|
||||||
if (layoutMode == 1) selectedSets.size.coerceAtLeast(1) else 1
|
if (layoutMode == 1) selectedSets.size.coerceAtLeast(1) else 1
|
||||||
})
|
})
|
||||||
@@ -261,50 +262,44 @@ fun HomepageScreen(
|
|||||||
HomepageModuleManageSheet(
|
HomepageModuleManageSheet(
|
||||||
data = if (uiState.isManageMode) Unit else null,
|
data = if (uiState.isManageMode) Unit else null,
|
||||||
onDismissRequest = { viewModel.toggleManageMode() },
|
onDismissRequest = { viewModel.toggleManageMode() },
|
||||||
sets = allSets,
|
state = uiState.manageState,
|
||||||
browseSources = browseSources,
|
actions = remember {
|
||||||
onToggleSet = { url, isEnabled -> viewModel.toggleSourceFilter(url, isEnabled) },
|
HomepageManageActions(
|
||||||
onGetModulesInSet = { viewModel.getJoinedModules(it) },
|
onToggleSet = { url, isEnabled ->
|
||||||
|
viewModel.toggleSourceFilter(
|
||||||
|
url,
|
||||||
|
isEnabled
|
||||||
|
)
|
||||||
|
},
|
||||||
onGetSourceModules = { url, setId -> viewModel.getSourceModules(url, setId) },
|
onGetSourceModules = { url, setId -> viewModel.getSourceModules(url, setId) },
|
||||||
onSyncSourceModules = { viewModel.syncSourceModules(it) },
|
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(sourceUrl, targetSetId, def)
|
||||||
sourceUrl,
|
|
||||||
targetSetId,
|
|
||||||
def
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onAddCustomModule = { sourceUrl, targetSetId, def ->
|
onAddCustomModule = { sourceUrl, targetSetId, def ->
|
||||||
viewModel.addCustomModule(
|
viewModel.addCustomModule(sourceUrl, targetSetId, def)
|
||||||
sourceUrl,
|
|
||||||
targetSetId,
|
|
||||||
def
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onAddButtonGroupFromKinds = { sourceUrl, targetSetId, title, kinds ->
|
onAddButtonGroupFromKinds = { sourceUrl, targetSetId, title, kinds ->
|
||||||
viewModel.addButtonGroupFromKinds(
|
viewModel.addButtonGroupFromKinds(sourceUrl, targetSetId, title, kinds)
|
||||||
sourceUrl,
|
|
||||||
targetSetId,
|
|
||||||
title,
|
|
||||||
kinds
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onGetExploreKinds = { viewModel.getSourceExploreKinds(it) },
|
onGetExploreKinds = { viewModel.getSourceExploreKinds(it) },
|
||||||
onUpdateModule = { globalId, def -> viewModel.updateModule(globalId, def) },
|
onUpdateModule = { globalId, def -> viewModel.updateModule(globalId, def) },
|
||||||
onDeleteModule = { viewModel.deleteModule(it) },
|
onDeleteModule = { viewModel.deleteModule(it) },
|
||||||
onReorderModules = { ids -> viewModel.reorderJoinedModules(ids) },
|
onReorderModules = { ids -> viewModel.reorderJoinedModules(ids) },
|
||||||
onReorderSets = { urls -> viewModel.reorderCustomSets(urls) },
|
onReorderSets = { urls -> viewModel.reorderCustomSets(urls) },
|
||||||
onSetCustomSetTitle = { id, title -> viewModel.setModuleCustomSetTitle(id, title) },
|
onSetCustomSetTitle = { id, title ->
|
||||||
|
viewModel.setModuleCustomSetTitle(
|
||||||
|
id,
|
||||||
|
title
|
||||||
|
)
|
||||||
|
},
|
||||||
onCreateCustomSet = { viewModel.createCustomSet(it) },
|
onCreateCustomSet = { viewModel.createCustomSet(it) },
|
||||||
onRenameCustomSet = { id, name -> viewModel.renameCustomSet(id, name) },
|
onRenameCustomSet = { id, name -> viewModel.renameCustomSet(id, name) },
|
||||||
onDeleteCustomSet = { viewModel.deleteCustomSet(it) },
|
onDeleteCustomSet = { viewModel.deleteCustomSet(it) },
|
||||||
onGetAllModulesGroupedBySource = { viewModel.getAllModulesGroupedBySource() },
|
|
||||||
onGetSourceName = { viewModel.getSourceName(it) },
|
|
||||||
onAssignModuleToCustomSet = { id, setId ->
|
onAssignModuleToCustomSet = { id, setId ->
|
||||||
viewModel.assignModuleToCustomSet(
|
viewModel.assignModuleToCustomSet(id, setId)
|
||||||
id,
|
}
|
||||||
setId
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -82,46 +82,113 @@ class HomepageViewModel(
|
|||||||
val effects = _effects.asSharedFlow()
|
val effects = _effects.asSharedFlow()
|
||||||
|
|
||||||
private val loadJobs = ConcurrentHashMap<String, Job>()
|
private val loadJobs = ConcurrentHashMap<String, Job>()
|
||||||
private val initModulesSyncFlow = bookSourceRepository.flowHomepageModules()
|
|
||||||
private val exploreSourcesFlow = bookSourceRepository.flowExploreSources()
|
private val exploreSourcesFlow = bookSourceRepository.flowExploreSources()
|
||||||
|
|
||||||
|
// 1. 基础原始状态
|
||||||
private val _isRefreshing = MutableStateFlow(false)
|
private val _isRefreshing = MutableStateFlow(false)
|
||||||
private val _isManageMode = MutableStateFlow(false)
|
private val _isManageMode = MutableStateFlow(false)
|
||||||
private val _isConfigMode = MutableStateFlow(false)
|
private val _isConfigMode = MutableStateFlow(false)
|
||||||
private val _configVersion = MutableStateFlow(0L)
|
private val _configVersion = MutableStateFlow(0L)
|
||||||
private val _moduleContentStates = MutableStateFlow<Map<String, ModuleLoadState>>(emptyMap())
|
private val _moduleContentStates = MutableStateFlow<Map<String, ModuleLoadState>>(emptyMap())
|
||||||
|
|
||||||
private val localModulesFlow = gateway.flowEnabled()
|
|
||||||
private val _bookSourcesCache = MutableStateFlow<Map<String, BookSource>>(emptyMap())
|
private val _bookSourcesCache = MutableStateFlow<Map<String, BookSource>>(emptyMap())
|
||||||
private val _layoutConfigCache = MutableStateFlow<Map<String, Map<String, String>>>(emptyMap())
|
private val _layoutConfigCache = MutableStateFlow<Map<String, Map<String, String>>>(emptyMap())
|
||||||
|
private val _pendingEnabled = MutableStateFlow<Map<String, Boolean>>(emptyMap())
|
||||||
|
private val _pendingUserModules = MutableStateFlow<List<ModuleItem>>(emptyList())
|
||||||
|
private val _exploreKindsCache =
|
||||||
|
MutableStateFlow<Map<String, List<Pair<String, String>>>>(emptyMap())
|
||||||
|
|
||||||
val allModulesCache = gateway.flowAll()
|
// 2. 数据库同步流
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
private val localModulesFlow = gateway.flowEnabled()
|
||||||
|
val allModulesCache =
|
||||||
|
gateway.flowAll().stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||||
val customSetsFlow = gateway.flowCustomSets()
|
val customSetsFlow = gateway.flowCustomSets()
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||||
|
|
||||||
|
// 3. 业务加工流
|
||||||
private val orderedModuleDefsFlow = combine(localModulesFlow, _configVersion) { modules, _ ->
|
private val orderedModuleDefsFlow = combine(localModulesFlow, _configVersion) { modules, _ ->
|
||||||
modules.groupBySourceOrdered()
|
modules.groupBySourceOrdered()
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
|
||||||
|
|
||||||
|
val setsFlow = combine(
|
||||||
|
localModulesFlow,
|
||||||
|
allModulesCache,
|
||||||
|
customSetsFlow,
|
||||||
|
_configVersion
|
||||||
|
) { _, allModules, customSets, _ ->
|
||||||
|
val hiddenSourceUrls = GSON.fromJsonArray<String>(HomepageConfig.homepageSourceHidden)
|
||||||
|
.getOrDefault(emptyList()).toSet()
|
||||||
|
val moduleCountsBySet =
|
||||||
|
allModules.mapNotNull { it.customSetId }.groupBy { it }.mapValues { it.value.size }
|
||||||
|
|
||||||
|
customSets.sortedBy { it.sortOrder }.map { set ->
|
||||||
|
HomepageSourceManageUi(
|
||||||
|
sourceUrl = customSetUrl(set.id),
|
||||||
|
sourceName = set.name,
|
||||||
|
sourceGroup = null,
|
||||||
|
isSelected = customSetUrl(set.id) !in hiddenSourceUrls,
|
||||||
|
moduleCount = moduleCountsBySet[set.id] ?: 0,
|
||||||
|
isCustomSet = true,
|
||||||
|
)
|
||||||
|
}.toImmutableList()
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf())
|
||||||
|
|
||||||
|
val browseSourcesFlow = exploreSourcesFlow.map { sources ->
|
||||||
|
sources.map { source ->
|
||||||
|
HomepageSourceManageUi(
|
||||||
|
sourceUrl = source.bookSourceUrl,
|
||||||
|
sourceName = source.bookSourceName,
|
||||||
|
sourceGroup = source.bookSourceGroup,
|
||||||
|
)
|
||||||
|
}.toImmutableList()
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf())
|
||||||
|
|
||||||
|
// 4. 聚合层
|
||||||
private val uiFlagsFlow =
|
private val uiFlagsFlow =
|
||||||
combine(_isRefreshing, _isManageMode, _isConfigMode) { refreshing, manage, config ->
|
combine(_isRefreshing, _isManageMode, _isConfigMode) { refreshing, manage, config ->
|
||||||
HomepageUiFlags(refreshing, manage, config)
|
HomepageUiFlags(refreshing, manage, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
val uiState: StateFlow<HomepageUiState> = combine(
|
private val manageStateFlow = combine(
|
||||||
|
setsFlow,
|
||||||
|
browseSourcesFlow,
|
||||||
|
allModulesCache,
|
||||||
|
_bookSourcesCache,
|
||||||
|
_pendingEnabled
|
||||||
|
) { sets, browseSources, allModules, sourcesCache, pendingEnabled ->
|
||||||
|
HomepageManageUiState(
|
||||||
|
sets = sets,
|
||||||
|
browseSources = browseSources,
|
||||||
|
allJoinedModules = allModules.map { module ->
|
||||||
|
HomepageModuleManageUi(
|
||||||
|
id = module.id,
|
||||||
|
sourceUrl = module.sourceUrl,
|
||||||
|
moduleKey = module.moduleKey,
|
||||||
|
title = module.displayTitle,
|
||||||
|
customSetTitle = module.customSetTitle,
|
||||||
|
customSetId = module.customSetId,
|
||||||
|
isVisible = pendingEnabled[module.id] ?: module.isEnabled,
|
||||||
|
type = module.type,
|
||||||
|
url = module.url,
|
||||||
|
args = module.args,
|
||||||
|
layoutConfig = module.layoutConfig,
|
||||||
|
originalTitle = module.title,
|
||||||
|
)
|
||||||
|
}.toImmutableList(),
|
||||||
|
sourceNames = sourcesCache.mapValues { it.value.bookSourceName }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val displayModulesFlow = combine(
|
||||||
orderedModuleDefsFlow,
|
orderedModuleDefsFlow,
|
||||||
_moduleContentStates,
|
_moduleContentStates,
|
||||||
uiFlagsFlow,
|
|
||||||
_bookSourcesCache,
|
_bookSourcesCache,
|
||||||
customSetsFlow
|
customSetsFlow,
|
||||||
) { grouped, contentStates, flags, sourcesCache, customSets ->
|
_layoutConfigCache
|
||||||
|
) { grouped, contentStates, sourcesCache, customSets, configCache ->
|
||||||
val setNames = customSets.associate { it.id to it.name }
|
val setNames = customSets.associate { it.id to it.name }
|
||||||
val sortedSetIds = customSets.sortedBy { it.sortOrder }.map { it.id }
|
val sortedSetIds = customSets.sortedBy { it.sortOrder }.map { it.id }
|
||||||
val configCache = _layoutConfigCache.value
|
|
||||||
|
|
||||||
val displayModules = sortedSetIds.flatMap { setId ->
|
sortedSetIds.flatMap { setId ->
|
||||||
val setUrl = customSetUrl(setId)
|
val setUrl = customSetUrl(setId)
|
||||||
val mods = grouped[setUrl] ?: emptyList()
|
val mods = grouped[setUrl] ?: emptyList()
|
||||||
mods.map { module ->
|
mods.map { module ->
|
||||||
@@ -144,60 +211,26 @@ class HomepageViewModel(
|
|||||||
config = configMap
|
config = configMap
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. 最终 UI 状态
|
||||||
|
val uiState: StateFlow<HomepageUiState> = combine(
|
||||||
|
displayModulesFlow,
|
||||||
|
uiFlagsFlow,
|
||||||
|
manageStateFlow
|
||||||
|
) { modules, flags, manageState ->
|
||||||
HomepageUiState(
|
HomepageUiState(
|
||||||
modules = displayModules.toImmutableList(),
|
modules = modules,
|
||||||
isRefreshing = flags.isRefreshing,
|
isRefreshing = flags.isRefreshing,
|
||||||
isManageMode = flags.isManageMode,
|
isManageMode = flags.isManageMode,
|
||||||
isConfigMode = flags.isConfigMode,
|
isConfigMode = flags.isConfigMode,
|
||||||
|
manageState = manageState
|
||||||
)
|
)
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), HomepageUiState())
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), HomepageUiState())
|
||||||
|
|
||||||
val setsFlow = combine(
|
|
||||||
localModulesFlow,
|
|
||||||
allModulesCache,
|
|
||||||
customSetsFlow,
|
|
||||||
_configVersion
|
|
||||||
) { _, allModules, customSets, _ ->
|
|
||||||
val hiddenSourceUrls = GSON.fromJsonArray<String>(HomepageConfig.homepageSourceHidden)
|
|
||||||
.getOrDefault(emptyList()).toSet()
|
|
||||||
val moduleCountsBySet =
|
|
||||||
allModules.mapNotNull { it.customSetId }.groupBy { it }.mapValues { it.value.size }
|
|
||||||
|
|
||||||
val list = mutableListOf<HomepageSourceManageUi>()
|
|
||||||
customSets.sortedBy { it.sortOrder }.forEach { set ->
|
|
||||||
list.add(
|
|
||||||
HomepageSourceManageUi(
|
|
||||||
sourceUrl = customSetUrl(set.id),
|
|
||||||
sourceName = set.name,
|
|
||||||
sourceGroup = null,
|
|
||||||
isSelected = customSetUrl(set.id) !in hiddenSourceUrls,
|
|
||||||
moduleCount = moduleCountsBySet[set.id] ?: 0,
|
|
||||||
isCustomSet = true,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
list.toImmutableList()
|
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf())
|
|
||||||
|
|
||||||
/** 用于「浏览书源模块」:列出有 homepageModules 的书源 */
|
|
||||||
val browseSourcesFlow = exploreSourcesFlow.map { sources ->
|
|
||||||
sources.map { source ->
|
|
||||||
HomepageSourceManageUi(
|
|
||||||
sourceUrl = source.bookSourceUrl,
|
|
||||||
sourceName = source.bookSourceName,
|
|
||||||
sourceGroup = source.bookSourceGroup,
|
|
||||||
)
|
|
||||||
}.toImmutableList()
|
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf())
|
|
||||||
|
|
||||||
private val _exploreKindsCache =
|
|
||||||
MutableStateFlow<Map<String, List<Pair<String, String>>>>(emptyMap())
|
|
||||||
private val _pendingEnabled = MutableStateFlow<Map<String, Boolean>>(emptyMap())
|
|
||||||
private val _pendingUserModules = MutableStateFlow<List<ModuleItem>>(emptyList())
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// 解析并缓存模块 layoutConfig,避免在 combine 中重复解析
|
// 解析并缓存模块 layoutConfig
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
localModulesFlow.collect { modules ->
|
localModulesFlow.collect { modules ->
|
||||||
val cache = mutableMapOf<String, Map<String, String>>()
|
val cache = mutableMapOf<String, Map<String, String>>()
|
||||||
@@ -217,7 +250,6 @@ class HomepageViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理 _pendingUserModules 中已入库的条目
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
exploreSourcesFlow.collect { sources ->
|
exploreSourcesFlow.collect { sources ->
|
||||||
_bookSourcesCache.value = sources.associateBy { it.bookSourceUrl }
|
_bookSourcesCache.value = sources.associateBy { it.bookSourceUrl }
|
||||||
@@ -235,7 +267,6 @@ class HomepageViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理 _pendingUserModules 中已入库的条目
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
allModulesCache.collect { modules ->
|
allModulesCache.collect { modules ->
|
||||||
val dbIds = modules.map { it.id }.toSet()
|
val dbIds = modules.map { it.id }.toSet()
|
||||||
@@ -243,7 +274,6 @@ class HomepageViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 一次性迁移:将 customSetId=null 的存量模块归属到书源集,并确保所有集存在
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val allModules = allModulesCache.first()
|
val allModules = allModulesCache.first()
|
||||||
val orphans = allModules.filter { it.customSetId == null }
|
val orphans = allModules.filter { it.customSetId == null }
|
||||||
@@ -251,20 +281,15 @@ class HomepageViewModel(
|
|||||||
orphans.groupBy { it.sourceUrl }.forEach { (sourceUrl, modules) ->
|
orphans.groupBy { it.sourceUrl }.forEach { (sourceUrl, modules) ->
|
||||||
val source = bookSourceRepository.getBookSource(sourceUrl) ?: return@forEach
|
val source = bookSourceRepository.getBookSource(sourceUrl) ?: return@forEach
|
||||||
ensureSetForSource(sourceUrl, source.bookSourceName)
|
ensureSetForSource(sourceUrl, source.bookSourceName)
|
||||||
modules.forEach { m ->
|
modules.forEach { m -> gateway.setCustomSetId(m.id, "src_$sourceUrl") }
|
||||||
gateway.setCustomSetId(m.id, "src_$sourceUrl")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// 确保所有 customSetId 对应的集都存在
|
|
||||||
allModules.mapNotNull { it.customSetId }.distinct().forEach { setId ->
|
allModules.mapNotNull { it.customSetId }.distinct().forEach { setId ->
|
||||||
val isSrcSet = setId.startsWith("src_")
|
val isSrcSet = setId.startsWith("src_")
|
||||||
if (isSrcSet && gateway.getCustomSetById(setId) == null) {
|
if (isSrcSet && gateway.getCustomSetById(setId) == null) {
|
||||||
val sourceUrl = setId.removePrefix("src_")
|
val sourceUrl = setId.removePrefix("src_")
|
||||||
val source = bookSourceRepository.getBookSource(sourceUrl)
|
val source = bookSourceRepository.getBookSource(sourceUrl)
|
||||||
if (source != null) {
|
if (source != null) ensureSetForSource(sourceUrl, source.bookSourceName)
|
||||||
ensureSetForSource(sourceUrl, source.bookSourceName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,22 +306,16 @@ class HomepageViewModel(
|
|||||||
val parsedIds = parsedDefs.map { it.globalId }.toSet()
|
val parsedIds = parsedDefs.map { it.globalId }.toSet()
|
||||||
|
|
||||||
val toUpsert = mutableListOf<ModuleItem>()
|
val toUpsert = mutableListOf<ModuleItem>()
|
||||||
|
|
||||||
for (i in parsedDefs.indices) {
|
for (i in parsedDefs.indices) {
|
||||||
val def = parsedDefs[i]
|
val def = parsedDefs[i]
|
||||||
val existing = existingById[def.globalId]
|
val existing = existingById[def.globalId]
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
// 用户编辑过的模块不被 JSON 覆盖
|
|
||||||
if (existing.isUserCreated) continue
|
if (existing.isUserCreated) continue
|
||||||
if (existing.sourceJsonHash == newHash) continue
|
if (existing.sourceJsonHash == newHash) continue
|
||||||
toUpsert.add(
|
toUpsert.add(
|
||||||
existing.copy(
|
existing.copy(
|
||||||
type = def.type,
|
type = def.type, title = def.title, args = def.args, url = def.url,
|
||||||
title = def.title,
|
sourceJsonHash = newHash, syncedAt = System.currentTimeMillis()
|
||||||
args = def.args,
|
|
||||||
url = def.url,
|
|
||||||
sourceJsonHash = newHash,
|
|
||||||
syncedAt = System.currentTimeMillis(),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -313,19 +332,13 @@ class HomepageViewModel(
|
|||||||
customSetId = "src_${source.bookSourceUrl}",
|
customSetId = "src_${source.bookSourceUrl}",
|
||||||
sortOrder = i,
|
sortOrder = i,
|
||||||
sourceJsonHash = newHash,
|
sourceJsonHash = newHash,
|
||||||
syncedAt = System.currentTimeMillis(),
|
syncedAt = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (toUpsert.isNotEmpty()) gateway.upsertAll(toUpsert)
|
||||||
if (toUpsert.isNotEmpty()) {
|
if (parsedIds.isNotEmpty()) gateway.deleteStale(source.bookSourceUrl, parsedIds.toList())
|
||||||
gateway.upsertAll(toUpsert)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedIds.isNotEmpty()) {
|
|
||||||
gateway.deleteStale(source.bookSourceUrl, parsedIds.toList())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadModule(module: ModuleItem) {
|
private fun loadModule(module: ModuleItem) {
|
||||||
@@ -336,74 +349,50 @@ class HomepageViewModel(
|
|||||||
val source = bookSourceRepository.getBookSource(module.sourceUrl)
|
val source = bookSourceRepository.getBookSource(module.sourceUrl)
|
||||||
?: throw Exception("Source not found")
|
?: throw Exception("Source not found")
|
||||||
val allKinds = withContext(Dispatchers.IO) { source.exploreKinds() }
|
val allKinds = withContext(Dispatchers.IO) { source.exploreKinds() }
|
||||||
|
val selectedTitles =
|
||||||
val selectedTitles = module.args?.let { argsStr ->
|
module.args?.let { GSON.fromJsonArray<String>(it).getOrNull() }
|
||||||
GSON.fromJsonArray<String>(argsStr).getOrNull()
|
if (selectedTitles.isNullOrEmpty()) allKinds.take(
|
||||||
}
|
HOMEPAGE_MAX_BUTTON_GROUP_KINDS
|
||||||
|
)
|
||||||
if (selectedTitles.isNullOrEmpty()) {
|
else selectedTitles.mapNotNull { t -> allKinds.find { it.title == t } }
|
||||||
allKinds.take(HOMEPAGE_MAX_BUTTON_GROUP_KINDS)
|
|
||||||
} else {
|
|
||||||
selectedTitles.mapNotNull { t -> allKinds.find { it.title == t } }
|
|
||||||
}
|
|
||||||
}.onSuccess { kinds ->
|
}.onSuccess { kinds ->
|
||||||
_moduleContentStates.update { it + (module.id to ModuleLoadState.Buttons(kinds.toImmutableList())) }
|
_moduleContentStates.update { it + (module.id to ModuleLoadState.Buttons(kinds.toImmutableList())) }
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
_moduleContentStates.update {
|
_moduleContentStates.update { it + (module.id to ModuleLoadState.Error(e.stackTraceStr)) }
|
||||||
it + (module.id to ModuleLoadState.Error(
|
|
||||||
e.stackTraceStr
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
it.invokeOnCompletion { loadJobs.remove(module.id) }
|
|
||||||
}
|
}
|
||||||
|
}.also { it.invokeOnCompletion { loadJobs.remove(module.id) } }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadJobs[module.id] = viewModelScope.launch {
|
loadJobs[module.id] = viewModelScope.launch {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val isRanking = module.type == HomepageModuleType.Ranking.key
|
val isRanking =
|
||||||
|| module.type == HomepageModuleType.GridRanking.key
|
module.type == HomepageModuleType.Ranking.key || module.type == HomepageModuleType.GridRanking.key
|
||||||
val books = if (isRanking) {
|
val books = if (isRanking) exploreBooksUseCase.executeForRanking(
|
||||||
exploreBooksUseCase.executeForRanking(module.sourceUrl, module.url, module.args)
|
module.sourceUrl,
|
||||||
} else {
|
module.url,
|
||||||
exploreBooksUseCase.execute(module.sourceUrl, module.url, module.args).books
|
module.args
|
||||||
}
|
)
|
||||||
val layout = try {
|
else exploreBooksUseCase.execute(module.sourceUrl, module.url, module.args).books
|
||||||
GSON.fromJson(module.layoutConfig, Map::class.java)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val rows = (layout?.get("rows") as? Number)?.toInt() ?: HOMEPAGE_DEFAULT_GRID_ROWS
|
|
||||||
val hasMore = isInfinite(module.type, module.layoutConfig) && books.isNotEmpty()
|
val hasMore = isInfinite(module.type, module.layoutConfig) && books.isNotEmpty()
|
||||||
books to hasMore
|
books to hasMore
|
||||||
}.onSuccess { (books, hasMore) ->
|
}.onSuccess { (books, hasMore) ->
|
||||||
_moduleContentStates.update {
|
_moduleContentStates.update {
|
||||||
it + (module.id to ModuleLoadState.Loaded(
|
it + (module.id to ModuleLoadState.Loaded(
|
||||||
books.toImmutableList(),
|
books.toImmutableList(),
|
||||||
hasMore = hasMore,
|
hasMore = hasMore
|
||||||
page = 1
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
_moduleContentStates.update {
|
_moduleContentStates.update { it + (module.id to ModuleLoadState.Error(e.stackTraceStr)) }
|
||||||
it + (module.id to ModuleLoadState.Error(
|
|
||||||
e.stackTraceStr
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
it.invokeOnCompletion { loadJobs.remove(module.id) }
|
|
||||||
}
|
}
|
||||||
|
}.also { it.invokeOnCompletion { loadJobs.remove(module.id) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMoreModule(globalId: String) {
|
fun loadMoreModule(globalId: String) {
|
||||||
val currentState = _moduleContentStates.value[globalId] as? ModuleLoadState.Loaded ?: return
|
val currentState = _moduleContentStates.value[globalId] as? ModuleLoadState.Loaded ?: return
|
||||||
if (currentState.isLoadingMore || !currentState.hasMore) return
|
if (currentState.isLoadingMore || !currentState.hasMore) return
|
||||||
|
|
||||||
val nextPage = currentState.page + 1
|
val nextPage = currentState.page + 1
|
||||||
_moduleContentStates.update { it + (globalId to currentState.copy(isLoadingMore = true)) }
|
_moduleContentStates.update { it + (globalId to currentState.copy(isLoadingMore = true)) }
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val module = gateway.getById(globalId) ?: throw Exception("Module not found")
|
val module = gateway.getById(globalId) ?: throw Exception("Module not found")
|
||||||
@@ -414,18 +403,14 @@ class HomepageViewModel(
|
|||||||
page = nextPage
|
page = nextPage
|
||||||
)
|
)
|
||||||
}.onSuccess { result ->
|
}.onSuccess { result ->
|
||||||
val newBooks = result.books
|
|
||||||
_moduleContentStates.update { states ->
|
_moduleContentStates.update { states ->
|
||||||
val lastState =
|
val lastState =
|
||||||
states[globalId] as? ModuleLoadState.Loaded ?: return@update states
|
states[globalId] as? ModuleLoadState.Loaded ?: return@update states
|
||||||
val existingUrls = lastState.books.map { it.bookUrl }.toSet()
|
val existingUrls = lastState.books.map { it.bookUrl }.toSet()
|
||||||
val deduped = newBooks.filter { it.bookUrl !in existingUrls }
|
val deduped = result.books.filter { it.bookUrl !in existingUrls }
|
||||||
val combinedBooks = (lastState.books + deduped).toImmutableList()
|
|
||||||
states + (globalId to ModuleLoadState.Loaded(
|
states + (globalId to ModuleLoadState.Loaded(
|
||||||
books = combinedBooks,
|
books = (lastState.books + deduped).toImmutableList(),
|
||||||
hasMore = deduped.isNotEmpty(),
|
hasMore = deduped.isNotEmpty(), isLoadingMore = false, page = nextPage
|
||||||
isLoadingMore = false,
|
|
||||||
page = nextPage
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
@@ -461,17 +446,12 @@ class HomepageViewModel(
|
|||||||
_isRefreshing.value = true
|
_isRefreshing.value = true
|
||||||
loadJobs.values.forEach { it.cancel() }
|
loadJobs.values.forEach { it.cancel() }
|
||||||
loadJobs.clear()
|
loadJobs.clear()
|
||||||
|
uiState.value.modules.map { it.sourceUrl }.distinct().forEach { url ->
|
||||||
// 刷新时同步当前已启用模块所属书源的定义
|
|
||||||
val activeSourceUrls = uiState.value.modules.map { it.sourceUrl }.distinct()
|
|
||||||
activeSourceUrls.forEach { url ->
|
|
||||||
resolveBookSource(url)?.let { syncModulesFromSource(it) }
|
resolveBookSource(url)?.let { syncModulesFromSource(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
_moduleContentStates.value = emptyMap()
|
_moduleContentStates.value = emptyMap()
|
||||||
uiState.map { it.modules }.first { modules ->
|
uiState.map { it.modules }
|
||||||
modules.all { it.state !is ModuleLoadState.Loading }
|
.first { modules -> modules.all { it.state !is ModuleLoadState.Loading } }
|
||||||
}
|
|
||||||
_isRefreshing.value = false
|
_isRefreshing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,14 +466,11 @@ class HomepageViewModel(
|
|||||||
fun setModuleVisible(id: String, visible: Boolean) {
|
fun setModuleVisible(id: String, visible: Boolean) {
|
||||||
_pendingEnabled.update { it + (id to visible) }
|
_pendingEnabled.update { it + (id to visible) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val existing = gateway.getById(id)
|
if (gateway.getById(id) != null) gateway.setEnabled(id, visible)
|
||||||
if (existing != null) {
|
else {
|
||||||
gateway.setEnabled(id, visible)
|
|
||||||
} else {
|
|
||||||
// 如果模块尚未入库(虚拟状态),则根据 ID 规则解析并入库
|
|
||||||
val parts = id.split("::")
|
val parts = id.split("::")
|
||||||
if (parts.size >= 3) {
|
if (parts.size >= 3) {
|
||||||
val setId = parts[0]
|
val setId = parts[0];
|
||||||
val sourceUrl = parts[1]
|
val sourceUrl = parts[1]
|
||||||
val key = parts.subList(2, parts.size).joinToString("::")
|
val key = parts.subList(2, parts.size).joinToString("::")
|
||||||
ensureModuleInDb(sourceUrl, key, id, setId)
|
ensureModuleInDb(sourceUrl, key, id, setId)
|
||||||
@@ -520,24 +497,25 @@ class HomepageViewModel(
|
|||||||
|
|
||||||
private suspend fun ensureSetForSource(sourceUrl: String, sourceName: String): String {
|
private suspend fun ensureSetForSource(sourceUrl: String, sourceName: String): String {
|
||||||
val setId = "src_$sourceUrl"
|
val setId = "src_$sourceUrl"
|
||||||
if (gateway.getCustomSetById(setId) == null) {
|
if (gateway.getCustomSetById(setId) == null) gateway.upsertCustomSet(
|
||||||
gateway.upsertCustomSet(CustomSetItem(id = setId, name = sourceName))
|
CustomSetItem(
|
||||||
}
|
id = setId,
|
||||||
|
name = sourceName
|
||||||
|
)
|
||||||
|
)
|
||||||
return setId
|
return setId
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addCustomModule(sourceUrl: String, targetSetId: String?, def: ModuleDef) {
|
fun addCustomModule(sourceUrl: String, targetSetId: String?, def: ModuleDef) {
|
||||||
val key = def.key.ifBlank { def.title }
|
val key = def.key.ifBlank { def.title };
|
||||||
val setId = targetSetId ?: "src_$sourceUrl"
|
val setId = targetSetId ?: "src_$sourceUrl"
|
||||||
|
|
||||||
if (isInfinite(def.type, def.layoutConfig)) {
|
if (isInfinite(def.type, def.layoutConfig)) {
|
||||||
val hasInfinite = allModulesCache.value.any {
|
if (allModulesCache.value.any {
|
||||||
it.customSetId == setId && isInfinite(
|
it.customSetId == setId && isInfinite(
|
||||||
it.type,
|
it.type,
|
||||||
it.layoutConfig
|
it.layoutConfig
|
||||||
)
|
)
|
||||||
}
|
}) {
|
||||||
if (hasInfinite) {
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_effects.emit(
|
_effects.emit(
|
||||||
HomepageEffect.ShowSnackbar(
|
HomepageEffect.ShowSnackbar(
|
||||||
@@ -550,21 +528,11 @@ class HomepageViewModel(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val id = ModuleDef.globalIdOf(sourceUrl, key, setId)
|
val id = ModuleDef.globalIdOf(sourceUrl, key, setId)
|
||||||
val module = ModuleItem(
|
val module = ModuleItem(
|
||||||
id = id,
|
id = id, sourceUrl = sourceUrl, moduleKey = key, type = def.type, title = def.title,
|
||||||
sourceUrl = sourceUrl,
|
args = def.args, layoutConfig = def.layoutConfig, url = def.url, isEnabled = true,
|
||||||
moduleKey = key,
|
isUserCreated = true, customSetId = setId, syncedAt = System.currentTimeMillis()
|
||||||
type = def.type,
|
|
||||||
title = def.title,
|
|
||||||
args = def.args,
|
|
||||||
layoutConfig = def.layoutConfig,
|
|
||||||
url = def.url,
|
|
||||||
isEnabled = true,
|
|
||||||
isUserCreated = true,
|
|
||||||
customSetId = setId,
|
|
||||||
syncedAt = System.currentTimeMillis(),
|
|
||||||
)
|
)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val source = bookSourceRepository.getBookSource(sourceUrl)
|
val source = bookSourceRepository.getBookSource(sourceUrl)
|
||||||
@@ -575,23 +543,37 @@ class HomepageViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourceExploreKinds(sourceUrl: String): List<Pair<String, String>> {
|
fun getSourceExploreKinds(sourceUrl: String): List<Pair<String, String>> =
|
||||||
return _exploreKindsCache.value[sourceUrl].orEmpty()
|
_exploreKindsCache.value[sourceUrl].orEmpty()
|
||||||
}
|
|
||||||
|
|
||||||
fun updateModule(globalId: String, def: ModuleDef) {
|
fun updateModule(globalId: String, def: ModuleDef) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val existing = gateway.getById(globalId) ?: return@launch
|
val existing = gateway.getById(globalId) ?: return@launch
|
||||||
|
// 校验:如果是变更为无限流,检查集内是否已有其他无限流模块
|
||||||
|
if (isInfinite(def.type, def.layoutConfig)) {
|
||||||
|
val hasOtherInfinite = allModulesCache.value.any {
|
||||||
|
it.customSetId == existing.customSetId && it.id != globalId && isInfinite(
|
||||||
|
it.type,
|
||||||
|
it.layoutConfig
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (hasOtherInfinite) {
|
||||||
|
_effects.emit(
|
||||||
|
HomepageEffect.ShowSnackbar(
|
||||||
|
getApplication<Application>().getString(
|
||||||
|
R.string.homepage_module_duplicate_infinite
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
gateway.upsertAll(
|
gateway.upsertAll(
|
||||||
listOf(
|
listOf(
|
||||||
existing.copy(
|
existing.copy(
|
||||||
customTitle = def.title.takeIf { it != existing.title },
|
customTitle = def.title.takeIf { it != existing.title }, type = def.type,
|
||||||
type = def.type,
|
url = def.url, args = def.args, layoutConfig = def.layoutConfig,
|
||||||
url = def.url,
|
isUserCreated = true, syncedAt = System.currentTimeMillis()
|
||||||
args = def.args,
|
|
||||||
layoutConfig = def.layoutConfig,
|
|
||||||
isUserCreated = true, // 标记为用户编辑,阻止 JSON 同步覆盖
|
|
||||||
syncedAt = System.currentTimeMillis(),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -601,17 +583,17 @@ class HomepageViewModel(
|
|||||||
|
|
||||||
fun setModuleCustomSetTitle(globalId: String, customSetTitle: String?) {
|
fun setModuleCustomSetTitle(globalId: String, customSetTitle: String?) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
gateway.setCustomSetTitle(globalId, customSetTitle)
|
gateway.setCustomSetTitle(
|
||||||
notifyConfigChanged()
|
globalId,
|
||||||
|
customSetTitle
|
||||||
|
); notifyConfigChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteModule(globalId: String) {
|
fun deleteModule(globalId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
gateway.delete(globalId)
|
gateway.delete(globalId); _moduleContentStates.update { it - globalId }
|
||||||
_moduleContentStates.update { it - globalId }
|
loadJobs.remove(globalId)?.cancel(); _pendingEnabled.update { it - globalId }
|
||||||
loadJobs.remove(globalId)?.cancel()
|
|
||||||
_pendingEnabled.update { it - globalId }
|
|
||||||
_pendingUserModules.update { it.filter { m -> m.id != globalId } }
|
_pendingUserModules.update { it.filter { m -> m.id != globalId } }
|
||||||
notifyConfigChanged()
|
notifyConfigChanged()
|
||||||
}
|
}
|
||||||
@@ -620,30 +602,23 @@ class HomepageViewModel(
|
|||||||
fun reorderJoinedModules(orderedIds: List<String>) {
|
fun reorderJoinedModules(orderedIds: List<String>) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val orders = orderedIds.mapIndexed { index, id -> id to index }.toMap()
|
val orders = orderedIds.mapIndexed { index, id -> id to index }.toMap()
|
||||||
gateway.batchSetSortOrders(orders)
|
gateway.batchSetSortOrders(orders); notifyConfigChanged()
|
||||||
notifyConfigChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reorderCustomSets(orderedUrls: List<String>) {
|
fun reorderCustomSets(orderedUrls: List<String>) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val orders = orderedUrls.mapIndexed { index, url ->
|
val orders =
|
||||||
customSetIdFromUrl(url) to index
|
orderedUrls.mapIndexed { index, url -> customSetIdFromUrl(url) to index }.toMap()
|
||||||
}.toMap()
|
gateway.batchSetCustomSetSortOrders(orders); notifyConfigChanged()
|
||||||
gateway.batchSetCustomSetSortOrders(orders)
|
|
||||||
notifyConfigChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取指定集内的模块(sourceUrl 可以是书源 URL 或 custom://xxx) */
|
|
||||||
fun getJoinedModules(sourceUrl: String): List<HomepageModuleManageUi> {
|
fun getJoinedModules(sourceUrl: String): List<HomepageModuleManageUi> {
|
||||||
val isSet = isCustomSetUrl(sourceUrl)
|
val isSet = isCustomSetUrl(sourceUrl);
|
||||||
val setId = if (isSet) customSetIdFromUrl(sourceUrl) else null
|
val setId = if (isSet) customSetIdFromUrl(sourceUrl) else null
|
||||||
val dbModules = if (isSet) {
|
val dbModules =
|
||||||
allModulesCache.value.filter { it.customSetId == setId }
|
if (isSet) allModulesCache.value.filter { it.customSetId == setId } else allModulesCache.value.filter { it.sourceUrl == sourceUrl }
|
||||||
} else {
|
|
||||||
allModulesCache.value.filter { it.sourceUrl == sourceUrl }
|
|
||||||
}
|
|
||||||
val dbIds = dbModules.map { it.id }.toSet()
|
val dbIds = dbModules.map { it.id }.toSet()
|
||||||
val pendingModules = _pendingUserModules.value.filter { pending ->
|
val pendingModules = _pendingUserModules.value.filter { pending ->
|
||||||
val matches =
|
val matches =
|
||||||
@@ -653,67 +628,70 @@ class HomepageViewModel(
|
|||||||
return (dbModules + pendingModules).map { uiFromModule(it) }
|
return (dbModules + pendingModules).map { uiFromModule(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 所有已添加的模块,按书源分组(用于自定义集添加模块) */
|
fun getAllModulesGroupedBySource(): Map<String, List<HomepageModuleManageUi>> =
|
||||||
fun getAllModulesGroupedBySource(): Map<String, List<HomepageModuleManageUi>> {
|
allModulesCache.value.distinctBy { it.sourceUrl to it.moduleKey }.map { uiFromModule(it) }
|
||||||
return allModulesCache.value
|
|
||||||
.distinctBy { it.sourceUrl to it.moduleKey }
|
|
||||||
.map { uiFromModule(it) }
|
|
||||||
.groupBy { it.sourceUrl }
|
.groupBy { it.sourceUrl }
|
||||||
}
|
|
||||||
|
|
||||||
fun getSourceName(sourceUrl: String): String {
|
fun getSourceName(sourceUrl: String): String =
|
||||||
return _bookSourcesCache.value[sourceUrl]?.bookSourceName ?: sourceUrl
|
_bookSourcesCache.value[sourceUrl]?.bookSourceName ?: sourceUrl
|
||||||
}
|
|
||||||
|
|
||||||
fun assignModuleToCustomSet(moduleId: String, customSetId: String?) {
|
fun assignModuleToCustomSet(moduleId: String, customSetId: String?) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val existing = gateway.getById(moduleId) ?: return@launch
|
val existing = gateway.getById(moduleId) ?: return@launch
|
||||||
if (customSetId == null) {
|
if (customSetId == null) {
|
||||||
// 如果是取消分配,且它不是归属于书源默认集的,则直接删除该副本
|
if (existing.customSetId != "src_${existing.sourceUrl}") gateway.delete(moduleId)
|
||||||
if (existing.customSetId != "src_${existing.sourceUrl}") {
|
|
||||||
gateway.delete(moduleId)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 核心逻辑:分配 = 复制。生成带新 setId 的 ID
|
// 校验:目标集是否已存在无限模块
|
||||||
|
if (isInfinite(existing.type, existing.layoutConfig)) {
|
||||||
|
val hasInfinite = allModulesCache.value.any {
|
||||||
|
it.customSetId == customSetId && isInfinite(it.type, it.layoutConfig)
|
||||||
|
}
|
||||||
|
if (hasInfinite) {
|
||||||
|
_effects.emit(
|
||||||
|
HomepageEffect.ShowSnackbar(
|
||||||
|
getApplication<Application>().getString(
|
||||||
|
R.string.homepage_module_duplicate_infinite
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
val newId =
|
val newId =
|
||||||
ModuleDef.globalIdOf(existing.sourceUrl, existing.moduleKey, customSetId)
|
ModuleDef.globalIdOf(existing.sourceUrl, existing.moduleKey, customSetId)
|
||||||
val newModule = existing.copy(
|
gateway.upsertAll(
|
||||||
|
listOf(
|
||||||
|
existing.copy(
|
||||||
id = newId,
|
id = newId,
|
||||||
customSetId = customSetId,
|
customSetId = customSetId,
|
||||||
isEnabled = true, // 分配到新集时默认开启
|
isEnabled = true,
|
||||||
isUserCreated = true // 标记为用户创建,避免被同步清理
|
isUserCreated = true
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
gateway.upsertAll(listOf(newModule))
|
|
||||||
}
|
}
|
||||||
notifyConfigChanged()
|
notifyConfigChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun syncSourceModules(sourceUrl: String) {
|
fun syncSourceModules(sourceUrl: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch { resolveBookSource(sourceUrl)?.let { syncModulesFromSource(it) } }
|
||||||
resolveBookSource(sourceUrl)?.let { syncModulesFromSource(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 「书源模块」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 = parseModuleDefs(source, json)
|
||||||
|
|
||||||
val effectiveSetId = targetSetId ?: "src_$sourceUrl"
|
val effectiveSetId = targetSetId ?: "src_$sourceUrl"
|
||||||
val joinedKeys = allModulesCache.value
|
val joinedKeys =
|
||||||
.filter { it.sourceUrl == sourceUrl && it.customSetId == effectiveSetId }
|
allModulesCache.value.filter { it.sourceUrl == sourceUrl && it.customSetId == effectiveSetId }
|
||||||
.map { it.moduleKey }.toSet()
|
.map { it.moduleKey }.toSet()
|
||||||
|
|
||||||
return jsonDefs.map { def ->
|
return jsonDefs.map { def ->
|
||||||
val id = ModuleDef.globalIdOf(sourceUrl, def.key, effectiveSetId)
|
|
||||||
HomepageModuleManageUi(
|
HomepageModuleManageUi(
|
||||||
id = id,
|
id = ModuleDef.globalIdOf(sourceUrl, def.key, effectiveSetId),
|
||||||
sourceUrl = def.sourceUrl,
|
sourceUrl = def.sourceUrl,
|
||||||
moduleKey = def.key,
|
moduleKey = def.key,
|
||||||
title = def.title,
|
title = def.title,
|
||||||
@@ -727,15 +705,13 @@ class HomepageViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从书源模块「加入」→ 写入 DB,自动归属到该书源的集 */
|
|
||||||
/** 从发现页 Kind 创建一个 ButtonGroup 模块,args 存储选中 Kind 标题的 JSON 数组 */
|
|
||||||
fun addButtonGroupFromKinds(
|
fun addButtonGroupFromKinds(
|
||||||
sourceUrl: String,
|
sourceUrl: String,
|
||||||
targetSetId: String?,
|
targetSetId: String?,
|
||||||
title: String,
|
title: String,
|
||||||
kindTitles: List<String>
|
kindTitles: List<String>
|
||||||
) {
|
) {
|
||||||
val key = kindTitles.firstOrNull() ?: title
|
val key = kindTitles.firstOrNull() ?: title;
|
||||||
val setId = targetSetId ?: "src_$sourceUrl"
|
val setId = targetSetId ?: "src_$sourceUrl"
|
||||||
val id = ModuleDef.globalIdOf(sourceUrl, key, setId)
|
val id = ModuleDef.globalIdOf(sourceUrl, key, setId)
|
||||||
val module = ModuleItem(
|
val module = ModuleItem(
|
||||||
@@ -759,52 +735,8 @@ class HomepageViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun joinModule(sourceUrl: String, targetSetId: String?, def: ModuleDef) {
|
fun joinModule(sourceUrl: String, targetSetId: String?, def: ModuleDef) =
|
||||||
val setId = targetSetId ?: "src_$sourceUrl"
|
addCustomModule(sourceUrl, targetSetId, def)
|
||||||
|
|
||||||
if (isInfinite(def.type, def.layoutConfig)) {
|
|
||||||
val hasInfinite = allModulesCache.value.any {
|
|
||||||
it.customSetId == setId && isInfinite(
|
|
||||||
it.type,
|
|
||||||
it.layoutConfig
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (hasInfinite) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_effects.emit(
|
|
||||||
HomepageEffect.ShowSnackbar(
|
|
||||||
getApplication<Application>().getString(
|
|
||||||
R.string.homepage_module_duplicate_infinite
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val id = ModuleDef.globalIdOf(sourceUrl, def.key, setId)
|
|
||||||
val module = ModuleItem(
|
|
||||||
id = id,
|
|
||||||
sourceUrl = sourceUrl,
|
|
||||||
moduleKey = def.key,
|
|
||||||
type = def.type,
|
|
||||||
title = def.title,
|
|
||||||
args = def.args,
|
|
||||||
layoutConfig = def.layoutConfig,
|
|
||||||
url = def.url,
|
|
||||||
isEnabled = true,
|
|
||||||
customSetId = setId,
|
|
||||||
syncedAt = System.currentTimeMillis(),
|
|
||||||
)
|
|
||||||
viewModelScope.launch {
|
|
||||||
val source = bookSourceRepository.getBookSource(sourceUrl)
|
|
||||||
if (source != null) ensureSetForSource(sourceUrl, source.bookSourceName)
|
|
||||||
gateway.upsertAll(listOf(module))
|
|
||||||
_pendingUserModules.update { list -> if (list.any { it.id == id }) list else list + module }
|
|
||||||
notifyConfigChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun uiFromModule(module: ModuleItem) = HomepageModuleManageUi(
|
private fun uiFromModule(module: ModuleItem) = HomepageModuleManageUi(
|
||||||
id = module.id,
|
id = module.id,
|
||||||
@@ -822,27 +754,19 @@ class HomepageViewModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun createCustomSet(name: String) {
|
fun createCustomSet(name: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch { gateway.createCustomSet(name); notifyConfigChanged() }
|
||||||
gateway.createCustomSet(name)
|
|
||||||
notifyConfigChanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun renameCustomSet(id: String, name: String) {
|
fun renameCustomSet(id: String, name: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch { gateway.renameCustomSet(id, name); notifyConfigChanged() }
|
||||||
gateway.renameCustomSet(id, name)
|
|
||||||
notifyConfigChanged()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteCustomSet(id: String) {
|
fun deleteCustomSet(id: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val moduleIds = allModulesCache.value.filter { it.customSetId == id }.map { it.id }
|
val ids = allModulesCache.value.filter { it.customSetId == id }.map { it.id }
|
||||||
gateway.deleteCustomSet(id)
|
gateway.deleteCustomSet(id)
|
||||||
moduleIds.forEach { mid ->
|
ids.forEach { mid ->
|
||||||
_moduleContentStates.update { it - mid }
|
_moduleContentStates.update { it - mid }; loadJobs.remove(mid)
|
||||||
loadJobs.remove(mid)?.cancel()
|
?.cancel(); _pendingEnabled.update { it - mid }
|
||||||
_pendingEnabled.update { it - mid }
|
|
||||||
}
|
}
|
||||||
notifyConfigChanged()
|
notifyConfigChanged()
|
||||||
}
|
}
|
||||||
@@ -853,12 +777,12 @@ class HomepageViewModel(
|
|||||||
saveSearchBooksUseCase.save(book)
|
saveSearchBooksUseCase.save(book)
|
||||||
_effects.emit(
|
_effects.emit(
|
||||||
HomepageEffect.NavigateToBookInfo(
|
HomepageEffect.NavigateToBookInfo(
|
||||||
name = book.name,
|
book.name,
|
||||||
author = book.author,
|
book.author,
|
||||||
bookUrl = book.bookUrl,
|
book.bookUrl,
|
||||||
origin = book.origin,
|
book.origin,
|
||||||
coverPath = book.coverUrl,
|
book.coverUrl,
|
||||||
sharedCoverKey = sharedCoverKey
|
sharedCoverKey
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -866,14 +790,18 @@ class HomepageViewModel(
|
|||||||
|
|
||||||
fun onModuleHeaderClick(sourceUrl: String, exploreUrl: String?, title: String?) {
|
fun onModuleHeaderClick(sourceUrl: String, exploreUrl: String?, title: String?) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_effects.emit(HomepageEffect.NavigateToExploreShow(title, sourceUrl, exploreUrl))
|
_effects.emit(
|
||||||
|
HomepageEffect.NavigateToExploreShow(
|
||||||
|
title,
|
||||||
|
sourceUrl,
|
||||||
|
exploreUrl
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveBookSource(sourceUrl: String): BookSource? {
|
private fun resolveBookSource(sourceUrl: String): BookSource? =
|
||||||
return _bookSourcesCache.value[sourceUrl]
|
_bookSourcesCache.value[sourceUrl] ?: bookSourceRepository.getBookSourceSync(sourceUrl)
|
||||||
?: bookSourceRepository.getBookSourceSync(sourceUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun ensureModuleInDb(
|
private suspend fun ensureModuleInDb(
|
||||||
sourceUrl: String,
|
sourceUrl: String,
|
||||||
@@ -883,8 +811,7 @@ class HomepageViewModel(
|
|||||||
) {
|
) {
|
||||||
if (gateway.getById(id) != null) return
|
if (gateway.getById(id) != null) return
|
||||||
val source = resolveBookSource(sourceUrl) ?: return
|
val source = resolveBookSource(sourceUrl) ?: return
|
||||||
val json = source.homepageModules ?: return
|
val defs = parseModuleDefs(source, source.homepageModules ?: return)
|
||||||
val defs = parseBookSourceModules(source, json)
|
|
||||||
val def = defs.find { it.key == moduleKey } ?: return
|
val def = defs.find { it.key == moduleKey } ?: return
|
||||||
gateway.upsertAll(
|
gateway.upsertAll(
|
||||||
listOf(
|
listOf(
|
||||||
@@ -906,10 +833,6 @@ class HomepageViewModel(
|
|||||||
private fun notifyConfigChanged() {
|
private fun notifyConfigChanged() {
|
||||||
_configVersion.update { it + 1 }
|
_configVersion.update { it + 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseBookSourceModules(source: BookSource, json: String): List<ModuleDef> =
|
|
||||||
parseModuleDefs(source, json)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class HomepageUiFlags(
|
private data class HomepageUiFlags(
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package io.legado.app.ui.main.homepage.manage
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.domain.model.HomepageModuleType
|
||||||
|
import io.legado.app.domain.model.ModuleDef
|
||||||
|
import io.legado.app.ui.main.homepage.HomepageViewModel
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.widget.components.AppTextField
|
||||||
|
import io.legado.app.ui.widget.components.JsonConfigEditor
|
||||||
|
import io.legado.app.ui.widget.components.JsonRawEditor
|
||||||
|
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.divider.PillHeaderDivider
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.CompactClickableSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
|
||||||
|
data class AddDialogPrefill(
|
||||||
|
val title: String = "",
|
||||||
|
val url: String = "",
|
||||||
|
val type: String = "card"
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> AddCustomModuleDialog(
|
||||||
|
data: T?,
|
||||||
|
sourceUrl: String = "",
|
||||||
|
targetSetId: String = "",
|
||||||
|
prefillTitle: String = "",
|
||||||
|
prefillUrl: String = "",
|
||||||
|
prefillType: String = "card",
|
||||||
|
prefillArgs: String = "",
|
||||||
|
prefillLayoutConfig: String = "",
|
||||||
|
canSelectInfinite: Boolean = true,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onConfirm: (ModuleDef) -> Unit,
|
||||||
|
) {
|
||||||
|
var title by remember(data) { mutableStateOf(prefillTitle) }
|
||||||
|
var url by remember(data) { mutableStateOf(prefillUrl) }
|
||||||
|
var type by remember(data) { mutableStateOf(prefillType) }
|
||||||
|
var args by remember(data) { mutableStateOf(prefillArgs) }
|
||||||
|
var layoutConfig by remember(data) { mutableStateOf(prefillLayoutConfig) }
|
||||||
|
var showRawLayoutConfig by remember(data) { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val hasVisualizableKeys = remember(layoutConfig) {
|
||||||
|
runCatching {
|
||||||
|
val jsonObject = JsonParser.parseString(layoutConfig).asJsonObject
|
||||||
|
jsonObject.keySet().any { key ->
|
||||||
|
key == "columns" || key == "rows"
|
||||||
|
}
|
||||||
|
}.getOrElse { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
AppAlertDialog(
|
||||||
|
data = data,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = if (prefillTitle.isEmpty()) stringResource(R.string.homepage_add_module) else stringResource(
|
||||||
|
R.string.homepage_edit_module
|
||||||
|
),
|
||||||
|
content = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(400.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
AppTextField(
|
||||||
|
value = title,
|
||||||
|
onValueChange = { title = it },
|
||||||
|
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
label = stringResource(R.string.homepage_title_label),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
AppTextField(
|
||||||
|
value = url,
|
||||||
|
onValueChange = { url = it },
|
||||||
|
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
label = "URL",
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
val typeList = remember(canSelectInfinite) {
|
||||||
|
HomepageModuleType.entries.filter {
|
||||||
|
it != HomepageModuleType.Unknown && (canSelectInfinite || !HomepageViewModel.isInfinite(
|
||||||
|
it.key,
|
||||||
|
null
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlassCard(
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent
|
||||||
|
) {
|
||||||
|
DropdownListSettingItem(
|
||||||
|
title = stringResource(R.string.homepage_type_label),
|
||||||
|
selectedValue = type,
|
||||||
|
displayEntries = typeList.map { it.title }.toTypedArray(),
|
||||||
|
entryValues = typeList.map { it.key }.toTypedArray(),
|
||||||
|
onValueChange = { type = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HomepageViewModel.isInfinite(type, null) && !canSelectInfinite) {
|
||||||
|
AppText(
|
||||||
|
text = stringResource(R.string.homepage_module_duplicate_infinite),
|
||||||
|
color = LegadoTheme.colorScheme.error,
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AppTextField(
|
||||||
|
value = args,
|
||||||
|
onValueChange = { args = it },
|
||||||
|
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
label = "Args (JSON)",
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
PillHeaderDivider(
|
||||||
|
title = stringResource(R.string.homepage_layout_config_label)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasVisualizableKeys) {
|
||||||
|
JsonConfigEditor(
|
||||||
|
jsonString = layoutConfig,
|
||||||
|
onJsonStringChange = { layoutConfig = it },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
CompactClickableSettingItem(
|
||||||
|
title = stringResource(R.string.homepage_edit_raw_json),
|
||||||
|
onClick = { showRawLayoutConfig = !showRawLayoutConfig }
|
||||||
|
)
|
||||||
|
if (showRawLayoutConfig) {
|
||||||
|
JsonRawEditor(
|
||||||
|
value = layoutConfig,
|
||||||
|
onValueChange = { layoutConfig = it },
|
||||||
|
label = "LayoutConfig (JSON) RAW",
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
JsonRawEditor(
|
||||||
|
value = layoutConfig,
|
||||||
|
onValueChange = { layoutConfig = it },
|
||||||
|
label = "LayoutConfig (JSON)",
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onConfirm = {
|
||||||
|
onConfirm(
|
||||||
|
ModuleDef(
|
||||||
|
title = title,
|
||||||
|
url = url,
|
||||||
|
type = type,
|
||||||
|
args = args,
|
||||||
|
layoutConfig = layoutConfig
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmText = stringResource(R.string.dialog_confirm),
|
||||||
|
dismissText = stringResource(R.string.dialog_cancel),
|
||||||
|
onDismiss = onDismissRequest
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package io.legado.app.ui.main.homepage.manage
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.main.homepage.HomepageModuleManageUi
|
||||||
|
import io.legado.app.ui.main.homepage.HomepageSourceManageUi
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BrowseSourcesPage(
|
||||||
|
browseSources: ImmutableList<HomepageSourceManageUi>,
|
||||||
|
getSourceModules: (String, String?) -> List<HomepageModuleManageUi>,
|
||||||
|
onSelectSource: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
val sources = remember(browseSources) {
|
||||||
|
browseSources.distinctBy { it.sourceUrl }
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(sources, key = { it.sourceUrl }) { source ->
|
||||||
|
val moduleCount = getSourceModules(source.sourceUrl, null).size
|
||||||
|
SelectionItemCard(
|
||||||
|
title = source.sourceName,
|
||||||
|
subtitle = stringResource(R.string.homepage_n_modules, moduleCount),
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
onToggleSelection = {
|
||||||
|
onSelectSource(source.sourceUrl)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package io.legado.app.ui.main.homepage.manage
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
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.runtime.Composable
|
||||||
|
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.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.main.homepage.HomepageModuleManageUi
|
||||||
|
import io.legado.app.ui.main.homepage.HomepageViewModel
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||||
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CustomSetAddModulesPage(
|
||||||
|
setUrl: String,
|
||||||
|
allJoinedModules: ImmutableList<HomepageModuleManageUi>,
|
||||||
|
sourceNames: Map<String, String>,
|
||||||
|
onToggleModuleToSet: (HomepageModuleManageUi, inCurrentSet: Boolean, isBlocked: Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
val setId = HomepageViewModel.customSetIdFromUrl(setUrl)
|
||||||
|
val initialJoined = allJoinedModules
|
||||||
|
.filter { it.customSetId == setId }
|
||||||
|
.associateBy({ it.moduleKey }, { it.id })
|
||||||
|
var joinedInCurrent by remember(initialJoined) { mutableStateOf(initialJoined) }
|
||||||
|
|
||||||
|
val hasInfiniteInCurrentSet = remember(allJoinedModules) {
|
||||||
|
allJoinedModules.any {
|
||||||
|
it.customSetId == setId && HomepageViewModel.isInfinite(
|
||||||
|
it.type,
|
||||||
|
it.layoutConfig
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val grouped = remember(allJoinedModules) {
|
||||||
|
allJoinedModules
|
||||||
|
.distinctBy { it.sourceUrl to it.moduleKey }
|
||||||
|
.groupBy { it.sourceUrl }
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
grouped.forEach { (sourceUrl, modules) ->
|
||||||
|
item(key = "header_$sourceUrl") {
|
||||||
|
AppText(
|
||||||
|
text = sourceNames[sourceUrl] ?: sourceUrl,
|
||||||
|
style = LegadoTheme.typography.labelLarge,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(modules, key = { it.id }) { module ->
|
||||||
|
val instanceIdInCurrentSet = joinedInCurrent[module.moduleKey]
|
||||||
|
val inCurrentSet = instanceIdInCurrentSet != null
|
||||||
|
val isInfinite = HomepageViewModel.isInfinite(module.type, module.layoutConfig)
|
||||||
|
val isBlocked = !inCurrentSet && isInfinite && hasInfiniteInCurrentSet
|
||||||
|
|
||||||
|
SelectionItemCard(
|
||||||
|
title = module.title,
|
||||||
|
subtitle = module.moduleKey + if (isBlocked) " (${stringResource(R.string.homepage_module_duplicate_infinite)})" else "",
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
isSelected = inCurrentSet,
|
||||||
|
inSelectionMode = true,
|
||||||
|
isEnabled = !isBlocked,
|
||||||
|
onToggleSelection = {
|
||||||
|
onToggleModuleToSet(module, inCurrentSet, isBlocked)
|
||||||
|
if (inCurrentSet) {
|
||||||
|
joinedInCurrent = joinedInCurrent - module.moduleKey
|
||||||
|
} else if (!isBlocked) {
|
||||||
|
joinedInCurrent =
|
||||||
|
joinedInCurrent + (module.moduleKey to "temp_${module.id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package io.legado.app.ui.main.homepage.manage
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
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.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.domain.model.HomepageModuleType
|
||||||
|
import io.legado.app.ui.main.homepage.HomepageModuleManageUi
|
||||||
|
import io.legado.app.ui.main.homepage.HomepageViewModel
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.widget.components.button.SecondaryButton
|
||||||
|
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||||
|
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
|
||||||
|
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||||
|
import io.legado.app.ui.widget.components.divider.PillDivider
|
||||||
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
import io.legado.app.utils.move
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SetDetailPage(
|
||||||
|
setUrl: String,
|
||||||
|
allJoinedModules: ImmutableList<HomepageModuleManageUi>,
|
||||||
|
onToggleModule: (String, Boolean) -> Unit,
|
||||||
|
onReorderModules: (List<String>) -> Unit,
|
||||||
|
onEditModule: (HomepageModuleManageUi) -> Unit,
|
||||||
|
onRequestDeleteModule: (String) -> Unit,
|
||||||
|
onBrowseSourceModules: () -> Unit,
|
||||||
|
onAddModules: () -> Unit,
|
||||||
|
) {
|
||||||
|
val setId = HomepageViewModel.customSetIdFromUrl(setUrl)
|
||||||
|
val modules = remember(setId, allJoinedModules) {
|
||||||
|
allJoinedModules.filter { it.customSetId == setId }.distinctBy { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
val standardModules = remember(modules) {
|
||||||
|
modules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
||||||
|
}
|
||||||
|
val infiniteModules = remember(modules) {
|
||||||
|
modules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modules.isEmpty()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
AppText(stringResource(R.string.homepage_no_modules))
|
||||||
|
SecondaryButton(
|
||||||
|
text = stringResource(R.string.homepage_browse_to_add),
|
||||||
|
onClick = {
|
||||||
|
if (setId.startsWith("src_")) onBrowseSourceModules()
|
||||||
|
else onAddModules()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var listData by remember(setUrl, standardModules) {
|
||||||
|
mutableStateOf(standardModules)
|
||||||
|
}
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
|
||||||
|
listData = listData.toMutableList().apply {
|
||||||
|
if (isEmpty()) return@apply
|
||||||
|
val fromIndex = (from.index - 1).coerceIn(0, lastIndex)
|
||||||
|
val toIndex = (to.index - 1).coerceIn(0, lastIndex)
|
||||||
|
if (fromIndex in indices && toIndex in indices) {
|
||||||
|
move(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(standardModules) {
|
||||||
|
if (!reorderableState.isAnyItemDragging) listData = standardModules
|
||||||
|
}
|
||||||
|
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
||||||
|
if (!reorderableState.isAnyItemDragging) {
|
||||||
|
val orderedIds =
|
||||||
|
(listData.map { it.id } + infiniteModules.map { it.id }).distinct()
|
||||||
|
if (orderedIds != modules.map { it.id }) onReorderModules(orderedIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
if (listData.isNotEmpty()) {
|
||||||
|
item(key = "header_std_detail") {
|
||||||
|
AppText(
|
||||||
|
text = stringResource(R.string.homepage_standard_module),
|
||||||
|
style = LegadoTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(listData, key = { it.id }) { module ->
|
||||||
|
ReorderableSelectionItem(
|
||||||
|
state = reorderableState,
|
||||||
|
key = module.id,
|
||||||
|
title = module.title,
|
||||||
|
subtitle = HomepageModuleType.fromKey(module.type).title,
|
||||||
|
isEnabled = module.isVisible,
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
onEnabledChange = { enabled ->
|
||||||
|
onToggleModule(module.id, enabled)
|
||||||
|
listData = listData.map {
|
||||||
|
if (it.id == module.id) it.copy(isVisible = enabled) else it
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingAction = {
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { onEditModule(module) },
|
||||||
|
imageVector = Icons.Default.Edit
|
||||||
|
)
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { onRequestDeleteModule(module.id) },
|
||||||
|
imageVector = Icons.Default.Delete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infiniteModules.isNotEmpty()) {
|
||||||
|
item(key = "header_inf_detail") {
|
||||||
|
PillDivider()
|
||||||
|
AppText(
|
||||||
|
text = stringResource(R.string.homepage_infinite_module_slot),
|
||||||
|
style = LegadoTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(infiniteModules, key = { it.id }) { module ->
|
||||||
|
val isEffective = infiniteModules.firstOrNull() == module
|
||||||
|
SelectionItemCard(
|
||||||
|
title = module.title,
|
||||||
|
subtitle = HomepageModuleType.fromKey(module.type).title,
|
||||||
|
isEnabled = module.isVisible,
|
||||||
|
containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
onEnabledChange = { onToggleModule(module.id, it) },
|
||||||
|
trailingAction = {
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { onEditModule(module) },
|
||||||
|
imageVector = Icons.Default.Edit
|
||||||
|
)
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { onRequestDeleteModule(module.id) },
|
||||||
|
imageVector = Icons.Default.Delete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item(key = "browse_from_set") {
|
||||||
|
SecondaryButton(
|
||||||
|
text = stringResource(R.string.homepage_browse_source_modules),
|
||||||
|
onClick = {
|
||||||
|
if (setId.startsWith("src_")) onBrowseSourceModules()
|
||||||
|
else onAddModules()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package io.legado.app.ui.main.homepage.manage
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
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.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.DriveFileRenameOutline
|
||||||
|
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.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.main.homepage.HomepageSourceManageUi
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.widget.components.button.SecondaryButton
|
||||||
|
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||||
|
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
|
||||||
|
import io.legado.app.ui.widget.components.divider.PillDivider
|
||||||
|
import io.legado.app.utils.move
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SetListPage(
|
||||||
|
sets: ImmutableList<HomepageSourceManageUi>,
|
||||||
|
onToggleSet: (String, Boolean) -> Unit,
|
||||||
|
onReorderSets: (List<String>) -> Unit,
|
||||||
|
onSelectSet: (String) -> Unit,
|
||||||
|
onRenameSet: (String) -> Unit,
|
||||||
|
onDeleteSet: (String) -> Unit,
|
||||||
|
onCreateSet: () -> Unit,
|
||||||
|
onBrowseSources: () -> Unit,
|
||||||
|
) {
|
||||||
|
var localSets by remember { mutableStateOf(sets.distinctBy { it.sourceUrl }) }
|
||||||
|
val setsListState = rememberLazyListState()
|
||||||
|
val setsReorderableState =
|
||||||
|
rememberReorderableLazyListState(setsListState) { from, to ->
|
||||||
|
localSets = localSets.toMutableList().apply {
|
||||||
|
if (isEmpty()) return@apply
|
||||||
|
val fromIndex = from.index.coerceIn(0, lastIndex)
|
||||||
|
val toIndex = to.index.coerceIn(0, lastIndex)
|
||||||
|
if (fromIndex in indices && toIndex in indices) {
|
||||||
|
move(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(sets) {
|
||||||
|
if (!setsReorderableState.isAnyItemDragging) localSets = sets.distinctBy { it.sourceUrl }
|
||||||
|
}
|
||||||
|
LaunchedEffect(setsReorderableState.isAnyItemDragging) {
|
||||||
|
if (!setsReorderableState.isAnyItemDragging) {
|
||||||
|
val orderedUrls = localSets.map { it.sourceUrl }
|
||||||
|
if (orderedUrls != sets.map { it.sourceUrl }) onReorderSets(orderedUrls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
state = setsListState,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(localSets, key = { it.sourceUrl }) { set ->
|
||||||
|
ReorderableSelectionItem(
|
||||||
|
state = setsReorderableState,
|
||||||
|
key = set.sourceUrl,
|
||||||
|
title = set.sourceName,
|
||||||
|
subtitle = stringResource(R.string.homepage_n_modules, set.moduleCount),
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
isEnabled = set.isSelected,
|
||||||
|
onToggleSelection = { onSelectSet(set.sourceUrl) },
|
||||||
|
onEnabledChange = { enabled ->
|
||||||
|
onToggleSet(set.sourceUrl, enabled)
|
||||||
|
localSets = localSets.map {
|
||||||
|
if (it.sourceUrl == set.sourceUrl) it.copy(isSelected = enabled) else it
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingAction = {
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { onRenameSet(set.sourceUrl) },
|
||||||
|
imageVector = Icons.Default.DriveFileRenameOutline
|
||||||
|
)
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { onDeleteSet(set.sourceUrl) },
|
||||||
|
imageVector = Icons.Default.Delete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
PillDivider(
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item(key = "create_set") {
|
||||||
|
SecondaryButton(
|
||||||
|
text = stringResource(R.string.homepage_new_custom_set),
|
||||||
|
onClick = onCreateSet,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item(key = "browse_sources") {
|
||||||
|
SecondaryButton(
|
||||||
|
text = stringResource(R.string.homepage_browse_source_modules),
|
||||||
|
onClick = onBrowseSources,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
package io.legado.app.ui.main.homepage.manage
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
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.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.domain.model.HomepageModuleType
|
||||||
|
import io.legado.app.domain.model.ModuleDef
|
||||||
|
import io.legado.app.ui.main.homepage.HomepageModuleManageUi
|
||||||
|
import io.legado.app.ui.main.homepage.HomepageViewModel
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.widget.components.button.SecondaryButton
|
||||||
|
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||||
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
|
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
|
||||||
|
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||||
|
import io.legado.app.ui.widget.components.divider.PillDivider
|
||||||
|
import io.legado.app.ui.widget.components.explore.ExploreKindSelectSheet
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||||
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
import io.legado.app.utils.move
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SourceBrowseDetailPage(
|
||||||
|
browseUrl: String,
|
||||||
|
selectingSetUrl: String?,
|
||||||
|
allJoinedModules: ImmutableList<HomepageModuleManageUi>,
|
||||||
|
canSelectInfiniteGlobal: Boolean,
|
||||||
|
onGetSourceModules: (String, String?) -> List<HomepageModuleManageUi>,
|
||||||
|
onGetExploreKinds: (String) -> List<Pair<String, String>>,
|
||||||
|
onToggleModule: (String, Boolean) -> Unit,
|
||||||
|
onJoinModule: (String, String?, ModuleDef) -> Unit,
|
||||||
|
onRequestDeleteModule: (String) -> Unit,
|
||||||
|
onReorderModules: (List<String>) -> Unit,
|
||||||
|
onEditModule: (HomepageModuleManageUi) -> Unit,
|
||||||
|
onAddDialogPrefill: (AddDialogPrefill?) -> Unit,
|
||||||
|
onShowAddButtonGroupDialog: () -> Unit,
|
||||||
|
browseTab: Int,
|
||||||
|
onBrowseTabChange: (Int) -> Unit,
|
||||||
|
browseModuleType: String,
|
||||||
|
onBrowseModuleTypeChange: (String) -> Unit,
|
||||||
|
selectedKindTitles: Set<String>,
|
||||||
|
onSelectedKindTitlesChange: (Set<String>) -> Unit,
|
||||||
|
) {
|
||||||
|
var showKindSelect by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val displaySetUrl =
|
||||||
|
selectingSetUrl ?: HomepageViewModel.customSetUrl("src_$browseUrl")
|
||||||
|
val currentSetId = HomepageViewModel.customSetIdFromUrl(displaySetUrl)
|
||||||
|
|
||||||
|
val joinedModules = remember(displaySetUrl, allJoinedModules) {
|
||||||
|
allJoinedModules.filter { it.customSetId == currentSetId }
|
||||||
|
}
|
||||||
|
|
||||||
|
val standardModules = remember(joinedModules) {
|
||||||
|
joinedModules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
||||||
|
}
|
||||||
|
val infiniteModules = remember(joinedModules) {
|
||||||
|
joinedModules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
|
||||||
|
}
|
||||||
|
val hasInfiniteInSet = infiniteModules.isNotEmpty()
|
||||||
|
|
||||||
|
val joinedKeys = joinedModules.map { it.moduleKey }.toSet()
|
||||||
|
val sourceModules = onGetSourceModules(browseUrl, currentSetId)
|
||||||
|
Column {
|
||||||
|
AppTabRow(
|
||||||
|
tabTitles = listOf(
|
||||||
|
stringResource(R.string.homepage_tab_joined),
|
||||||
|
stringResource(R.string.homepage_tab_source_modules),
|
||||||
|
stringResource(R.string.homepage_tab_discover)
|
||||||
|
),
|
||||||
|
selectedTabIndex = browseTab,
|
||||||
|
onTabSelected = { onBrowseTabChange(it) }
|
||||||
|
)
|
||||||
|
when (browseTab) {
|
||||||
|
0 -> {
|
||||||
|
if (joinedModules.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
AppText(stringResource(R.string.homepage_no_joined_modules))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var listData by remember(displaySetUrl, standardModules) {
|
||||||
|
mutableStateOf(standardModules.distinctBy { it.id })
|
||||||
|
}
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val reorderableState =
|
||||||
|
rememberReorderableLazyListState(listState) { from, to ->
|
||||||
|
listData = listData.toMutableList().apply {
|
||||||
|
if (isEmpty()) return@apply
|
||||||
|
val fromIndex = (from.index - 1).coerceIn(0, lastIndex)
|
||||||
|
val toIndex = (to.index - 1).coerceIn(0, lastIndex)
|
||||||
|
if (fromIndex in indices && toIndex in indices) {
|
||||||
|
move(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(standardModules) {
|
||||||
|
if (!reorderableState.isAnyItemDragging) listData =
|
||||||
|
standardModules.distinctBy { it.id }
|
||||||
|
}
|
||||||
|
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
||||||
|
if (!reorderableState.isAnyItemDragging) {
|
||||||
|
val orderedIds =
|
||||||
|
(listData.map { it.id } + infiniteModules.map { it.id }).distinct()
|
||||||
|
if (orderedIds != joinedModules.map { it.id }) {
|
||||||
|
onReorderModules(orderedIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
item(key = "header_standard") {
|
||||||
|
AppText(
|
||||||
|
text = stringResource(R.string.homepage_standard_modules_sortable),
|
||||||
|
style = LegadoTheme.typography.labelMedium,
|
||||||
|
color = LegadoTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
horizontal = 16.dp,
|
||||||
|
vertical = 4.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(listData, key = { it.id }) { module ->
|
||||||
|
ReorderableSelectionItem(
|
||||||
|
state = reorderableState,
|
||||||
|
key = module.id,
|
||||||
|
title = module.title,
|
||||||
|
subtitle = HomepageModuleType.fromKey(module.type).title,
|
||||||
|
isEnabled = module.isVisible,
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
onEnabledChange = { enabled ->
|
||||||
|
onToggleModule(module.id, enabled)
|
||||||
|
listData = listData.map {
|
||||||
|
if (it.id == module.id) it.copy(isVisible = enabled) else it
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingAction = {
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { onEditModule(module) },
|
||||||
|
imageVector = Icons.Default.Edit
|
||||||
|
)
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { onRequestDeleteModule(module.id) },
|
||||||
|
imageVector = Icons.Default.Delete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infiniteModules.isNotEmpty()) {
|
||||||
|
item(key = "header_infinite") {
|
||||||
|
PillDivider(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
vertical = 8.dp,
|
||||||
|
horizontal = 16.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AppText(
|
||||||
|
text = stringResource(R.string.homepage_infinite_module_bottom),
|
||||||
|
style = LegadoTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
horizontal = 16.dp,
|
||||||
|
vertical = 4.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(infiniteModules, key = { it.id }) { module ->
|
||||||
|
val isEffective =
|
||||||
|
infiniteModules.firstOrNull() == module
|
||||||
|
SelectionItemCard(
|
||||||
|
title = module.title,
|
||||||
|
subtitle = HomepageModuleType.fromKey(module.type).title + if (isEffective) stringResource(
|
||||||
|
R.string.homepage_status_in_effect
|
||||||
|
) else stringResource(R.string.homepage_status_blocked),
|
||||||
|
isEnabled = module.isVisible,
|
||||||
|
containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
onEnabledChange = { onToggleModule(module.id, it) },
|
||||||
|
trailingAction = {
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { onEditModule(module) },
|
||||||
|
imageVector = Icons.Default.Edit
|
||||||
|
)
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { onRequestDeleteModule(module.id) },
|
||||||
|
imageVector = Icons.Default.Delete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
if (sourceModules.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
AppText(stringResource(R.string.homepage_source_json_empty))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
sourceModules.distinctBy { it.id },
|
||||||
|
key = { it.id }) { module ->
|
||||||
|
val isJoined = joinedKeys.contains(module.moduleKey)
|
||||||
|
val isInfinite =
|
||||||
|
HomepageViewModel.isInfinite(module.type, module.layoutConfig)
|
||||||
|
val isBlocked = !isJoined && isInfinite && hasInfiniteInSet
|
||||||
|
|
||||||
|
SelectionItemCard(
|
||||||
|
title = module.title,
|
||||||
|
subtitle = module.moduleKey + if (isJoined) stringResource(
|
||||||
|
R.string.homepage_status_joined
|
||||||
|
) else if (isBlocked) " (${stringResource(R.string.homepage_module_duplicate_infinite)})" else "",
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
isSelected = isJoined,
|
||||||
|
inSelectionMode = true,
|
||||||
|
isEnabled = !isBlocked,
|
||||||
|
onToggleSelection = {
|
||||||
|
if (!isJoined && !isBlocked) {
|
||||||
|
onJoinModule(
|
||||||
|
browseUrl, currentSetId, ModuleDef(
|
||||||
|
key = module.moduleKey,
|
||||||
|
type = module.type,
|
||||||
|
title = module.title,
|
||||||
|
sourceUrl = browseUrl,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
val effectiveTargetSetId = currentSetId
|
||||||
|
val isButtonGroup = browseModuleType == "buttonGroup"
|
||||||
|
Column {
|
||||||
|
val typeList = remember(canSelectInfiniteGlobal) {
|
||||||
|
HomepageModuleType.entries.filter {
|
||||||
|
it != HomepageModuleType.Unknown && (canSelectInfiniteGlobal || !HomepageViewModel.isInfinite(
|
||||||
|
it.key,
|
||||||
|
null
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(canSelectInfiniteGlobal) {
|
||||||
|
if (!canSelectInfiniteGlobal && HomepageViewModel.isInfinite(
|
||||||
|
browseModuleType,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
onBrowseModuleTypeChange("card")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlassCard(
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
cornerRadius = 12.dp
|
||||||
|
) {
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = stringResource(R.string.homepage_module_type),
|
||||||
|
selectedValue = browseModuleType,
|
||||||
|
displayEntries = typeList.map { it.title }.toTypedArray(),
|
||||||
|
entryValues = typeList.map { it.key }.toTypedArray(),
|
||||||
|
onValueChange = {
|
||||||
|
onBrowseModuleTypeChange(it)
|
||||||
|
onSelectedKindTitlesChange(emptySet())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
SelectionItemCard(
|
||||||
|
title = stringResource(R.string.homepage_select_from_kinds),
|
||||||
|
subtitle = if (isButtonGroup) {
|
||||||
|
if (selectedKindTitles.isEmpty()) stringResource(R.string.homepage_select_multiple_kinds)
|
||||||
|
else stringResource(
|
||||||
|
R.string.homepage_n_selected,
|
||||||
|
selectedKindTitles.size
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.homepage_select_one_kind)
|
||||||
|
},
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
onToggleSelection = { showKindSelect = true },
|
||||||
|
trailingAction = {
|
||||||
|
if (isButtonGroup && selectedKindTitles.isNotEmpty()) {
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { onShowAddButtonGroupDialog() },
|
||||||
|
imageVector = Icons.Default.Check
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ExploreKindSelectSheet(
|
||||||
|
show = showKindSelect,
|
||||||
|
onDismissRequest = { showKindSelect = false },
|
||||||
|
sourceUrl = browseUrl,
|
||||||
|
multiple = isButtonGroup,
|
||||||
|
initialSelectedTitles = selectedKindTitles.toList(),
|
||||||
|
onSelected = { kinds ->
|
||||||
|
if (isButtonGroup) {
|
||||||
|
onSelectedKindTitlesChange(kinds.map { it.title }.toSet())
|
||||||
|
} else {
|
||||||
|
kinds.firstOrNull()?.let { kind ->
|
||||||
|
onAddDialogPrefill(
|
||||||
|
AddDialogPrefill(
|
||||||
|
title = kind.title,
|
||||||
|
url = kind.url ?: "",
|
||||||
|
type = browseModuleType
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
PillDivider(
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
SecondaryButton(
|
||||||
|
text = stringResource(R.string.homepage_manual_add),
|
||||||
|
onClick = {
|
||||||
|
onAddDialogPrefill(AddDialogPrefill(type = browseModuleType))
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -229,6 +229,7 @@ fun SearchBookGridItem(
|
|||||||
style = LegadoTheme.typography.bodySmall,
|
style = LegadoTheme.typography.bodySmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
|
minLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -77,7 +77,7 @@ fun AppModalBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
insideMargin = DpSize(16.dp, 12.dp),
|
insideMargin = DpSize(16.dp, 0.dp),
|
||||||
backgroundColor = sheetContainerColor,
|
backgroundColor = sheetContainerColor,
|
||||||
dragHandleColor = sheetDragHandleColor,
|
dragHandleColor = sheetDragHandleColor,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
@@ -120,7 +120,7 @@ fun AppModalBottomSheet(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 24.dp)
|
.padding(start = 16.dp, end = 16.dp, bottom = 0.dp)
|
||||||
.heightIn(max = maxHeight)
|
.heightIn(max = maxHeight)
|
||||||
.animateContentSize()
|
.animateContentSize()
|
||||||
.then(modifier)
|
.then(modifier)
|
||||||
|
|||||||
@@ -1689,7 +1689,7 @@
|
|||||||
<string name="homepage_tab_discover">发现</string>
|
<string name="homepage_tab_discover">发现</string>
|
||||||
<string name="homepage_no_joined_modules">暂无已加入的模块</string>
|
<string name="homepage_no_joined_modules">暂无已加入的模块</string>
|
||||||
<string name="homepage_standard_modules_sortable">标准模块 (可拖拽排序)</string>
|
<string name="homepage_standard_modules_sortable">标准模块 (可拖拽排序)</string>
|
||||||
<string name="homepage_infinite_module_bottom">底栏无限模块</string>
|
<string name="homepage_infinite_module_bottom">底栏模块 (无限加载,仅可存在一个)</string>
|
||||||
<string name="homepage_in_effect">当前生效</string>
|
<string name="homepage_in_effect">当前生效</string>
|
||||||
<string name="homepage_blocked">已被屏蔽</string>
|
<string name="homepage_blocked">已被屏蔽</string>
|
||||||
<string name="homepage_source_json_empty">该书源的 homepageModules JSON 为空</string>
|
<string name="homepage_source_json_empty">该书源的 homepageModules JSON 为空</string>
|
||||||
|
|||||||
+13
-49
@@ -1,20 +1,20 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
accompanistWebview = "0.36.0"
|
accompanistWebview = "0.36.0"
|
||||||
adaptive = "1.3.0-beta01"
|
adaptive = "1.3.0-beta02"
|
||||||
adaptiveLayout = "1.3.0-beta01"
|
adaptiveLayout = "1.3.0-beta02"
|
||||||
adaptiveNavigation = "1.3.0-beta01"
|
adaptiveNavigation = "1.3.0-beta02"
|
||||||
animation = "1.11.1"
|
animation = "1.11.2"
|
||||||
biometric = "1.4.0-alpha07"
|
biometric = "1.4.0-alpha07"
|
||||||
coilCompose = "2.7.0"
|
coilCompose = "2.7.0"
|
||||||
composeBom = "2026.05.00"
|
composeBom = "2026.05.01"
|
||||||
constraintlayoutCompose = "1.1.1"
|
constraintlayoutCompose = "1.1.1"
|
||||||
coreSplashscreen = "1.2.0"
|
coreSplashscreen = "1.2.0"
|
||||||
datastorePreferences = "1.2.1"
|
datastorePreferences = "1.2.1"
|
||||||
foundation = "1.11.1"
|
foundation = "1.11.2"
|
||||||
kotlin = "2.3.21"
|
kotlin = "2.3.21"
|
||||||
kotlinxSerialization = "1.11.0"
|
kotlinxSerialization = "1.11.0"
|
||||||
kotlinxCoroutinesAndroid = "1.10.2"
|
kotlinxCoroutinesAndroid = "1.11.0"
|
||||||
kotlinxSerializationJson = "1.11.0"
|
kotlinxSerializationJson = "1.11.0"
|
||||||
kotlinxCollectionsImmutable = "0.4.0"
|
kotlinxCollectionsImmutable = "0.4.0"
|
||||||
ksp = "2.3.6"
|
ksp = "2.3.6"
|
||||||
@@ -45,11 +45,11 @@ jsoupxpath = "2.5.3"
|
|||||||
coroutines = "1.10.2"
|
coroutines = "1.10.2"
|
||||||
lifecycleViewmodelCompose = "2.10.0"
|
lifecycleViewmodelCompose = "2.10.0"
|
||||||
liveeventbus = "1.8.14"
|
liveeventbus = "1.8.14"
|
||||||
navigation3 = "1.1.1"
|
navigation3 = "1.1.2"
|
||||||
markdownRenderer = "0.40.2"
|
markdownRenderer = "0.40.2"
|
||||||
markwon = "4.6.2"
|
markwon = "4.6.2"
|
||||||
material = "1.14.0-rc01"
|
material = "1.14.0"
|
||||||
material3 = "1.5.0-alpha19"
|
material3 = "1.5.0-alpha20"
|
||||||
material3IconsExtended = "1.7.8"
|
material3IconsExtended = "1.7.8"
|
||||||
materialKolor = "4.1.1"
|
materialKolor = "4.1.1"
|
||||||
media = "1.8.0"
|
media = "1.8.0"
|
||||||
@@ -59,7 +59,7 @@ okhttp = "5.3.2"
|
|||||||
palette = "1.0.0"
|
palette = "1.0.0"
|
||||||
preference = "1.2.1"
|
preference = "1.2.1"
|
||||||
protobufJavalite = "4.26.1"
|
protobufJavalite = "4.26.1"
|
||||||
quickChineseTransfer = "0.2.16"
|
quickChineseTransfer = "0.2.17"
|
||||||
room = "2.8.4"
|
room = "2.8.4"
|
||||||
splitties = "3.0.0"
|
splitties = "3.0.0"
|
||||||
rhino = "1.9.1"
|
rhino = "1.9.1"
|
||||||
@@ -96,15 +96,10 @@ profileinstaller = "1.4.1"
|
|||||||
[libraries]
|
[libraries]
|
||||||
|
|
||||||
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanistWebview" }
|
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanistWebview" }
|
||||||
activity-activity = { module = "androidx.activity:activity", version.ref = "activity" }
|
|
||||||
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
|
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
|
||||||
activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity" }
|
activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity" }
|
||||||
|
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose" }
|
androidx-annotation = { group = "androidx.annotation", name = "annotation", version = "1.10.0" }
|
||||||
androidx-annotation = { group = "androidx.annotation", name = "annotation", version = "1.9.1" }
|
|
||||||
#androidx-annotation-experimental = { group = "androidx.annotation", name = "annotation-experimental", version = "1.3.1" }
|
|
||||||
|
|
||||||
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
|
|
||||||
androidx-collection = { module = "androidx.collection:collection", version.ref = "collection" }
|
androidx-collection = { module = "androidx.collection:collection", version.ref = "collection" }
|
||||||
androidx-compose-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" }
|
androidx-compose-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" }
|
||||||
androidx-compose-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "adaptiveLayout" }
|
androidx-compose-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "adaptiveLayout" }
|
||||||
@@ -143,7 +138,6 @@ koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmo
|
|||||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
|
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
|
||||||
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coilCompose" }
|
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coilCompose" }
|
||||||
coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coilCompose" }
|
coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coilCompose" }
|
||||||
core-core = { module = "androidx.core:core", version.ref = "core" }
|
|
||||||
core-ktx = { module = "androidx.core:core-ktx", version.ref = "core" }
|
core-ktx = { module = "androidx.core:core-ktx", version.ref = "core" }
|
||||||
|
|
||||||
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version = "3.7.0" }
|
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version = "3.7.0" }
|
||||||
@@ -153,17 +147,13 @@ firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
|||||||
firebase-perf = { module = "com.google.firebase:firebase-perf" }
|
firebase-perf = { module = "com.google.firebase:firebase-perf" }
|
||||||
|
|
||||||
|
|
||||||
fragment-fragment = { module = "androidx.fragment:fragment", version.ref = "fragment" }
|
|
||||||
fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment" }
|
fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment" }
|
||||||
fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "fragment" }
|
|
||||||
|
|
||||||
androidx-junit = { module = "androidx.test.ext:junit", version = "1.3.0" }
|
androidx-junit = { module = "androidx.test.ext:junit", version = "1.3.0" }
|
||||||
|
|
||||||
hutool-crypto = { module = "cn.hutool:hutool-crypto", version.ref = "hutool" }
|
hutool-crypto = { module = "cn.hutool:hutool-crypto", version.ref = "hutool" }
|
||||||
|
|
||||||
kotlinx-coroutines-android-v181 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
|
||||||
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
|
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
|
||||||
kotlinx-serialization-json-v163 = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
|
||||||
libarchive = { module = "me.zhanghai.android.libarchive:library", version.ref = "libarchive" }
|
libarchive = { module = "me.zhanghai.android.libarchive:library", version.ref = "libarchive" }
|
||||||
lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "lifecycle" }
|
lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "lifecycle" }
|
||||||
lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle" }
|
lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle" }
|
||||||
@@ -173,7 +163,6 @@ material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "m
|
|||||||
media-media = { module = "androidx.media:media", version.ref = "media" }
|
media-media = { module = "androidx.media:media", version.ref = "media" }
|
||||||
media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
|
media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
|
||||||
media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
||||||
media3-session = { module = "androidx.media3:media3-session", version.ref = "media3" }
|
|
||||||
|
|
||||||
markwon-core = { module = "io.noties.markwon:core", version.ref = "markwon" }
|
markwon-core = { module = "io.noties.markwon:core", version.ref = "markwon" }
|
||||||
markwon-image-glide = { module = "io.noties.markwon:image-glide", version.ref = "markwon" }
|
markwon-image-glide = { module = "io.noties.markwon:image-glide", version.ref = "markwon" }
|
||||||
@@ -187,8 +176,6 @@ ktor-serialization-gson = { module = "io.ktor:ktor-serialization-gson", version.
|
|||||||
ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" }
|
ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" }
|
||||||
ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
|
ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
|
||||||
|
|
||||||
navigation-compose = { module = "androidx.navigation:navigation-compose" }
|
|
||||||
preference-preference = { module = "androidx.preference:preference", version.ref = "preference" }
|
|
||||||
preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preference" }
|
preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preference" }
|
||||||
|
|
||||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
@@ -204,16 +191,11 @@ room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
|
|||||||
glide-glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
|
glide-glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
|
||||||
glide-okhttp= { module = "com.github.bumptech.glide:okhttp3-integration", version.ref ="glide" }
|
glide-okhttp= { module = "com.github.bumptech.glide:okhttp3-integration", version.ref ="glide" }
|
||||||
glide-recyclerview={module="com.github.bumptech.glide:recyclerview-integration",version.ref="glide"}
|
glide-recyclerview={module="com.github.bumptech.glide:recyclerview-integration",version.ref="glide"}
|
||||||
glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
|
|
||||||
glide-compose = { module = "com.github.bumptech.glide:compose", version = "1.0.0-beta08" }
|
|
||||||
glide-ksp = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
|
glide-ksp = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
|
||||||
glide-avif = { module = "com.github.bumptech.glide:avif-integration", version.ref = "glide" }
|
|
||||||
avif = { module = "org.aomedia.avif.android:avif", version = "1.3.0.841110fd" }
|
|
||||||
glide-svg = { module = "com.github.qoqa:glide-svg", version = "4.0.2" }
|
glide-svg = { module = "com.github.qoqa:glide-svg", version = "4.0.2" }
|
||||||
androidsvg = { module = "com.caverock:androidsvg-aar", version = "1.4" }
|
androidsvg = { module = "com.caverock:androidsvg-aar", version = "1.4" }
|
||||||
|
|
||||||
splitties-appctx = { module = "com.louiscad.splitties:splitties-appctx", version.ref = "splitties" }
|
splitties-appctx = { module = "com.louiscad.splitties:splitties-appctx", version.ref = "splitties" }
|
||||||
splitties-activities = { module = "com.louiscad.splitties:splitties-activities", version.ref = "splitties" }
|
|
||||||
splitties-systemservices = { module = "com.louiscad.splitties:splitties-systemservices", version.ref = "splitties" }
|
splitties-systemservices = { module = "com.louiscad.splitties:splitties-systemservices", version.ref = "splitties" }
|
||||||
splitties-views = { module = "com.louiscad.splitties:splitties-views", version.ref = "splitties" }
|
splitties-views = { module = "com.louiscad.splitties:splitties-views", version.ref = "splitties" }
|
||||||
|
|
||||||
@@ -237,18 +219,12 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine
|
|||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
||||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
|
||||||
|
|
||||||
|
|
||||||
#kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
|
||||||
#kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinxSerialization" }
|
|
||||||
liveeventbus = { module = "com.github.michaellee123:LiveEventBus", version.ref = "liveeventbus" }
|
liveeventbus = { module = "com.github.michaellee123:LiveEventBus", version.ref = "liveeventbus" }
|
||||||
material = { module = "com.google.android.material:material", version.ref = "material" }
|
material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||||
|
|
||||||
mozilla-rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
mozilla-rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||||
renderscript-intrinsics-replacement-toolkit = { module = "com.github.TomasValenta:renderscript-intrinsics-replacement-toolkit", version = "8eaa829ddd" }
|
|
||||||
zxing-lite = { module = "com.github.jenly1314:zxing-lite", version.ref = "zxingLite" }
|
zxing-lite = { module = "com.github.jenly1314:zxing-lite", version.ref = "zxingLite" }
|
||||||
androidx-biometric-ktx = { group = "androidx.biometric", name = "biometric-ktx", version.ref = "biometricKtx" }
|
|
||||||
|
|
||||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||||
|
|
||||||
@@ -260,7 +236,6 @@ miuix-core = { module = "top.yukonga.miuix.kmp:miuix-core", version.ref = "miuix
|
|||||||
miuix-blur-android = { module = "top.yukonga.miuix.kmp:miuix-blur-android", version.ref = "miuix" }
|
miuix-blur-android = { module = "top.yukonga.miuix.kmp:miuix-blur-android", version.ref = "miuix" }
|
||||||
miuix-icons-android = { module = "top.yukonga.miuix.kmp:miuix-icons-android", version.ref = "miuix" }
|
miuix-icons-android = { module = "top.yukonga.miuix.kmp:miuix-icons-android", version.ref = "miuix" }
|
||||||
miuix-preference-android = { module = "top.yukonga.miuix.kmp:miuix-preference-android", version.ref = "miuix" }
|
miuix-preference-android = { module = "top.yukonga.miuix.kmp:miuix-preference-android", version.ref = "miuix" }
|
||||||
miuix-shapes-android = { module = "top.yukonga.miuix.kmp:miuix-shapes-android", version.ref = "miuix" }
|
|
||||||
miuix-ui-android = { module = "top.yukonga.miuix.kmp:miuix-ui-android", version.ref = "miuix" }
|
miuix-ui-android = { module = "top.yukonga.miuix.kmp:miuix-ui-android", version.ref = "miuix" }
|
||||||
|
|
||||||
capsule = { module = "io.github.kyant0:capsule", version.ref = "capsule" }
|
capsule = { module = "io.github.kyant0:capsule", version.ref = "capsule" }
|
||||||
@@ -280,16 +255,7 @@ android-application = { id = "com.android.application", version.ref = "agp" }
|
|||||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
android-test = { id = "com.android.test", version.ref = "agp" }
|
android-test = { id = "com.android.test", version.ref = "agp" }
|
||||||
|
|
||||||
#androidx-benchmark = { id = "androidx.benchmark", version = "1.3.3" }
|
|
||||||
|
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
|
||||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
|
||||||
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
|
|
||||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
kotlin-allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" }
|
|
||||||
kotlin-noarg = { id = "org.jetbrains.kotlin.plugin.noarg", version.ref = "kotlin" }
|
|
||||||
kotlin-sam = { id = "org.jetbrains.kotlin.plugin.sam.with.receiver", version.ref = "kotlin" }
|
|
||||||
kotlin-lombok = { id = "org.jetbrains.kotlin.plugin.lombok", version.ref = "kotlin" }
|
|
||||||
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||||
|
|
||||||
google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
|
google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
|
||||||
@@ -297,7 +263,5 @@ google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
|
|||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
room = { id = "androidx.room", version.ref = "room" }
|
room = { id = "androidx.room", version.ref = "room" }
|
||||||
|
|
||||||
#protobuf = { id = "com.google.protobuf", version = "0.9.4" }
|
download = { id = "de.undercouch.download", version = "5.7.0" }
|
||||||
|
|
||||||
download = { id = "de.undercouch.download", version = "5.6.0" }
|
|
||||||
baselineprofile = { id = "androidx.baselineprofile", version.ref = "baselineprofile" }
|
baselineprofile = { id = "androidx.baselineprofile", version.ref = "baselineprofile" }
|
||||||
Reference in new issue
Block a user