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