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.history.manga.MangaHistoryRepositoryImpl
import tachiyomi.data.items.chapter.ChapterRepositoryImpl import tachiyomi.data.items.chapter.ChapterRepositoryImpl
import tachiyomi.data.items.episode.EpisodeRepositoryImpl 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.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.MangaSourceRepositoryImpl
import tachiyomi.data.source.manga.MangaStubSourceRepositoryImpl
import tachiyomi.data.track.anime.AnimeTrackRepositoryImpl import tachiyomi.data.track.anime.AnimeTrackRepositoryImpl
import tachiyomi.data.track.manga.MangaTrackRepositoryImpl import tachiyomi.data.track.manga.MangaTrackRepositoryImpl
import tachiyomi.data.updates.anime.AnimeUpdatesRepositoryImpl 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.ShouldUpdateDbEpisode
import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.repository.EpisodeRepository 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.GetAnimeSourcesWithNonLibraryAnime
import tachiyomi.domain.source.anime.interactor.GetRemoteAnime 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.AnimeSourceRepository
import tachiyomi.domain.source.anime.repository.AnimeStubSourceRepository
import tachiyomi.domain.source.manga.interactor.GetMangaSourcesWithNonLibraryManga import tachiyomi.domain.source.manga.interactor.GetMangaSourcesWithNonLibraryManga
import tachiyomi.domain.source.manga.interactor.GetRemoteManga 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.MangaSourceRepository
import tachiyomi.domain.source.manga.repository.MangaStubSourceRepository
import tachiyomi.domain.track.anime.interactor.DeleteAnimeTrack import tachiyomi.domain.track.anime.interactor.DeleteAnimeTrack
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
import tachiyomi.domain.track.anime.interactor.GetTracksPerAnime import tachiyomi.domain.track.anime.interactor.GetTracksPerAnime
@ -206,6 +209,9 @@ class DomainModule : InjektModule {
addFactory { UpdateManga(get()) } addFactory { UpdateManga(get()) }
addFactory { SetMangaCategories(get()) } addFactory { SetMangaCategories(get()) }
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
addFactory { GetApplicationRelease(get(), get()) }
addSingletonFactory<AnimeTrackRepository> { AnimeTrackRepositoryImpl(get()) } addSingletonFactory<AnimeTrackRepository> { AnimeTrackRepositoryImpl(get()) }
addFactory { DeleteAnimeTrack(get()) } addFactory { DeleteAnimeTrack(get()) }
addFactory { GetTracksPerAnime(get()) } addFactory { GetTracksPerAnime(get()) }
@ -266,7 +272,7 @@ class DomainModule : InjektModule {
addFactory { GetMangaUpdates(get()) } addFactory { GetMangaUpdates(get()) }
addSingletonFactory<AnimeSourceRepository> { AnimeSourceRepositoryImpl(get(), get()) } addSingletonFactory<AnimeSourceRepository> { AnimeSourceRepositoryImpl(get(), get()) }
addSingletonFactory<AnimeSourceDataRepository> { AnimeSourceDataRepositoryImpl(get()) } addSingletonFactory<AnimeStubSourceRepository> { AnimeStubSourceRepositoryImpl(get()) }
addFactory { GetEnabledAnimeSources(get(), get()) } addFactory { GetEnabledAnimeSources(get(), get()) }
addFactory { GetLanguagesWithAnimeSources(get(), get()) } addFactory { GetLanguagesWithAnimeSources(get(), get()) }
addFactory { GetRemoteAnime(get()) } addFactory { GetRemoteAnime(get()) }
@ -276,7 +282,7 @@ class DomainModule : InjektModule {
addFactory { ToggleAnimeSourcePin(get()) } addFactory { ToggleAnimeSourcePin(get()) }
addSingletonFactory<MangaSourceRepository> { MangaSourceRepositoryImpl(get(), get()) } addSingletonFactory<MangaSourceRepository> { MangaSourceRepositoryImpl(get(), get()) }
addSingletonFactory<MangaSourceDataRepository> { MangaSourceDataRepositoryImpl(get()) } addSingletonFactory<MangaStubSourceRepository> { MangaStubSourceRepositoryImpl(get()) }
addFactory { GetEnabledMangaSources(get(), get()) } addFactory { GetEnabledMangaSources(get(), get()) }
addFactory { GetLanguagesWithMangaSources(get(), get()) } addFactory { GetLanguagesWithMangaSources(get(), get()) }
addFactory { GetRemoteManga(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.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker 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.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
@ -41,6 +40,7 @@ import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.presentation.core.components.LinkIcon import tachiyomi.presentation.core.components.LinkIcon
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold 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. * 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() val updateChecker = AppUpdateChecker()
withUIContext { withUIContext {
context.toast(R.string.update_check_look_for_updates) context.toast(R.string.update_check_look_for_updates)
try { try {
when (val result = withIOContext { updateChecker.checkForUpdate(context, isUserPrompt = true) }) { when (val result = withIOContext { updateChecker.checkForUpdate(context, forceCheck = true) }) {
is AppUpdateResult.NewUpdate -> { is GetApplicationRelease.Result.NewUpdate -> {
onAvailableUpdate(result) onAvailableUpdate(result)
} }
is AppUpdateResult.NoNewUpdate -> { is GetApplicationRelease.Result.NoNewUpdate -> {
context.toast(R.string.update_check_no_new_updates) context.toast(R.string.update_check_no_new_updates)
} }
else -> {} else -> {}

View file

@ -11,83 +11,38 @@ import kotlinx.serialization.json.Json
import tachiyomi.core.preference.Preference import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.release.interactor.GetApplicationRelease
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Date import java.util.Date
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
class AppUpdateChecker { class AppUpdateChecker {
private val networkService: NetworkHelper by injectLazy() private val getApplicationRelease: GetApplicationRelease by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy()
private val json: Json 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 { return withIOContext {
val result = with(json) { val result = getApplicationRelease.await(
networkService.client GetApplicationRelease.Arguments(
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest")) BuildConfig.PREVIEW,
.awaitSuccess() context.isInstalledFromFDroid(),
.parseAs<GithubRelease>() BuildConfig.COMMIT_COUNT.toInt(),
.let { BuildConfig.VERSION_NAME,
lastAppCheck.set(Date().time) GITHUB_REPO,
forceCheck,
// 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
}
}
}
when (result) { when (result) {
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release) is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate() is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
else -> {} else -> {}
} }
result 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 { 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.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notify import eu.kanade.tachiyomi.util.system.notify
import tachiyomi.domain.release.model.Release
internal class AppUpdateNotifier(private val context: Context) { internal class AppUpdateNotifier(private val context: Context) {
@ -27,18 +28,22 @@ internal class AppUpdateNotifier(private val context: Context) {
context.notify(id, build()) context.notify(id, build())
} }
fun cancel() {
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATER)
}
@SuppressLint("LaunchActivityFromNotification") @SuppressLint("LaunchActivityFromNotification")
fun promptUpdate(release: GithubRelease) { fun promptUpdate(release: Release) {
val intent = Intent(context, AppUpdateService::class.java).apply { val updateIntent = Intent(context, AppUpdateService::class.java).run {
putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink()) putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink())
putExtra(AppUpdateService.EXTRA_DOWNLOAD_TITLE, release.version) 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 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) { with(notificationBuilder) {
setContentTitle(context.getString(R.string.update_check_notification_update_available)) setContentTitle(context.getString(R.string.update_check_notification_update_available))
@ -55,7 +60,7 @@ internal class AppUpdateNotifier(private val context: Context) {
addAction( addAction(
R.drawable.ic_info_24dp, R.drawable.ic_info_24dp,
context.getString(R.string.whats_new), context.getString(R.string.whats_new),
releaseInfoIntent, releaseIntent,
) )
} }
notificationBuilder.show() notificationBuilder.show()
@ -164,13 +169,12 @@ internal class AppUpdateNotifier(private val context: Context) {
addAction( addAction(
R.drawable.ic_close_24dp, R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel), 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) 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.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job import kotlinx.coroutines.CoroutineScope
import logcat.LogPriority import kotlinx.coroutines.Dispatchers
import okhttp3.Call import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import okhttp3.internal.http2.ErrorCode import okhttp3.internal.http2.ErrorCode
import okhttp3.internal.http2.StreamResetException import okhttp3.internal.http2.StreamResetException
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
@ -41,8 +41,8 @@ class AppUpdateService : Service() {
private lateinit var notifier: AppUpdateNotifier private lateinit var notifier: AppUpdateNotifier
private var runningJob: Job? = null private val job = SupervisorJob()
private var runningCall: Call? = null private val serviceScope = CoroutineScope(Dispatchers.IO + job)
override fun onCreate() { override fun onCreate() {
notifier = AppUpdateNotifier(this) notifier = AppUpdateNotifier(this)
@ -62,11 +62,11 @@ class AppUpdateService : Service() {
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name) val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
runningJob = launchIO { serviceScope.launch {
downloadApk(title, url) downloadApk(title, url)
} }
runningJob?.invokeOnCompletion { stopSelf(startId) } job.invokeOnCompletion { stopSelf(startId) }
return START_NOT_STICKY return START_NOT_STICKY
} }
@ -80,8 +80,8 @@ class AppUpdateService : Service() {
} }
private fun destroyJob() { private fun destroyJob() {
runningJob?.cancel() serviceScope.cancel()
runningCall?.cancel() job.cancel()
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
wakeLock.release() wakeLock.release()
} }
@ -116,9 +116,8 @@ class AppUpdateService : Service() {
try { try {
// Download the new update. // Download the new update.
val call = network.client.newCachelessCallWithProgress(GET(url), progressListener) val response = network.client.newCachelessCallWithProgress(GET(url), progressListener)
runningCall = call .await()
val response = call.await()
// File where the apk will be saved. // File where the apk will be saved.
val apkFile = File(externalCacheDir, "update.apk") val apkFile = File(externalCacheDir, "update.apk")
@ -131,10 +130,9 @@ class AppUpdateService : Service() {
} }
notifier.promptInstall(apkFile.getUriCompat(this)) notifier.promptInstall(apkFile.getUriCompat(this))
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) val shouldCancel = e is CancellationException ||
if (e is CancellationException ||
(e is StreamResetException && e.errorCode == ErrorCode.CANCEL) (e is StreamResetException && e.errorCode == ErrorCode.CANCEL)
) { if (shouldCancel) {
notifier.cancel() notifier.cancel()
} else { } else {
notifier.onDownloadError(url) notifier.onDownloadError(url)
@ -165,11 +163,11 @@ class AppUpdateService : Service() {
fun start(context: Context, url: String, title: String? = context.getString(R.string.app_name)) { fun start(context: Context, url: String, title: String? = context.getString(R.string.app_name)) {
if (isRunning(context)) return 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_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url) putExtra(EXTRA_DOWNLOAD_URL, url)
ContextCompat.startForegroundService(context, this)
} }
ContextCompat.startForegroundService(context, intent)
} }
/** /**
@ -188,10 +186,10 @@ class AppUpdateService : Service() {
* @return [PendingIntent] * @return [PendingIntent]
*/ */
internal fun downloadApkPendingService(context: Context, url: String): 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) 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.api.AnimeExtensionGithubApi
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult 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.AnimeExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader 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.preference.plusAssign
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -22,7 +22,7 @@ import rx.Observable
import tachiyomi.core.util.lang.launchNow import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Locale import java.util.Locale
@ -73,12 +73,12 @@ class AnimeExtensionManager(
private val _availableAnimeExtensionsFlow = MutableStateFlow(emptyList<AnimeExtension.Available>()) private val _availableAnimeExtensionsFlow = MutableStateFlow(emptyList<AnimeExtension.Available>())
val availableExtensionsFlow = _availableAnimeExtensionsFlow.asStateFlow() 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>) { private fun setupAvailableAnimeExtensionsSourcesDataMap(animeextensions: List<AnimeExtension.Available>) {
if (animeextensions.isEmpty()) return if (animeextensions.isEmpty()) return
availableAnimeExtensionsSourcesData = animeextensions availableAnimeExtensionsSourcesData = animeextensions
.flatMap { ext -> ext.sources.map { it.toAnimeSourceData() } } .flatMap { ext -> ext.sources.map { it.toStubSource() } }
.associateBy { it.id } .associateBy { it.id }
} }
@ -145,8 +145,8 @@ class AnimeExtensionManager(
// Use the source lang as some aren't present on the animeextension level. // Use the source lang as some aren't present on the animeextension level.
val availableLanguages = animeextensions val availableLanguages = animeextensions
.flatMap(AnimeExtension.Available::sources) .flatMap(AnimeExtension.Available::sources)
.distinctBy(AvailableAnimeSources::lang) .distinctBy(AnimeExtension.Available.AnimeSource::lang)
.map(AvailableAnimeSources::lang) .map(AnimeExtension.Available.AnimeSource::lang)
val deviceLanguage = Locale.getDefault().language val deviceLanguage = Locale.getDefault().language
val defaultLanguages = preferences.enabledLanguages().defaultValue() 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.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult 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.extension.anime.util.AnimeExtensionLoader
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
@ -126,24 +125,13 @@ internal class AnimeExtensionGithubApi {
isNsfw = it.nsfw == 1, isNsfw = it.nsfw == 1,
hasReadme = it.hasReadme == 1, hasReadme = it.hasReadme == 1,
hasChangelog = it.hasChangelog == 1, hasChangelog = it.hasChangelog == 1,
sources = it.sources?.toAnimeExtensionSources().orEmpty(), sources = it.sources?.map(extensionAnimeSourceMapper).orEmpty(),
apkName = it.apk, apkName = it.apk,
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}", 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 { fun getApkUrl(extension: AnimeExtension.Available): String {
return "${getUrlPrefix()}apk/${extension.apkName}" return "${getUrlPrefix()}apk/${extension.apkName}"
} }
@ -185,3 +173,12 @@ private data class AnimeExtensionSourceJsonObject(
val name: String, val name: String,
val baseUrl: 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 android.graphics.drawable.Drawable
import eu.kanade.tachiyomi.animesource.AnimeSource import eu.kanade.tachiyomi.animesource.AnimeSource
import tachiyomi.domain.source.anime.model.AnimeSourceData import tachiyomi.domain.source.anime.model.StubAnimeSource
sealed class AnimeExtension { sealed class AnimeExtension {
@ -44,10 +44,26 @@ sealed class AnimeExtension {
override val isNsfw: Boolean, override val isNsfw: Boolean,
override val hasReadme: Boolean, override val hasReadme: Boolean,
override val hasChangelog: Boolean, override val hasChangelog: Boolean,
val sources: List<AvailableAnimeSources>, val sources: List<AnimeSource>,
val apkName: String, val apkName: String,
val iconUrl: 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( data class Untrusted(
override val name: String, override val name: String,
@ -62,18 +78,3 @@ sealed class AnimeExtension {
override val hasChangelog: Boolean = false, override val hasChangelog: Boolean = false,
) : AnimeExtension() ) : 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.R
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi 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.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallReceiver import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader 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.preference.plusAssign
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -22,7 +22,7 @@ import rx.Observable
import tachiyomi.core.util.lang.launchNow import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Locale import java.util.Locale
@ -73,12 +73,12 @@ class MangaExtensionManager(
private val _availableExtensionsFlow = MutableStateFlow(emptyList<MangaExtension.Available>()) private val _availableExtensionsFlow = MutableStateFlow(emptyList<MangaExtension.Available>())
val availableExtensionsFlow = _availableExtensionsFlow.asStateFlow() 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>) { private fun setupAvailableExtensionsSourcesDataMap(extensions: List<MangaExtension.Available>) {
if (extensions.isEmpty()) return if (extensions.isEmpty()) return
availableExtensionsSourcesData = extensions availableExtensionsSourcesData = extensions
.flatMap { ext -> ext.sources.map { it.toSourceData() } } .flatMap { ext -> ext.sources.map { it.toStubSource() } }
.associateBy { it.id } .associateBy { it.id }
} }
@ -145,8 +145,8 @@ class MangaExtensionManager(
// Use the source lang as some aren't present on the extension level. // Use the source lang as some aren't present on the extension level.
val availableLanguages = extensions val availableLanguages = extensions
.flatMap(MangaExtension.Available::sources) .flatMap(MangaExtension.Available::sources)
.distinctBy(AvailableMangaSources::lang) .distinctBy(MangaExtension.Available.MangaSource::lang)
.map(AvailableMangaSources::lang) .map(MangaExtension.Available.MangaSource::lang)
val deviceLanguage = Locale.getDefault().language val deviceLanguage = Locale.getDefault().language
val defaultLanguages = preferences.enabledLanguages().defaultValue() val defaultLanguages = preferences.enabledLanguages().defaultValue()

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.extension.manga.api
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager 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.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
@ -125,24 +124,13 @@ internal class MangaExtensionGithubApi {
isNsfw = it.nsfw == 1, isNsfw = it.nsfw == 1,
hasReadme = it.hasReadme == 1, hasReadme = it.hasReadme == 1,
hasChangelog = it.hasChangelog == 1, hasChangelog = it.hasChangelog == 1,
sources = it.sources?.toExtensionSources().orEmpty(), sources = it.sources?.map(extensionSourceMapper).orEmpty(),
apkName = it.apk, apkName = it.apk,
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}", 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 { fun getApkUrl(extension: MangaExtension.Available): String {
return "${getUrlPrefix()}apk/${extension.apkName}" return "${getUrlPrefix()}apk/${extension.apkName}"
} }
@ -184,3 +172,12 @@ private data class ExtensionSourceJsonObject(
val name: String, val name: String,
val baseUrl: 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 android.graphics.drawable.Drawable
import eu.kanade.tachiyomi.source.MangaSource import eu.kanade.tachiyomi.source.MangaSource
import tachiyomi.domain.source.manga.model.MangaSourceData import tachiyomi.domain.source.manga.model.StubMangaSource
sealed class MangaExtension { sealed class MangaExtension {
@ -44,10 +44,26 @@ sealed class MangaExtension {
override val isNsfw: Boolean, override val isNsfw: Boolean,
override val hasReadme: Boolean, override val hasReadme: Boolean,
override val hasChangelog: Boolean, override val hasChangelog: Boolean,
val sources: List<AvailableMangaSources>, val sources: List<MangaSource>,
val apkName: String, val apkName: String,
val iconUrl: 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( data class Untrusted(
override val name: String, override val name: String,
@ -62,18 +78,3 @@ sealed class MangaExtension {
override val hasChangelog: Boolean = false, override val hasChangelog: Boolean = false,
) : MangaExtension() ) : 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.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import tachiyomi.domain.source.anime.model.AnimeSourceData
import tachiyomi.domain.source.anime.model.StubAnimeSource 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.domain.source.anime.service.AnimeSourceManager
import tachiyomi.source.local.entries.anime.LocalAnimeSource import tachiyomi.source.local.entries.anime.LocalAnimeSource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -28,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap
class AndroidAnimeSourceManager( class AndroidAnimeSourceManager(
private val context: Context, private val context: Context,
private val extensionManager: AnimeExtensionManager, private val extensionManager: AnimeExtensionManager,
private val sourceRepository: AnimeSourceDataRepository, private val sourceRepository: AnimeStubSourceRepository,
) : AnimeSourceManager { ) : AnimeSourceManager {
private val downloadManager: AnimeDownloadManager by injectLazy() private val downloadManager: AnimeDownloadManager by injectLazy()
@ -56,7 +55,7 @@ class AndroidAnimeSourceManager(
extensions.forEach { extension -> extensions.forEach { extension ->
extension.sources.forEach { extension.sources.forEach {
mutableMap[it.id] = it mutableMap[it.id] = it
registerStubSource(it.toSourceData()) registerStubSource(it.toStubSource())
} }
} }
sourcesMapFlow.value = mutableMap sourcesMapFlow.value = mutableMap
@ -68,7 +67,7 @@ class AndroidAnimeSourceManager(
.collectLatest { sources -> .collectLatest { sources ->
val mutableMap = stubSourcesMap.toMutableMap() val mutableMap = stubSourcesMap.toMutableMap()
sources.forEach { 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 } return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
} }
private fun registerStubSource(sourceData: AnimeSourceData) { private fun registerStubSource(source: StubAnimeSource) {
scope.launch { scope.launch {
val (id, lang, name) = sourceData val dbSource = sourceRepository.getStubAnimeSource(source.id)
val dbSourceData = sourceRepository.getAnimeSourceData(id) if (dbSource == source) return@launch
if (dbSourceData == sourceData) return@launch sourceRepository.upsertStubAnimeSource(source.id, source.lang, source.name)
sourceRepository.upsertAnimeSourceData(id, lang, name) if (dbSource != null) {
if (dbSourceData != null) { downloadManager.renameSource(dbSource, source)
downloadManager.renameSource(
StubAnimeSource(dbSourceData),
StubAnimeSource(sourceData),
)
} }
} }
} }
private suspend fun createStubSource(id: Long): StubAnimeSource { private suspend fun createStubSource(id: Long): StubAnimeSource {
sourceRepository.getAnimeSourceData(id)?.let { sourceRepository.getStubAnimeSource(id)?.let {
return StubAnimeSource(it) 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.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.animesource.AnimeSource import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import tachiyomi.domain.source.anime.model.AnimeSourceData
import tachiyomi.domain.source.anime.model.StubAnimeSource import tachiyomi.domain.source.anime.model.StubAnimeSource
import tachiyomi.source.local.entries.anime.isLocal import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.Injekt 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.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 { fun AnimeSource.getNameForAnimeInfo(): String {
val preferences = Injekt.get<SourcePreferences>() val preferences = Injekt.get<SourcePreferences>()

View file

@ -15,9 +15,8 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import tachiyomi.domain.source.manga.model.MangaSourceData
import tachiyomi.domain.source.manga.model.StubMangaSource 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.domain.source.manga.service.MangaSourceManager
import tachiyomi.source.local.entries.manga.LocalMangaSource import tachiyomi.source.local.entries.manga.LocalMangaSource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -28,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap
class AndroidMangaSourceManager( class AndroidMangaSourceManager(
private val context: Context, private val context: Context,
private val extensionManager: MangaExtensionManager, private val extensionManager: MangaExtensionManager,
private val sourceRepository: MangaSourceDataRepository, private val sourceRepository: MangaStubSourceRepository,
) : MangaSourceManager { ) : MangaSourceManager {
private val downloadManager: MangaDownloadManager by injectLazy() private val downloadManager: MangaDownloadManager by injectLazy()
@ -56,7 +55,7 @@ class AndroidMangaSourceManager(
extensions.forEach { extension -> extensions.forEach { extension ->
extension.sources.forEach { extension.sources.forEach {
mutableMap[it.id] = it mutableMap[it.id] = it
registerStubSource(it.toSourceData()) registerStubSource(it.toStubSource())
} }
} }
sourcesMapFlow.value = mutableMap sourcesMapFlow.value = mutableMap
@ -68,7 +67,7 @@ class AndroidMangaSourceManager(
.collectLatest { sources -> .collectLatest { sources ->
val mutableMap = stubSourcesMap.toMutableMap() val mutableMap = stubSourcesMap.toMutableMap()
sources.forEach { 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 } return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
} }
private fun registerStubSource(sourceData: MangaSourceData) { private fun registerStubSource(source: StubMangaSource) {
scope.launch { scope.launch {
val (id, lang, name) = sourceData val dbSource = sourceRepository.getStubMangaSource(source.id)
val dbSourceData = sourceRepository.getMangaSourceData(id) if (dbSource == source) return@launch
if (dbSourceData == sourceData) return@launch sourceRepository.upsertStubMangaSource(source.id, source.lang, source.name)
sourceRepository.upsertMangaSourceData(id, lang, name) if (dbSource != null) {
if (dbSourceData != null) { downloadManager.renameSource(dbSource, source)
downloadManager.renameSource(StubMangaSource(dbSourceData), StubMangaSource(sourceData))
} }
} }
} }
private suspend fun createStubSource(id: Long): StubMangaSource { private suspend fun createStubSource(id: Long): StubMangaSource {
sourceRepository.getMangaSourceData(id)?.let { sourceRepository.getStubMangaSource(id)?.let {
return StubMangaSource(it) return it
} }
extensionManager.getSourceData(id)?.let { extensionManager.getSourceData(id)?.let {
registerStubSource(it) 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.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.source.MangaSource import eu.kanade.tachiyomi.source.MangaSource
import tachiyomi.domain.source.manga.model.MangaSourceData
import tachiyomi.domain.source.manga.model.StubMangaSource import tachiyomi.domain.source.manga.model.StubMangaSource
import tachiyomi.source.local.entries.manga.isLocal import tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.Injekt 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.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 { fun MangaSource.getNameForMangaInfo(): String {
val preferences = Injekt.get<SourcePreferences>() 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.download.manga.MangaDownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.RELEASE_URL import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
@ -110,6 +109,7 @@ import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -352,7 +352,7 @@ class MainActivity : BaseActivity() {
if (BuildConfig.INCLUDE_UPDATER) { if (BuildConfig.INCLUDE_UPDATER) {
try { try {
val result = AppUpdateChecker().checkForUpdate(context) val result = AppUpdateChecker().checkForUpdate(context)
if (result is AppUpdateResult.NewUpdate) { if (result is GetApplicationRelease.Result.NewUpdate) {
val updateScreen = NewUpdateScreen( val updateScreen = NewUpdateScreen(
versionName = result.release.version, versionName = result.release.version,
changelogInfo = result.release.info, changelogInfo = result.release.info,

View file

@ -1,6 +1,7 @@
plugins { plugins {
id("com.android.library") id("com.android.library")
kotlin("android") kotlin("android")
kotlin("plugin.serialization")
id("com.squareup.sqldelight") id("com.squareup.sqldelight")
} }
@ -34,3 +35,12 @@ dependencies {
api(libs.sqldelight.coroutines) api(libs.sqldelight.coroutines)
api(libs.sqldelight.android.paging) 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 package tachiyomi.data.source.anime
import tachiyomi.domain.source.anime.model.AnimeSource 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 -> val animeSourceMapper: (eu.kanade.tachiyomi.animesource.AnimeSource) -> AnimeSource = { source ->
AnimeSource( AnimeSource(
@ -13,6 +13,6 @@ val animeSourceMapper: (eu.kanade.tachiyomi.animesource.AnimeSource) -> AnimeSou
) )
} }
val animeSourceDataMapper: (Long, String, String) -> AnimeSourceData = { id, lang, name -> val animeSourceDataMapper: (Long, String, String) -> StubAnimeSource = { id, lang, name ->
AnimeSourceData(id, lang, name) StubAnimeSource(id, lang, name)
} }

View file

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

View file

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

View file

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

View file

@ -23,4 +23,13 @@ dependencies {
api(libs.sqldelight.android.paging) api(libs.sqldelight.android.paging)
testImplementation(libs.bundles.test) 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 import eu.kanade.tachiyomi.animesource.model.Video
@Suppress("OverridingDeprecatedMember") @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 val isInvalid: Boolean = name.isBlank() || lang.isBlank()
override val name: String = sourceData.name.ifBlank { id.toString() }
override val lang: String = sourceData.lang
override suspend fun getAnimeDetails(anime: SAnime): SAnime { override suspend fun getAnimeDetails(anime: SAnime): SAnime {
throw AnimeSourceNotInstalledException() throw AnimeSourceNotInstalledException()
@ -27,7 +27,7 @@ class StubAnimeSource(private val sourceData: AnimeSourceData) : AnimeSource {
} }
override fun toString(): String { 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() 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 import rx.Observable
@Suppress("OverridingDeprecatedMember") @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 val isInvalid: Boolean = name.isBlank() || lang.isBlank()
override val name: String = sourceData.name.ifBlank { id.toString() }
override val lang: String = sourceData.lang
override suspend fun getMangaDetails(manga: SManga): SManga { override suspend fun getMangaDetails(manga: SManga): SManga {
throw SourceNotInstalledException() throw SourceNotInstalledException()
@ -43,7 +43,7 @@ class StubMangaSource(private val sourceData: MangaSourceData) : MangaSource {
} }
override fun toString(): String { 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-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" } 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 = { 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" } 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" directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
insetter = "dev.chrisbanes.insetter:insetter:0.6.1" insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
compose-cascade = "me.saket.cascade:cascade-compose:2.0.0-rc02" 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" compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0"
logcat = "com.squareup.logcat:logcat:0.1" 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" 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" aniyomi-mpv = "com.github.aniyomiorg:aniyomi-mpv-lib:1.10.n"
ffmpeg-kit = "com.github.jmir1:ffmpeg-kit:1.10" ffmpeg-kit = "com.github.jmir1:ffmpeg-kit:1.10"
arthenica-smartexceptions = "com.arthenica:smart-exception-java:0.1.1" 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"] shizuku = ["shizuku-api", "shizuku-provider"]
voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"] voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"]
richtext = ["richtext-commonmark", "richtext-m3"] richtext = ["richtext-commonmark", "richtext-m3"]
test = ["junit", "kotest-assertions"] test = ["junit", "kotest-assertions", "mockk"]