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
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdk = AndroidConfig.targetSdk
|
targetSdk = AndroidConfig.targetSdk
|
||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
versionCode = 129
|
versionCode = 130
|
||||||
versionName = "1.8.2"
|
versionName = "1.8.2"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
package dev.yokai.core.di
|
package dev.yokai.core.di
|
||||||
|
|
||||||
import android.app.Application
|
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
|
||||||
import dev.yokai.data.extension.repo.ExtensionRepoRepository
|
import dev.yokai.data.extension.repo.ExtensionRepoRepositoryImpl
|
||||||
import dev.yokai.domain.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.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
|
import uy.kohesive.injekt.api.addFactory
|
||||||
import uy.kohesive.injekt.api.addSingletonFactory
|
import uy.kohesive.injekt.api.addSingletonFactory
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class DomainModule(val app: Application) : InjektModule {
|
class DomainModule : InjektModule {
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
|
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
|
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||||
|
|
||||||
class SourcePreferences(private val preferenceStore: PreferenceStore) {
|
class SourcePreferences(private val preferenceStore: PreferenceStore) {
|
||||||
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
|
||||||
fun trustedExtensions() = preferenceStore.getStringSet("trusted_extensions", 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.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ExtensionOff
|
import androidx.compose.material.icons.filled.ExtensionOff
|
||||||
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
@ -25,9 +26,11 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import dev.yokai.domain.ComposableAlertDialog
|
import dev.yokai.domain.ComposableAlertDialog
|
||||||
|
import dev.yokai.domain.extension.repo.model.ExtensionRepo
|
||||||
import dev.yokai.presentation.AppBarType
|
import dev.yokai.presentation.AppBarType
|
||||||
import dev.yokai.presentation.YokaiScaffold
|
import dev.yokai.presentation.YokaiScaffold
|
||||||
import dev.yokai.presentation.component.EmptyScreen
|
import dev.yokai.presentation.component.EmptyScreen
|
||||||
|
import dev.yokai.presentation.component.ToolTipButton
|
||||||
import dev.yokai.presentation.extension.repo.component.ExtensionRepoInput
|
import dev.yokai.presentation.extension.repo.component.ExtensionRepoInput
|
||||||
import dev.yokai.presentation.extension.repo.component.ExtensionRepoItem
|
import dev.yokai.presentation.extension.repo.component.ExtensionRepoItem
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
@ -58,6 +61,13 @@ fun ExtensionRepoScreen(
|
||||||
state = rememberTopAppBarState(),
|
state = rememberTopAppBarState(),
|
||||||
canScroll = { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 },
|
canScroll = { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 },
|
||||||
),
|
),
|
||||||
|
actions = {
|
||||||
|
ToolTipButton(
|
||||||
|
toolTipLabel = stringResource(R.string.refresh),
|
||||||
|
icon = Icons.Outlined.Refresh,
|
||||||
|
buttonClicked = { viewModel.refreshRepos() },
|
||||||
|
)
|
||||||
|
},
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
if (repoState.value is ExtensionRepoState.Loading) return@YokaiScaffold
|
if (repoState.value is ExtensionRepoState.Loading) return@YokaiScaffold
|
||||||
|
|
||||||
|
@ -94,7 +104,7 @@ fun ExtensionRepoScreen(
|
||||||
repos.forEach { repo ->
|
repos.forEach { repo ->
|
||||||
item {
|
item {
|
||||||
ExtensionRepoItem(
|
ExtensionRepoItem(
|
||||||
repoUrl = repo,
|
extensionRepo = repo,
|
||||||
onDeleteClick = { repoToDelete ->
|
onDeleteClick = { repoToDelete ->
|
||||||
alertDialog.content = { ExtensionRepoDeletePrompt(repoToDelete, alertDialog, viewModel) }
|
alertDialog.content = { ExtensionRepoDeletePrompt(repoToDelete, alertDialog, viewModel) }
|
||||||
},
|
},
|
||||||
|
@ -114,10 +124,54 @@ fun ExtensionRepoScreen(
|
||||||
context.toast(event.stringRes)
|
context.toast(event.stringRes)
|
||||||
if (event is ExtensionRepoEvent.Success)
|
if (event is ExtensionRepoEvent.Success)
|
||||||
inputText = ""
|
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
|
@Composable
|
||||||
fun ExtensionRepoDeletePrompt(repoToDelete: String, alertDialog: ComposableAlertDialog, viewModel: ExtensionRepoViewModel) {
|
fun ExtensionRepoDeletePrompt(repoToDelete: String, alertDialog: ComposableAlertDialog, viewModel: ExtensionRepoViewModel) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
|
|
|
@ -4,8 +4,14 @@ import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.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.R
|
||||||
import eu.kanade.tachiyomi.util.system.launchIO
|
import eu.kanade.tachiyomi.util.system.launchIO
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -19,7 +25,12 @@ import uy.kohesive.injekt.injectLazy
|
||||||
class ExtensionRepoViewModel :
|
class ExtensionRepoViewModel :
|
||||||
ViewModel() {
|
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)
|
private val mutableRepoState: MutableStateFlow<ExtensionRepoState> = MutableStateFlow(ExtensionRepoState.Loading)
|
||||||
val repoState: StateFlow<ExtensionRepoState> = mutableRepoState.asStateFlow()
|
val repoState: StateFlow<ExtensionRepoState> = mutableRepoState.asStateFlow()
|
||||||
|
|
||||||
|
@ -28,7 +39,7 @@ class ExtensionRepoViewModel :
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
repository.getRepoFlow().collectLatest { repos ->
|
getExtensionRepo.subscribeAll().collectLatest { repos ->
|
||||||
mutableRepoState.update { ExtensionRepoState.Success(repos = repos.toImmutableList()) }
|
mutableRepoState.update { ExtensionRepoState.Success(repos = repos.toImmutableList()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,25 +47,50 @@ class ExtensionRepoViewModel :
|
||||||
|
|
||||||
fun addRepo(url: String) {
|
fun addRepo(url: String) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val result = repository.addRepo(url)
|
when (val result = createExtensionRepo.await(url)) {
|
||||||
when (result) {
|
is CreateExtensionRepo.Result.Success -> internalEvent.value = ExtensionRepoEvent.Success
|
||||||
is Result.Error -> internalEvent.value = ExtensionRepoEvent.InvalidUrl
|
is CreateExtensionRepo.Result.Error -> internalEvent.value = ExtensionRepoEvent.InvalidUrl
|
||||||
is Result.Success -> internalEvent.value = ExtensionRepoEvent.Success
|
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
|
else -> internalEvent.value = ExtensionRepoEvent.NoOp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteRepo(repo: String) {
|
fun replaceRepo(newRepo: ExtensionRepo) {
|
||||||
viewModelScope.launchIO {
|
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 ExtensionRepoEvent {
|
||||||
sealed class LocalizedMessage(@StringRes val stringRes: Int) : ExtensionRepoEvent()
|
sealed class LocalizedMessage(@StringRes val stringRes: Int) : ExtensionRepoEvent()
|
||||||
data object InvalidUrl : LocalizedMessage(R.string.invalid_repo_url)
|
data object InvalidUrl : LocalizedMessage(R.string.invalid_repo_url)
|
||||||
|
data object RepoAlreadyExists : LocalizedMessage(R.string.repo_already_exists)
|
||||||
|
data class ShowDialog(val dialog: RepoDialog) : ExtensionRepoEvent()
|
||||||
data object NoOp : ExtensionRepoEvent()
|
data object NoOp : ExtensionRepoEvent()
|
||||||
data object Success : ExtensionRepoEvent()
|
data object Success : ExtensionRepoEvent()
|
||||||
}
|
}
|
||||||
|
@ -66,7 +102,7 @@ sealed class ExtensionRepoState {
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Success(
|
data class Success(
|
||||||
val repos: List<String>,
|
val repos: List<ExtensionRepo>,
|
||||||
) : ExtensionRepoState() {
|
) : ExtensionRepoState() {
|
||||||
|
|
||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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
|
import eu.kanade.tachiyomi.util.compose.textHint
|
||||||
|
|
||||||
// TODO: Redesign
|
// TODO: Redesign
|
||||||
// - Edit
|
// - Edit
|
||||||
// - Show display name
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionRepoItem(
|
fun ExtensionRepoItem(
|
||||||
repoUrl: String,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
extensionRepo: ExtensionRepo,
|
||||||
onDeleteClick: (String) -> Unit = {},
|
onDeleteClick: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
@ -50,15 +53,28 @@ fun ExtensionRepoItem(
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
Text(
|
Column(
|
||||||
modifier = Modifier
|
modifier = modifier.weight(1.0f),
|
||||||
.weight(1.0f)
|
) {
|
||||||
.basicMarquee(),
|
Text(
|
||||||
text = repoUrl,
|
modifier = Modifier
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
.fillMaxWidth()
|
||||||
fontSize = 16.sp,
|
.basicMarquee(),
|
||||||
)
|
text = extensionRepo.name,
|
||||||
IconButton(onClick = { onDeleteClick(repoUrl) }) {
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
Gap(Size.tiny)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.basicMarquee(),
|
||||||
|
text = extensionRepo.baseUrl,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = { onDeleteClick(extensionRepo.baseUrl) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Delete,
|
imageVector = Icons.Filled.Delete,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
@ -142,7 +158,7 @@ fun ExtensionRepoItemPreview() {
|
||||||
val input = "https://raw.githubusercontent.com/null2264/totally-real-extensions/repo/index.min.json"
|
val input = "https://raw.githubusercontent.com/null2264/totally-real-extensions/repo/index.min.json"
|
||||||
Surface {
|
Surface {
|
||||||
Column {
|
Column {
|
||||||
ExtensionRepoItem(repoUrl = input)
|
ExtensionRepoItem(extensionRepo = ExtensionRepo("", "", "", "", ""))
|
||||||
ExtensionRepoInput(inputHint = "Input")
|
ExtensionRepoInput(inputHint = "Input")
|
||||||
ExtensionRepoInput(inputHint = "", inputText = input)
|
ExtensionRepoInput(inputHint = "", inputText = input)
|
||||||
ExtensionRepoInput(inputHint = "", inputText = input, isLoading = true)
|
ExtensionRepoInput(inputHint = "", inputText = input, isLoading = true)
|
||||||
|
|
|
@ -91,7 +91,7 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
|
||||||
Injekt.apply {
|
Injekt.apply {
|
||||||
importModule(PreferenceModule(this@App))
|
importModule(PreferenceModule(this@App))
|
||||||
importModule(AppModule(this@App))
|
importModule(AppModule(this@App))
|
||||||
importModule(DomainModule(this@App))
|
importModule(DomainModule())
|
||||||
}
|
}
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
|
|
|
@ -3,23 +3,24 @@ package eu.kanade.tachiyomi
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import dev.yokai.domain.base.BasePreferences
|
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
|
||||||
import dev.yokai.domain.ui.settings.ReaderPreferences.CutoutBehaviour
|
import dev.yokai.domain.ui.settings.ReaderPreferences.CutoutBehaviour
|
||||||
import dev.yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour
|
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.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
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.data.preference.PreferenceKeys
|
||||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
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.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
|
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
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.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
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.launchIO
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@ -308,6 +312,28 @@ object Migrations {
|
||||||
readerPreferences.landscapeCutoutBehavior().set(LandscapeCutoutBehaviour.DEFAULT)
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package eu.kanade.tachiyomi.extension.api
|
package eu.kanade.tachiyomi.extension.api
|
||||||
|
|
||||||
import android.content.Context
|
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.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
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.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import eu.kanade.tachiyomi.util.system.withIOContext
|
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -22,27 +26,22 @@ internal class ExtensionApi {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
private val networkService: NetworkHelper 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> {
|
suspend fun findExtensions(): List<Extension.Available> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val repos = sourcePreferences.extensionRepos().get()
|
getExtensionRepo.getAll()
|
||||||
if (repos.isEmpty()) {
|
.map { async { getExtensions(it) } }
|
||||||
return@withIOContext emptyList()
|
.awaitAll()
|
||||||
}
|
.flatten()
|
||||||
val extensions = repos.flatMap { getExtensions(it) }
|
|
||||||
|
|
||||||
if (extensions.isEmpty()) {
|
|
||||||
throw Exception()
|
|
||||||
}
|
|
||||||
|
|
||||||
extensions
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getExtensions(
|
private suspend fun getExtensions(
|
||||||
repoBaseUrl: String,
|
repo: ExtensionRepo,
|
||||||
): List<Extension.Available> {
|
): List<Extension.Available> {
|
||||||
|
val repoBaseUrl = repo.baseUrl
|
||||||
return try {
|
return try {
|
||||||
val response = networkService.client
|
val response = networkService.client
|
||||||
.newCall(GET("$repoBaseUrl/index.min.json"))
|
.newCall(GET("$repoBaseUrl/index.min.json"))
|
||||||
|
@ -63,6 +62,9 @@ internal class ExtensionApi {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val extensions = prefetchedExtensions ?: findExtensions()
|
val extensions = prefetchedExtensions ?: findExtensions()
|
||||||
|
|
||||||
|
// Update extension repo details
|
||||||
|
updateExtensionRepo.awaitAll()
|
||||||
|
|
||||||
val extensionManager: ExtensionManager = Injekt.get()
|
val extensionManager: ExtensionManager = Injekt.get()
|
||||||
val installedExtensions = extensionManager.installedExtensionsFlow.value.ifEmpty {
|
val installedExtensions = extensionManager.installedExtensionsFlow.value.ifEmpty {
|
||||||
ExtensionLoader.loadExtensionAsync(context)
|
ExtensionLoader.loadExtensionAsync(context)
|
||||||
|
|
|
@ -945,9 +945,13 @@
|
||||||
<string name="label_add_repo">Add new repo</string>
|
<string name="label_add_repo">Add new repo</string>
|
||||||
<string name="action_add_repo">Add repo</string>
|
<string name="action_add_repo">Add repo</string>
|
||||||
<string name="invalid_repo_url">Invalid repo url</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="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_title">Delete repo?</string>
|
||||||
<string name="confirm_delete_repo">Are you sure you wish to delete the repo \"%s\"?</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 -->
|
<!-- About section -->
|
||||||
<string name="version">Version</string>
|
<string name="version">Version</string>
|
||||||
|
@ -1242,4 +1246,6 @@
|
||||||
<string name="crash_screen_title">An Unexpected Error Occurred</string>
|
<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_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="crash_screen_restart_application">Restart the application</string>
|
||||||
|
|
||||||
|
<string name="refresh">Refresh</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -5,3 +5,53 @@ CREATE TABLE extension_repos (
|
||||||
website TEXT NOT NULL,
|
website TEXT NOT NULL,
|
||||||
signing_key_fingerprint TEXT UNIQUE 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