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