Last Commit Merged: 531e1c62bb
This commit is contained in:
LuftVerbot 2023-10-22 18:08:27 +02:00
parent c85576e06b
commit da3265fb7a
24 changed files with 219 additions and 235 deletions

View file

@ -226,6 +226,7 @@ dependencies {
implementation(libs.injekt.core)
// Image loading
implementation(platform(libs.coil.bom))
implementation(libs.bundles.coil)
implementation(libs.subsamplingscaleimageview) {
exclude(module = "image-decoder")
@ -295,8 +296,6 @@ tasks {
withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-Xcontext-receivers",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
@ -305,6 +304,8 @@ tasks {
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",

View file

@ -13,8 +13,6 @@ class BasePreferences(
private val preferenceStore: PreferenceStore,
) {
fun confirmExit() = preferenceStore.getBoolean("pref_confirm_exit", false)
fun downloadedOnly() = preferenceStore.getBoolean("pref_downloaded_only", false)
fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false)

View file

@ -39,7 +39,6 @@ object SettingsGeneralScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val prefs = remember { Injekt.get<BasePreferences>() }
val libraryPrefs = remember { Injekt.get<LibraryPreferences>() }
val context = LocalContext.current
@ -58,7 +57,10 @@ object SettingsGeneralScreen : SearchableSettings {
}
}
return mutableListOf<Preference>().apply {
val langs = remember { getLangs(context) }
var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") }
return buildList {
add(
Preference.PreferenceItem.ListPreference(
pref = libraryPrefs.bottomNavStyle(),
@ -84,13 +86,6 @@ object SettingsGeneralScreen : SearchableSettings {
),
)
add(
Preference.PreferenceItem.SwitchPreference(
pref = prefs.confirmExit(),
title = stringResource(R.string.pref_confirm_exit),
),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
add(
Preference.PreferenceItem.TextPreference(
@ -105,8 +100,6 @@ object SettingsGeneralScreen : SearchableSettings {
)
}
val langs = remember { getLangs(context) }
var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") }
add(
Preference.PreferenceItem.BasicListPreference(
value = currentLanguage,

View file

@ -42,6 +42,7 @@ import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob
import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.ui.category.CategoriesTab
import eu.kanade.tachiyomi.util.system.isDevFlavor
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
@ -295,17 +296,20 @@ object SettingsLibraryScreen : SearchableSettings {
true
},
),
// TODO: remove isDevFlavor checks once functionality is available
Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryUpdateDeviceRestrictionPref,
enabled = libraryUpdateInterval > 0,
title = stringResource(R.string.pref_library_update_restriction),
subtitle = stringResource(R.string.restrictions),
entries = mapOf(
DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered),
DEVICE_CHARGING to stringResource(R.string.charging),
DEVICE_BATTERY_NOT_LOW to stringResource(R.string.battery_not_low),
),
entries = buildMap {
put(ENTRY_HAS_UNVIEWED, stringResource(R.string.pref_update_only_completely_read))
put(ENTRY_NON_VIEWED, stringResource(R.string.pref_update_only_started))
put(ENTRY_NON_COMPLETED, stringResource(R.string.pref_update_only_non_completed))
if (isDevFlavor) {
put(ENTRY_OUTSIDE_RELEASE_PERIOD, stringResource(R.string.pref_update_only_in_release_period))
}
},
onValueChanged = {
// Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute {
@ -361,10 +365,10 @@ object SettingsLibraryScreen : SearchableSettings {
pluralStringResource(R.plurals.pref_update_release_following_days, followMangaRange, followMangaRange),
).joinToString(),
onClick = { showFetchMangaRangesDialog = true },
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction },
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction && isDevFlavor },
Preference.PreferenceItem.InfoPreference(
title = stringResource(R.string.pref_update_release_grace_period_info),
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction },
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction && isDevFlavor },
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_update_anime_release_grace_period),
@ -373,10 +377,10 @@ object SettingsLibraryScreen : SearchableSettings {
pluralStringResource(R.plurals.pref_update_release_following_days, followAnimeRange, followAnimeRange),
).joinToString(),
onClick = { showFetchAnimeRangesDialog = true },
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction },
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction && isDevFlavor },
Preference.PreferenceItem.InfoPreference(
title = stringResource(R.string.pref_update_release_grace_period_info),
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction },
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction && isDevFlavor },
),
)
}
@ -482,13 +486,7 @@ object SettingsLibraryScreen : SearchableSettings {
contentAlignment = Alignment.Center,
) {
val size = DpSize(width = maxWidth / 2, height = 128.dp)
val items = (0..28).map {
if (it == 0) {
stringResource(R.string.label_default)
} else {
it.toString()
}
}
val items = (0..28).map(Int::toString)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {

View file

@ -184,9 +184,9 @@ class BackupRestorer(
} else {
// Manga in database
// Copy information from manga already in database
val manga = backupManager.restoreExistingManga(manga, dbManga)
val updateManga = backupManager.restoreExistingManga(manga, dbManga)
// Fetch rest of manga information
restoreNewManga(manga, chapters, categories, history, tracks, backupCategories)
restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories)
}
} catch (e: Exception) {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
@ -251,9 +251,9 @@ class BackupRestorer(
} else {
// Anime in database
// Copy information from anime already in database
val anime = backupManager.restoreExistingAnime(anime, dbAnime)
val updateAnime = backupManager.restoreExistingAnime(anime, dbAnime)
// Fetch rest of anime information
restoreNewAnime(anime, episodes, categories, history, tracks, backupCategories)
restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories)
}
} catch (e: Exception) {
val sourceName = sourceMapping[anime.source] ?: anime.source.toString()

View file

@ -34,7 +34,7 @@ import java.io.File
/**
* A [Fetcher] that fetches cover image for [Anime] object.
*
* It uses [Anime.thumbnail_url] if custom cover is not set by the user.
* It uses [Anime.thumbnailUrl] if custom cover is not set by the user.
* Disk caching for library items is handled by [AnimeCoverCache], otherwise
* handled by Coil's [DiskCache].
*

View file

@ -34,7 +34,7 @@ import java.io.File
/**
* A [Fetcher] that fetches cover image for [Manga] object.
*
* It uses [Manga.thumbnail_url] if custom cover is not set by the user.
* It uses [Manga.thumbnailUrl] if custom cover is not set by the user.
* Disk caching for library items is handled by [MangaCoverCache], otherwise
* handled by Coil's [DiskCache].
*
@ -222,18 +222,22 @@ class MangaCoverFetcher(
}
private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey] else null
return if (options.diskCachePolicy.readEnabled) {
diskCacheLazy.value.openSnapshot(diskCacheKey)
} else {
null
}
}
private fun writeToDiskCache(
response: Response,
): DiskCache.Snapshot? {
val editor = diskCacheLazy.value.edit(diskCacheKey) ?: return null
val editor = diskCacheLazy.value.openEditor(diskCacheKey) ?: return null
try {
diskCacheLazy.value.fileSystem.write(editor.data) {
response.body.source().readAll(this)
}
return editor.commitAndGet()
return editor.commitAndOpenSnapshot()
} catch (e: Exception) {
try {
editor.abort()

View file

@ -48,11 +48,7 @@ class MangaUpdatesApi(
suspend fun getSeriesListItem(track: MangaTrack): Pair<ListItem, Rating?> {
val listItem = with(json) {
authClient.newCall(
GET(
url = "$baseUrl/v1/lists/series/${track.media_id}",
),
)
authClient.newCall(GET("$baseUrl/v1/lists/series/${track.media_id}"))
.awaitSuccess()
.parseAs<ListItem>()
}
@ -110,14 +106,10 @@ class MangaUpdatesApi(
updateSeriesRating(track)
}
suspend fun getSeriesRating(track: MangaTrack): Rating? {
private suspend fun getSeriesRating(track: MangaTrack): Rating? {
return try {
with(json) {
authClient.newCall(
GET(
url = "$baseUrl/v1/series/${track.media_id}/rating",
),
)
authClient.newCall(GET("$baseUrl/v1/series/${track.media_id}/rating"))
.awaitSuccess()
.parseAs<Rating>()
}
@ -126,7 +118,7 @@ class MangaUpdatesApi(
}
}
suspend fun updateSeriesRating(track: MangaTrack) {
private suspend fun updateSeriesRating(track: MangaTrack) {
if (track.score != 0f) {
val body = buildJsonObject {
put("rating", track.score)

View file

@ -15,10 +15,11 @@ 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
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import logcat.LogPriority
import rx.Observable
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
@ -201,7 +202,7 @@ class AnimeExtensionManager(
*
* @param extension The anime extension to be installed.
*/
fun installExtension(extension: AnimeExtension.Available): Observable<InstallStep> {
fun installExtension(extension: AnimeExtension.Available): Flow<InstallStep> {
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
}
@ -212,9 +213,9 @@ class AnimeExtensionManager(
*
* @param extension The anime extension to be updated.
*/
fun updateExtension(extension: AnimeExtension.Installed): Observable<InstallStep> {
fun updateExtension(extension: AnimeExtension.Installed): Flow<InstallStep> {
val availableExt = _availableAnimeExtensionsFlow.value.find { it.pkgName == extension.pkgName }
?: return Observable.empty()
?: return emptyFlow()
return installExtension(availableExt)
}

View file

@ -10,20 +10,27 @@ import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.util.storage.getUriCompat
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.transformWhile
import logcat.LogPriority
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds
/**
* The installer which installs, updates and uninstalls the extensions.
@ -48,10 +55,7 @@ internal class AnimeExtensionInstaller(private val context: Context) {
*/
private val activeDownloads = hashMapOf<String, Long>()
/**
* Relay used to notify the installation step of every download.
*/
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
private val downloadsStateFlows = hashMapOf<Long, MutableStateFlow<InstallStep>>()
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
@ -62,7 +66,7 @@ internal class AnimeExtensionInstaller(private val context: Context) {
* @param url The url of the apk.
* @param extension The extension to install.
*/
fun downloadAndInstall(url: String, extension: AnimeExtension) = Observable.defer {
fun downloadAndInstall(url: String, extension: AnimeExtension): Flow<InstallStep> {
val pkgName = extension.pkgName
val oldDownload = activeDownloads[pkgName]
@ -83,48 +87,60 @@ internal class AnimeExtensionInstaller(private val context: Context) {
val id = downloadManager.enqueue(request)
activeDownloads[pkgName] = id
downloadsRelay.filter { it.first == id }
.map { it.second }
// Poll download status
.mergeWith(pollStatus(id))
val downloadStateFlow = MutableStateFlow(InstallStep.Pending)
downloadsStateFlows[id] = downloadStateFlow
// Poll download status
val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus ->
// Map to our model
when (downloadStatus) {
DownloadManager.STATUS_PENDING -> InstallStep.Pending
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
else -> null
}
}
return merge(downloadStateFlow, pollStatusFlow).transformWhile {
emit(it)
// Stop when the application is installed or errors
.takeUntil { it.isCompleted() }
!it.isCompleted()
}.onCompletion {
// Always notify on main thread
.observeOn(AndroidSchedulers.mainThread())
// Always remove the download when unsubscribed
.doOnUnsubscribe { deleteDownload(pkgName) }
withUIContext {
// Always remove the download when unsubscribed
deleteDownload(pkgName)
}
}
}
/**
* Returns an observable that polls the given download id for its status every second, as the
* Returns a flow that polls the given download id for its status every second, as the
* manager doesn't have any notification system. It'll stop once the download finishes.
*
* @param id The id of the download to poll.
*/
private fun pollStatus(id: Long): Observable<InstallStep> {
private fun downloadStatusFlow(id: Long): Flow<Int> = flow {
val query = DownloadManager.Query().setFilterById(id)
return Observable.interval(0, 1, TimeUnit.SECONDS)
while (true) {
// Get the current download status
.map {
downloadManager.query(query).use { cursor ->
cursor.moveToFirst()
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
}
val downloadStatus = downloadManager.query(query).use { cursor ->
if (!cursor.moveToFirst()) return@flow
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
}
// Ignore duplicate results
.distinctUntilChanged()
emit(downloadStatus)
// Stop polling when the download fails or finishes
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
// Map to our model
.flatMap { status ->
when (status) {
DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading)
else -> Observable.empty()
}
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
return@flow
}
delay(1.seconds)
}
}
// Ignore duplicate results
.distinctUntilChanged()
/**
* Starts an intent to install the extension at the given uri.
@ -177,7 +193,7 @@ internal class AnimeExtensionInstaller(private val context: Context) {
* @param step New install step.
*/
fun updateInstallStep(downloadId: Long, step: InstallStep) {
downloadsRelay.call(downloadId to step)
downloadsStateFlows[downloadId]?.let { it.value = step }
}
/**
@ -189,6 +205,7 @@ internal class AnimeExtensionInstaller(private val context: Context) {
val downloadId = activeDownloads.remove(pkgName)
if (downloadId != null) {
downloadManager.remove(downloadId)
downloadsStateFlows.remove(downloadId)
}
if (activeDownloads.isEmpty()) {
downloadReceiver.unregister()
@ -241,7 +258,7 @@ internal class AnimeExtensionInstaller(private val context: Context) {
// Set next installation step
if (uri == null) {
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
downloadsRelay.call(id to InstallStep.Error)
updateInstallStep(id, InstallStep.Error)
return
}

View file

@ -15,10 +15,11 @@ 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
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import logcat.LogPriority
import rx.Observable
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
@ -201,7 +202,7 @@ class MangaExtensionManager(
*
* @param extension The extension to be installed.
*/
fun installExtension(extension: MangaExtension.Available): Observable<InstallStep> {
fun installExtension(extension: MangaExtension.Available): Flow<InstallStep> {
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
}
@ -212,9 +213,9 @@ class MangaExtensionManager(
*
* @param extension The extension to be updated.
*/
fun updateExtension(extension: MangaExtension.Installed): Observable<InstallStep> {
fun updateExtension(extension: MangaExtension.Installed): Flow<InstallStep> {
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == extension.pkgName }
?: return Observable.empty()
?: return emptyFlow()
return installExtension(availableExt)
}

View file

@ -10,20 +10,27 @@ import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.util.storage.getUriCompat
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.transformWhile
import logcat.LogPriority
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds
/**
* The installer which installs, updates and uninstalls the extensions.
@ -48,10 +55,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
*/
private val activeDownloads = hashMapOf<String, Long>()
/**
* Relay used to notify the installation step of every download.
*/
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
private val downloadsStateFlows = hashMapOf<Long, MutableStateFlow<InstallStep>>()
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
@ -62,7 +66,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
* @param url The url of the apk.
* @param extension The extension to install.
*/
fun downloadAndInstall(url: String, extension: MangaExtension) = Observable.defer {
fun downloadAndInstall(url: String, extension: MangaExtension): Flow<InstallStep> {
val pkgName = extension.pkgName
val oldDownload = activeDownloads[pkgName]
@ -83,48 +87,60 @@ internal class MangaExtensionInstaller(private val context: Context) {
val id = downloadManager.enqueue(request)
activeDownloads[pkgName] = id
downloadsRelay.filter { it.first == id }
.map { it.second }
// Poll download status
.mergeWith(pollStatus(id))
val downloadStateFlow = MutableStateFlow(InstallStep.Pending)
downloadsStateFlows[id] = downloadStateFlow
// Poll download status
val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus ->
// Map to our model
when (downloadStatus) {
DownloadManager.STATUS_PENDING -> InstallStep.Pending
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
else -> null
}
}
return merge(downloadStateFlow, pollStatusFlow).transformWhile {
emit(it)
// Stop when the application is installed or errors
.takeUntil { it.isCompleted() }
!it.isCompleted()
}.onCompletion {
// Always notify on main thread
.observeOn(AndroidSchedulers.mainThread())
// Always remove the download when unsubscribed
.doOnUnsubscribe { deleteDownload(pkgName) }
withUIContext {
// Always remove the download when unsubscribed
deleteDownload(pkgName)
}
}
}
/**
* Returns an observable that polls the given download id for its status every second, as the
* Returns a flow that polls the given download id for its status every second, as the
* manager doesn't have any notification system. It'll stop once the download finishes.
*
* @param id The id of the download to poll.
*/
private fun pollStatus(id: Long): Observable<InstallStep> {
private fun downloadStatusFlow(id: Long): Flow<Int> = flow {
val query = DownloadManager.Query().setFilterById(id)
return Observable.interval(0, 1, TimeUnit.SECONDS)
while (true) {
// Get the current download status
.map {
downloadManager.query(query).use { cursor ->
cursor.moveToFirst()
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
}
val downloadStatus = downloadManager.query(query).use { cursor ->
if (!cursor.moveToFirst()) return@flow
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
}
// Ignore duplicate results
.distinctUntilChanged()
emit(downloadStatus)
// Stop polling when the download fails or finishes
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
// Map to our model
.flatMap { status ->
when (status) {
DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading)
else -> Observable.empty()
}
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
return@flow
}
delay(1.seconds)
}
}
// Ignore duplicate results
.distinctUntilChanged()
/**
* Starts an intent to install the extension at the given uri.
@ -177,7 +193,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
* @param step New install step.
*/
fun updateInstallStep(downloadId: Long, step: InstallStep) {
downloadsRelay.call(downloadId to step)
downloadsStateFlows[downloadId]?.let { it.value = step }
}
/**
@ -189,6 +205,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
val downloadId = activeDownloads.remove(pkgName)
if (downloadId != null) {
downloadManager.remove(downloadId)
downloadsStateFlows.remove(downloadId)
}
if (activeDownloads.isEmpty()) {
downloadReceiver.unregister()
@ -241,7 +258,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
// Set next installation step
if (uri == null) {
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
downloadsRelay.call(id to InstallStep.Error)
updateInstallStep(id, InstallStep.Error)
return
}

View file

@ -14,16 +14,18 @@ import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import rx.Observable
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -130,29 +132,24 @@ class AnimeExtensionsScreenModel(
fun updateAllExtensions() {
coroutineScope.launchIO {
with(state.value) {
if (isEmpty) return@launchIO
items.values
.flatten()
.mapNotNull {
when {
it !is AnimeExtensionUiModel.Item -> null
it.extension !is AnimeExtension.Installed -> null
!it.extension.hasUpdate -> null
else -> it.extension
}
}
.forEach(::updateExtension)
}
state.value.items.values.flatten()
.map { it.extension }
.filterIsInstance<AnimeExtension.Installed>()
.filter { it.hasUpdate }
.forEach(::updateExtension)
}
}
fun installExtension(extension: AnimeExtension.Available) {
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
coroutineScope.launchIO {
extensionManager.installExtension(extension).collectToInstallUpdate(extension)
}
}
fun updateExtension(extension: AnimeExtension.Installed) {
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
coroutineScope.launchIO {
extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
}
}
fun cancelInstallUpdateExtension(extension: AnimeExtension) {
@ -160,29 +157,18 @@ class AnimeExtensionsScreenModel(
}
private fun removeDownloadState(extension: AnimeExtension) {
_currentDownloads.update { _map ->
val map = _map.toMutableMap()
map.remove(extension.pkgName)
map
}
_currentDownloads.update { it - extension.pkgName }
}
private fun addDownloadState(extension: AnimeExtension, installStep: InstallStep) {
_currentDownloads.update { _map ->
val map = _map.toMutableMap()
map[extension.pkgName] = installStep
map
}
_currentDownloads.update { it + Pair(extension.pkgName, installStep) }
}
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: AnimeExtension) {
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: AnimeExtension) =
this
.doOnUnsubscribe { removeDownloadState(extension) }
.subscribe(
{ installStep -> addDownloadState(extension, installStep) },
{ removeDownloadState(extension) },
)
}
.onEach { installStep -> addDownloadState(extension, installStep) }
.onCompletion { removeDownloadState(extension) }
.collect()
fun uninstallExtension(pkgName: String) {
extensionManager.uninstallExtension(pkgName)

View file

@ -14,16 +14,18 @@ import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import rx.Observable
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -131,30 +133,24 @@ class MangaExtensionsScreenModel(
fun updateAllExtensions() {
coroutineScope.launchIO {
with(state.value) {
if (isEmpty) return@launchIO
items
items.values
.flatten()
.mapNotNull {
when {
it !is MangaExtensionUiModel.Item -> null
it.extension !is MangaExtension.Installed -> null
!it.extension.hasUpdate -> null
else -> it.extension
}
}
.forEach(::updateExtension)
}
state.value.items.values.flatten()
.map { it.extension }
.filterIsInstance<MangaExtension.Installed>()
.filter { it.hasUpdate }
.forEach(::updateExtension)
}
}
fun installExtension(extension: MangaExtension.Available) {
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
coroutineScope.launchIO {
extensionManager.installExtension(extension).collectToInstallUpdate(extension)
}
}
fun updateExtension(extension: MangaExtension.Installed) {
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
coroutineScope.launchIO {
extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
}
}
fun cancelInstallUpdateExtension(extension: MangaExtension) {
@ -162,29 +158,18 @@ class MangaExtensionsScreenModel(
}
private fun removeDownloadState(extension: MangaExtension) {
_currentDownloads.update { _map ->
val map = _map.toMutableMap()
map.remove(extension.pkgName)
map
}
_currentDownloads.update { it - extension.pkgName }
}
private fun addDownloadState(extension: MangaExtension, installStep: InstallStep) {
_currentDownloads.update { _map ->
val map = _map.toMutableMap()
map[extension.pkgName] = installStep
map
}
_currentDownloads.update { it + Pair(extension.pkgName, installStep) }
}
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: MangaExtension) {
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: MangaExtension) =
this
.doOnUnsubscribe { removeDownloadState(extension) }
.subscribe(
{ installStep -> addDownloadState(extension, installStep) },
{ removeDownloadState(extension) },
)
}
.onEach { installStep -> addDownloadState(extension, installStep) }
.onCompletion { removeDownloadState(extension) }
.collect()
fun uninstallExtension(pkgName: String) {
extensionManager.uninstallExtension(pkgName)

View file

@ -209,9 +209,6 @@ class MainActivity : BaseActivity() {
screen = HomeScreen,
disposeBehavior = NavigatorDisposeBehavior(disposeNestedNavigators = false, disposeSteps = true),
) { navigator ->
if (navigator.size == 1) {
ConfirmExit()
}
LaunchedEffect(navigator) {
this@MainActivity.navigator = navigator
@ -311,22 +308,6 @@ class MainActivity : BaseActivity() {
}
}
@Composable
private fun ConfirmExit() {
val scope = rememberCoroutineScope()
val confirmExit by preferences.confirmExit().collectAsState()
var waitingConfirmation by remember { mutableStateOf(false) }
BackHandler(enabled = !waitingConfirmation && confirmExit) {
scope.launch {
waitingConfirmation = true
val toast = toast(R.string.confirm_exit, Toast.LENGTH_LONG)
delay(2.seconds)
toast.cancel()
waitingConfirmation = false
}
}
}
@Composable
fun HandleOnNewIntent(context: Context, navigator: Navigator) {
LaunchedEffect(Unit) {

View file

@ -10,6 +10,7 @@ android {
kotlinOptions {
freeCompilerArgs += listOf(
"-Xcontext-receivers",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}

View file

@ -329,7 +329,7 @@ object ImageUtil {
"$partCount parts @ ${optimalSplitHeight}px height per part"
}
return mutableListOf<SplitData>().apply {
return buildList {
val range = 0 until partCount
for (index in range) {
// Only continue if the list is empty or there is image remaining

View file

@ -1,5 +1,5 @@
[versions]
agp_version = "8.0.1"
agp_version = "8.0.2"
lifecycle_version = "2.6.1"
[libraries]

View file

@ -1,6 +1,6 @@
[versions]
kotlin_version = "1.8.21"
serialization_version = "1.5.0"
serialization_version = "1.5.1"
xml_serialization_version = "0.86.0"
[libraries]

View file

@ -1,7 +1,6 @@
[versions]
aboutlib_version = "10.6.2"
aboutlib_version = "10.7.0"
okhttp_version = "5.0.0-alpha.11"
coil_version = "2.3.0"
shizuku_version = "12.2.0"
sqlite = "2.3.1"
sqldelight = "1.5.5"
@ -15,7 +14,6 @@ android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1
rxandroid = "io.reactivex:rxandroid:1.2.1"
rxjava = "io.reactivex:rxjava:1.3.8"
rxrelay = "com.jakewharton.rxrelay:rxrelay:1.2.0"
flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
@ -41,9 +39,10 @@ preferencektx = "androidx.preference:preference-ktx:1.2.0"
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
coil-core = { module = "io.coil-kt:coil", version.ref = "coil_version" }
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil_version" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil_version" }
coil-bom = { module = "io.coil-kt:coil-bom", version = "2.4.0" }
coil-core = { module = "io.coil-kt:coil" }
coil-gif = { module = "io.coil-kt:coil-gif" }
coil-compose = { module = "io.coil-kt:coil-compose" }
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:c8e2650"
image-decoder = "com.github.tachiyomiorg:image-decoder:7879b45"
@ -97,7 +96,7 @@ arthenica-smartexceptions = "com.arthenica:smart-exception-java:0.1.1"
seeker = "io.github.2307vivek:seeker:1.1.1"
[bundles]
reactivex = ["rxandroid", "rxjava", "rxrelay"]
reactivex = ["rxandroid", "rxjava"]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
js-engine = ["quickjs-android"]
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]

View file

@ -208,7 +208,6 @@
<string name="pref_relative_time_long">Long (Short+, n days ago)</string>
<string name="pref_date_format">Date format</string>
<string name="pref_confirm_exit">Confirm exit</string>
<string name="pref_manage_notifications">Manage notifications</string>
<string name="pref_app_language">App language</string>

View file

@ -38,7 +38,6 @@ tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
@ -47,6 +46,8 @@ tasks {
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
)
}
}

View file

@ -27,6 +27,8 @@ dependencies {
implementation(androidx.glance)
implementation(platform(libs.coil.bom))
implementation(libs.coil.core)
api(libs.injekt.core)
}

View file

@ -40,3 +40,11 @@ android {
implementation(libs.ffmpeg.kit)
}
}
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}