Last Commit Merged: f63573f25f
This commit is contained in:
LuftVerbot 2023-10-04 13:08:36 +02:00
parent 861a5ad913
commit c3062d2ed7
40 changed files with 598 additions and 344 deletions

View file

@ -38,10 +38,11 @@ import tachiyomi.data.history.anime.AnimeHistoryRepositoryImpl
import tachiyomi.data.history.manga.MangaHistoryRepositoryImpl
import tachiyomi.data.items.chapter.ChapterRepositoryImpl
import tachiyomi.data.items.episode.EpisodeRepositoryImpl
import tachiyomi.data.source.anime.AnimeSourceDataRepositoryImpl
import tachiyomi.data.release.ReleaseServiceImpl
import tachiyomi.data.source.anime.AnimeSourceRepositoryImpl
import tachiyomi.data.source.manga.MangaSourceDataRepositoryImpl
import tachiyomi.data.source.anime.AnimeStubSourceRepositoryImpl
import tachiyomi.data.source.manga.MangaSourceRepositoryImpl
import tachiyomi.data.source.manga.MangaStubSourceRepositoryImpl
import tachiyomi.data.track.anime.AnimeTrackRepositoryImpl
import tachiyomi.data.track.manga.MangaTrackRepositoryImpl
import tachiyomi.data.updates.anime.AnimeUpdatesRepositoryImpl
@ -113,14 +114,16 @@ import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags
import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode
import tachiyomi.domain.items.episode.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.repository.EpisodeRepository
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.service.ReleaseService
import tachiyomi.domain.source.anime.interactor.GetAnimeSourcesWithNonLibraryAnime
import tachiyomi.domain.source.anime.interactor.GetRemoteAnime
import tachiyomi.domain.source.anime.repository.AnimeSourceDataRepository
import tachiyomi.domain.source.anime.repository.AnimeSourceRepository
import tachiyomi.domain.source.anime.repository.AnimeStubSourceRepository
import tachiyomi.domain.source.manga.interactor.GetMangaSourcesWithNonLibraryManga
import tachiyomi.domain.source.manga.interactor.GetRemoteManga
import tachiyomi.domain.source.manga.repository.MangaSourceDataRepository
import tachiyomi.domain.source.manga.repository.MangaSourceRepository
import tachiyomi.domain.source.manga.repository.MangaStubSourceRepository
import tachiyomi.domain.track.anime.interactor.DeleteAnimeTrack
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
import tachiyomi.domain.track.anime.interactor.GetTracksPerAnime
@ -206,6 +209,9 @@ class DomainModule : InjektModule {
addFactory { UpdateManga(get()) }
addFactory { SetMangaCategories(get()) }
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
addFactory { GetApplicationRelease(get(), get()) }
addSingletonFactory<AnimeTrackRepository> { AnimeTrackRepositoryImpl(get()) }
addFactory { DeleteAnimeTrack(get()) }
addFactory { GetTracksPerAnime(get()) }
@ -266,7 +272,7 @@ class DomainModule : InjektModule {
addFactory { GetMangaUpdates(get()) }
addSingletonFactory<AnimeSourceRepository> { AnimeSourceRepositoryImpl(get(), get()) }
addSingletonFactory<AnimeSourceDataRepository> { AnimeSourceDataRepositoryImpl(get()) }
addSingletonFactory<AnimeStubSourceRepository> { AnimeStubSourceRepositoryImpl(get()) }
addFactory { GetEnabledAnimeSources(get(), get()) }
addFactory { GetLanguagesWithAnimeSources(get(), get()) }
addFactory { GetRemoteAnime(get()) }
@ -276,7 +282,7 @@ class DomainModule : InjektModule {
addFactory { ToggleAnimeSourcePin(get()) }
addSingletonFactory<MangaSourceRepository> { MangaSourceRepositoryImpl(get(), get()) }
addSingletonFactory<MangaSourceDataRepository> { MangaSourceDataRepositoryImpl(get()) }
addSingletonFactory<MangaStubSourceRepository> { MangaStubSourceRepositoryImpl(get()) }
addFactory { GetEnabledMangaSources(get(), get()) }
addFactory { GetLanguagesWithMangaSources(get(), get()) }
addFactory { GetRemoteManga(get()) }

View file

@ -29,7 +29,6 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.util.CrashLogUtil
@ -41,6 +40,7 @@ import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.presentation.core.components.LinkIcon
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
@ -174,16 +174,16 @@ object AboutScreen : Screen() {
/**
* Checks version and shows a user prompt if an update is available.
*/
private suspend fun checkVersion(context: Context, onAvailableUpdate: (AppUpdateResult.NewUpdate) -> Unit) {
private suspend fun checkVersion(context: Context, onAvailableUpdate: (GetApplicationRelease.Result.NewUpdate) -> Unit) {
val updateChecker = AppUpdateChecker()
withUIContext {
context.toast(R.string.update_check_look_for_updates)
try {
when (val result = withIOContext { updateChecker.checkForUpdate(context, isUserPrompt = true) }) {
is AppUpdateResult.NewUpdate -> {
when (val result = withIOContext { updateChecker.checkForUpdate(context, forceCheck = true) }) {
is GetApplicationRelease.Result.NewUpdate -> {
onAvailableUpdate(result)
}
is AppUpdateResult.NoNewUpdate -> {
is GetApplicationRelease.Result.NoNewUpdate -> {
context.toast(R.string.update_check_no_new_updates)
}
else -> {}

View file

@ -11,83 +11,38 @@ import kotlinx.serialization.json.Json
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.release.interactor.GetApplicationRelease
import uy.kohesive.injekt.injectLazy
import java.util.Date
import kotlin.time.Duration.Companion.days
class AppUpdateChecker {
private val networkService: NetworkHelper by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy()
private val json: Json by injectLazy()
private val getApplicationRelease: GetApplicationRelease by injectLazy()
private val lastAppCheck: Preference<Long> by lazy {
preferenceStore.getLong("last_app_check", 0)
}
suspend fun checkForUpdate(context: Context, isUserPrompt: Boolean = false): AppUpdateResult {
// Limit checks to once a every 3 days at most
if (isUserPrompt.not() && Date().time < lastAppCheck.get() + 3.days.inWholeMilliseconds) {
return AppUpdateResult.NoNewUpdate
}
suspend fun checkForUpdate(context: Context, forceCheck: Boolean = false): GetApplicationRelease.Result {
return withIOContext {
val result = with(json) {
networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
.awaitSuccess()
.parseAs<GithubRelease>()
.let {
lastAppCheck.set(Date().time)
// Check if latest version is different from current version
if (isNewVersion(it.version)) {
if (context.isInstalledFromFDroid()) {
AppUpdateResult.NewUpdateFdroidInstallation
} else {
AppUpdateResult.NewUpdate(it)
}
} else {
AppUpdateResult.NoNewUpdate
}
}
}
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
BuildConfig.PREVIEW,
context.isInstalledFromFDroid(),
BuildConfig.COMMIT_COUNT.toInt(),
BuildConfig.VERSION_NAME,
GITHUB_REPO,
forceCheck,
),
)
when (result) {
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
else -> {}
}
result
}
}
private fun isNewVersion(versionTag: String): Boolean {
// Removes prefixes like "r" or "v"
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
return if (BuildConfig.PREVIEW) {
// Preview builds: based on releases in "aniyomiorg/aniyomi-preview" repo
// tagged as something like "r1234"
newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt()
} else {
// Release builds: based on releases in "aniyomiorg/aniyomi" repo
// tagged as something like "v0.1.2"
val oldVersion = BuildConfig.VERSION_NAME.replace("[^\\d.]".toRegex(), "")
val newSemVer = newVersion.split(".").map { it.toInt() }
val oldSemVer = oldVersion.split(".").map { it.toInt() }
oldSemVer.mapIndexed { index, i ->
if (newSemVer[index] > i) {
return true
}
}
false
}
}
}
val GITHUB_REPO: String by lazy {

View file

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notify
import tachiyomi.domain.release.model.Release
internal class AppUpdateNotifier(private val context: Context) {
@ -27,18 +28,22 @@ internal class AppUpdateNotifier(private val context: Context) {
context.notify(id, build())
}
fun cancel() {
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATER)
}
@SuppressLint("LaunchActivityFromNotification")
fun promptUpdate(release: GithubRelease) {
val intent = Intent(context, AppUpdateService::class.java).apply {
fun promptUpdate(release: Release) {
val updateIntent = Intent(context, AppUpdateService::class.java).run {
putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink())
putExtra(AppUpdateService.EXTRA_DOWNLOAD_TITLE, release.version)
PendingIntent.getService(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
val updateIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).apply {
val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).run {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
PendingIntent.getActivity(context, release.hashCode(), this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
val releaseInfoIntent = PendingIntent.getActivity(context, release.hashCode(), releaseIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
with(notificationBuilder) {
setContentTitle(context.getString(R.string.update_check_notification_update_available))
@ -55,7 +60,7 @@ internal class AppUpdateNotifier(private val context: Context) {
addAction(
R.drawable.ic_info_24dp,
context.getString(R.string.whats_new),
releaseInfoIntent,
releaseIntent,
)
}
notificationBuilder.show()
@ -164,13 +169,12 @@ internal class AppUpdateNotifier(private val context: Context) {
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATER),
NotificationReceiver.dismissNotificationPendingBroadcast(
context,
Notifications.ID_APP_UPDATER
),
)
}
notificationBuilder.show(Notifications.ID_APP_UPDATER)
}
fun cancel() {
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATER)
}
}

View file

@ -1,7 +0,0 @@
package eu.kanade.tachiyomi.data.updater
sealed class AppUpdateResult {
class NewUpdate(val release: GithubRelease) : AppUpdateResult()
object NewUpdateFdroidInstallation : AppUpdateResult()
object NoNewUpdate : AppUpdateResult()
}

View file

@ -20,13 +20,13 @@ import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import logcat.LogPriority
import okhttp3.Call
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import okhttp3.internal.http2.ErrorCode
import okhttp3.internal.http2.StreamResetException
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.io.File
@ -41,8 +41,8 @@ class AppUpdateService : Service() {
private lateinit var notifier: AppUpdateNotifier
private var runningJob: Job? = null
private var runningCall: Call? = null
private val job = SupervisorJob()
private val serviceScope = CoroutineScope(Dispatchers.IO + job)
override fun onCreate() {
notifier = AppUpdateNotifier(this)
@ -62,11 +62,11 @@ class AppUpdateService : Service() {
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
runningJob = launchIO {
serviceScope.launch {
downloadApk(title, url)
}
runningJob?.invokeOnCompletion { stopSelf(startId) }
job.invokeOnCompletion { stopSelf(startId) }
return START_NOT_STICKY
}
@ -80,8 +80,8 @@ class AppUpdateService : Service() {
}
private fun destroyJob() {
runningJob?.cancel()
runningCall?.cancel()
serviceScope.cancel()
job.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
@ -116,9 +116,8 @@ class AppUpdateService : Service() {
try {
// Download the new update.
val call = network.client.newCachelessCallWithProgress(GET(url), progressListener)
runningCall = call
val response = call.await()
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener)
.await()
// File where the apk will be saved.
val apkFile = File(externalCacheDir, "update.apk")
@ -131,10 +130,9 @@ class AppUpdateService : Service() {
}
notifier.promptInstall(apkFile.getUriCompat(this))
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
if (e is CancellationException ||
val shouldCancel = e is CancellationException ||
(e is StreamResetException && e.errorCode == ErrorCode.CANCEL)
) {
if (shouldCancel) {
notifier.cancel()
} else {
notifier.onDownloadError(url)
@ -165,11 +163,11 @@ class AppUpdateService : Service() {
fun start(context: Context, url: String, title: String? = context.getString(R.string.app_name)) {
if (isRunning(context)) return
val intent = Intent(context, AppUpdateService::class.java).apply {
Intent(context, AppUpdateService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url)
ContextCompat.startForegroundService(context, this)
}
ContextCompat.startForegroundService(context, intent)
}
/**
@ -188,10 +186,10 @@ class AppUpdateService : Service() {
* @return [PendingIntent]
*/
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
val intent = Intent(context, AppUpdateService::class.java).apply {
return Intent(context, AppUpdateService::class.java).run {
putExtra(EXTRA_DOWNLOAD_URL, url)
PendingIntent.getService(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
}
}

View file

@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.data.updater
import android.os.Build
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Contains information about the latest release from GitHub.
*/
@Serializable
data class GithubRelease(
@SerialName("tag_name") val version: String,
@SerialName("body") val info: String,
@SerialName("html_url") val releaseLink: String,
@SerialName("assets") private val assets: List<Assets>,
) {
/**
* Get download link of latest release from the assets.
* @return download link of latest release.
*/
fun getDownloadLink(): String {
val apkVariant = when (Build.SUPPORTED_ABIS[0]) {
"arm64-v8a" -> "-arm64-v8a"
"armeabi-v7a" -> "-armeabi-v7a"
"x86" -> "-x86"
"x86_64" -> "-x86_64"
else -> ""
}
return assets.find { it.downloadLink.contains("aniyomi$apkVariant-") }?.downloadLink
?: assets[0].downloadLink
}
/**
* Assets class containing download url.
*/
@Serializable
data class Assets(@SerialName("browser_download_url") val downloadLink: String)
}

View file

@ -8,10 +8,10 @@ import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
import eu.kanade.tachiyomi.source.anime.toStubSource
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async
@ -22,7 +22,7 @@ import rx.Observable
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.source.anime.model.AnimeSourceData
import tachiyomi.domain.source.anime.model.StubAnimeSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
@ -73,12 +73,12 @@ class AnimeExtensionManager(
private val _availableAnimeExtensionsFlow = MutableStateFlow(emptyList<AnimeExtension.Available>())
val availableExtensionsFlow = _availableAnimeExtensionsFlow.asStateFlow()
private var availableAnimeExtensionsSourcesData: Map<Long, AnimeSourceData> = emptyMap()
private var availableAnimeExtensionsSourcesData: Map<Long, StubAnimeSource> = emptyMap()
private fun setupAvailableAnimeExtensionsSourcesDataMap(animeextensions: List<AnimeExtension.Available>) {
if (animeextensions.isEmpty()) return
availableAnimeExtensionsSourcesData = animeextensions
.flatMap { ext -> ext.sources.map { it.toAnimeSourceData() } }
.flatMap { ext -> ext.sources.map { it.toStubSource() } }
.associateBy { it.id }
}
@ -145,8 +145,8 @@ class AnimeExtensionManager(
// Use the source lang as some aren't present on the animeextension level.
val availableLanguages = animeextensions
.flatMap(AnimeExtension.Available::sources)
.distinctBy(AvailableAnimeSources::lang)
.map(AvailableAnimeSources::lang)
.distinctBy(AnimeExtension.Available.AnimeSource::lang)
.map(AnimeExtension.Available.AnimeSource::lang)
val deviceLanguage = Locale.getDefault().language
val defaultLanguages = preferences.enabledLanguages().defaultValue()

View file

@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
@ -126,24 +125,13 @@ internal class AnimeExtensionGithubApi {
isNsfw = it.nsfw == 1,
hasReadme = it.hasReadme == 1,
hasChangelog = it.hasChangelog == 1,
sources = it.sources?.toAnimeExtensionSources().orEmpty(),
sources = it.sources?.map(extensionAnimeSourceMapper).orEmpty(),
apkName = it.apk,
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
)
}
}
private fun List<AnimeExtensionSourceJsonObject>.toAnimeExtensionSources(): List<AvailableAnimeSources> {
return this.map {
AvailableAnimeSources(
id = it.id,
lang = it.lang,
name = it.name,
baseUrl = it.baseUrl,
)
}
}
fun getApkUrl(extension: AnimeExtension.Available): String {
return "${getUrlPrefix()}apk/${extension.apkName}"
}
@ -185,3 +173,12 @@ private data class AnimeExtensionSourceJsonObject(
val name: String,
val baseUrl: String,
)
private val extensionAnimeSourceMapper: (AnimeExtensionSourceJsonObject) -> AnimeExtension.Available.AnimeSource = {
AnimeExtension.Available.AnimeSource(
id = it.id,
lang = it.lang,
name = it.name,
baseUrl = it.baseUrl,
)
}

View file

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.extension.anime.model
import android.graphics.drawable.Drawable
import eu.kanade.tachiyomi.animesource.AnimeSource
import tachiyomi.domain.source.anime.model.AnimeSourceData
import tachiyomi.domain.source.anime.model.StubAnimeSource
sealed class AnimeExtension {
@ -44,10 +44,26 @@ sealed class AnimeExtension {
override val isNsfw: Boolean,
override val hasReadme: Boolean,
override val hasChangelog: Boolean,
val sources: List<AvailableAnimeSources>,
val sources: List<AnimeSource>,
val apkName: String,
val iconUrl: String,
) : AnimeExtension()
) : AnimeExtension() {
data class AnimeSource(
val id: Long,
val lang: String,
val name: String,
val baseUrl: String,
) {
fun toStubSource(): StubAnimeSource {
return StubAnimeSource(
id = this.id,
lang = this.lang,
name = this.name,
)
}
}
}
data class Untrusted(
override val name: String,
@ -62,18 +78,3 @@ sealed class AnimeExtension {
override val hasChangelog: Boolean = false,
) : AnimeExtension()
}
data class AvailableAnimeSources(
val id: Long,
val lang: String,
val name: String,
val baseUrl: String,
) {
fun toAnimeSourceData(): AnimeSourceData {
return AnimeSourceData(
id = this.id,
lang = this.lang,
name = this.name,
)
}
}

View file

@ -6,12 +6,12 @@ import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
import eu.kanade.tachiyomi.source.manga.toStubSource
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async
@ -22,7 +22,7 @@ import rx.Observable
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.source.manga.model.MangaSourceData
import tachiyomi.domain.source.manga.model.StubMangaSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
@ -73,12 +73,12 @@ class MangaExtensionManager(
private val _availableExtensionsFlow = MutableStateFlow(emptyList<MangaExtension.Available>())
val availableExtensionsFlow = _availableExtensionsFlow.asStateFlow()
private var availableExtensionsSourcesData: Map<Long, MangaSourceData> = emptyMap()
private var availableExtensionsSourcesData: Map<Long, StubMangaSource> = emptyMap()
private fun setupAvailableExtensionsSourcesDataMap(extensions: List<MangaExtension.Available>) {
if (extensions.isEmpty()) return
availableExtensionsSourcesData = extensions
.flatMap { ext -> ext.sources.map { it.toSourceData() } }
.flatMap { ext -> ext.sources.map { it.toStubSource() } }
.associateBy { it.id }
}
@ -145,8 +145,8 @@ class MangaExtensionManager(
// Use the source lang as some aren't present on the extension level.
val availableLanguages = extensions
.flatMap(MangaExtension.Available::sources)
.distinctBy(AvailableMangaSources::lang)
.map(AvailableMangaSources::lang)
.distinctBy(MangaExtension.Available.MangaSource::lang)
.map(MangaExtension.Available.MangaSource::lang)
val deviceLanguage = Locale.getDefault().language
val defaultLanguages = preferences.enabledLanguages().defaultValue()

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.extension.manga.api
import android.content.Context
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
@ -125,24 +124,13 @@ internal class MangaExtensionGithubApi {
isNsfw = it.nsfw == 1,
hasReadme = it.hasReadme == 1,
hasChangelog = it.hasChangelog == 1,
sources = it.sources?.toExtensionSources().orEmpty(),
sources = it.sources?.map(extensionSourceMapper).orEmpty(),
apkName = it.apk,
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
)
}
}
private fun List<ExtensionSourceJsonObject>.toExtensionSources(): List<AvailableMangaSources> {
return this.map {
AvailableMangaSources(
id = it.id,
lang = it.lang,
name = it.name,
baseUrl = it.baseUrl,
)
}
}
fun getApkUrl(extension: MangaExtension.Available): String {
return "${getUrlPrefix()}apk/${extension.apkName}"
}
@ -184,3 +172,12 @@ private data class ExtensionSourceJsonObject(
val name: String,
val baseUrl: String,
)
private val extensionSourceMapper: (ExtensionSourceJsonObject) -> MangaExtension.Available.MangaSource = {
MangaExtension.Available.MangaSource(
id = it.id,
lang = it.lang,
name = it.name,
baseUrl = it.baseUrl,
)
}

View file

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.extension.manga.model
import android.graphics.drawable.Drawable
import eu.kanade.tachiyomi.source.MangaSource
import tachiyomi.domain.source.manga.model.MangaSourceData
import tachiyomi.domain.source.manga.model.StubMangaSource
sealed class MangaExtension {
@ -44,10 +44,26 @@ sealed class MangaExtension {
override val isNsfw: Boolean,
override val hasReadme: Boolean,
override val hasChangelog: Boolean,
val sources: List<AvailableMangaSources>,
val sources: List<MangaSource>,
val apkName: String,
val iconUrl: String,
) : MangaExtension()
) : MangaExtension() {
data class MangaSource(
val id: Long,
val lang: String,
val name: String,
val baseUrl: String,
) {
fun toStubSource(): StubMangaSource {
return StubMangaSource(
id = this.id,
lang = this.lang,
name = this.name,
)
}
}
}
data class Untrusted(
override val name: String,
@ -62,18 +78,3 @@ sealed class MangaExtension {
override val hasChangelog: Boolean = false,
) : MangaExtension()
}
data class AvailableMangaSources(
val id: Long,
val lang: String,
val name: String,
val baseUrl: String,
) {
fun toSourceData(): MangaSourceData {
return MangaSourceData(
id = this.id,
lang = this.lang,
name = this.name,
)
}
}

View file

@ -15,9 +15,8 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.source.anime.model.AnimeSourceData
import tachiyomi.domain.source.anime.model.StubAnimeSource
import tachiyomi.domain.source.anime.repository.AnimeSourceDataRepository
import tachiyomi.domain.source.anime.repository.AnimeStubSourceRepository
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.source.local.entries.anime.LocalAnimeSource
import uy.kohesive.injekt.Injekt
@ -28,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap
class AndroidAnimeSourceManager(
private val context: Context,
private val extensionManager: AnimeExtensionManager,
private val sourceRepository: AnimeSourceDataRepository,
private val sourceRepository: AnimeStubSourceRepository,
) : AnimeSourceManager {
private val downloadManager: AnimeDownloadManager by injectLazy()
@ -56,7 +55,7 @@ class AndroidAnimeSourceManager(
extensions.forEach { extension ->
extension.sources.forEach {
mutableMap[it.id] = it
registerStubSource(it.toSourceData())
registerStubSource(it.toStubSource())
}
}
sourcesMapFlow.value = mutableMap
@ -68,7 +67,7 @@ class AndroidAnimeSourceManager(
.collectLatest { sources ->
val mutableMap = stubSourcesMap.toMutableMap()
sources.forEach {
mutableMap[it.id] = StubAnimeSource(it)
mutableMap[it.id] = it
}
}
}
@ -93,25 +92,25 @@ class AndroidAnimeSourceManager(
return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
}
private fun registerStubSource(sourceData: AnimeSourceData) {
private fun registerStubSource(source: StubAnimeSource) {
scope.launch {
val (id, lang, name) = sourceData
val dbSourceData = sourceRepository.getAnimeSourceData(id)
if (dbSourceData == sourceData) return@launch
sourceRepository.upsertAnimeSourceData(id, lang, name)
if (dbSourceData != null) {
downloadManager.renameSource(
StubAnimeSource(dbSourceData),
StubAnimeSource(sourceData),
)
val dbSource = sourceRepository.getStubAnimeSource(source.id)
if (dbSource == source) return@launch
sourceRepository.upsertStubAnimeSource(source.id, source.lang, source.name)
if (dbSource != null) {
downloadManager.renameSource(dbSource, source)
}
}
}
private suspend fun createStubSource(id: Long): StubAnimeSource {
sourceRepository.getAnimeSourceData(id)?.let {
return StubAnimeSource(it)
sourceRepository.getStubAnimeSource(id)?.let {
return it
}
return StubAnimeSource(AnimeSourceData(id, "", ""))
extensionManager.getSourceData(id)?.let {
registerStubSource(it)
return it
}
return StubAnimeSource(id, "", "")
}
}

View file

@ -4,7 +4,6 @@ import android.graphics.drawable.Drawable
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import tachiyomi.domain.source.anime.model.AnimeSourceData
import tachiyomi.domain.source.anime.model.StubAnimeSource
import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.Injekt
@ -14,7 +13,7 @@ fun AnimeSource.icon(): Drawable? = Injekt.get<AnimeExtensionManager>().getAppIc
fun AnimeSource.getPreferenceKey(): String = "source_$id"
fun AnimeSource.toSourceData(): AnimeSourceData = AnimeSourceData(id = id, lang = lang, name = name)
fun AnimeSource.toStubSource(): StubAnimeSource = StubAnimeSource(id = id, lang = lang, name = name)
fun AnimeSource.getNameForAnimeInfo(): String {
val preferences = Injekt.get<SourcePreferences>()

View file

@ -15,9 +15,8 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.source.manga.model.MangaSourceData
import tachiyomi.domain.source.manga.model.StubMangaSource
import tachiyomi.domain.source.manga.repository.MangaSourceDataRepository
import tachiyomi.domain.source.manga.repository.MangaStubSourceRepository
import tachiyomi.domain.source.manga.service.MangaSourceManager
import tachiyomi.source.local.entries.manga.LocalMangaSource
import uy.kohesive.injekt.Injekt
@ -28,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap
class AndroidMangaSourceManager(
private val context: Context,
private val extensionManager: MangaExtensionManager,
private val sourceRepository: MangaSourceDataRepository,
private val sourceRepository: MangaStubSourceRepository,
) : MangaSourceManager {
private val downloadManager: MangaDownloadManager by injectLazy()
@ -56,7 +55,7 @@ class AndroidMangaSourceManager(
extensions.forEach { extension ->
extension.sources.forEach {
mutableMap[it.id] = it
registerStubSource(it.toSourceData())
registerStubSource(it.toStubSource())
}
}
sourcesMapFlow.value = mutableMap
@ -68,7 +67,7 @@ class AndroidMangaSourceManager(
.collectLatest { sources ->
val mutableMap = stubSourcesMap.toMutableMap()
sources.forEach {
mutableMap[it.id] = StubMangaSource(it)
mutableMap[it.id] = it
}
}
}
@ -93,26 +92,25 @@ class AndroidMangaSourceManager(
return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
}
private fun registerStubSource(sourceData: MangaSourceData) {
private fun registerStubSource(source: StubMangaSource) {
scope.launch {
val (id, lang, name) = sourceData
val dbSourceData = sourceRepository.getMangaSourceData(id)
if (dbSourceData == sourceData) return@launch
sourceRepository.upsertMangaSourceData(id, lang, name)
if (dbSourceData != null) {
downloadManager.renameSource(StubMangaSource(dbSourceData), StubMangaSource(sourceData))
val dbSource = sourceRepository.getStubMangaSource(source.id)
if (dbSource == source) return@launch
sourceRepository.upsertStubMangaSource(source.id, source.lang, source.name)
if (dbSource != null) {
downloadManager.renameSource(dbSource, source)
}
}
}
private suspend fun createStubSource(id: Long): StubMangaSource {
sourceRepository.getMangaSourceData(id)?.let {
return StubMangaSource(it)
sourceRepository.getStubMangaSource(id)?.let {
return it
}
extensionManager.getSourceData(id)?.let {
registerStubSource(it)
return StubMangaSource(it)
return it
}
return StubMangaSource(MangaSourceData(id, "", ""))
return StubMangaSource(id, "", "")
}
}

View file

@ -4,7 +4,6 @@ import android.graphics.drawable.Drawable
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.source.MangaSource
import tachiyomi.domain.source.manga.model.MangaSourceData
import tachiyomi.domain.source.manga.model.StubMangaSource
import tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.Injekt
@ -14,7 +13,7 @@ fun MangaSource.icon(): Drawable? = Injekt.get<MangaExtensionManager>().getAppIc
fun MangaSource.getPreferenceKey(): String = "source_$id"
fun MangaSource.toSourceData(): MangaSourceData = MangaSourceData(id = id, lang = lang, name = name)
fun MangaSource.toStubSource(): StubMangaSource = StubMangaSource(id = id, lang = lang, name = name)
fun MangaSource.getNameForMangaInfo(): String {
val preferences = Injekt.get<SourcePreferences>()

View file

@ -78,7 +78,6 @@ import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
@ -110,6 +109,7 @@ import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.presentation.core.components.material.Scaffold
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -352,7 +352,7 @@ class MainActivity : BaseActivity() {
if (BuildConfig.INCLUDE_UPDATER) {
try {
val result = AppUpdateChecker().checkForUpdate(context)
if (result is AppUpdateResult.NewUpdate) {
if (result is GetApplicationRelease.Result.NewUpdate) {
val updateScreen = NewUpdateScreen(
versionName = result.release.version,
changelogInfo = result.release.info,

View file

@ -1,6 +1,7 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("plugin.serialization")
id("com.squareup.sqldelight")
}
@ -34,3 +35,12 @@ dependencies {
api(libs.sqldelight.coroutines)
api(libs.sqldelight.android.paging)
}
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-Xcontext-receivers",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}

View file

@ -0,0 +1,31 @@
package tachiyomi.data.release
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import tachiyomi.domain.release.model.Release
/**
* Contains information about the latest release from GitHub.
*/
@Serializable
data class GithubRelease(
@SerialName("tag_name") val version: String,
@SerialName("body") val info: String,
@SerialName("html_url") val releaseLink: String,
@SerialName("assets") val assets: List<GitHubAssets>,
)
/**
* Assets class containing download url.
*/
@Serializable
data class GitHubAssets(@SerialName("browser_download_url") val downloadLink: String)
val releaseMapper: (GithubRelease) -> Release = {
Release(
it.version,
it.info,
it.releaseLink,
it.assets.map(GitHubAssets::downloadLink),
)
}

View file

@ -0,0 +1,25 @@
package tachiyomi.data.release
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import tachiyomi.domain.release.model.Release
import tachiyomi.domain.release.service.ReleaseService
class ReleaseServiceImpl(
private val networkService: NetworkHelper,
private val json: Json,
) : ReleaseService {
override suspend fun latest(repository: String): Release {
return with(json) {
networkService.client
.newCall(GET("https://api.github.com/repos/$repository/releases/latest"))
.awaitSuccess()
.parseAs<GithubRelease>()
.let(releaseMapper)
}
}
}

View file

@ -1,7 +1,7 @@
package tachiyomi.data.source.anime
import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.domain.source.anime.model.AnimeSourceData
import tachiyomi.domain.source.anime.model.StubAnimeSource
val animeSourceMapper: (eu.kanade.tachiyomi.animesource.AnimeSource) -> AnimeSource = { source ->
AnimeSource(
@ -13,6 +13,6 @@ val animeSourceMapper: (eu.kanade.tachiyomi.animesource.AnimeSource) -> AnimeSou
)
}
val animeSourceDataMapper: (Long, String, String) -> AnimeSourceData = { id, lang, name ->
AnimeSourceData(id, lang, name)
val animeSourceDataMapper: (Long, String, String) -> StubAnimeSource = { id, lang, name ->
StubAnimeSource(id, lang, name)
}

View file

@ -2,18 +2,18 @@ package tachiyomi.data.source.anime
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
import tachiyomi.domain.source.anime.model.AnimeSourceData
import tachiyomi.domain.source.anime.repository.AnimeSourceDataRepository
import tachiyomi.domain.source.anime.model.StubAnimeSource
import tachiyomi.domain.source.anime.repository.AnimeStubSourceRepository
class AnimeSourceDataRepositoryImpl(
class AnimeStubSourceRepositoryImpl(
private val handler: AnimeDatabaseHandler,
) : AnimeSourceDataRepository {
) : AnimeStubSourceRepository {
override fun subscribeAllAnime(): Flow<List<AnimeSourceData>> {
override fun subscribeAllAnime(): Flow<List<StubAnimeSource>> {
return handler.subscribeToList { animesourcesQueries.findAll(animeSourceDataMapper) }
}
override suspend fun getAnimeSourceData(id: Long): AnimeSourceData? {
override suspend fun getStubAnimeSource(id: Long): StubAnimeSource? {
return handler.awaitOneOrNull {
animesourcesQueries.findOne(
id,
@ -22,7 +22,7 @@ class AnimeSourceDataRepositoryImpl(
}
}
override suspend fun upsertAnimeSourceData(id: Long, lang: String, name: String) {
override suspend fun upsertStubAnimeSource(id: Long, lang: String, name: String) {
handler.await { animesourcesQueries.upsert(id, lang, name) }
}
}

View file

@ -1,7 +1,7 @@
package tachiyomi.data.source.manga
import tachiyomi.domain.source.manga.model.MangaSourceData
import tachiyomi.domain.source.manga.model.Source
import tachiyomi.domain.source.manga.model.StubMangaSource
val mangaSourceMapper: (eu.kanade.tachiyomi.source.MangaSource) -> Source = { source ->
Source(
@ -13,6 +13,6 @@ val mangaSourceMapper: (eu.kanade.tachiyomi.source.MangaSource) -> Source = { so
)
}
val mangaSourceDataMapper: (Long, String, String) -> MangaSourceData = { id, lang, name ->
MangaSourceData(id, lang, name)
val mangaSourceDataMapper: (Long, String, String) -> StubMangaSource = { id, lang, name ->
StubMangaSource(id, lang, name)
}

View file

@ -2,18 +2,18 @@ package tachiyomi.data.source.manga
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
import tachiyomi.domain.source.manga.model.MangaSourceData
import tachiyomi.domain.source.manga.repository.MangaSourceDataRepository
import tachiyomi.domain.source.manga.model.StubMangaSource
import tachiyomi.domain.source.manga.repository.MangaStubSourceRepository
class MangaSourceDataRepositoryImpl(
class MangaStubSourceRepositoryImpl(
private val handler: MangaDatabaseHandler,
) : MangaSourceDataRepository {
) : MangaStubSourceRepository {
override fun subscribeAllManga(): Flow<List<MangaSourceData>> {
override fun subscribeAllManga(): Flow<List<StubMangaSource>> {
return handler.subscribeToList { sourcesQueries.findAll(mangaSourceDataMapper) }
}
override suspend fun getMangaSourceData(id: Long): MangaSourceData? {
override suspend fun getStubMangaSource(id: Long): StubMangaSource? {
return handler.awaitOneOrNull {
sourcesQueries.findOne(
id,
@ -22,7 +22,7 @@ class MangaSourceDataRepositoryImpl(
}
}
override suspend fun upsertMangaSourceData(id: Long, lang: String, name: String) {
override suspend fun upsertStubMangaSource(id: Long, lang: String, name: String) {
handler.await { sourcesQueries.upsert(id, lang, name) }
}
}

View file

@ -23,4 +23,13 @@ dependencies {
api(libs.sqldelight.android.paging)
testImplementation(libs.bundles.test)
testImplementation(kotlinx.coroutines.test)
}
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
)
}
}

View file

@ -0,0 +1,79 @@
package tachiyomi.domain.release.interactor
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.release.model.Release
import tachiyomi.domain.release.service.ReleaseService
import java.time.Instant
import java.time.temporal.ChronoUnit
class GetApplicationRelease(
private val service: ReleaseService,
private val preferenceStore: PreferenceStore,
) {
private val lastChecked: Preference<Long> by lazy {
preferenceStore.getLong("last_app_check", 0)
}
suspend fun await(arguments: Arguments): Result {
val now = Instant.now()
// Limit checks to once every 3 days at most
if (arguments.forceCheck.not() && now.isBefore(Instant.ofEpochMilli(lastChecked.get()).plus(3, ChronoUnit.DAYS))) {
return Result.NoNewUpdate
}
val release = service.latest(arguments.repository)
lastChecked.set(now.toEpochMilli())
// Check if latest version is different from current version
val isNewVersion = isNewVersion(arguments.isPreview, arguments.commitCount, arguments.versionName, release.version)
return when {
isNewVersion && arguments.isThirdParty -> Result.ThirdPartyInstallation
isNewVersion -> Result.NewUpdate(release)
else -> Result.NoNewUpdate
}
}
private fun isNewVersion(isPreview: Boolean, commitCount: Int, versionName: String, versionTag: String): Boolean {
// Removes prefixes like "r" or "v"
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
return if (isPreview) {
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
// tagged as something like "r1234"
newVersion.toInt() > commitCount
} else {
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
// tagged as something like "v0.1.2"
val oldVersion = versionName.replace("[^\\d.]".toRegex(), "")
val newSemVer = newVersion.split(".").map { it.toInt() }
val oldSemVer = oldVersion.split(".").map { it.toInt() }
oldSemVer.mapIndexed { index, i ->
if (newSemVer[index] > i) {
return true
}
}
false
}
}
data class Arguments(
val isPreview: Boolean,
val isThirdParty: Boolean,
val commitCount: Int,
val versionName: String,
val repository: String,
val forceCheck: Boolean = false,
)
sealed class Result {
class NewUpdate(val release: Release) : Result()
object NoNewUpdate : Result()
object ThirdPartyInstallation : Result()
}
}

View file

@ -0,0 +1,35 @@
package tachiyomi.domain.release.model
import android.os.Build
/**
* Contains information about the latest release.
*/
data class Release(
val version: String,
val info: String,
val releaseLink: String,
private val assets: List<String>,
) {
/**
* Get download link of latest release from the assets.
* @return download link of latest release.
*/
fun getDownloadLink(): String {
val apkVariant = when (Build.SUPPORTED_ABIS[0]) {
"arm64-v8a" -> "-arm64-v8a"
"armeabi-v7a" -> "-armeabi-v7a"
"x86" -> "-x86"
"x86_64" -> "-x86_64"
else -> ""
}
return assets.find { it.contains("aniyomi$apkVariant-") } ?: assets[0]
}
/**
* Assets class containing download url.
*/
data class Assets(val downloadLink: String)
}

View file

@ -0,0 +1,8 @@
package tachiyomi.domain.release.service
import tachiyomi.domain.release.model.Release
interface ReleaseService {
suspend fun latest(repository: String): Release
}

View file

@ -1,10 +0,0 @@
package tachiyomi.domain.source.anime.model
data class AnimeSourceData(
val id: Long,
val lang: String,
val name: String,
) {
val isMissingInfo: Boolean = name.isBlank() || lang.isBlank()
}

View file

@ -6,13 +6,13 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
@Suppress("OverridingDeprecatedMember")
class StubAnimeSource(private val sourceData: AnimeSourceData) : AnimeSource {
class StubAnimeSource(
override val id: Long,
override val name: String,
override val lang: String,
) : AnimeSource {
override val id: Long = sourceData.id
override val name: String = sourceData.name.ifBlank { id.toString() }
override val lang: String = sourceData.lang
val isInvalid: Boolean = name.isBlank() || lang.isBlank()
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
throw AnimeSourceNotInstalledException()
@ -27,7 +27,7 @@ class StubAnimeSource(private val sourceData: AnimeSourceData) : AnimeSource {
}
override fun toString(): String {
return if (sourceData.isMissingInfo.not()) "$name (${lang.uppercase()})" else id.toString()
return if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
}
}
class AnimeSourceNotInstalledException : Exception()

View file

@ -1,12 +0,0 @@
package tachiyomi.domain.source.anime.repository
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.source.anime.model.AnimeSourceData
interface AnimeSourceDataRepository {
fun subscribeAllAnime(): Flow<List<AnimeSourceData>>
suspend fun getAnimeSourceData(id: Long): AnimeSourceData?
suspend fun upsertAnimeSourceData(id: Long, lang: String, name: String)
}

View file

@ -0,0 +1,13 @@
package tachiyomi.domain.source.anime.repository
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.source.anime.model.StubAnimeSource
interface AnimeStubSourceRepository {
fun subscribeAllAnime(): Flow<List<StubAnimeSource>>
suspend fun getStubAnimeSource(id: Long): StubAnimeSource?
suspend fun upsertStubAnimeSource(id: Long, lang: String, name: String)
}

View file

@ -1,10 +0,0 @@
package tachiyomi.domain.source.manga.model
data class MangaSourceData(
val id: Long,
val lang: String,
val name: String,
) {
val isMissingInfo: Boolean = name.isBlank() || lang.isBlank()
}

View file

@ -7,13 +7,13 @@ import eu.kanade.tachiyomi.source.model.SManga
import rx.Observable
@Suppress("OverridingDeprecatedMember")
class StubMangaSource(private val sourceData: MangaSourceData) : MangaSource {
class StubMangaSource(
override val id: Long,
override val name: String,
override val lang: String,
) : MangaSource {
override val id: Long = sourceData.id
override val name: String = sourceData.name.ifBlank { id.toString() }
override val lang: String = sourceData.lang
val isInvalid: Boolean = name.isBlank() || lang.isBlank()
override suspend fun getMangaDetails(manga: SManga): SManga {
throw SourceNotInstalledException()
@ -43,7 +43,7 @@ class StubMangaSource(private val sourceData: MangaSourceData) : MangaSource {
}
override fun toString(): String {
return if (sourceData.isMissingInfo.not()) "$name (${lang.uppercase()})" else id.toString()
return if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
}
}

View file

@ -1,12 +0,0 @@
package tachiyomi.domain.source.manga.repository
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.source.manga.model.MangaSourceData
interface MangaSourceDataRepository {
fun subscribeAllManga(): Flow<List<MangaSourceData>>
suspend fun getMangaSourceData(id: Long): MangaSourceData?
suspend fun upsertMangaSourceData(id: Long, lang: String, name: String)
}

View file

@ -0,0 +1,12 @@
package tachiyomi.domain.source.manga.repository
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.source.manga.model.StubMangaSource
interface MangaStubSourceRepository {
fun subscribeAllManga(): Flow<List<StubMangaSource>>
suspend fun getStubMangaSource(id: Long): StubMangaSource?
suspend fun upsertStubMangaSource(id: Long, lang: String, name: String)
}

View file

@ -0,0 +1,166 @@
package tachiyomi.domain.release.interactor
import io.kotest.matchers.shouldBe
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.release.model.Release
import tachiyomi.domain.release.service.ReleaseService
import java.time.Instant
class GetApplicationReleaseTest {
lateinit var getApplicationRelease: GetApplicationRelease
lateinit var releaseService: ReleaseService
lateinit var preference: Preference<Long>
@BeforeEach
fun beforeEach() {
val preferenceStore = mockk<PreferenceStore>()
preference = mockk()
every { preferenceStore.getLong(any(), any()) } returns preference
releaseService = mockk()
getApplicationRelease = GetApplicationRelease(releaseService, preferenceStore)
}
@Test
fun `When has update but is third party expect third party installation`() = runTest {
every { preference.get() } returns 0
every { preference.set(any()) }.answers { }
coEvery { releaseService.latest(any()) } returns Release(
"v2.0.0",
"info",
"http://example.com/release_link",
listOf("http://example.com/assets"),
)
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
isPreview = false,
isThirdParty = true,
commitCount = 0,
versionName = "v1.0.0",
repository = "test",
),
)
result shouldBe GetApplicationRelease.Result.ThirdPartyInstallation
}
@Test
fun `When has update but is preview expect new update`() = runTest {
every { preference.get() } returns 0
every { preference.set(any()) }.answers { }
val release = Release(
"r2000",
"info",
"http://example.com/release_link",
listOf("http://example.com/assets"),
)
coEvery { releaseService.latest(any()) } returns release
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
isPreview = true,
isThirdParty = false,
commitCount = 1000,
versionName = "",
repository = "test",
),
)
(result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate(release).release
}
@Test
fun `When has update expect new update`() = runTest {
every { preference.get() } returns 0
every { preference.set(any()) }.answers { }
val release = Release(
"v2.0.0",
"info",
"http://example.com/release_link",
listOf("http://example.com/assets"),
)
coEvery { releaseService.latest(any()) } returns release
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
isPreview = false,
isThirdParty = false,
commitCount = 0,
versionName = "v1.0.0",
repository = "test",
),
)
(result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate(release).release
}
@Test
fun `When has no update expect no new update`() = runTest {
every { preference.get() } returns 0
every { preference.set(any()) }.answers { }
val release = Release(
"v1.0.0",
"info",
"http://example.com/release_link",
listOf("http://example.com/assets"),
)
coEvery { releaseService.latest(any()) } returns release
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
isPreview = false,
isThirdParty = false,
commitCount = 0,
versionName = "v2.0.0",
repository = "test",
),
)
result shouldBe GetApplicationRelease.Result.NoNewUpdate
}
@Test
fun `When now is before three days expect no new update`() = runTest {
every { preference.get() } returns Instant.now().toEpochMilli()
every { preference.set(any()) }.answers { }
val release = Release(
"v1.0.0",
"info",
"http://example.com/release_link",
listOf("http://example.com/assets"),
)
coEvery { releaseService.latest(any()) } returns release
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
isPreview = false,
isThirdParty = false,
commitCount = 0,
versionName = "v2.0.0",
repository = "test",
),
)
coVerify(exactly = 0) { releaseService.latest(any()) }
result shouldBe GetApplicationRelease.Result.NoNewUpdate
}
}

View file

@ -11,6 +11,7 @@ coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", vers
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" }
serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization_version" }

View file

@ -60,7 +60,7 @@ photoview = "com.github.chrisbanes:PhotoView:2.3.0"
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
compose-cascade = "me.saket.cascade:cascade-compose:2.0.0-rc02"
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:0.11.3"
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:0.12.1"
compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0"
logcat = "com.squareup.logcat:logcat:0.1"
@ -91,6 +91,8 @@ voyager-transitions = { module = "ca.gosyer:voyager-transitions", version.ref =
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
mockk = "io.mockk:mockk:1.13.5"
aniyomi-mpv = "com.github.aniyomiorg:aniyomi-mpv-lib:1.10.n"
ffmpeg-kit = "com.github.jmir1:ffmpeg-kit:1.10"
arthenica-smartexceptions = "com.arthenica:smart-exception-java:0.1.1"
@ -106,4 +108,4 @@ coil = ["coil-core", "coil-gif", "coil-compose"]
shizuku = ["shizuku-api", "shizuku-provider"]
voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"]
richtext = ["richtext-commonmark", "richtext-m3"]
test = ["junit", "kotest-assertions"]
test = ["junit", "kotest-assertions", "mockk"]