diff --git a/app/src/main/java/dev/yokai/domain/source/SourcePreferences.kt b/app/src/main/java/dev/yokai/domain/source/SourcePreferences.kt index 6c7f2a5c31..d5adad7457 100644 --- a/app/src/main/java/dev/yokai/domain/source/SourcePreferences.kt +++ b/app/src/main/java/dev/yokai/domain/source/SourcePreferences.kt @@ -4,4 +4,5 @@ import eu.kanade.tachiyomi.core.preference.PreferenceStore class SourcePreferences(private val preferenceStore: PreferenceStore) { fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet()) + fun trustedExtensions() = preferenceStore.getStringSet("trusted_extensions", emptySet()) } diff --git a/app/src/main/java/dev/yokai/presentation/component/ToolTip.kt b/app/src/main/java/dev/yokai/presentation/component/ToolTip.kt index c71fd32af7..9dc940996a 100644 --- a/app/src/main/java/dev/yokai/presentation/component/ToolTip.kt +++ b/app/src/main/java/dev/yokai/presentation/component/ToolTip.kt @@ -96,7 +96,7 @@ fun CombinedClickableIconButton( if (enabled) { enabledTint } else { - MaterialTheme.colorScheme.onSurface + MaterialTheme.colorScheme.onBackground .copy(alpha = .38f) } CompositionLocalProvider(LocalContentColor provides contentColor, content = content) diff --git a/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoScreen.kt b/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoScreen.kt index dc0276beed..25ffdd472a 100644 --- a/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoScreen.kt +++ b/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoScreen.kt @@ -1,6 +1,8 @@ package dev.yokai.presentation.extension.repo +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add 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.MaterialTheme 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.platform.LocalContext +import androidx.lifecycle.viewmodel.compose.viewModel import dev.yokai.presentation.AppBarType import dev.yokai.presentation.YokaiScaffold import dev.yokai.presentation.component.EmptyScreen +import dev.yokai.presentation.extension.repo.component.ExtensionRepoItem import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.collectLatest @Composable fun ExtensionRepoScreen( title: String, onBackPress: () -> Unit, + viewModel: ExtensionRepoViewModel = viewModel(), ) { val context = LocalContext.current + val repoState = viewModel.repoState.collectAsState() + var inputText by remember { mutableStateOf("") } YokaiScaffold( onNavigationIconClicked = onBackPress, @@ -35,10 +49,52 @@ fun ExtensionRepoScreen( }, appBarType = AppBarType.SMALL, ) { innerPadding -> - EmptyScreen( + if (repoState.value is ExtensionRepoState.Loading) return@YokaiScaffold + + val repos = (repoState.value as ExtensionRepoState.Success).repos + + LazyColumn( modifier = Modifier.padding(innerPadding), - image = Icons.Filled.ExtensionOff, - message = "No extension repo found", - ) + userScrollEnabled = true, + 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 = "" + } } } diff --git a/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt b/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt index 721368a151..bbf80c0350 100644 --- a/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt +++ b/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt @@ -28,29 +28,27 @@ class ExtensionRepoViewModel : val event: StateFlow = internalEvent.asStateFlow() init { - refresh() + viewModelScope.launchIO { + repository.getRepo().collectLatest { repos -> + mutableRepoState.update { ExtensionRepoState.Success(repos = repos.toImmutableList()) } + } + } } fun addRepo(url: String) { viewModelScope.launchIO { val result = repository.addRepo(url) - if (result is Result.Error) return@launchIO - refresh() + if (result is Result.Error) { + internalEvent.value = ExtensionRepoEvent.InvalidUrl + return@launchIO + } + internalEvent.value = ExtensionRepoEvent.Success } } fun deleteRepo(repo: String) { viewModelScope.launchIO { 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() data object InvalidUrl : LocalizedMessage(R.string.invalid_repo_url) data object NoOp : ExtensionRepoEvent() + data object Success : ExtensionRepoEvent() } sealed class ExtensionRepoState { diff --git a/app/src/main/java/dev/yokai/presentation/extension/repo/component/ExtensionRepoItem.kt b/app/src/main/java/dev/yokai/presentation/extension/repo/component/ExtensionRepoItem.kt index 54d3ee4cab..8d017fc6c8 100644 --- a/app/src/main/java/dev/yokai/presentation/extension/repo/component/ExtensionRepoItem.kt +++ b/app/src/main/java/dev/yokai/presentation/extension/repo/component/ExtensionRepoItem.kt @@ -1,26 +1,102 @@ 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.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.filled.Add +import androidx.compose.material.icons.filled.Check 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.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import eu.kanade.tachiyomi.util.compose.textHint @Composable fun ExtensionRepoItem( 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( - modifier = modifier, + modifier = modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Text( - modifier = Modifier.fillMaxWidth(), - text = repoUrl, + Icon( + modifier = Modifier.padding(horizontal = 8.dp), + 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, + ) + } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt index bcc9f126f8..f5d1c6bc5d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt @@ -28,9 +28,8 @@ internal class ExtensionApi { return withIOContext { val extensions = sourcePreferences.extensionRepos().get().flatMap { getExtensions(it) } - // Sanity check - a small number of extensions probably means something broke - // with the repo generator - if (extensions.size < 50) { + // Sanity check - empty probably means something broke + if (extensions.isEmpty()) { 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 private data class ExtensionJsonObject( val name: String, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt index a5d5bb6cd8..5ed1a0d044 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt @@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.extension.util.ExtensionInstaller import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.main.MainActivity 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.withFadeTransaction import uy.kohesive.injekt.injectLazy @@ -41,7 +42,7 @@ class SettingsBrowseController : SettingsController() { preferenceCategory { titleRes = R.string.extensions preference { - titleRes = R.string.source_repos + title = context.getString(R.string.source_repos).addBetaTag(context) onClick { router.pushController(ExtensionRepoController().withFadeTransaction()) } // TODO: Enable once it's finished summary = "Temporarily disabled, will be enabled once it's fully implemented"