mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
refactor: Grab extension repo detail from repo.json
and include in DB
Co-authored-by: Matthew Witman <mnwranger@gmail.com>
This commit is contained in:
parent
e9a3facba8
commit
55455090d1
24 changed files with 622 additions and 89 deletions
|
@ -36,7 +36,7 @@ android {
|
|||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
versionCode = 129
|
||||
versionCode = 130
|
||||
versionName = "1.8.2"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled = true
|
||||
|
|
|
@ -1,15 +1,27 @@
|
|||
package dev.yokai.core.di
|
||||
|
||||
import android.app.Application
|
||||
import dev.yokai.data.extension.repo.ExtensionRepoRepository
|
||||
import dev.yokai.domain.extension.repo.ExtensionRepoRepositoryImpl
|
||||
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
|
||||
import dev.yokai.data.extension.repo.ExtensionRepoRepositoryImpl
|
||||
import dev.yokai.domain.extension.repo.interactor.CreateExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.interactor.DeleteExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.interactor.GetExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.interactor.GetExtensionRepoCount
|
||||
import dev.yokai.domain.extension.repo.interactor.ReplaceExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.interactor.UpdateExtensionRepo
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addFactory
|
||||
import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class DomainModule(val app: Application) : InjektModule {
|
||||
class DomainModule : InjektModule {
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
|
||||
addFactory { CreateExtensionRepo(get()) }
|
||||
addFactory { DeleteExtensionRepo(get()) }
|
||||
addFactory { GetExtensionRepo(get()) }
|
||||
addFactory { GetExtensionRepoCount(get()) }
|
||||
addFactory { ReplaceExtensionRepo(get()) }
|
||||
addFactory { UpdateExtensionRepo(get(), get()) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package dev.yokai.data.extension.repo
|
||||
|
||||
import dev.yokai.domain.Result
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ExtensionRepoRepository {
|
||||
fun addRepo(url: String): Result<Nothing>
|
||||
|
||||
fun deleteRepo(repo: String)
|
||||
|
||||
fun getRepoFlow(): Flow<List<String>>
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package dev.yokai.data.extension.repo
|
||||
|
||||
import android.database.sqlite.SQLiteException
|
||||
import dev.yokai.data.DatabaseHandler
|
||||
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
|
||||
import dev.yokai.domain.extension.repo.exception.SaveExtensionRepoException
|
||||
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class ExtensionRepoRepositoryImpl(private val handler: DatabaseHandler): ExtensionRepoRepository {
|
||||
/*
|
||||
override fun addRepo(url: String): Result<Nothing> {
|
||||
if (!url.matches(repoRegex))
|
||||
return Result.Error("Invalid URL")
|
||||
|
||||
sourcePreferences.extensionRepos() += url.substringBeforeLast("/index.min.json")
|
||||
|
||||
return Result.Success()
|
||||
}
|
||||
|
||||
override fun deleteRepo(repo: String) {
|
||||
sourcePreferences.extensionRepos() -= repo
|
||||
}
|
||||
|
||||
override fun getRepoFlow() =
|
||||
sourcePreferences.extensionRepos().changes()
|
||||
.map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
|
||||
*/
|
||||
override fun subscribeAll(): Flow<List<ExtensionRepo>> =
|
||||
handler.subscribeToList { extension_reposQueries.findAll(::mapExtensionRepo) }
|
||||
|
||||
override suspend fun getAll(): List<ExtensionRepo> =
|
||||
handler.awaitList { extension_reposQueries.findAll(::mapExtensionRepo) }
|
||||
|
||||
override suspend fun getRepository(baseUrl: String): ExtensionRepo? =
|
||||
handler.awaitOneOrNull { extension_reposQueries.findOne(baseUrl, ::mapExtensionRepo) }
|
||||
|
||||
override suspend fun getRepositoryBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? =
|
||||
handler.awaitOneOrNull { extension_reposQueries.findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo) }
|
||||
|
||||
override fun getCount(): Flow<Int> =
|
||||
handler.subscribeToOne { extension_reposQueries.count() }.map { it.toInt() }
|
||||
|
||||
override suspend fun insertRepository(
|
||||
baseUrl: String,
|
||||
name: String,
|
||||
shortName: String?,
|
||||
website: String,
|
||||
signingKeyFingerprint: String
|
||||
) {
|
||||
try {
|
||||
handler.await { extension_reposQueries.insert(baseUrl, name, shortName, website, signingKeyFingerprint) }
|
||||
} catch (exc: SQLiteException) {
|
||||
throw SaveExtensionRepoException(exc)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun upsertRepository(
|
||||
baseUrl: String,
|
||||
name: String,
|
||||
shortName: String?,
|
||||
website: String,
|
||||
signingKeyFingerprint: String
|
||||
) {
|
||||
try {
|
||||
handler.await { extension_reposQueries.upsert(baseUrl, name, shortName, website, signingKeyFingerprint) }
|
||||
} catch (exc: SQLiteException) {
|
||||
throw SaveExtensionRepoException(exc)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun replaceRepository(newRepo: ExtensionRepo) {
|
||||
handler.await {
|
||||
extension_reposQueries.replace(
|
||||
newRepo.baseUrl,
|
||||
newRepo.name,
|
||||
newRepo.shortName,
|
||||
newRepo.website,
|
||||
newRepo.signingKeyFingerprint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteRepository(baseUrl: String) {
|
||||
handler.await { extension_reposQueries.delete(baseUrl) }
|
||||
}
|
||||
|
||||
private fun mapExtensionRepo(
|
||||
baseUrl: String,
|
||||
name: String,
|
||||
shortName: String?,
|
||||
website: String,
|
||||
signingKeyFingerprint: String,
|
||||
): ExtensionRepo = ExtensionRepo(baseUrl, name, shortName, website, signingKeyFingerprint)
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package dev.yokai.domain.extension.repo
|
||||
|
||||
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ExtensionRepoRepository {
|
||||
fun subscribeAll(): Flow<List<ExtensionRepo>>
|
||||
suspend fun getAll(): List<ExtensionRepo>
|
||||
suspend fun getRepository(baseUrl: String): ExtensionRepo?
|
||||
suspend fun getRepositoryBySigningKeyFingerprint(fingerprint: String): ExtensionRepo?
|
||||
fun getCount(): Flow<Int>
|
||||
suspend fun insertRepository(
|
||||
baseUrl: String,
|
||||
name: String,
|
||||
shortName: String?,
|
||||
website: String,
|
||||
signingKeyFingerprint: String,
|
||||
)
|
||||
suspend fun upsertRepository(
|
||||
baseUrl: String,
|
||||
name: String,
|
||||
shortName: String?,
|
||||
website: String,
|
||||
signingKeyFingerprint: String,
|
||||
)
|
||||
suspend fun upsertRepository(repo: ExtensionRepo) {
|
||||
upsertRepository(
|
||||
baseUrl = repo.baseUrl,
|
||||
name = repo.name,
|
||||
shortName = repo.shortName,
|
||||
website = repo.website,
|
||||
signingKeyFingerprint = repo.signingKeyFingerprint,
|
||||
)
|
||||
}
|
||||
suspend fun replaceRepository(newRepo: ExtensionRepo)
|
||||
suspend fun deleteRepository(baseUrl: String)
|
||||
/*
|
||||
fun addRepo(url: String): Result<Nothing>
|
||||
|
||||
fun deleteRepo(repo: String)
|
||||
|
||||
fun getRepoFlow(): Flow<List<String>>
|
||||
*/
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package dev.yokai.domain.extension.repo
|
||||
|
||||
import dev.yokai.data.extension.repo.ExtensionRepoRepository
|
||||
import dev.yokai.domain.source.SourcePreferences
|
||||
import dev.yokai.domain.Result
|
||||
import eu.kanade.tachiyomi.data.preference.minusAssign
|
||||
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class ExtensionRepoRepositoryImpl(private val sourcePreferences: SourcePreferences): ExtensionRepoRepository {
|
||||
override fun addRepo(url: String): Result<Nothing> {
|
||||
if (!url.matches(repoRegex))
|
||||
return Result.Error("Invalid URL")
|
||||
|
||||
sourcePreferences.extensionRepos() += url.substringBeforeLast("/index.min.json")
|
||||
|
||||
return Result.Success()
|
||||
}
|
||||
|
||||
override fun deleteRepo(repo: String) {
|
||||
sourcePreferences.extensionRepos() -= repo
|
||||
}
|
||||
|
||||
override fun getRepoFlow() =
|
||||
sourcePreferences.extensionRepos().changes()
|
||||
.map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
|
||||
}
|
||||
|
||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
|
@ -0,0 +1,10 @@
|
|||
package dev.yokai.domain.extension.repo.exception
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Exception to abstract over SQLiteException and SQLiteConstraintException for multiplatform.
|
||||
*
|
||||
* @param throwable the source throwable to include for tracing.
|
||||
*/
|
||||
class SaveExtensionRepoException(throwable: Throwable) : IOException("Error Saving Repository to Database", throwable)
|
|
@ -0,0 +1,80 @@
|
|||
package dev.yokai.domain.extension.repo.interactor
|
||||
|
||||
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
|
||||
import dev.yokai.domain.extension.repo.exception.SaveExtensionRepoException
|
||||
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.service.ExtensionRepoService
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import okhttp3.OkHttpClient
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class CreateExtensionRepo(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository
|
||||
) {
|
||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
||||
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
|
||||
private val client: OkHttpClient
|
||||
get() = networkService.client
|
||||
|
||||
private val extensionRepoService = ExtensionRepoService(client)
|
||||
|
||||
suspend fun await(repoUrl: String): Result {
|
||||
if (!repoUrl.matches(repoRegex)) {
|
||||
return Result.InvalidUrl
|
||||
}
|
||||
|
||||
val baseUrl = repoUrl.removeSuffix("/index.min.json")
|
||||
return extensionRepoService.fetchRepoDetails(baseUrl)?.let { insert(it) } ?: Result.InvalidUrl
|
||||
}
|
||||
|
||||
private suspend fun insert(repo: ExtensionRepo): Result {
|
||||
return try {
|
||||
extensionRepoRepository.insertRepository(
|
||||
repo.baseUrl,
|
||||
repo.name,
|
||||
repo.shortName,
|
||||
repo.website,
|
||||
repo.signingKeyFingerprint,
|
||||
)
|
||||
Result.Success
|
||||
} catch (e: SaveExtensionRepoException) {
|
||||
Timber.e(e, "SQL Conflict attempting to add new repository ${repo.baseUrl}")
|
||||
return handleInsertionError(repo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error Handler for insert when there are trying to create new repositories
|
||||
*
|
||||
* SaveExtensionRepoException doesn't provide constraint info in exceptions.
|
||||
* First check if the conflict was on primary key. if so return RepoAlreadyExists
|
||||
* Then check if the conflict was on fingerprint. if so Return DuplicateFingerprint
|
||||
* If neither are found, there was some other Error, and return Result.Error
|
||||
*
|
||||
* @param repo Extension Repo holder for passing to DB/Error Dialog
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
private suspend fun handleInsertionError(repo: ExtensionRepo): Result {
|
||||
val repoExists = extensionRepoRepository.getRepository(repo.baseUrl)
|
||||
if (repoExists != null) {
|
||||
return Result.RepoAlreadyExists
|
||||
}
|
||||
val matchingFingerprintRepo =
|
||||
extensionRepoRepository.getRepositoryBySigningKeyFingerprint(repo.signingKeyFingerprint)
|
||||
if (matchingFingerprintRepo != null) {
|
||||
return Result.DuplicateFingerprint(matchingFingerprintRepo, repo)
|
||||
}
|
||||
return Result.Error
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
data class DuplicateFingerprint(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : Result
|
||||
data object InvalidUrl : Result
|
||||
data object RepoAlreadyExists : Result
|
||||
data object Success : Result
|
||||
data object Error : Result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package dev.yokai.domain.extension.repo.interactor
|
||||
|
||||
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
|
||||
|
||||
class DeleteExtensionRepo(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository
|
||||
) {
|
||||
suspend fun await(baseUrl: String) {
|
||||
extensionRepoRepository.deleteRepository(baseUrl)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package dev.yokai.domain.extension.repo.interactor
|
||||
|
||||
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
|
||||
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetExtensionRepo(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository
|
||||
) {
|
||||
fun subscribeAll(): Flow<List<ExtensionRepo>> = extensionRepoRepository.subscribeAll()
|
||||
|
||||
suspend fun getAll(): List<ExtensionRepo> = extensionRepoRepository.getAll()
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package dev.yokai.domain.extension.repo.interactor
|
||||
|
||||
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
|
||||
|
||||
class GetExtensionRepoCount(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository
|
||||
) {
|
||||
fun subscribe() = extensionRepoRepository.getCount()
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package dev.yokai.domain.extension.repo.interactor
|
||||
|
||||
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
|
||||
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||
|
||||
class ReplaceExtensionRepo(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository
|
||||
) {
|
||||
suspend fun await(repo: ExtensionRepo) {
|
||||
extensionRepoRepository.replaceRepository(repo)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package dev.yokai.domain.extension.repo.interactor
|
||||
|
||||
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
|
||||
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.service.ExtensionRepoService
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
||||
class UpdateExtensionRepo(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository,
|
||||
networkService: NetworkHelper,
|
||||
) {
|
||||
private val extensionRepoService = ExtensionRepoService(networkService.client)
|
||||
|
||||
suspend fun awaitAll() = coroutineScope {
|
||||
extensionRepoRepository.getAll()
|
||||
.map { async { await(it) } }
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
suspend fun await(repo: ExtensionRepo) {
|
||||
val newRepo = extensionRepoService.fetchRepoDetails(repo.baseUrl) ?: return
|
||||
if (
|
||||
repo.signingKeyFingerprint.startsWith("NOFINGERPRINT") ||
|
||||
repo.signingKeyFingerprint == newRepo.signingKeyFingerprint
|
||||
) {
|
||||
extensionRepoRepository.upsertRepository(newRepo)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package dev.yokai.domain.extension.repo.model
|
||||
|
||||
data class ExtensionRepo(
|
||||
val baseUrl: String,
|
||||
val name: String,
|
||||
val shortName: String?,
|
||||
val website: String,
|
||||
val signingKeyFingerprint: String,
|
||||
)
|
|
@ -0,0 +1,57 @@
|
|||
package dev.yokai.domain.extension.repo.service
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.HttpException
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ExtensionRepoService(
|
||||
private val client: OkHttpClient,
|
||||
) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
suspend fun fetchRepoDetails(
|
||||
repo: String,
|
||||
): ExtensionRepo? {
|
||||
return withIOContext {
|
||||
val url = "$repo/repo.json".toUri()
|
||||
|
||||
try {
|
||||
val response = with(json) {
|
||||
client.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<JsonObject>()
|
||||
}
|
||||
response["meta"]
|
||||
?.jsonObject
|
||||
?.let { jsonToExtensionRepo(baseUrl = repo, it) }
|
||||
} catch (_: HttpException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToExtensionRepo(baseUrl: String, obj: JsonObject): ExtensionRepo? {
|
||||
return try {
|
||||
ExtensionRepo(
|
||||
baseUrl = baseUrl,
|
||||
name = obj["name"]!!.jsonPrimitive.content,
|
||||
shortName = obj["shortName"]?.jsonPrimitive?.content,
|
||||
website = obj["website"]!!.jsonPrimitive.content,
|
||||
signingKeyFingerprint = obj["signingKeyFingerprint"]!!.jsonPrimitive.content,
|
||||
)
|
||||
} catch (_: NullPointerException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,5 @@ package dev.yokai.domain.source
|
|||
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())
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExtensionOff
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
@ -25,9 +26,11 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import dev.yokai.domain.ComposableAlertDialog
|
||||
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||
import dev.yokai.presentation.AppBarType
|
||||
import dev.yokai.presentation.YokaiScaffold
|
||||
import dev.yokai.presentation.component.EmptyScreen
|
||||
import dev.yokai.presentation.component.ToolTipButton
|
||||
import dev.yokai.presentation.extension.repo.component.ExtensionRepoInput
|
||||
import dev.yokai.presentation.extension.repo.component.ExtensionRepoItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
@ -58,6 +61,13 @@ fun ExtensionRepoScreen(
|
|||
state = rememberTopAppBarState(),
|
||||
canScroll = { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 },
|
||||
),
|
||||
actions = {
|
||||
ToolTipButton(
|
||||
toolTipLabel = stringResource(R.string.refresh),
|
||||
icon = Icons.Outlined.Refresh,
|
||||
buttonClicked = { viewModel.refreshRepos() },
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
if (repoState.value is ExtensionRepoState.Loading) return@YokaiScaffold
|
||||
|
||||
|
@ -94,7 +104,7 @@ fun ExtensionRepoScreen(
|
|||
repos.forEach { repo ->
|
||||
item {
|
||||
ExtensionRepoItem(
|
||||
repoUrl = repo,
|
||||
extensionRepo = repo,
|
||||
onDeleteClick = { repoToDelete ->
|
||||
alertDialog.content = { ExtensionRepoDeletePrompt(repoToDelete, alertDialog, viewModel) }
|
||||
},
|
||||
|
@ -114,9 +124,53 @@ fun ExtensionRepoScreen(
|
|||
context.toast(event.stringRes)
|
||||
if (event is ExtensionRepoEvent.Success)
|
||||
inputText = ""
|
||||
if (event is ExtensionRepoEvent.ShowDialog)
|
||||
alertDialog.content = {
|
||||
if (event.dialog is RepoDialog.Conflict) {
|
||||
ExtensionRepoReplacePrompt(
|
||||
oldRepo = event.dialog.oldRepo,
|
||||
newRepo = event.dialog.newRepo,
|
||||
onDismissRequest = { alertDialog.content = null },
|
||||
onMigrate = { viewModel.replaceRepo(event.dialog.newRepo) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoReplacePrompt(
|
||||
oldRepo: ExtensionRepo,
|
||||
newRepo: ExtensionRepo,
|
||||
onDismissRequest: () -> Unit,
|
||||
onMigrate: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onMigrate()
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.action_replace_repo))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.action_replace_repo_title))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.action_replace_repo_message, newRepo.name, oldRepo.name))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoDeletePrompt(repoToDelete: String, alertDialog: ComposableAlertDialog, viewModel: ExtensionRepoViewModel) {
|
||||
|
|
|
@ -4,8 +4,14 @@ import androidx.annotation.StringRes
|
|||
import androidx.compose.runtime.Immutable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.yokai.data.extension.repo.ExtensionRepoRepository
|
||||
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
|
||||
import dev.yokai.domain.Result
|
||||
import dev.yokai.domain.extension.repo.interactor.CreateExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.interactor.DeleteExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.interactor.GetExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.interactor.ReplaceExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.interactor.UpdateExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -19,7 +25,12 @@ import uy.kohesive.injekt.injectLazy
|
|||
class ExtensionRepoViewModel :
|
||||
ViewModel() {
|
||||
|
||||
private val repository: ExtensionRepoRepository by injectLazy()
|
||||
private val getExtensionRepo: GetExtensionRepo by injectLazy()
|
||||
private val createExtensionRepo: CreateExtensionRepo by injectLazy()
|
||||
private val deleteExtensionRepo: DeleteExtensionRepo by injectLazy()
|
||||
private val replaceExtensionRepo: ReplaceExtensionRepo by injectLazy()
|
||||
private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
|
||||
|
||||
private val mutableRepoState: MutableStateFlow<ExtensionRepoState> = MutableStateFlow(ExtensionRepoState.Loading)
|
||||
val repoState: StateFlow<ExtensionRepoState> = mutableRepoState.asStateFlow()
|
||||
|
||||
|
@ -28,7 +39,7 @@ class ExtensionRepoViewModel :
|
|||
|
||||
init {
|
||||
viewModelScope.launchIO {
|
||||
repository.getRepoFlow().collectLatest { repos ->
|
||||
getExtensionRepo.subscribeAll().collectLatest { repos ->
|
||||
mutableRepoState.update { ExtensionRepoState.Success(repos = repos.toImmutableList()) }
|
||||
}
|
||||
}
|
||||
|
@ -36,25 +47,50 @@ class ExtensionRepoViewModel :
|
|||
|
||||
fun addRepo(url: String) {
|
||||
viewModelScope.launchIO {
|
||||
val result = repository.addRepo(url)
|
||||
when (result) {
|
||||
is Result.Error -> internalEvent.value = ExtensionRepoEvent.InvalidUrl
|
||||
is Result.Success -> internalEvent.value = ExtensionRepoEvent.Success
|
||||
when (val result = createExtensionRepo.await(url)) {
|
||||
is CreateExtensionRepo.Result.Success -> internalEvent.value = ExtensionRepoEvent.Success
|
||||
is CreateExtensionRepo.Result.Error -> internalEvent.value = ExtensionRepoEvent.InvalidUrl
|
||||
is CreateExtensionRepo.Result.RepoAlreadyExists -> internalEvent.value = ExtensionRepoEvent.RepoAlreadyExists
|
||||
is CreateExtensionRepo.Result.DuplicateFingerprint -> {
|
||||
internalEvent.value = ExtensionRepoEvent.ShowDialog(RepoDialog.Conflict(result.oldRepo, result.newRepo))
|
||||
}
|
||||
else -> internalEvent.value = ExtensionRepoEvent.NoOp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteRepo(repo: String) {
|
||||
fun replaceRepo(newRepo: ExtensionRepo) {
|
||||
viewModelScope.launchIO {
|
||||
repository.deleteRepo(repo)
|
||||
replaceExtensionRepo.await(newRepo)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshRepos() {
|
||||
val status = repoState.value
|
||||
|
||||
if (status is ExtensionRepoState.Success) {
|
||||
viewModelScope.launchIO {
|
||||
updateExtensionRepo.awaitAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteRepo(url: String) {
|
||||
viewModelScope.launchIO {
|
||||
deleteExtensionRepo.await(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class RepoDialog {
|
||||
data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog()
|
||||
}
|
||||
|
||||
sealed class ExtensionRepoEvent {
|
||||
sealed class LocalizedMessage(@StringRes val stringRes: Int) : ExtensionRepoEvent()
|
||||
data object InvalidUrl : LocalizedMessage(R.string.invalid_repo_url)
|
||||
data object RepoAlreadyExists : LocalizedMessage(R.string.repo_already_exists)
|
||||
data class ShowDialog(val dialog: RepoDialog) : ExtensionRepoEvent()
|
||||
data object NoOp : ExtensionRepoEvent()
|
||||
data object Success : ExtensionRepoEvent()
|
||||
}
|
||||
|
@ -66,7 +102,7 @@ sealed class ExtensionRepoState {
|
|||
|
||||
@Immutable
|
||||
data class Success(
|
||||
val repos: List<String>,
|
||||
val repos: List<ExtensionRepo>,
|
||||
) : ExtensionRepoState() {
|
||||
|
||||
val isEmpty: Boolean
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.basicMarquee
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
|
@ -29,15 +30,17 @@ import androidx.compose.ui.text.TextStyle
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||
import dev.yokai.presentation.component.Gap
|
||||
import dev.yokai.presentation.theme.Size
|
||||
import eu.kanade.tachiyomi.util.compose.textHint
|
||||
|
||||
// TODO: Redesign
|
||||
// - Edit
|
||||
// - Show display name
|
||||
@Composable
|
||||
fun ExtensionRepoItem(
|
||||
repoUrl: String,
|
||||
modifier: Modifier = Modifier,
|
||||
extensionRepo: ExtensionRepo,
|
||||
onDeleteClick: (String) -> Unit = {},
|
||||
) {
|
||||
Row(
|
||||
|
@ -50,15 +53,28 @@ fun ExtensionRepoItem(
|
|||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Column(
|
||||
modifier = modifier.weight(1.0f),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1.0f)
|
||||
.fillMaxWidth()
|
||||
.basicMarquee(),
|
||||
text = repoUrl,
|
||||
text = extensionRepo.name,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
IconButton(onClick = { onDeleteClick(repoUrl) }) {
|
||||
Gap(Size.tiny)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.basicMarquee(),
|
||||
text = extensionRepo.baseUrl,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { onDeleteClick(extensionRepo.baseUrl) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Delete,
|
||||
contentDescription = null,
|
||||
|
@ -142,7 +158,7 @@ fun ExtensionRepoItemPreview() {
|
|||
val input = "https://raw.githubusercontent.com/null2264/totally-real-extensions/repo/index.min.json"
|
||||
Surface {
|
||||
Column {
|
||||
ExtensionRepoItem(repoUrl = input)
|
||||
ExtensionRepoItem(extensionRepo = ExtensionRepo("", "", "", "", ""))
|
||||
ExtensionRepoInput(inputHint = "Input")
|
||||
ExtensionRepoInput(inputHint = "", inputText = input)
|
||||
ExtensionRepoInput(inputHint = "", inputText = input, isLoading = true)
|
||||
|
|
|
@ -91,7 +91,7 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
|
|||
Injekt.apply {
|
||||
importModule(PreferenceModule(this@App))
|
||||
importModule(AppModule(this@App))
|
||||
importModule(DomainModule(this@App))
|
||||
importModule(DomainModule())
|
||||
}
|
||||
|
||||
setupNotificationChannels()
|
||||
|
|
|
@ -3,23 +3,24 @@ package eu.kanade.tachiyomi
|
|||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.yokai.domain.base.BasePreferences
|
||||
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
|
||||
import dev.yokai.domain.extension.repo.exception.SaveExtensionRepoException
|
||||
import dev.yokai.domain.ui.settings.ReaderPreferences
|
||||
import dev.yokai.domain.ui.settings.ReaderPreferences.CutoutBehaviour
|
||||
import dev.yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour
|
||||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.core.preference.plusAssign
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.core.preference.plusAssign
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
|
||||
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||
|
@ -29,8 +30,11 @@ import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
|
|||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
|
||||
|
@ -308,6 +312,28 @@ object Migrations {
|
|||
readerPreferences.landscapeCutoutBehavior().set(LandscapeCutoutBehaviour.DEFAULT)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 130) {
|
||||
val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
val extensionRepoRepository: ExtensionRepoRepository by injectLazy()
|
||||
val extensionRepos: Preference<Set<String>> = preferenceStore.getStringSet("extension_repos", emptySet())
|
||||
|
||||
coroutineScope.launchIO {
|
||||
for ((index, source) in extensionRepos.get().withIndex()) {
|
||||
try {
|
||||
extensionRepoRepository.upsertRepository(
|
||||
source,
|
||||
"Repo #${index + 1}",
|
||||
null,
|
||||
source,
|
||||
"NOFINGERPRINT-${index + 1}",
|
||||
)
|
||||
} catch (e: SaveExtensionRepoException) {
|
||||
Timber.e(e, "Error Migrating Extension Repo with baseUrl: $source")
|
||||
}
|
||||
}
|
||||
extensionRepos.delete()
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import android.content.Context
|
||||
import dev.yokai.domain.source.SourcePreferences
|
||||
import dev.yokai.domain.extension.repo.interactor.GetExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.interactor.UpdateExtensionRepo
|
||||
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
|
@ -11,6 +13,8 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
|||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
|
@ -22,27 +26,22 @@ internal class ExtensionApi {
|
|||
|
||||
private val json: Json by injectLazy()
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
private val sourcePreferences: SourcePreferences by injectLazy()
|
||||
private val getExtensionRepo: GetExtensionRepo by injectLazy()
|
||||
private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
|
||||
|
||||
suspend fun findExtensions(): List<Extension.Available> {
|
||||
return withIOContext {
|
||||
val repos = sourcePreferences.extensionRepos().get()
|
||||
if (repos.isEmpty()) {
|
||||
return@withIOContext emptyList()
|
||||
}
|
||||
val extensions = repos.flatMap { getExtensions(it) }
|
||||
|
||||
if (extensions.isEmpty()) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
extensions
|
||||
getExtensionRepo.getAll()
|
||||
.map { async { getExtensions(it) } }
|
||||
.awaitAll()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getExtensions(
|
||||
repoBaseUrl: String,
|
||||
repo: ExtensionRepo,
|
||||
): List<Extension.Available> {
|
||||
val repoBaseUrl = repo.baseUrl
|
||||
return try {
|
||||
val response = networkService.client
|
||||
.newCall(GET("$repoBaseUrl/index.min.json"))
|
||||
|
@ -63,6 +62,9 @@ internal class ExtensionApi {
|
|||
return withIOContext {
|
||||
val extensions = prefetchedExtensions ?: findExtensions()
|
||||
|
||||
// Update extension repo details
|
||||
updateExtensionRepo.awaitAll()
|
||||
|
||||
val extensionManager: ExtensionManager = Injekt.get()
|
||||
val installedExtensions = extensionManager.installedExtensionsFlow.value.ifEmpty {
|
||||
ExtensionLoader.loadExtensionAsync(context)
|
||||
|
|
|
@ -945,9 +945,13 @@
|
|||
<string name="label_add_repo">Add new repo</string>
|
||||
<string name="action_add_repo">Add repo</string>
|
||||
<string name="invalid_repo_url">Invalid repo url</string>
|
||||
<string name="repo_already_exists">Repo already exists!</string>
|
||||
<string name="information_empty_repos">You haven\'t added any repos yet.</string>
|
||||
<string name="confirm_delete_repo_title">Delete repo?</string>
|
||||
<string name="confirm_delete_repo">Are you sure you wish to delete the repo \"%s\"?</string>
|
||||
<string name="action_replace_repo">Replace</string>
|
||||
<string name="action_replace_repo_title">Signing Key Fingerprint Already Exists</string>
|
||||
<string name="action_replace_repo_message">Repository %1$s has the same Signing Key Fingerprint as %2$s.\nIf this is expected, %2$s will be replaced, otherwise contact your repo maintainer.</string>
|
||||
|
||||
<!-- About section -->
|
||||
<string name="version">Version</string>
|
||||
|
@ -1242,4 +1246,6 @@
|
|||
<string name="crash_screen_title">An Unexpected Error Occurred</string>
|
||||
<string name="crash_screen_description">%s ran into an unexpected error. We suggest you screenshot this message, dump the crash logs, and then share it to a GitHub Issue.</string>
|
||||
<string name="crash_screen_restart_application">Restart the application</string>
|
||||
|
||||
<string name="refresh">Refresh</string>
|
||||
</resources>
|
||||
|
|
|
@ -5,3 +5,53 @@ CREATE TABLE extension_repos (
|
|||
website TEXT NOT NULL,
|
||||
signing_key_fingerprint TEXT UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
findOne:
|
||||
SELECT *
|
||||
FROM extension_repos
|
||||
WHERE base_url = :base_url;
|
||||
|
||||
findOneBySigningKeyFingerprint:
|
||||
SELECT *
|
||||
FROM extension_repos
|
||||
WHERE signing_key_fingerprint = :fingerprint;
|
||||
|
||||
findAll:
|
||||
SELECT *
|
||||
FROM extension_repos;
|
||||
|
||||
count:
|
||||
SELECT COUNT(*)
|
||||
FROM extension_repos;
|
||||
|
||||
insert:
|
||||
INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint)
|
||||
VALUES (:base_url, :name, :short_name, :website, :fingerprint);
|
||||
|
||||
upsert:
|
||||
INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint)
|
||||
VALUES (:base_url, :name, :short_name, :website, :fingerprint)
|
||||
ON CONFLICT(base_url)
|
||||
DO UPDATE
|
||||
SET
|
||||
name = :name,
|
||||
short_name = :short_name,
|
||||
website =: website,
|
||||
signing_key_fingerprint = :fingerprint
|
||||
WHERE base_url = base_url;
|
||||
|
||||
replace:
|
||||
INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint)
|
||||
VALUES (:base_url, :name, :short_name, :website, :fingerprint)
|
||||
ON CONFLICT(signing_key_fingerprint)
|
||||
DO UPDATE
|
||||
SET
|
||||
base_url = :base_url,
|
||||
name = :name,
|
||||
short_name = :short_name,
|
||||
website =: website
|
||||
WHERE signing_key_fingerprint = signing_key_fingerprint;
|
||||
|
||||
delete:
|
||||
DELETE FROM extension_repos
|
||||
WHERE base_url = :base_url;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue