feat: enable/disable repo (#288)

* Enable/Disable repo

* update extensions list when refresh
This commit is contained in:
Tran M. Cuong 2024-08-24 22:52:29 +07:00 committed by Cuong-Tran
parent 2536e5a77e
commit 50b837c976
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
8 changed files with 183 additions and 10 deletions

View file

@ -55,6 +55,10 @@ class SourcePreferences(
false,
)
// KMK -->
fun disabledRepos() = preferenceStore.getStringSet("disabled_repos", emptySet())
// KMK <--
// SY -->
fun enableSourceBlacklist() = preferenceStore.getBoolean("eh_enable_source_blacklist", true)

View file

@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.flow.collectLatest
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.screens.LoadingScreen
class ExtensionReposScreen(
@ -47,6 +48,16 @@ class ExtensionReposScreen(
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
onOpenWebsite = { context.openInBrowser(it.website) },
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
// KMK -->
onClickEnable = {
screenModel.enableRepo(it)
context.toast(KMR.strings.extensions_page_need_refresh)
},
onClickDisable = {
screenModel.disableRepo(it)
context.toast(KMR.strings.extensions_page_need_refresh)
},
// KMK <--
onClickRefresh = { screenModel.refreshRepos() },
navigateUp = navigator::pop,
)

View file

@ -4,10 +4,13 @@ import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.source.service.SourcePreferences
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
@ -27,6 +30,9 @@ class ExtensionReposScreenModel(
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
// KMK -->
private val sourcePreferences: SourcePreferences = Injekt.get(),
// KMK <--
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
@ -39,10 +45,26 @@ class ExtensionReposScreenModel(
mutableState.update {
RepoScreenState.Success(
repos = repos.toImmutableSet(),
// KMK -->
disabledRepos = sourcePreferences.disabledRepos().get(),
// KMK <--
)
}
}
}
// KMK -->
sourcePreferences.disabledRepos().changes()
.onEach { disabledRepos ->
mutableState.update {
when (it) {
is RepoScreenState.Success -> it.copy(disabledRepos = disabledRepos)
else -> it
}
}
}
.launchIn(screenModelScope)
// KMK <--
}
/**
@ -91,11 +113,35 @@ class ExtensionReposScreenModel(
* Deletes the given repo from the database
*/
fun deleteRepo(baseUrl: String) {
// KMK -->
// Remove repo from disabled list
enableRepo(baseUrl)
// KMK <--
screenModelScope.launchIO {
deleteExtensionRepo.await(baseUrl)
}
}
// KMK -->
fun enableRepo(baseUrl: String) {
val disabledRepos = sourcePreferences.disabledRepos().get()
if (baseUrl in disabledRepos) {
sourcePreferences.disabledRepos().set(
disabledRepos.filterNot { it == baseUrl }.toSet(),
)
}
}
fun disableRepo(baseUrl: String) {
val disabledRepos = sourcePreferences.disabledRepos().get()
if (baseUrl !in disabledRepos) {
sourcePreferences.disabledRepos().set(
disabledRepos + baseUrl,
)
}
}
// KMK <--
fun showDialog(dialog: RepoDialog) {
mutableState.update {
when (it) {
@ -137,6 +183,9 @@ sealed class RepoScreenState {
val repos: ImmutableSet<ExtensionRepo>,
val oldRepos: ImmutableSet<String>? = null,
val dialog: RepoDialog? = null,
// KMK -->
val disabledRepos: Set<String> = emptySet(),
// KMK <--
) : RepoScreenState() {
val isEmpty: Boolean

View file

@ -12,17 +12,23 @@ import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import mihon.domain.extensionrepo.model.ExtensionRepo
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
@ -35,6 +41,11 @@ fun ExtensionReposContent(
paddingValues: PaddingValues,
onOpenWebsite: (ExtensionRepo) -> Unit,
onClickDelete: (String) -> Unit,
// KMK -->
onClickEnable: (String) -> Unit,
onClickDisable: (String) -> Unit,
disabledRepos: Set<String>,
// KMK <--
modifier: Modifier = Modifier,
) {
LazyColumn(
@ -50,6 +61,11 @@ fun ExtensionReposContent(
repo = it,
onOpenWebsite = { onOpenWebsite(it) },
onDelete = { onClickDelete(it.baseUrl) },
// KMK -->
onEnable = { onClickEnable(it.baseUrl) },
onDisable = { onClickDisable(it.baseUrl) },
isDisabled = it.baseUrl in disabledRepos,
// KMK <--
)
}
}
@ -62,6 +78,11 @@ private fun ExtensionRepoListItem(
onOpenWebsite: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier,
// KMK -->
isDisabled: Boolean,
onEnable: () -> Unit,
onDisable: () -> Unit,
// KMK <--
) {
val context = LocalContext.current
@ -78,11 +99,21 @@ private fun ExtensionRepoListItem(
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Icon(
imageVector = Icons.AutoMirrored.Outlined.Label,
contentDescription = null,
// KMK -->
tint = LocalContentColor.current.let { if (isDisabled) it.copy(alpha = 0.6f) else it },
// KMK <--
)
Text(
text = repo.name,
modifier = Modifier.padding(start = MaterialTheme.padding.medium),
style = MaterialTheme.typography.titleMedium,
// KMK -->
color = LocalContentColor.current.let { if (isDisabled) it.copy(alpha = 0.6f) else it },
textDecoration = TextDecoration.LineThrough.takeIf { isDisabled },
// KMK <--
)
}
@ -109,6 +140,15 @@ private fun ExtensionRepoListItem(
)
}
// KMK -->
IconButton(onClick = if (isDisabled) onEnable else onDisable) {
Icon(
imageVector = if (isDisabled) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
contentDescription = stringResource(MR.strings.action_disable),
)
}
// KMK <--
IconButton(onClick = onDelete) {
Icon(
imageVector = Icons.Outlined.Delete,
@ -118,3 +158,24 @@ private fun ExtensionRepoListItem(
}
}
}
// KMK -->
@Preview
@Composable
fun ExtensionReposContentPreview() {
val repos = persistentSetOf(
ExtensionRepo("url1", "Repo 1", "", "", "key1"),
ExtensionRepo("url2", "Repo 2", "", "", "key2"),
)
ExtensionReposContent(
repos = repos,
lazyListState = LazyListState(),
paddingValues = PaddingValues(),
onOpenWebsite = {},
onClickDelete = {},
onClickEnable = {},
onClickDisable = {},
disabledRepos = setOf("url2"),
)
}
// KMK <--

View file

@ -40,6 +40,10 @@ fun ExtensionReposScreen(
onClickCreate: () -> Unit,
onOpenWebsite: (ExtensionRepo) -> Unit,
onClickDelete: (String) -> Unit,
// KMK -->
onClickEnable: (String) -> Unit,
onClickDisable: (String) -> Unit,
// KMK <--
onClickRefresh: () -> Unit,
navigateUp: () -> Unit,
) {
@ -93,6 +97,11 @@ fun ExtensionReposScreen(
PaddingValues(horizontal = MaterialTheme.padding.medium),
onOpenWebsite = onOpenWebsite,
onClickDelete = onClickDelete,
// KMK -->
onClickEnable = onClickEnable,
onClickDisable = onClickDisable,
disabledRepos = state.disabledRepos,
// KMK <--
)
}
}
@ -101,12 +110,36 @@ fun ExtensionReposScreen(
@Preview
@Composable
private fun ExtensionReposScreenPreview() {
val state = RepoScreenState.Success(
repos = persistentSetOf(
ExtensionRepo("url1", "Repo 1", "", "", "key1"),
ExtensionRepo("url2", "Repo 2", "", "", "key2"),
),
disabledRepos = setOf("url2"),
)
ExtensionReposScreen(
state = state,
onClickCreate = { },
onOpenWebsite = { },
onClickDelete = { },
onClickEnable = { },
onClickDisable = { },
onClickRefresh = { },
navigateUp = { },
)
}
@Preview
@Composable
private fun ExtensionReposScreenEmptyPreview() {
val state = RepoScreenState.Success(repos = persistentSetOf())
ExtensionReposScreen(
state = state,
onClickCreate = { },
onOpenWebsite = { },
onClickDelete = { },
onClickEnable = { },
onClickDisable = { },
onClickRefresh = { },
navigateUp = { },
)

View file

@ -214,25 +214,32 @@ class ExtensionManager(
* @param availableExtensions The list of extensions given by the [api].
*/
private fun updatedInstalledExtensionsStatuses(availableExtensions: List<Extension.Available>) {
if (availableExtensions.isEmpty()) {
preferences.extensionUpdatesCount().set(0)
return
}
// KMK -->
val noExtAvailable = availableExtensions.isEmpty()
// KMK <--
val installedExtensionsMap = installedExtensionMapFlow.value.toMutableMap()
var changed = false
for ((pkgName, extension) in installedExtensionsMap) {
val availableExt = availableExtensions.find { it.pkgName == pkgName }
if (availableExt == null && !extension.isObsolete) {
installedExtensionsMap[pkgName] = extension.copy(isObsolete = true)
changed = true
// KMK -->
if (availableExt == null/* KMK --> && !extension.isObsolete // KMK <-- */) {
// Clear hasUpdate & set isObsolete
val isObsolete = !noExtAvailable && !extension.isObsolete
// KMK: installedExtensionsMap[pkgName] = extension.copy(isObsolete = true)
installedExtensionsMap[pkgName] = extension.copy(
isObsolete = isObsolete,
hasUpdate = false,
)
// KMK: changed = true
changed = changed || isObsolete || noExtAvailable && (extension.isObsolete || extension.hasUpdate)
// KMK <--
// SY -->
} else if (extension.isBlacklisted() && !extension.isRedundant) {
installedExtensionsMap[pkgName] = extension.copy(isRedundant = true)
changed = true
// SY <--
} else if (availableExt != null) {
} else /* KMK --> if (availableExt != null) // KMK <-- */ {
val hasUpdate = extension.updateExists(availableExt)
if (extension.hasUpdate != hasUpdate) {
installedExtensionsMap[pkgName] = extension.copy(

View file

@ -39,6 +39,7 @@ internal class ExtensionApi {
private val sourcePreferences: SourcePreferences by injectLazy()
// SY <--
private val json: Json by injectLazy()
private val lastExtCheck: Preference<Long> by lazy {
@ -46,8 +47,14 @@ internal class ExtensionApi {
}
suspend fun findExtensions(): List<Extension.Available> {
// KMK -->
val disabledRepos = sourcePreferences.disabledRepos().get()
// KMK <--
return withIOContext {
getExtensionRepo.getAll()
// KMK -->
.filterNot { it.baseUrl in disabledRepos }
// KMK <--
.map { async { getExtensions(it) } }
.awaitAll()
.flatten()

View file

@ -53,6 +53,7 @@
<!-- Extension section -->
<string name="ext_unofficial">Unofficial</string>
<string name="unofficial_extension_message">This extension is not from the official repo.</string>
<string name="extensions_page_need_refresh">Refresh extension page to update list.</string>
<string name="extensions_page_more">More extensions...</string>
<!-- Extension repos -->