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) 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",

View file

@ -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)

View file

@ -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,

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.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),
) { ) {

View file

@ -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()

View file

@ -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].
* *

View file

@ -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()

View file

@ -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)

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.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)
} }

View file

@ -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
} }

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.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)
} }

View file

@ -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
} }

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.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)

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.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)

View file

@ -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) {

View file

@ -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",
) )
} }

View file

@ -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

View file

@ -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]

View file

@ -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]

View file

@ -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"]

View file

@ -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>

View file

@ -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",
) )
} }
} }

View file

@ -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)
} }

View file

@ -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",
)
}
}