mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
feat: Working extension repo frontend
This commit is contained in:
parent
64c7b54075
commit
67f32d400d
7 changed files with 161 additions and 34 deletions
|
@ -4,4 +4,5 @@ import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||||
|
|
||||||
class SourcePreferences(private val preferenceStore: PreferenceStore) {
|
class SourcePreferences(private val preferenceStore: PreferenceStore) {
|
||||||
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
||||||
|
fun trustedExtensions() = preferenceStore.getStringSet("trusted_extensions", emptySet())
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ fun CombinedClickableIconButton(
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
enabledTint
|
enabledTint
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurface
|
MaterialTheme.colorScheme.onBackground
|
||||||
.copy(alpha = .38f)
|
.copy(alpha = .38f)
|
||||||
}
|
}
|
||||||
CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
|
CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package dev.yokai.presentation.extension.repo
|
package dev.yokai.presentation.extension.repo
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.ExtensionOff
|
import androidx.compose.material.icons.filled.ExtensionOff
|
||||||
|
@ -8,19 +10,31 @@ import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
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.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import dev.yokai.presentation.AppBarType
|
import dev.yokai.presentation.AppBarType
|
||||||
import dev.yokai.presentation.YokaiScaffold
|
import dev.yokai.presentation.YokaiScaffold
|
||||||
import dev.yokai.presentation.component.EmptyScreen
|
import dev.yokai.presentation.component.EmptyScreen
|
||||||
|
import dev.yokai.presentation.extension.repo.component.ExtensionRepoItem
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionRepoScreen(
|
fun ExtensionRepoScreen(
|
||||||
title: String,
|
title: String,
|
||||||
onBackPress: () -> Unit,
|
onBackPress: () -> Unit,
|
||||||
|
viewModel: ExtensionRepoViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val repoState = viewModel.repoState.collectAsState()
|
||||||
|
var inputText by remember { mutableStateOf("") }
|
||||||
|
|
||||||
YokaiScaffold(
|
YokaiScaffold(
|
||||||
onNavigationIconClicked = onBackPress,
|
onNavigationIconClicked = onBackPress,
|
||||||
|
@ -35,10 +49,52 @@ fun ExtensionRepoScreen(
|
||||||
},
|
},
|
||||||
appBarType = AppBarType.SMALL,
|
appBarType = AppBarType.SMALL,
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
EmptyScreen(
|
if (repoState.value is ExtensionRepoState.Loading) return@YokaiScaffold
|
||||||
|
|
||||||
|
val repos = (repoState.value as ExtensionRepoState.Success).repos
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
modifier = Modifier.padding(innerPadding),
|
modifier = Modifier.padding(innerPadding),
|
||||||
image = Icons.Filled.ExtensionOff,
|
userScrollEnabled = true,
|
||||||
message = "No extension repo found",
|
verticalArrangement = Arrangement.Top,
|
||||||
)
|
) {
|
||||||
|
item {
|
||||||
|
ExtensionRepoItem(
|
||||||
|
inputText = inputText,
|
||||||
|
inputHint = "Add new repo",
|
||||||
|
onInputChange = { inputText = it },
|
||||||
|
onAddClick = { viewModel.addRepo(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repos.isEmpty()) {
|
||||||
|
item {
|
||||||
|
EmptyScreen(
|
||||||
|
modifier = Modifier.fillParentMaxSize(),
|
||||||
|
image = Icons.Filled.ExtensionOff,
|
||||||
|
message = "No extension repo found",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@LazyColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
repos.forEach { repo ->
|
||||||
|
item {
|
||||||
|
ExtensionRepoItem(
|
||||||
|
repoUrl = repo,
|
||||||
|
onDeleteClick = { viewModel.deleteRepo(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.event.collectLatest { event ->
|
||||||
|
if (event is ExtensionRepoEvent.LocalizedMessage)
|
||||||
|
context.toast(event.stringRes)
|
||||||
|
if (event is ExtensionRepoEvent.Success)
|
||||||
|
inputText = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,29 +28,27 @@ class ExtensionRepoViewModel :
|
||||||
val event: StateFlow<ExtensionRepoEvent> = internalEvent.asStateFlow()
|
val event: StateFlow<ExtensionRepoEvent> = internalEvent.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
refresh()
|
viewModelScope.launchIO {
|
||||||
|
repository.getRepo().collectLatest { repos ->
|
||||||
|
mutableRepoState.update { ExtensionRepoState.Success(repos = repos.toImmutableList()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addRepo(url: String) {
|
fun addRepo(url: String) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val result = repository.addRepo(url)
|
val result = repository.addRepo(url)
|
||||||
if (result is Result.Error) return@launchIO
|
if (result is Result.Error) {
|
||||||
refresh()
|
internalEvent.value = ExtensionRepoEvent.InvalidUrl
|
||||||
|
return@launchIO
|
||||||
|
}
|
||||||
|
internalEvent.value = ExtensionRepoEvent.Success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteRepo(repo: String) {
|
fun deleteRepo(repo: String) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
repository.deleteRepo(repo)
|
repository.deleteRepo(repo)
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh() {
|
|
||||||
viewModelScope.launchIO {
|
|
||||||
repository.getRepo().collectLatest { repos ->
|
|
||||||
mutableRepoState.update { ExtensionRepoState.Success(repos = repos.toImmutableList()) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,6 +57,7 @@ sealed class ExtensionRepoEvent {
|
||||||
sealed class LocalizedMessage(@StringRes val stringRes: Int) : ExtensionRepoEvent()
|
sealed class LocalizedMessage(@StringRes val stringRes: Int) : ExtensionRepoEvent()
|
||||||
data object InvalidUrl : LocalizedMessage(R.string.invalid_repo_url)
|
data object InvalidUrl : LocalizedMessage(R.string.invalid_repo_url)
|
||||||
data object NoOp : ExtensionRepoEvent()
|
data object NoOp : ExtensionRepoEvent()
|
||||||
|
data object Success : ExtensionRepoEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class ExtensionRepoState {
|
sealed class ExtensionRepoState {
|
||||||
|
|
|
@ -1,26 +1,102 @@
|
||||||
package dev.yokai.presentation.extension.repo.component
|
package dev.yokai.presentation.extension.repo.component
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.TextField
|
||||||
|
import androidx.compose.material.TextFieldDefaults
|
||||||
|
import androidx.compose.material.TextFieldDefaults.indicatorLine
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.Label
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import eu.kanade.tachiyomi.util.compose.textHint
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionRepoItem(
|
fun ExtensionRepoItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
repoUrl: String,
|
repoUrl: String? = null,
|
||||||
|
inputText: String = "",
|
||||||
|
onInputChange: (String) -> Unit = {},
|
||||||
|
inputHint: String? = null,
|
||||||
|
onAddClick: (String) -> Unit = {},
|
||||||
|
onDeleteClick: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
require(repoUrl != null || inputHint != null)
|
||||||
|
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Icon(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
text = repoUrl,
|
imageVector = if (repoUrl != null) Icons.Outlined.Label else Icons.Filled.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
Image(imageVector = Icons.Filled.Delete, contentDescription = null)
|
if (repoUrl != null) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(1.0f),
|
||||||
|
text = repoUrl,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
IconButton(onClick = { onDeleteClick(repoUrl) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Delete,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val colors = TextFieldDefaults.textFieldColors(
|
||||||
|
cursorColor = MaterialTheme.colorScheme.secondary,
|
||||||
|
placeholderColor = MaterialTheme.colorScheme.textHint,
|
||||||
|
textColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
errorIndicatorColor = Color.Transparent,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
|
)
|
||||||
|
TextField(
|
||||||
|
modifier = Modifier.indicatorLine(
|
||||||
|
enabled = false,
|
||||||
|
colors = colors,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
isError = true,
|
||||||
|
).weight(1.0f),
|
||||||
|
value = inputText,
|
||||||
|
onValueChange = onInputChange,
|
||||||
|
enabled = true,
|
||||||
|
placeholder = { Text(text = inputHint!!, fontSize = 16.sp) },
|
||||||
|
textStyle = TextStyle(fontSize = 16.sp),
|
||||||
|
colors = colors,
|
||||||
|
)
|
||||||
|
IconButton(
|
||||||
|
onClick = { onAddClick(inputText) },
|
||||||
|
enabled = inputText.isNotEmpty(),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,9 +28,8 @@ internal class ExtensionApi {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val extensions = sourcePreferences.extensionRepos().get().flatMap { getExtensions(it) }
|
val extensions = sourcePreferences.extensionRepos().get().flatMap { getExtensions(it) }
|
||||||
|
|
||||||
// Sanity check - a small number of extensions probably means something broke
|
// Sanity check - empty probably means something broke
|
||||||
// with the repo generator
|
if (extensions.isEmpty()) {
|
||||||
if (extensions.size < 50) {
|
|
||||||
throw Exception()
|
throw Exception()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,11 +117,6 @@ internal class ExtensionApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val BASE_URL = "https://raw.githubusercontent.com/"
|
|
||||||
private const val REPO_URL_PREFIX = "${BASE_URL}keiyoushi/extensions/repo/"
|
|
||||||
private const val FALLBACK_BASE_URL = "https://gcore.jsdelivr.net/gh/"
|
|
||||||
private const val FALLBACK_REPO_URL_PREFIX = "${FALLBACK_BASE_URL}keiyoushi/extensions@repo/"
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class ExtensionJsonObject(
|
private data class ExtensionJsonObject(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
|
@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.migration.MigrationController
|
import eu.kanade.tachiyomi.ui.migration.MigrationController
|
||||||
|
import eu.kanade.tachiyomi.util.lang.addBetaTag
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
import eu.kanade.tachiyomi.util.view.snack
|
||||||
import eu.kanade.tachiyomi.util.view.withFadeTransaction
|
import eu.kanade.tachiyomi.util.view.withFadeTransaction
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
@ -41,7 +42,7 @@ class SettingsBrowseController : SettingsController() {
|
||||||
preferenceCategory {
|
preferenceCategory {
|
||||||
titleRes = R.string.extensions
|
titleRes = R.string.extensions
|
||||||
preference {
|
preference {
|
||||||
titleRes = R.string.source_repos
|
title = context.getString(R.string.source_repos).addBetaTag(context)
|
||||||
onClick { router.pushController(ExtensionRepoController().withFadeTransaction()) }
|
onClick { router.pushController(ExtensionRepoController().withFadeTransaction()) }
|
||||||
// TODO: Enable once it's finished
|
// TODO: Enable once it's finished
|
||||||
summary = "Temporarily disabled, will be enabled once it's fully implemented"
|
summary = "Temporarily disabled, will be enabled once it's fully implemented"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue