Clean up external repos
- Accept full URL as input instead, which allows for non-GitHub - Remove automatic CDN fallback in favor of adding that as an external repo if needed
This commit is contained in:
parent
556f5a42a7
commit
9c899e97a9
@ -22,7 +22,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
|
||||
versionCode = 113
|
||||
versionCode = 114
|
||||
versionName = "0.14.7"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
|
@ -12,7 +12,7 @@ import eu.kanade.domain.manga.interactor.SetExcludedScanlators
|
||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.source.interactor.CreateSourceRepo
|
||||
import eu.kanade.domain.source.interactor.DeleteSourceRepos
|
||||
import eu.kanade.domain.source.interactor.DeleteSourceRepo
|
||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
||||
import eu.kanade.domain.source.interactor.GetSourceRepos
|
||||
@ -172,7 +172,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { ToggleSourcePin(get()) }
|
||||
|
||||
addFactory { CreateSourceRepo(get()) }
|
||||
addFactory { DeleteSourceRepos(get()) }
|
||||
addFactory { DeleteSourceRepo(get()) }
|
||||
addFactory { GetSourceRepos(get()) }
|
||||
}
|
||||
}
|
||||
|
@ -7,28 +7,20 @@ class CreateSourceRepo(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(name: String): Result {
|
||||
// Do not allow invalid formats
|
||||
if (!name.matches(repoRegex)) {
|
||||
return Result.InvalidName
|
||||
if (!name.matches(repoRegex) || name.startsWith(OFFICIAL_REPO_BASE_URL)) {
|
||||
return Result.InvalidUrl
|
||||
}
|
||||
|
||||
preferences.extensionRepos() += name
|
||||
preferences.extensionRepos() += name.substringBeforeLast("/index.min.json")
|
||||
|
||||
return Result.Success
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
data object InvalidName : Result()
|
||||
data object Success : Result()
|
||||
sealed interface Result {
|
||||
data object InvalidUrl : Result
|
||||
data object Success : Result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a repo with the given name already exists.
|
||||
*/
|
||||
private fun repoExists(name: String): Boolean {
|
||||
return preferences.extensionRepos().get().any { it.equals(name, true) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val repoRegex = """^[a-zA-Z0-9-_.]*?\/[a-zA-Z0-9-_.]*?$""".toRegex()
|
||||
}
|
||||
}
|
||||
const val OFFICIAL_REPO_BASE_URL = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo"
|
||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
||||
|
@ -0,0 +1,11 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.minusAssign
|
||||
|
||||
class DeleteSourceRepo(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(repo: String) {
|
||||
preferences.extensionRepos() -= repo
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
|
||||
class DeleteSourceRepos(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(repos: List<String>) {
|
||||
preferences.extensionRepos().set(
|
||||
preferences.extensionRepos().get().filterNot { it in repos }.toSet(),
|
||||
)
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.map
|
||||
class GetSourceRepos(private val preferences: SourcePreferences) {
|
||||
|
||||
fun subscribe(): Flow<List<String>> {
|
||||
return preferences.extensionRepos().changes().map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
|
||||
return preferences.extensionRepos().changes()
|
||||
.map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.core.preference.asToggleableState
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@ -43,9 +42,6 @@ fun CategoryCreateDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: (String) -> Unit,
|
||||
categories: ImmutableList<String>,
|
||||
title: String,
|
||||
extraMessage: String? = null,
|
||||
alreadyExistsError: StringResource = MR.strings.error_category_exists,
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
|
||||
@ -71,12 +67,9 @@ fun CategoryCreateDialog(
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = title)
|
||||
Text(text = stringResource(MR.strings.action_add_category))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
extraMessage?.let { Text(it) }
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester),
|
||||
@ -87,7 +80,7 @@ fun CategoryCreateDialog(
|
||||
},
|
||||
supportingText = {
|
||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
||||
alreadyExistsError
|
||||
MR.strings.error_category_exists
|
||||
} else {
|
||||
MR.strings.information_required_plain
|
||||
}
|
||||
@ -96,7 +89,6 @@ fun CategoryCreateDialog(
|
||||
isError = name.isNotEmpty() && nameAlreadyExists,
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@ -113,7 +105,6 @@ fun CategoryRenameDialog(
|
||||
onRename: (String) -> Unit,
|
||||
categories: ImmutableList<String>,
|
||||
category: String,
|
||||
alreadyExistsError: StringResource = MR.strings.error_category_exists,
|
||||
) {
|
||||
var name by remember { mutableStateOf(category) }
|
||||
var valueHasChanged by remember { mutableStateOf(false) }
|
||||
@ -153,7 +144,7 @@ fun CategoryRenameDialog(
|
||||
label = { Text(text = stringResource(MR.strings.name)) },
|
||||
supportingText = {
|
||||
val msgRes = if (valueHasChanged && nameAlreadyExists) {
|
||||
alreadyExistsError
|
||||
MR.strings.error_category_exists
|
||||
} else {
|
||||
MR.strings.information_required_plain
|
||||
}
|
||||
@ -176,8 +167,7 @@ fun CategoryRenameDialog(
|
||||
fun CategoryDeleteDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
title: String,
|
||||
text: String,
|
||||
category: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
@ -195,10 +185,10 @@ fun CategoryDeleteDialog(
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = title)
|
||||
Text(text = stringResource(MR.strings.delete_category))
|
||||
},
|
||||
text = {
|
||||
Text(text = text)
|
||||
Text(text = stringResource(MR.strings.delete_category_confirmation, category))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ fun CategoryListItem(
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||
Text(
|
||||
text = category.name,
|
||||
modifier = Modifier
|
||||
@ -61,13 +61,13 @@ fun CategoryListItem(
|
||||
onClick = { onMoveUp(category) },
|
||||
enabled = canMoveUp,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { onMoveDown(category) },
|
||||
enabled = canMoveDown,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = onRename) {
|
||||
|
@ -9,8 +9,8 @@ import androidx.fragment.app.FragmentActivity
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.presentation.category.repos.RepoScreen
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
@ -47,7 +47,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.label_extension_repos),
|
||||
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.size, reposCount.size),
|
||||
onClick = {
|
||||
navigator.push(RepoScreen())
|
||||
navigator.push(ExtensionReposScreen())
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.presentation.category.repos
|
||||
package eu.kanade.presentation.more.settings.screen.browse
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@ -8,23 +8,21 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.category.SourceRepoScreen
|
||||
import eu.kanade.presentation.category.components.CategoryCreateDialog
|
||||
import eu.kanade.presentation.category.components.CategoryDeleteDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
|
||||
class RepoScreen : Screen() {
|
||||
class ExtensionReposScreen : Screen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val screenModel = rememberScreenModel { RepoScreenModel() }
|
||||
val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
|
||||
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
@ -35,7 +33,7 @@ class RepoScreen : Screen() {
|
||||
|
||||
val successState = state as RepoScreenState.Success
|
||||
|
||||
SourceRepoScreen(
|
||||
ExtensionReposScreen(
|
||||
state = successState,
|
||||
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
|
||||
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
|
||||
@ -45,21 +43,17 @@ class RepoScreen : Screen() {
|
||||
when (val dialog = successState.dialog) {
|
||||
null -> {}
|
||||
RepoDialog.Create -> {
|
||||
CategoryCreateDialog(
|
||||
ExtensionRepoCreateDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = { screenModel.createRepo(it) },
|
||||
categories = successState.repos,
|
||||
title = stringResource(MR.strings.action_add_repo),
|
||||
extraMessage = stringResource(MR.strings.action_add_repo_message),
|
||||
alreadyExistsError = MR.strings.error_repo_exists,
|
||||
)
|
||||
}
|
||||
is RepoDialog.Delete -> {
|
||||
CategoryDeleteDialog(
|
||||
ExtensionRepoDeleteDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onDelete = { screenModel.deleteRepos(listOf(dialog.repo)) },
|
||||
title = stringResource(MR.strings.action_delete_repo),
|
||||
text = stringResource(MR.strings.delete_repo_confirmation, dialog.repo),
|
||||
onDelete = { screenModel.deleteRepo(dialog.repo) },
|
||||
repo = dialog.repo,
|
||||
)
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package eu.kanade.presentation.category.repos
|
||||
package eu.kanade.presentation.more.settings.screen.browse
|
||||
|
||||
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.interactor.CreateSourceRepo
|
||||
import eu.kanade.domain.source.interactor.DeleteSourceRepos
|
||||
import eu.kanade.domain.source.interactor.DeleteSourceRepo
|
||||
import eu.kanade.domain.source.interactor.GetSourceRepos
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@ -18,10 +18,10 @@ import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class RepoScreenModel(
|
||||
class ExtensionReposScreenModel(
|
||||
private val getSourceRepos: GetSourceRepos = Injekt.get(),
|
||||
private val createSourceRepo: CreateSourceRepo = Injekt.get(),
|
||||
private val deleteSourceRepos: DeleteSourceRepos = Injekt.get(),
|
||||
private val deleteSourceRepo: DeleteSourceRepo = Injekt.get(),
|
||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||
|
||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||
@ -48,20 +48,20 @@ class RepoScreenModel(
|
||||
fun createRepo(name: String) {
|
||||
screenModelScope.launchIO {
|
||||
when (createSourceRepo.await(name)) {
|
||||
is CreateSourceRepo.Result.InvalidName -> _events.send(RepoEvent.InvalidName)
|
||||
is CreateSourceRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given repos from the database.
|
||||
* Deletes the given repo from the database.
|
||||
*
|
||||
* @param repos The list of repos to delete.
|
||||
* @param repo The repo to delete.
|
||||
*/
|
||||
fun deleteRepos(repos: List<String>) {
|
||||
fun deleteRepo(repo: String) {
|
||||
screenModelScope.launchIO {
|
||||
deleteSourceRepos.await(repos)
|
||||
deleteSourceRepo.await(repo)
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,8 +86,7 @@ class RepoScreenModel(
|
||||
|
||||
sealed class RepoEvent {
|
||||
sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent()
|
||||
data object InvalidName : LocalizedMessage(MR.strings.invalid_repo_name)
|
||||
data object InternalError : LocalizedMessage(MR.strings.internal_error)
|
||||
data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name)
|
||||
}
|
||||
|
||||
sealed class RepoDialog {
|
@ -1,9 +1,8 @@
|
||||
package eu.kanade.presentation.category.components.repo
|
||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@ -24,7 +23,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
fun SourceRepoContent(
|
||||
fun ExtensionReposContent(
|
||||
repos: ImmutableList<String>,
|
||||
lazyListState: LazyListState,
|
||||
paddingValues: PaddingValues,
|
||||
@ -38,7 +37,7 @@ fun SourceRepoContent(
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(repos) { repo ->
|
||||
SourceRepoListItem(
|
||||
ExtensionRepoListItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
repo = repo,
|
||||
onDelete = { onClickDelete(repo) },
|
||||
@ -48,7 +47,7 @@ fun SourceRepoContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceRepoListItem(
|
||||
private fun ExtensionRepoListItem(
|
||||
repo: String,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@ -66,13 +65,16 @@ private fun SourceRepoListItem(
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||
Text(text = repo, modifier = Modifier.padding(start = MaterialTheme.padding.medium))
|
||||
}
|
||||
Row {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoCreateDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: (String) -> Unit,
|
||||
categories: ImmutableList<String>,
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = name.isNotEmpty() && !nameAlreadyExists,
|
||||
onClick = {
|
||||
onCreate(name)
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_add))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_add_repo))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text(text = stringResource(MR.strings.action_add_repo_message))
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.label_add_repo_input))
|
||||
},
|
||||
supportingText = {
|
||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
||||
MR.strings.error_repo_exists
|
||||
} else {
|
||||
MR.strings.information_required_plain
|
||||
}
|
||||
Text(text = stringResource(msgRes))
|
||||
},
|
||||
isError = name.isNotEmpty() && nameAlreadyExists,
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(focusRequester) {
|
||||
// TODO: https://issuetracker.google.com/issues/204502668
|
||||
delay(0.1.seconds)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoDeleteDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
repo: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onDelete()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_delete_repo))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.delete_repo_confirmation, repo))
|
||||
},
|
||||
)
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
package eu.kanade.presentation.category
|
||||
@file:JvmName("ExtensionReposScreenKt")
|
||||
|
||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -7,9 +9,8 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||
import eu.kanade.presentation.category.components.repo.SourceRepoContent
|
||||
import eu.kanade.presentation.category.repos.RepoScreenState
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
@ -19,7 +20,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
@Composable
|
||||
fun SourceRepoScreen(
|
||||
fun ExtensionReposScreen(
|
||||
state: RepoScreenState.Success,
|
||||
onClickCreate: () -> Unit,
|
||||
onClickDelete: (String) -> Unit,
|
||||
@ -49,7 +50,7 @@ fun SourceRepoScreen(
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
SourceRepoContent(
|
||||
ExtensionReposContent(
|
||||
repos = state.repos,
|
||||
lazyListState = lazyListState,
|
||||
paddingValues = paddingValues + topSmallPaddingValues +
|
@ -405,6 +405,11 @@ object Migrations {
|
||||
// Deleting old download cache index files, but might as well clear it all out
|
||||
context.cacheDir.deleteRecursively()
|
||||
}
|
||||
if (oldVersion < 114) {
|
||||
sourcePreferences.extensionRepos().getAndSet {
|
||||
it.map { "https://raw.githubusercontent.com/$it/repo" }.toSet()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.extension
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
@ -49,7 +49,7 @@ class ExtensionManager(
|
||||
/**
|
||||
* API where all the available extensions can be found.
|
||||
*/
|
||||
private val api = ExtensionGithubApi()
|
||||
private val api = ExtensionApi()
|
||||
|
||||
/**
|
||||
* The installer which installs, updates and uninstalls the extensions.
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.domain.source.interactor.OFFICIAL_REPO_BASE_URL
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
@ -21,7 +22,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
import java.time.Instant
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
internal class ExtensionGithubApi {
|
||||
internal class ExtensionApi {
|
||||
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
private val preferenceStore: PreferenceStore by injectLazy()
|
||||
@ -33,52 +34,16 @@ internal class ExtensionGithubApi {
|
||||
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
|
||||
}
|
||||
|
||||
private var requiresFallbackSource = false
|
||||
|
||||
suspend fun findExtensions(): List<Extension.Available> {
|
||||
return withIOContext {
|
||||
val githubResponse = if (requiresFallbackSource) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.awaitSuccess()
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to get extensions from GitHub" }
|
||||
requiresFallbackSource = true
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val response = githubResponse ?: run {
|
||||
networkService.client
|
||||
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
val extensions = with(json) {
|
||||
response
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions() + sourcePreferences.extensionRepos()
|
||||
.get()
|
||||
.flatMap { repoPath ->
|
||||
val url = if (requiresFallbackSource) {
|
||||
"$FALLBACK_BASE_URL$repoPath@repo/"
|
||||
} else {
|
||||
"$BASE_URL$repoPath/repo/"
|
||||
}
|
||||
networkService.client
|
||||
.newCall(GET("${url}index.min.json"))
|
||||
.awaitSuccess()
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions(url, repoSource = true)
|
||||
}
|
||||
val extensions = buildList {
|
||||
addAll(getExtensions(OFFICIAL_REPO_BASE_URL, true))
|
||||
sourcePreferences.extensionRepos().get().map { addAll(getExtensions(it, false)) }
|
||||
}
|
||||
|
||||
// Sanity check - a small number of extensions probably means something broke
|
||||
// with the repo generator
|
||||
if (extensions.size < 100) {
|
||||
if (extensions.size < 50) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
@ -86,6 +51,26 @@ internal class ExtensionGithubApi {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getExtensions(
|
||||
repoBaseUrl: String,
|
||||
isOfficialRepo: Boolean,
|
||||
): List<Extension.Available> {
|
||||
return try {
|
||||
val response = networkService.client
|
||||
.newCall(GET("$repoBaseUrl/index.min.json"))
|
||||
.awaitSuccess()
|
||||
|
||||
with(json) {
|
||||
response
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions(repoBaseUrl, isRepoSource = !isOfficialRepo)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" }
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdates(
|
||||
context: Context,
|
||||
fromAvailableExtensionList: Boolean = false,
|
||||
@ -127,8 +112,8 @@ internal class ExtensionGithubApi {
|
||||
}
|
||||
|
||||
private fun List<ExtensionJsonObject>.toExtensions(
|
||||
repoUrl: String = getUrlPrefix(),
|
||||
repoSource: Boolean = false,
|
||||
repoUrl: String,
|
||||
isRepoSource: Boolean,
|
||||
): List<Extension.Available> {
|
||||
return this
|
||||
.filter {
|
||||
@ -146,9 +131,9 @@ internal class ExtensionGithubApi {
|
||||
isNsfw = it.nsfw == 1,
|
||||
sources = it.sources?.map(extensionSourceMapper).orEmpty(),
|
||||
apkName = it.apk,
|
||||
iconUrl = "${repoUrl}icon/${it.pkg}.png",
|
||||
iconUrl = "$repoUrl/icon/${it.pkg}.png",
|
||||
repoUrl = repoUrl,
|
||||
isRepoSource = repoSource,
|
||||
isRepoSource = isRepoSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -157,24 +142,11 @@ internal class ExtensionGithubApi {
|
||||
return "${extension.repoUrl}/apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
private fun getUrlPrefix(): String {
|
||||
return if (requiresFallbackSource) {
|
||||
FALLBACK_REPO_URL_PREFIX
|
||||
} else {
|
||||
REPO_URL_PREFIX
|
||||
}
|
||||
}
|
||||
|
||||
private fun ExtensionJsonObject.extractLibVersion(): Double {
|
||||
return version.substringBeforeLast('.').toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
private const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||
private const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
|
||||
private const val FALLBACK_BASE_URL = "https://gcore.jsdelivr.net/gh/"
|
||||
private const val FALLBACK_REPO_URL_PREFIX = "${FALLBACK_BASE_URL}tachiyomiorg/tachiyomi-extensions@repo/"
|
||||
|
||||
@Serializable
|
||||
private data class ExtensionJsonObject(
|
||||
val name: String,
|
@ -18,8 +18,6 @@ import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
|
||||
class CategoryScreen : Screen() {
|
||||
@ -57,7 +55,6 @@ class CategoryScreen : Screen() {
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = screenModel::createCategory,
|
||||
categories = successState.categories.fastMap { it.name }.toImmutableList(),
|
||||
title = stringResource(MR.strings.action_add_category),
|
||||
)
|
||||
}
|
||||
is CategoryDialog.Rename -> {
|
||||
@ -72,8 +69,7 @@ class CategoryScreen : Screen() {
|
||||
CategoryDeleteDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onDelete = { screenModel.deleteCategory(dialog.category.id) },
|
||||
title = stringResource(MR.strings.delete_category),
|
||||
text = stringResource(MR.strings.delete_category_confirmation, dialog.category.name),
|
||||
category = dialog.category.name,
|
||||
)
|
||||
}
|
||||
is CategoryDialog.SortAlphabetically -> {
|
||||
|
@ -65,7 +65,7 @@ import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
|
||||
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
||||
@ -337,7 +337,7 @@ class MainActivity : BaseActivity() {
|
||||
// Extensions updates
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
ExtensionGithubApi().checkForUpdates(context)
|
||||
ExtensionApi().checkForUpdates(context)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
|
@ -340,10 +340,11 @@
|
||||
<string name="label_extension_repos">Extension repos</string>
|
||||
<string name="information_empty_repos">You have no repos set.</string>
|
||||
<string name="action_add_repo">Add repo</string>
|
||||
<string name="action_add_repo_message">Add additional repos to Tachiyomi, the format of a repo is \"username/repo\", with username being the repo owner, and repo being the repo name.</string>
|
||||
<string name="label_add_repo_input">Repo URL</string>
|
||||
<string name="action_add_repo_message">Add additional repos to Tachiyomi. This should be a URL that ends with \"index.min.json\".</string>
|
||||
<string name="error_repo_exists">This repo already exists!</string>
|
||||
<string name="action_delete_repo">Delete repo</string>
|
||||
<string name="invalid_repo_name">Invalid repo name</string>
|
||||
<string name="invalid_repo_name">Invalid repo URL</string>
|
||||
<string name="delete_repo_confirmation">Do you wish to delete the repo \"%s\"?</string>
|
||||
<string name="repo_extension_message">This extension is from an external repo. Tap to view the repo.</string>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user