mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-24 13:48:55 +03:00
parent
c85576e06b
commit
da3265fb7a
24 changed files with 219 additions and 235 deletions
|
@ -226,6 +226,7 @@ dependencies {
|
||||||
implementation(libs.injekt.core)
|
implementation(libs.injekt.core)
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
|
implementation(platform(libs.coil.bom))
|
||||||
implementation(libs.bundles.coil)
|
implementation(libs.bundles.coil)
|
||||||
implementation(libs.subsamplingscaleimageview) {
|
implementation(libs.subsamplingscaleimageview) {
|
||||||
exclude(module = "image-decoder")
|
exclude(module = "image-decoder")
|
||||||
|
@ -295,8 +296,6 @@ tasks {
|
||||||
withType<KotlinCompile> {
|
withType<KotlinCompile> {
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-Xcontext-receivers",
|
"-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.foundation.layout.ExperimentalLayoutApi",
|
||||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
|
@ -305,6 +304,8 @@ tasks {
|
||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
"-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.ExperimentalCoroutinesApi",
|
||||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
|
|
|
@ -13,8 +13,6 @@ class BasePreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun confirmExit() = preferenceStore.getBoolean("pref_confirm_exit", false)
|
|
||||||
|
|
||||||
fun downloadedOnly() = preferenceStore.getBoolean("pref_downloaded_only", false)
|
fun downloadedOnly() = preferenceStore.getBoolean("pref_downloaded_only", false)
|
||||||
|
|
||||||
fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false)
|
fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false)
|
||||||
|
|
|
@ -39,7 +39,6 @@ object SettingsGeneralScreen : SearchableSettings {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun getPreferences(): List<Preference> {
|
override fun getPreferences(): List<Preference> {
|
||||||
val prefs = remember { Injekt.get<BasePreferences>() }
|
|
||||||
val libraryPrefs = remember { Injekt.get<LibraryPreferences>() }
|
val libraryPrefs = remember { Injekt.get<LibraryPreferences>() }
|
||||||
val context = LocalContext.current
|
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(
|
add(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryPrefs.bottomNavStyle(),
|
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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
add(
|
add(
|
||||||
Preference.PreferenceItem.TextPreference(
|
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(
|
add(
|
||||||
Preference.PreferenceItem.BasicListPreference(
|
Preference.PreferenceItem.BasicListPreference(
|
||||||
value = currentLanguage,
|
value = currentLanguage,
|
||||||
|
|
|
@ -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.library.manga.MangaLibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoriesTab
|
import eu.kanade.tachiyomi.ui.category.CategoriesTab
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
||||||
|
@ -295,17 +296,20 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// TODO: remove isDevFlavor checks once functionality is available
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryUpdateDeviceRestrictionPref,
|
pref = libraryUpdateDeviceRestrictionPref,
|
||||||
enabled = libraryUpdateInterval > 0,
|
enabled = libraryUpdateInterval > 0,
|
||||||
title = stringResource(R.string.pref_library_update_restriction),
|
title = stringResource(R.string.pref_library_update_restriction),
|
||||||
subtitle = stringResource(R.string.restrictions),
|
subtitle = stringResource(R.string.restrictions),
|
||||||
entries = mapOf(
|
entries = buildMap {
|
||||||
DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi),
|
put(ENTRY_HAS_UNVIEWED, stringResource(R.string.pref_update_only_completely_read))
|
||||||
DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered),
|
put(ENTRY_NON_VIEWED, stringResource(R.string.pref_update_only_started))
|
||||||
DEVICE_CHARGING to stringResource(R.string.charging),
|
put(ENTRY_NON_COMPLETED, stringResource(R.string.pref_update_only_non_completed))
|
||||||
DEVICE_BATTERY_NOT_LOW to stringResource(R.string.battery_not_low),
|
if (isDevFlavor) {
|
||||||
),
|
put(ENTRY_OUTSIDE_RELEASE_PERIOD, stringResource(R.string.pref_update_only_in_release_period))
|
||||||
|
}
|
||||||
|
},
|
||||||
onValueChanged = {
|
onValueChanged = {
|
||||||
// Post to event looper to allow the preference to be updated.
|
// Post to event looper to allow the preference to be updated.
|
||||||
ContextCompat.getMainExecutor(context).execute {
|
ContextCompat.getMainExecutor(context).execute {
|
||||||
|
@ -361,10 +365,10 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
pluralStringResource(R.plurals.pref_update_release_following_days, followMangaRange, followMangaRange),
|
pluralStringResource(R.plurals.pref_update_release_following_days, followMangaRange, followMangaRange),
|
||||||
).joinToString(),
|
).joinToString(),
|
||||||
onClick = { showFetchMangaRangesDialog = true },
|
onClick = { showFetchMangaRangesDialog = true },
|
||||||
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction },
|
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction && isDevFlavor },
|
||||||
Preference.PreferenceItem.InfoPreference(
|
Preference.PreferenceItem.InfoPreference(
|
||||||
title = stringResource(R.string.pref_update_release_grace_period_info),
|
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(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(R.string.pref_update_anime_release_grace_period),
|
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),
|
pluralStringResource(R.plurals.pref_update_release_following_days, followAnimeRange, followAnimeRange),
|
||||||
).joinToString(),
|
).joinToString(),
|
||||||
onClick = { showFetchAnimeRangesDialog = true },
|
onClick = { showFetchAnimeRangesDialog = true },
|
||||||
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction },
|
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction && isDevFlavor },
|
||||||
Preference.PreferenceItem.InfoPreference(
|
Preference.PreferenceItem.InfoPreference(
|
||||||
title = stringResource(R.string.pref_update_release_grace_period_info),
|
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,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||||
val items = (0..28).map {
|
val items = (0..28).map(Int::toString)
|
||||||
if (it == 0) {
|
|
||||||
stringResource(R.string.label_default)
|
|
||||||
} else {
|
|
||||||
it.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -184,9 +184,9 @@ class BackupRestorer(
|
||||||
} else {
|
} else {
|
||||||
// Manga in database
|
// Manga in database
|
||||||
// Copy information from manga already 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
|
// Fetch rest of manga information
|
||||||
restoreNewManga(manga, chapters, categories, history, tracks, backupCategories)
|
restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
@ -251,9 +251,9 @@ class BackupRestorer(
|
||||||
} else {
|
} else {
|
||||||
// Anime in database
|
// Anime in database
|
||||||
// Copy information from anime already 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
|
// Fetch rest of anime information
|
||||||
restoreNewAnime(anime, episodes, categories, history, tracks, backupCategories)
|
restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val sourceName = sourceMapping[anime.source] ?: anime.source.toString()
|
val sourceName = sourceMapping[anime.source] ?: anime.source.toString()
|
||||||
|
|
|
@ -34,7 +34,7 @@ import java.io.File
|
||||||
/**
|
/**
|
||||||
* A [Fetcher] that fetches cover image for [Anime] object.
|
* 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
|
* Disk caching for library items is handled by [AnimeCoverCache], otherwise
|
||||||
* handled by Coil's [DiskCache].
|
* handled by Coil's [DiskCache].
|
||||||
*
|
*
|
||||||
|
|
|
@ -34,7 +34,7 @@ import java.io.File
|
||||||
/**
|
/**
|
||||||
* A [Fetcher] that fetches cover image for [Manga] object.
|
* 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
|
* Disk caching for library items is handled by [MangaCoverCache], otherwise
|
||||||
* handled by Coil's [DiskCache].
|
* handled by Coil's [DiskCache].
|
||||||
*
|
*
|
||||||
|
@ -222,18 +222,22 @@ class MangaCoverFetcher(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readFromDiskCache(): DiskCache.Snapshot? {
|
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(
|
private fun writeToDiskCache(
|
||||||
response: Response,
|
response: Response,
|
||||||
): DiskCache.Snapshot? {
|
): DiskCache.Snapshot? {
|
||||||
val editor = diskCacheLazy.value.edit(diskCacheKey) ?: return null
|
val editor = diskCacheLazy.value.openEditor(diskCacheKey) ?: return null
|
||||||
try {
|
try {
|
||||||
diskCacheLazy.value.fileSystem.write(editor.data) {
|
diskCacheLazy.value.fileSystem.write(editor.data) {
|
||||||
response.body.source().readAll(this)
|
response.body.source().readAll(this)
|
||||||
}
|
}
|
||||||
return editor.commitAndGet()
|
return editor.commitAndOpenSnapshot()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
try {
|
try {
|
||||||
editor.abort()
|
editor.abort()
|
||||||
|
|
|
@ -48,11 +48,7 @@ class MangaUpdatesApi(
|
||||||
|
|
||||||
suspend fun getSeriesListItem(track: MangaTrack): Pair<ListItem, Rating?> {
|
suspend fun getSeriesListItem(track: MangaTrack): Pair<ListItem, Rating?> {
|
||||||
val listItem = with(json) {
|
val listItem = with(json) {
|
||||||
authClient.newCall(
|
authClient.newCall(GET("$baseUrl/v1/lists/series/${track.media_id}"))
|
||||||
GET(
|
|
||||||
url = "$baseUrl/v1/lists/series/${track.media_id}",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<ListItem>()
|
.parseAs<ListItem>()
|
||||||
}
|
}
|
||||||
|
@ -110,14 +106,10 @@ class MangaUpdatesApi(
|
||||||
updateSeriesRating(track)
|
updateSeriesRating(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSeriesRating(track: MangaTrack): Rating? {
|
private suspend fun getSeriesRating(track: MangaTrack): Rating? {
|
||||||
return try {
|
return try {
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient.newCall(
|
authClient.newCall(GET("$baseUrl/v1/series/${track.media_id}/rating"))
|
||||||
GET(
|
|
||||||
url = "$baseUrl/v1/series/${track.media_id}/rating",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<Rating>()
|
.parseAs<Rating>()
|
||||||
}
|
}
|
||||||
|
@ -126,7 +118,7 @@ class MangaUpdatesApi(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateSeriesRating(track: MangaTrack) {
|
private suspend fun updateSeriesRating(track: MangaTrack) {
|
||||||
if (track.score != 0f) {
|
if (track.score != 0f) {
|
||||||
val body = buildJsonObject {
|
val body = buildJsonObject {
|
||||||
put("rating", track.score)
|
put("rating", track.score)
|
||||||
|
|
|
@ -15,10 +15,11 @@ 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
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
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
|
||||||
|
@ -201,7 +202,7 @@ class AnimeExtensionManager(
|
||||||
*
|
*
|
||||||
* @param extension The anime extension to be installed.
|
* @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)
|
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,9 +213,9 @@ class AnimeExtensionManager(
|
||||||
*
|
*
|
||||||
* @param extension The anime extension to be updated.
|
* @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 }
|
val availableExt = _availableAnimeExtensionsFlow.value.find { it.pkgName == extension.pkgName }
|
||||||
?: return Observable.empty()
|
?: return emptyFlow()
|
||||||
return installExtension(availableExt)
|
return installExtension(availableExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,20 +10,27 @@ import android.os.Environment
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
|
import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
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 logcat.LogPriority
|
||||||
import rx.Observable
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The installer which installs, updates and uninstalls the extensions.
|
* 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>()
|
private val activeDownloads = hashMapOf<String, Long>()
|
||||||
|
|
||||||
/**
|
private val downloadsStateFlows = hashMapOf<Long, MutableStateFlow<InstallStep>>()
|
||||||
* Relay used to notify the installation step of every download.
|
|
||||||
*/
|
|
||||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
|
||||||
|
|
||||||
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
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 url The url of the apk.
|
||||||
* @param extension The extension to install.
|
* @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 pkgName = extension.pkgName
|
||||||
|
|
||||||
val oldDownload = activeDownloads[pkgName]
|
val oldDownload = activeDownloads[pkgName]
|
||||||
|
@ -83,48 +87,60 @@ internal class AnimeExtensionInstaller(private val context: Context) {
|
||||||
val id = downloadManager.enqueue(request)
|
val id = downloadManager.enqueue(request)
|
||||||
activeDownloads[pkgName] = id
|
activeDownloads[pkgName] = id
|
||||||
|
|
||||||
downloadsRelay.filter { it.first == id }
|
val downloadStateFlow = MutableStateFlow(InstallStep.Pending)
|
||||||
.map { it.second }
|
downloadsStateFlows[id] = downloadStateFlow
|
||||||
// Poll download status
|
|
||||||
.mergeWith(pollStatus(id))
|
// 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
|
// Stop when the application is installed or errors
|
||||||
.takeUntil { it.isCompleted() }
|
!it.isCompleted()
|
||||||
|
}.onCompletion {
|
||||||
// Always notify on main thread
|
// Always notify on main thread
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
withUIContext {
|
||||||
// Always remove the download when unsubscribed
|
// Always remove the download when unsubscribed
|
||||||
.doOnUnsubscribe { deleteDownload(pkgName) }
|
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.
|
* manager doesn't have any notification system. It'll stop once the download finishes.
|
||||||
*
|
*
|
||||||
* @param id The id of the download to poll.
|
* @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)
|
val query = DownloadManager.Query().setFilterById(id)
|
||||||
|
|
||||||
return Observable.interval(0, 1, TimeUnit.SECONDS)
|
while (true) {
|
||||||
// Get the current download status
|
// Get the current download status
|
||||||
.map {
|
val downloadStatus = downloadManager.query(query).use { cursor ->
|
||||||
downloadManager.query(query).use { cursor ->
|
if (!cursor.moveToFirst()) return@flow
|
||||||
cursor.moveToFirst()
|
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
||||||
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Ignore duplicate results
|
|
||||||
.distinctUntilChanged()
|
emit(downloadStatus)
|
||||||
|
|
||||||
// Stop polling when the download fails or finishes
|
// Stop polling when the download fails or finishes
|
||||||
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
|
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
|
||||||
// Map to our model
|
return@flow
|
||||||
.flatMap { status ->
|
|
||||||
when (status) {
|
|
||||||
DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
|
|
||||||
DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading)
|
|
||||||
else -> Observable.empty()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delay(1.seconds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Ignore duplicate results
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts an intent to install the extension at the given uri.
|
* 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.
|
* @param step New install step.
|
||||||
*/
|
*/
|
||||||
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
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)
|
val downloadId = activeDownloads.remove(pkgName)
|
||||||
if (downloadId != null) {
|
if (downloadId != null) {
|
||||||
downloadManager.remove(downloadId)
|
downloadManager.remove(downloadId)
|
||||||
|
downloadsStateFlows.remove(downloadId)
|
||||||
}
|
}
|
||||||
if (activeDownloads.isEmpty()) {
|
if (activeDownloads.isEmpty()) {
|
||||||
downloadReceiver.unregister()
|
downloadReceiver.unregister()
|
||||||
|
@ -241,7 +258,7 @@ internal class AnimeExtensionInstaller(private val context: Context) {
|
||||||
// Set next installation step
|
// Set next installation step
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
|
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
|
||||||
downloadsRelay.call(id to InstallStep.Error)
|
updateInstallStep(id, InstallStep.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,11 @@ 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
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
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
|
||||||
|
@ -201,7 +202,7 @@ class MangaExtensionManager(
|
||||||
*
|
*
|
||||||
* @param extension The extension to be installed.
|
* @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)
|
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,9 +213,9 @@ class MangaExtensionManager(
|
||||||
*
|
*
|
||||||
* @param extension The extension to be updated.
|
* @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 }
|
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == extension.pkgName }
|
||||||
?: return Observable.empty()
|
?: return emptyFlow()
|
||||||
return installExtension(availableExt)
|
return installExtension(availableExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,20 +10,27 @@ import android.os.Environment
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
|
import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
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 logcat.LogPriority
|
||||||
import rx.Observable
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The installer which installs, updates and uninstalls the extensions.
|
* 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>()
|
private val activeDownloads = hashMapOf<String, Long>()
|
||||||
|
|
||||||
/**
|
private val downloadsStateFlows = hashMapOf<Long, MutableStateFlow<InstallStep>>()
|
||||||
* Relay used to notify the installation step of every download.
|
|
||||||
*/
|
|
||||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
|
||||||
|
|
||||||
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
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 url The url of the apk.
|
||||||
* @param extension The extension to install.
|
* @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 pkgName = extension.pkgName
|
||||||
|
|
||||||
val oldDownload = activeDownloads[pkgName]
|
val oldDownload = activeDownloads[pkgName]
|
||||||
|
@ -83,48 +87,60 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
val id = downloadManager.enqueue(request)
|
val id = downloadManager.enqueue(request)
|
||||||
activeDownloads[pkgName] = id
|
activeDownloads[pkgName] = id
|
||||||
|
|
||||||
downloadsRelay.filter { it.first == id }
|
val downloadStateFlow = MutableStateFlow(InstallStep.Pending)
|
||||||
.map { it.second }
|
downloadsStateFlows[id] = downloadStateFlow
|
||||||
// Poll download status
|
|
||||||
.mergeWith(pollStatus(id))
|
// 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
|
// Stop when the application is installed or errors
|
||||||
.takeUntil { it.isCompleted() }
|
!it.isCompleted()
|
||||||
|
}.onCompletion {
|
||||||
// Always notify on main thread
|
// Always notify on main thread
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
withUIContext {
|
||||||
// Always remove the download when unsubscribed
|
// Always remove the download when unsubscribed
|
||||||
.doOnUnsubscribe { deleteDownload(pkgName) }
|
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.
|
* manager doesn't have any notification system. It'll stop once the download finishes.
|
||||||
*
|
*
|
||||||
* @param id The id of the download to poll.
|
* @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)
|
val query = DownloadManager.Query().setFilterById(id)
|
||||||
|
|
||||||
return Observable.interval(0, 1, TimeUnit.SECONDS)
|
while (true) {
|
||||||
// Get the current download status
|
// Get the current download status
|
||||||
.map {
|
val downloadStatus = downloadManager.query(query).use { cursor ->
|
||||||
downloadManager.query(query).use { cursor ->
|
if (!cursor.moveToFirst()) return@flow
|
||||||
cursor.moveToFirst()
|
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
||||||
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Ignore duplicate results
|
|
||||||
.distinctUntilChanged()
|
emit(downloadStatus)
|
||||||
|
|
||||||
// Stop polling when the download fails or finishes
|
// Stop polling when the download fails or finishes
|
||||||
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
|
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
|
||||||
// Map to our model
|
return@flow
|
||||||
.flatMap { status ->
|
|
||||||
when (status) {
|
|
||||||
DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
|
|
||||||
DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading)
|
|
||||||
else -> Observable.empty()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delay(1.seconds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Ignore duplicate results
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts an intent to install the extension at the given uri.
|
* 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.
|
* @param step New install step.
|
||||||
*/
|
*/
|
||||||
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
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)
|
val downloadId = activeDownloads.remove(pkgName)
|
||||||
if (downloadId != null) {
|
if (downloadId != null) {
|
||||||
downloadManager.remove(downloadId)
|
downloadManager.remove(downloadId)
|
||||||
|
downloadsStateFlows.remove(downloadId)
|
||||||
}
|
}
|
||||||
if (activeDownloads.isEmpty()) {
|
if (activeDownloads.isEmpty()) {
|
||||||
downloadReceiver.unregister()
|
downloadReceiver.unregister()
|
||||||
|
@ -241,7 +258,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
// Set next installation step
|
// Set next installation step
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
|
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
|
||||||
downloadsRelay.call(id to InstallStep.Error)
|
updateInstallStep(id, InstallStep.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,16 +14,18 @@ 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.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import rx.Observable
|
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -130,29 +132,24 @@ class AnimeExtensionsScreenModel(
|
||||||
|
|
||||||
fun updateAllExtensions() {
|
fun updateAllExtensions() {
|
||||||
coroutineScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
with(state.value) {
|
state.value.items.values.flatten()
|
||||||
if (isEmpty) return@launchIO
|
.map { it.extension }
|
||||||
items.values
|
.filterIsInstance<AnimeExtension.Installed>()
|
||||||
.flatten()
|
.filter { it.hasUpdate }
|
||||||
.mapNotNull {
|
.forEach(::updateExtension)
|
||||||
when {
|
|
||||||
it !is AnimeExtensionUiModel.Item -> null
|
|
||||||
it.extension !is AnimeExtension.Installed -> null
|
|
||||||
!it.extension.hasUpdate -> null
|
|
||||||
else -> it.extension
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.forEach(::updateExtension)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun installExtension(extension: AnimeExtension.Available) {
|
fun installExtension(extension: AnimeExtension.Available) {
|
||||||
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
|
coroutineScope.launchIO {
|
||||||
|
extensionManager.installExtension(extension).collectToInstallUpdate(extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateExtension(extension: AnimeExtension.Installed) {
|
fun updateExtension(extension: AnimeExtension.Installed) {
|
||||||
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
|
coroutineScope.launchIO {
|
||||||
|
extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelInstallUpdateExtension(extension: AnimeExtension) {
|
fun cancelInstallUpdateExtension(extension: AnimeExtension) {
|
||||||
|
@ -160,29 +157,18 @@ class AnimeExtensionsScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeDownloadState(extension: AnimeExtension) {
|
private fun removeDownloadState(extension: AnimeExtension) {
|
||||||
_currentDownloads.update { _map ->
|
_currentDownloads.update { it - extension.pkgName }
|
||||||
val map = _map.toMutableMap()
|
|
||||||
map.remove(extension.pkgName)
|
|
||||||
map
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addDownloadState(extension: AnimeExtension, installStep: InstallStep) {
|
private fun addDownloadState(extension: AnimeExtension, installStep: InstallStep) {
|
||||||
_currentDownloads.update { _map ->
|
_currentDownloads.update { it + Pair(extension.pkgName, installStep) }
|
||||||
val map = _map.toMutableMap()
|
|
||||||
map[extension.pkgName] = installStep
|
|
||||||
map
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: AnimeExtension) {
|
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: AnimeExtension) =
|
||||||
this
|
this
|
||||||
.doOnUnsubscribe { removeDownloadState(extension) }
|
.onEach { installStep -> addDownloadState(extension, installStep) }
|
||||||
.subscribe(
|
.onCompletion { removeDownloadState(extension) }
|
||||||
{ installStep -> addDownloadState(extension, installStep) },
|
.collect()
|
||||||
{ removeDownloadState(extension) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uninstallExtension(pkgName: String) {
|
fun uninstallExtension(pkgName: String) {
|
||||||
extensionManager.uninstallExtension(pkgName)
|
extensionManager.uninstallExtension(pkgName)
|
||||||
|
|
|
@ -14,16 +14,18 @@ import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import rx.Observable
|
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -131,30 +133,24 @@ class MangaExtensionsScreenModel(
|
||||||
|
|
||||||
fun updateAllExtensions() {
|
fun updateAllExtensions() {
|
||||||
coroutineScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
with(state.value) {
|
state.value.items.values.flatten()
|
||||||
if (isEmpty) return@launchIO
|
.map { it.extension }
|
||||||
items
|
.filterIsInstance<MangaExtension.Installed>()
|
||||||
items.values
|
.filter { it.hasUpdate }
|
||||||
.flatten()
|
.forEach(::updateExtension)
|
||||||
.mapNotNull {
|
|
||||||
when {
|
|
||||||
it !is MangaExtensionUiModel.Item -> null
|
|
||||||
it.extension !is MangaExtension.Installed -> null
|
|
||||||
!it.extension.hasUpdate -> null
|
|
||||||
else -> it.extension
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.forEach(::updateExtension)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun installExtension(extension: MangaExtension.Available) {
|
fun installExtension(extension: MangaExtension.Available) {
|
||||||
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
|
coroutineScope.launchIO {
|
||||||
|
extensionManager.installExtension(extension).collectToInstallUpdate(extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateExtension(extension: MangaExtension.Installed) {
|
fun updateExtension(extension: MangaExtension.Installed) {
|
||||||
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
|
coroutineScope.launchIO {
|
||||||
|
extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelInstallUpdateExtension(extension: MangaExtension) {
|
fun cancelInstallUpdateExtension(extension: MangaExtension) {
|
||||||
|
@ -162,29 +158,18 @@ class MangaExtensionsScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeDownloadState(extension: MangaExtension) {
|
private fun removeDownloadState(extension: MangaExtension) {
|
||||||
_currentDownloads.update { _map ->
|
_currentDownloads.update { it - extension.pkgName }
|
||||||
val map = _map.toMutableMap()
|
|
||||||
map.remove(extension.pkgName)
|
|
||||||
map
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addDownloadState(extension: MangaExtension, installStep: InstallStep) {
|
private fun addDownloadState(extension: MangaExtension, installStep: InstallStep) {
|
||||||
_currentDownloads.update { _map ->
|
_currentDownloads.update { it + Pair(extension.pkgName, installStep) }
|
||||||
val map = _map.toMutableMap()
|
|
||||||
map[extension.pkgName] = installStep
|
|
||||||
map
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: MangaExtension) {
|
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: MangaExtension) =
|
||||||
this
|
this
|
||||||
.doOnUnsubscribe { removeDownloadState(extension) }
|
.onEach { installStep -> addDownloadState(extension, installStep) }
|
||||||
.subscribe(
|
.onCompletion { removeDownloadState(extension) }
|
||||||
{ installStep -> addDownloadState(extension, installStep) },
|
.collect()
|
||||||
{ removeDownloadState(extension) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uninstallExtension(pkgName: String) {
|
fun uninstallExtension(pkgName: String) {
|
||||||
extensionManager.uninstallExtension(pkgName)
|
extensionManager.uninstallExtension(pkgName)
|
||||||
|
|
|
@ -209,9 +209,6 @@ class MainActivity : BaseActivity() {
|
||||||
screen = HomeScreen,
|
screen = HomeScreen,
|
||||||
disposeBehavior = NavigatorDisposeBehavior(disposeNestedNavigators = false, disposeSteps = true),
|
disposeBehavior = NavigatorDisposeBehavior(disposeNestedNavigators = false, disposeSteps = true),
|
||||||
) { navigator ->
|
) { navigator ->
|
||||||
if (navigator.size == 1) {
|
|
||||||
ConfirmExit()
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(navigator) {
|
LaunchedEffect(navigator) {
|
||||||
this@MainActivity.navigator = 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
|
@Composable
|
||||||
fun HandleOnNewIntent(context: Context, navigator: Navigator) {
|
fun HandleOnNewIntent(context: Context, navigator: Navigator) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ android {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
freeCompilerArgs += listOf(
|
freeCompilerArgs += listOf(
|
||||||
"-Xcontext-receivers",
|
"-Xcontext-receivers",
|
||||||
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -329,7 +329,7 @@ object ImageUtil {
|
||||||
"$partCount parts @ ${optimalSplitHeight}px height per part"
|
"$partCount parts @ ${optimalSplitHeight}px height per part"
|
||||||
}
|
}
|
||||||
|
|
||||||
return mutableListOf<SplitData>().apply {
|
return buildList {
|
||||||
val range = 0 until partCount
|
val range = 0 until partCount
|
||||||
for (index in range) {
|
for (index in range) {
|
||||||
// Only continue if the list is empty or there is image remaining
|
// Only continue if the list is empty or there is image remaining
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[versions]
|
[versions]
|
||||||
agp_version = "8.0.1"
|
agp_version = "8.0.2"
|
||||||
lifecycle_version = "2.6.1"
|
lifecycle_version = "2.6.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[versions]
|
[versions]
|
||||||
kotlin_version = "1.8.21"
|
kotlin_version = "1.8.21"
|
||||||
serialization_version = "1.5.0"
|
serialization_version = "1.5.1"
|
||||||
xml_serialization_version = "0.86.0"
|
xml_serialization_version = "0.86.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
[versions]
|
[versions]
|
||||||
aboutlib_version = "10.6.2"
|
aboutlib_version = "10.7.0"
|
||||||
okhttp_version = "5.0.0-alpha.11"
|
okhttp_version = "5.0.0-alpha.11"
|
||||||
coil_version = "2.3.0"
|
|
||||||
shizuku_version = "12.2.0"
|
shizuku_version = "12.2.0"
|
||||||
sqlite = "2.3.1"
|
sqlite = "2.3.1"
|
||||||
sqldelight = "1.5.5"
|
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"
|
rxandroid = "io.reactivex:rxandroid:1.2.1"
|
||||||
rxjava = "io.reactivex:rxjava:1.3.8"
|
rxjava = "io.reactivex:rxjava:1.3.8"
|
||||||
rxrelay = "com.jakewharton.rxrelay:rxrelay:1.2.0"
|
|
||||||
flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
|
flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
|
||||||
|
|
||||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
|
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"
|
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
|
||||||
|
|
||||||
coil-core = { module = "io.coil-kt:coil", version.ref = "coil_version" }
|
coil-bom = { module = "io.coil-kt:coil-bom", version = "2.4.0" }
|
||||||
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil_version" }
|
coil-core = { module = "io.coil-kt:coil" }
|
||||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil_version" }
|
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"
|
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:c8e2650"
|
||||||
image-decoder = "com.github.tachiyomiorg:image-decoder:7879b45"
|
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"
|
seeker = "io.github.2307vivek:seeker:1.1.1"
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
reactivex = ["rxandroid", "rxjava", "rxrelay"]
|
reactivex = ["rxandroid", "rxjava"]
|
||||||
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
|
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
|
||||||
js-engine = ["quickjs-android"]
|
js-engine = ["quickjs-android"]
|
||||||
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
|
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
|
||||||
|
|
|
@ -208,7 +208,6 @@
|
||||||
<string name="pref_relative_time_long">Long (Short+, n days ago)</string>
|
<string name="pref_relative_time_long">Long (Short+, n days ago)</string>
|
||||||
<string name="pref_date_format">Date format</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_manage_notifications">Manage notifications</string>
|
||||||
<string name="pref_app_language">App language</string>
|
<string name="pref_app_language">App language</string>
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,6 @@ tasks {
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
|
||||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
|
@ -47,6 +46,8 @@ tasks {
|
||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||||
|
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@ dependencies {
|
||||||
|
|
||||||
implementation(androidx.glance)
|
implementation(androidx.glance)
|
||||||
|
|
||||||
|
implementation(platform(libs.coil.bom))
|
||||||
implementation(libs.coil.core)
|
implementation(libs.coil.core)
|
||||||
|
|
||||||
api(libs.injekt.core)
|
api(libs.injekt.core)
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,3 +40,11 @@ android {
|
||||||
implementation(libs.ffmpeg.kit)
|
implementation(libs.ffmpeg.kit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||||
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue