mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 20:27:06 +03:00
parent
fa7b8427a2
commit
2c4230376c
104 changed files with 1370 additions and 592 deletions
7
.github/workflows/issue_moderator.yml
vendored
7
.github/workflows/issue_moderator.yml
vendored
|
@ -27,6 +27,13 @@ jobs:
|
||||||
"type": "body",
|
"type": "body",
|
||||||
"regex": ".*\\* (Aniyomi version|Android version|Device): \\?.*",
|
"regex": ".*\\* (Aniyomi version|Android version|Device): \\?.*",
|
||||||
"message": "Requested information in the template was not filled out."
|
"message": "Requested information in the template was not filled out."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "both",
|
||||||
|
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
|
||||||
|
"ignoreCase": true,
|
||||||
|
"labels": ["Cloudflare protected"],
|
||||||
|
"message": "Refer to the **Solving Cloudflare issues** section at https://aniyomi.org/help/guides/troubleshooting/#solving-cloudflare-issues. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
auto-close-ignore-label: do-not-autoclose
|
auto-close-ignore-label: do-not-autoclose
|
||||||
|
|
|
@ -202,7 +202,7 @@ dependencies {
|
||||||
implementation(androidx.bundles.workmanager)
|
implementation(androidx.bundles.workmanager)
|
||||||
|
|
||||||
// RxJava
|
// RxJava
|
||||||
implementation(libs.bundles.reactivex)
|
implementation(libs.rxjava)
|
||||||
implementation(libs.flowreactivenetwork)
|
implementation(libs.flowreactivenetwork)
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
public static final ** CREATOR;
|
public static final ** CREATOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep class androidx.annotation.Keep
|
-keep class androidx.annotation.Keep
|
||||||
|
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
@ -14,8 +14,8 @@
|
||||||
-keep,allowoptimization class kotlin.time.** { public protected *; }
|
-keep,allowoptimization class kotlin.time.** { public protected *; }
|
||||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||||
-keep,allowoptimization class okio.** { public protected *; }
|
-keep,allowoptimization class okio.** { public protected *; }
|
||||||
-keep,allowoptimization class rx.** { public protected *; }
|
|
||||||
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
||||||
|
-keep,allowoptimization class rx.** { public protected *; }
|
||||||
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
||||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||||
-keep,allowoptimization class is.xyz.mpv.** { public protected *; }
|
-keep,allowoptimization class is.xyz.mpv.** { public protected *; }
|
||||||
|
|
|
@ -63,10 +63,10 @@
|
||||||
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.DeepLinkAnimeActivity"
|
android:name=".ui.deeplink.anime.DeepLinkAnimeActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
android:label="@string/action_global_anime_search"
|
android:label="@string/action_search"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
|
@ -90,10 +90,10 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.DeepLinkMangaActivity"
|
android:name=".ui.deeplink.manga.DeepLinkMangaActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
android:label="@string/action_global_manga_search"
|
android:label="@string/action_search"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package eu.kanade.core.util
|
package eu.kanade.core.util
|
||||||
|
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
|
@ -20,15 +19,6 @@ fun <T : R, R : Any> List<T>.insertSeparators(
|
||||||
return newList
|
return newList
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new map containing only the key entries of [transform] that are not null.
|
|
||||||
*/
|
|
||||||
inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): ConcurrentHashMap<R, V> {
|
|
||||||
val mutableMap = ConcurrentHashMap<R, V>()
|
|
||||||
forEach { element -> transform(element)?.let { mutableMap[it] = element.value } }
|
|
||||||
return mutableMap
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
||||||
if (shouldAdd) {
|
if (shouldAdd) {
|
||||||
add(value)
|
add(value)
|
||||||
|
|
|
@ -234,7 +234,7 @@ class DomainModule : InjektModule {
|
||||||
addFactory { UpdateEpisode(get()) }
|
addFactory { UpdateEpisode(get()) }
|
||||||
addFactory { SetSeenStatus(get(), get(), get(), get()) }
|
addFactory { SetSeenStatus(get(), get(), get(), get()) }
|
||||||
addFactory { ShouldUpdateDbEpisode() }
|
addFactory { ShouldUpdateDbEpisode() }
|
||||||
addFactory { SyncEpisodesWithSource(get(), get(), get(), get()) }
|
addFactory { SyncEpisodesWithSource(get(), get(), get(), get(), get(), get(), get()) }
|
||||||
addFactory { SyncEpisodesWithTrackServiceTwoWay(get(), get()) }
|
addFactory { SyncEpisodesWithTrackServiceTwoWay(get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||||
|
@ -243,7 +243,7 @@ class DomainModule : InjektModule {
|
||||||
addFactory { UpdateChapter(get()) }
|
addFactory { UpdateChapter(get()) }
|
||||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||||
addFactory { ShouldUpdateDbChapter() }
|
addFactory { ShouldUpdateDbChapter() }
|
||||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) }
|
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) }
|
||||||
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
|
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory<AnimeHistoryRepository> { AnimeHistoryRepositoryImpl(get()) }
|
addSingletonFactory<AnimeHistoryRepository> { AnimeHistoryRepositoryImpl(get()) }
|
||||||
|
|
|
@ -28,5 +28,6 @@ class BasePreferences(
|
||||||
LEGACY(R.string.ext_installer_legacy),
|
LEGACY(R.string.ext_installer_legacy),
|
||||||
PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
|
PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
|
||||||
SHIZUKU(R.string.ext_installer_shizuku),
|
SHIZUKU(R.string.ext_installer_shizuku),
|
||||||
|
PRIVATE(R.string.ext_installer_private),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import tachiyomi.domain.items.chapter.model.toChapterUpdate
|
||||||
import tachiyomi.domain.items.chapter.repository.ChapterRepository
|
import tachiyomi.domain.items.chapter.repository.ChapterRepository
|
||||||
import tachiyomi.domain.items.chapter.service.ChapterRecognition
|
import tachiyomi.domain.items.chapter.service.ChapterRecognition
|
||||||
import tachiyomi.source.local.entries.manga.isLocal
|
import tachiyomi.source.local.entries.manga.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.lang.Long.max
|
import java.lang.Long.max
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
|
@ -28,13 +27,13 @@ import java.util.Date
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
|
||||||
class SyncChaptersWithSource(
|
class SyncChaptersWithSource(
|
||||||
private val downloadManager: MangaDownloadManager = Injekt.get(),
|
private val downloadManager: MangaDownloadManager,
|
||||||
private val downloadProvider: MangaDownloadProvider = Injekt.get(),
|
private val downloadProvider: MangaDownloadProvider,
|
||||||
private val chapterRepository: ChapterRepository = Injekt.get(),
|
private val chapterRepository: ChapterRepository,
|
||||||
private val shouldUpdateDbChapter: ShouldUpdateDbChapter = Injekt.get(),
|
private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
private val updateManga: UpdateManga,
|
||||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
private val updateChapter: UpdateChapter,
|
||||||
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
|
private val getChapterByMangaId: GetChapterByMangaId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,12 +9,11 @@ import tachiyomi.domain.items.chapter.model.Chapter
|
||||||
import tachiyomi.domain.items.chapter.model.toChapterUpdate
|
import tachiyomi.domain.items.chapter.model.toChapterUpdate
|
||||||
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
|
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
|
||||||
import tachiyomi.domain.track.manga.model.MangaTrack
|
import tachiyomi.domain.track.manga.model.MangaTrack
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class SyncChaptersWithTrackServiceTwoWay(
|
class SyncChaptersWithTrackServiceTwoWay(
|
||||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
private val updateChapter: UpdateChapter,
|
||||||
private val insertTrack: InsertMangaTrack = Injekt.get(),
|
private val insertTrack: InsertMangaTrack,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(
|
suspend fun await(
|
||||||
|
|
|
@ -20,7 +20,6 @@ import tachiyomi.domain.items.episode.model.toEpisodeUpdate
|
||||||
import tachiyomi.domain.items.episode.repository.EpisodeRepository
|
import tachiyomi.domain.items.episode.repository.EpisodeRepository
|
||||||
import tachiyomi.domain.items.episode.service.EpisodeRecognition
|
import tachiyomi.domain.items.episode.service.EpisodeRecognition
|
||||||
import tachiyomi.source.local.entries.anime.isLocal
|
import tachiyomi.source.local.entries.anime.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.lang.Long.max
|
import java.lang.Long.max
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
|
@ -28,13 +27,13 @@ import java.util.Date
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
|
||||||
class SyncEpisodesWithSource(
|
class SyncEpisodesWithSource(
|
||||||
private val downloadManager: AnimeDownloadManager = Injekt.get(),
|
private val downloadManager: AnimeDownloadManager,
|
||||||
private val downloadProvider: AnimeDownloadProvider = Injekt.get(),
|
private val downloadProvider: AnimeDownloadProvider,
|
||||||
private val episodeRepository: EpisodeRepository = Injekt.get(),
|
private val episodeRepository: EpisodeRepository,
|
||||||
private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode = Injekt.get(),
|
private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode,
|
||||||
private val updateAnime: UpdateAnime = Injekt.get(),
|
private val updateAnime: UpdateAnime,
|
||||||
private val updateEpisode: UpdateEpisode = Injekt.get(),
|
private val updateEpisode: UpdateEpisode,
|
||||||
private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(),
|
private val getEpisodeByAnimeId: GetEpisodeByAnimeId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,12 +9,11 @@ import tachiyomi.domain.items.episode.model.Episode
|
||||||
import tachiyomi.domain.items.episode.model.toEpisodeUpdate
|
import tachiyomi.domain.items.episode.model.toEpisodeUpdate
|
||||||
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
|
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
|
||||||
import tachiyomi.domain.track.anime.model.AnimeTrack
|
import tachiyomi.domain.track.anime.model.AnimeTrack
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class SyncEpisodesWithTrackServiceTwoWay(
|
class SyncEpisodesWithTrackServiceTwoWay(
|
||||||
private val updateEpisode: UpdateEpisode = Injekt.get(),
|
private val updateEpisode: UpdateEpisode,
|
||||||
private val insertTrack: InsertAnimeTrack = Injekt.get(),
|
private val insertTrack: InsertAnimeTrack,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(
|
suspend fun await(
|
||||||
|
|
|
@ -10,12 +10,10 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.HelpOutline
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
|
@ -175,7 +173,8 @@ private fun AnimeExtensionDetails(
|
||||||
data = Uri.fromParts("package", extension.pkgName, null)
|
data = Uri.fromParts("package", extension.pkgName, null)
|
||||||
context.startActivity(this)
|
context.startActivity(this)
|
||||||
}
|
}
|
||||||
},
|
Unit
|
||||||
|
}.takeIf { extension.isShared },
|
||||||
onClickAgeRating = {
|
onClickAgeRating = {
|
||||||
showNsfwWarning = true
|
showNsfwWarning = true
|
||||||
},
|
},
|
||||||
|
@ -208,7 +207,7 @@ private fun DetailsHeader(
|
||||||
extension: AnimeExtension,
|
extension: AnimeExtension,
|
||||||
onClickAgeRating: () -> Unit,
|
onClickAgeRating: () -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
onClickAppInfo: () -> Unit,
|
onClickAppInfo: (() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
@ -292,6 +291,7 @@ private fun DetailsHeader(
|
||||||
top = MaterialTheme.padding.small,
|
top = MaterialTheme.padding.small,
|
||||||
bottom = MaterialTheme.padding.medium,
|
bottom = MaterialTheme.padding.medium,
|
||||||
),
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
@ -300,16 +300,16 @@ private fun DetailsHeader(
|
||||||
Text(stringResource(R.string.ext_uninstall))
|
Text(stringResource(R.string.ext_uninstall))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.width(16.dp))
|
if (onClickAppInfo != null) {
|
||||||
|
Button(
|
||||||
Button(
|
modifier = Modifier.weight(1f),
|
||||||
modifier = Modifier.weight(1f),
|
onClick = onClickAppInfo,
|
||||||
onClick = onClickAppInfo,
|
) {
|
||||||
) {
|
Text(
|
||||||
Text(
|
text = stringResource(R.string.ext_app_info),
|
||||||
text = stringResource(R.string.ext_app_info),
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ fun AnimeExtensionScreen(
|
||||||
enabled = !state.isLoading,
|
enabled = !state.isLoading,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.isEmpty -> {
|
state.isEmpty -> {
|
||||||
val msg = if (!searchQuery.isNullOrEmpty()) {
|
val msg = if (!searchQuery.isNullOrEmpty()) {
|
||||||
R.string.no_results_found
|
R.string.no_results_found
|
||||||
|
|
|
@ -47,7 +47,7 @@ fun AnimeSourcesScreen(
|
||||||
onLongClickItem: (AnimeSource) -> Unit,
|
onLongClickItem: (AnimeSource) -> Unit,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.source_empty_screen,
|
textResource = R.string.source_empty_screen,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
|
|
@ -51,7 +51,7 @@ fun MigrateAnimeSourceScreen(
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.information_empty_library,
|
textResource = R.string.information_empty_library,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.presentation.browse.anime.components
|
package eu.kanade.presentation.browse.anime.components
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
@ -31,6 +30,7 @@ import eu.kanade.domain.source.anime.model.icon
|
||||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.domain.source.anime.model.AnimeSource
|
import tachiyomi.domain.source.anime.model.AnimeSource
|
||||||
import tachiyomi.source.local.entries.anime.LocalAnimeSource
|
import tachiyomi.source.local.entries.anime.LocalAnimeSource
|
||||||
|
@ -127,7 +127,7 @@ private fun AnimeExtension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT
|
||||||
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
value = try {
|
value = try {
|
||||||
val appInfo = context.packageManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
val appInfo = AnimeExtensionLoader.getAnimeExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
|
||||||
val appResources = context.packageManager.getResourcesForApplication(appInfo)
|
val appResources = context.packageManager.getResourcesForApplication(appInfo)
|
||||||
Result.Success(
|
Result.Success(
|
||||||
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
|
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
|
||||||
|
|
|
@ -10,12 +10,10 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.HelpOutline
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
|
@ -176,7 +174,8 @@ private fun ExtensionDetails(
|
||||||
data = Uri.fromParts("package", extension.pkgName, null)
|
data = Uri.fromParts("package", extension.pkgName, null)
|
||||||
context.startActivity(this)
|
context.startActivity(this)
|
||||||
}
|
}
|
||||||
},
|
Unit
|
||||||
|
}.takeIf { extension.isShared },
|
||||||
onClickAgeRating = {
|
onClickAgeRating = {
|
||||||
showNsfwWarning = true
|
showNsfwWarning = true
|
||||||
},
|
},
|
||||||
|
@ -209,7 +208,7 @@ private fun DetailsHeader(
|
||||||
extension: MangaExtension,
|
extension: MangaExtension,
|
||||||
onClickAgeRating: () -> Unit,
|
onClickAgeRating: () -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
onClickAppInfo: () -> Unit,
|
onClickAppInfo: (() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
@ -293,6 +292,7 @@ private fun DetailsHeader(
|
||||||
top = MaterialTheme.padding.small,
|
top = MaterialTheme.padding.small,
|
||||||
bottom = MaterialTheme.padding.medium,
|
bottom = MaterialTheme.padding.medium,
|
||||||
),
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
@ -301,16 +301,16 @@ private fun DetailsHeader(
|
||||||
Text(stringResource(R.string.ext_uninstall))
|
Text(stringResource(R.string.ext_uninstall))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.width(16.dp))
|
if (onClickAppInfo != null) {
|
||||||
|
Button(
|
||||||
Button(
|
modifier = Modifier.weight(1f),
|
||||||
modifier = Modifier.weight(1f),
|
onClick = onClickAppInfo,
|
||||||
onClick = onClickAppInfo,
|
) {
|
||||||
) {
|
Text(
|
||||||
Text(
|
text = stringResource(R.string.ext_app_info),
|
||||||
text = stringResource(R.string.ext_app_info),
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,7 @@ fun MangaExtensionScreen(
|
||||||
enabled = !state.isLoading,
|
enabled = !state.isLoading,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.isEmpty -> {
|
state.isEmpty -> {
|
||||||
val msg = if (!searchQuery.isNullOrEmpty()) {
|
val msg = if (!searchQuery.isNullOrEmpty()) {
|
||||||
R.string.no_results_found
|
R.string.no_results_found
|
||||||
|
|
|
@ -47,7 +47,7 @@ fun MangaSourcesScreen(
|
||||||
onLongClickItem: (Source) -> Unit,
|
onLongClickItem: (Source) -> Unit,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.source_empty_screen,
|
textResource = R.string.source_empty_screen,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
|
|
@ -51,7 +51,7 @@ fun MigrateMangaSourceScreen(
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.information_empty_library,
|
textResource = R.string.information_empty_library,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.presentation.browse.manga.components
|
package eu.kanade.presentation.browse.manga.components
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
@ -31,6 +30,7 @@ import eu.kanade.domain.source.manga.model.icon
|
||||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.domain.source.manga.model.Source
|
import tachiyomi.domain.source.manga.model.Source
|
||||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||||
|
@ -127,7 +127,7 @@ private fun MangaExtension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT
|
||||||
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
value = try {
|
value = try {
|
||||||
val appInfo = context.packageManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
val appInfo = MangaExtensionLoader.getMangaExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
|
||||||
val appResources = context.packageManager.getResourcesForApplication(appInfo)
|
val appResources = context.packageManager.getResourcesForApplication(appInfo)
|
||||||
Result.Success(
|
Result.Success(
|
||||||
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
|
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
|
||||||
|
|
|
@ -30,7 +30,7 @@ fun AnimeHistoryScreen(
|
||||||
) { _ ->
|
) { _ ->
|
||||||
state.list.let {
|
state.list.let {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
LoadingScreen(modifier = Modifier.padding(contentPadding))
|
LoadingScreen(Modifier.padding(contentPadding))
|
||||||
} else if (it.isEmpty()) {
|
} else if (it.isEmpty()) {
|
||||||
val msg = if (!searchQuery.isNullOrEmpty()) {
|
val msg = if (!searchQuery.isNullOrEmpty()) {
|
||||||
R.string.no_results_found
|
R.string.no_results_found
|
||||||
|
|
|
@ -29,7 +29,7 @@ fun MangaHistoryScreen(
|
||||||
) { _ ->
|
) { _ ->
|
||||||
state.list.let {
|
state.list.let {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
LoadingScreen(modifier = Modifier.padding(contentPadding))
|
LoadingScreen(Modifier.padding(contentPadding))
|
||||||
} else if (it.isEmpty()) {
|
} else if (it.isEmpty()) {
|
||||||
val msg = if (!searchQuery.isNullOrEmpty()) {
|
val msg = if (!searchQuery.isNullOrEmpty()) {
|
||||||
R.string.no_results_found
|
R.string.no_results_found
|
||||||
|
|
|
@ -181,7 +181,7 @@ private val displayModes = listOf(
|
||||||
private fun ColumnScope.DisplayPage(
|
private fun ColumnScope.DisplayPage(
|
||||||
screenModel: AnimeLibrarySettingsScreenModel,
|
screenModel: AnimeLibrarySettingsScreenModel,
|
||||||
) {
|
) {
|
||||||
val displayMode by screenModel.libraryPreferences.libraryDisplayMode().collectAsState()
|
val displayMode by screenModel.libraryPreferences.displayMode().collectAsState()
|
||||||
SettingsChipRow(R.string.action_display_mode) {
|
SettingsChipRow(R.string.action_display_mode) {
|
||||||
displayModes.map { (titleRes, mode) ->
|
displayModes.map { (titleRes, mode) ->
|
||||||
FilterChip(
|
FilterChip(
|
||||||
|
|
|
@ -180,7 +180,7 @@ private val displayModes = listOf(
|
||||||
private fun ColumnScope.DisplayPage(
|
private fun ColumnScope.DisplayPage(
|
||||||
screenModel: MangaLibrarySettingsScreenModel,
|
screenModel: MangaLibrarySettingsScreenModel,
|
||||||
) {
|
) {
|
||||||
val displayMode by screenModel.libraryPreferences.libraryDisplayMode().collectAsState()
|
val displayMode by screenModel.libraryPreferences.displayMode().collectAsState()
|
||||||
SettingsChipRow(R.string.action_display_mode) {
|
SettingsChipRow(R.string.action_display_mode) {
|
||||||
displayModes.map { (titleRes, mode) ->
|
displayModes.map { (titleRes, mode) ->
|
||||||
FilterChip(
|
FilterChip(
|
||||||
|
|
|
@ -33,6 +33,9 @@ import tachiyomi.domain.category.manga.interactor.GetMangaCategories
|
||||||
import tachiyomi.domain.category.manga.interactor.ResetMangaCategoryFlags
|
import tachiyomi.domain.category.manga.interactor.ResetMangaCategoryFlags
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_NETWORK_NOT_METERED
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_UNVIEWED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_UNVIEWED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
|
||||||
|
@ -163,15 +166,15 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval()
|
val autoUpdateIntervalPref = libraryPreferences.autoUpdateInterval()
|
||||||
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
|
val autoUpdateInterval by autoUpdateIntervalPref.collectAsState()
|
||||||
|
|
||||||
val animelibUpdateCategoriesPref = libraryPreferences.animeLibraryUpdateCategories()
|
val animeAutoUpdateCategoriesPref = libraryPreferences.animeUpdateCategories()
|
||||||
val animelibUpdateCategoriesExcludePref =
|
val animeAutoUpdateCategoriesExcludePref =
|
||||||
libraryPreferences.animeLibraryUpdateCategoriesExclude()
|
libraryPreferences.animeUpdateCategoriesExclude()
|
||||||
|
|
||||||
val includedAnime by animelibUpdateCategoriesPref.collectAsState()
|
val includedAnime by animeAutoUpdateCategoriesPref.collectAsState()
|
||||||
val excludedAnime by animelibUpdateCategoriesExcludePref.collectAsState()
|
val excludedAnime by animeAutoUpdateCategoriesExcludePref.collectAsState()
|
||||||
var showAnimeCategoriesDialog by rememberSaveable { mutableStateOf(false) }
|
var showAnimeCategoriesDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
if (showAnimeCategoriesDialog) {
|
if (showAnimeCategoriesDialog) {
|
||||||
TriStateListDialog(
|
TriStateListDialog(
|
||||||
|
@ -183,8 +186,8 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
itemLabel = { it.visualName },
|
itemLabel = { it.visualName },
|
||||||
onDismissRequest = { showAnimeCategoriesDialog = false },
|
onDismissRequest = { showAnimeCategoriesDialog = false },
|
||||||
onValueChanged = { newIncluded, newExcluded ->
|
onValueChanged = { newIncluded, newExcluded ->
|
||||||
animelibUpdateCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet())
|
animeAutoUpdateCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet())
|
||||||
animelibUpdateCategoriesExcludePref.set(
|
animeAutoUpdateCategoriesExcludePref.set(
|
||||||
newExcluded.map { it.id.toString() }
|
newExcluded.map { it.id.toString() }
|
||||||
.toSet(),
|
.toSet(),
|
||||||
)
|
)
|
||||||
|
@ -193,12 +196,12 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val libraryUpdateCategoriesPref = libraryPreferences.mangaLibraryUpdateCategories()
|
val autoUpdateCategoriesPref = libraryPreferences.mangaUpdateCategories()
|
||||||
val libraryUpdateCategoriesExcludePref =
|
val autoUpdateCategoriesExcludePref =
|
||||||
libraryPreferences.mangaLibraryUpdateCategoriesExclude()
|
libraryPreferences.mangaUpdateCategoriesExclude()
|
||||||
|
|
||||||
val includedManga by libraryUpdateCategoriesPref.collectAsState()
|
val includedManga by autoUpdateCategoriesPref.collectAsState()
|
||||||
val excludedManga by libraryUpdateCategoriesExcludePref.collectAsState()
|
val excludedManga by autoUpdateCategoriesExcludePref.collectAsState()
|
||||||
var showMangaCategoriesDialog by rememberSaveable { mutableStateOf(false) }
|
var showMangaCategoriesDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
if (showMangaCategoriesDialog) {
|
if (showMangaCategoriesDialog) {
|
||||||
TriStateListDialog(
|
TriStateListDialog(
|
||||||
|
@ -210,8 +213,8 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
itemLabel = { it.visualName },
|
itemLabel = { it.visualName },
|
||||||
onDismissRequest = { showMangaCategoriesDialog = false },
|
onDismissRequest = { showMangaCategoriesDialog = false },
|
||||||
onValueChanged = { newIncluded, newExcluded ->
|
onValueChanged = { newIncluded, newExcluded ->
|
||||||
libraryUpdateCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet())
|
autoUpdateCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet())
|
||||||
libraryUpdateCategoriesExcludePref.set(
|
autoUpdateCategoriesExcludePref.set(
|
||||||
newExcluded.map { it.id.toString() }
|
newExcluded.map { it.id.toString() }
|
||||||
.toSet(),
|
.toSet(),
|
||||||
)
|
)
|
||||||
|
@ -224,7 +227,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
title = stringResource(R.string.pref_category_library_update),
|
title = stringResource(R.string.pref_category_library_update),
|
||||||
preferenceItems = listOf(
|
preferenceItems = listOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryUpdateIntervalPref,
|
pref = autoUpdateIntervalPref,
|
||||||
title = stringResource(R.string.pref_library_update_interval),
|
title = stringResource(R.string.pref_library_update_interval),
|
||||||
entries = mapOf(
|
entries = mapOf(
|
||||||
0 to stringResource(R.string.update_never),
|
0 to stringResource(R.string.update_never),
|
||||||
|
@ -241,15 +244,14 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryPreferences.libraryUpdateDeviceRestriction(),
|
pref = libraryPreferences.autoUpdateDeviceRestrictions(),
|
||||||
enabled = libraryUpdateInterval > 0,
|
enabled = autoUpdateInterval > 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 = mapOf(
|
||||||
ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read),
|
DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi),
|
||||||
ENTRY_NON_VIEWED to stringResource(R.string.pref_update_only_started),
|
DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered),
|
||||||
ENTRY_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
|
DEVICE_CHARGING to stringResource(R.string.charging),
|
||||||
ENTRY_OUTSIDE_RELEASE_PERIOD to 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.
|
||||||
|
@ -290,7 +292,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryPreferences.libraryUpdateItemRestriction(),
|
pref = libraryPreferences.autoUpdateItemRestrictions(),
|
||||||
title = stringResource(R.string.pref_library_update_manga_restriction),
|
title = stringResource(R.string.pref_library_update_manga_restriction),
|
||||||
entries = mapOf(
|
entries = mapOf(
|
||||||
ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read),
|
ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read),
|
||||||
|
|
|
@ -69,7 +69,7 @@ fun AnimeUpdateScreen(
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.items.isEmpty() -> EmptyScreen(
|
state.items.isEmpty() -> EmptyScreen(
|
||||||
textResource = R.string.information_no_recent,
|
textResource = R.string.information_no_recent,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
|
|
@ -65,7 +65,7 @@ fun MangaUpdateScreen(
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.items.isEmpty() -> EmptyScreen(
|
state.items.isEmpty() -> EmptyScreen(
|
||||||
textResource = R.string.information_no_recent,
|
textResource = R.string.information_no_recent,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
|
|
@ -2,19 +2,30 @@ package eu.kanade.presentation.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.LicensedEntryItemsException
|
||||||
import eu.kanade.tachiyomi.network.HttpException
|
import eu.kanade.tachiyomi.network.HttpException
|
||||||
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
import tachiyomi.domain.items.chapter.model.NoChaptersException
|
import tachiyomi.domain.items.chapter.model.NoChaptersException
|
||||||
import tachiyomi.domain.items.episode.model.NoEpisodesException
|
import tachiyomi.domain.items.episode.model.NoEpisodesException
|
||||||
import tachiyomi.domain.source.anime.model.AnimeSourceNotInstalledException
|
import tachiyomi.domain.source.anime.model.AnimeSourceNotInstalledException
|
||||||
import tachiyomi.domain.source.manga.model.SourceNotInstalledException
|
import tachiyomi.domain.source.manga.model.SourceNotInstalledException
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
context(Context)
|
context(Context)
|
||||||
val Throwable.formattedMessage: String
|
val Throwable.formattedMessage: String
|
||||||
get() {
|
get() {
|
||||||
when (this) {
|
when (this) {
|
||||||
|
is HttpException -> return getString(R.string.exception_http, code)
|
||||||
|
is UnknownHostException -> {
|
||||||
|
return if (!isOnline()) {
|
||||||
|
getString(R.string.exception_offline)
|
||||||
|
} else {
|
||||||
|
getString(R.string.exception_unknown_host, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
is NoChaptersException, is NoEpisodesException -> return getString(R.string.no_results_found)
|
is NoChaptersException, is NoEpisodesException -> return getString(R.string.no_results_found)
|
||||||
is SourceNotInstalledException, is AnimeSourceNotInstalledException -> return getString(R.string.loader_not_implemented_error)
|
is SourceNotInstalledException, is AnimeSourceNotInstalledException -> return getString(R.string.loader_not_implemented_error)
|
||||||
is HttpException -> return "$message: ${getString(R.string.http_error_hint)}"
|
is LicensedEntryItemsException -> return getString(R.string.licensed_manga_chapters_error)
|
||||||
}
|
}
|
||||||
return when (val className = this::class.simpleName) {
|
return when (val className = this::class.simpleName) {
|
||||||
"Exception", "IOException" -> message ?: className
|
"Exception", "IOException" -> message ?: className
|
||||||
|
|
|
@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
import eu.kanade.tachiyomi.util.system.workManager
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
import tachiyomi.core.preference.TriState
|
import tachiyomi.core.preference.TriState
|
||||||
|
import tachiyomi.core.preference.getAndSet
|
||||||
import tachiyomi.core.preference.getEnum
|
import tachiyomi.core.preference.getEnum
|
||||||
import tachiyomi.core.preference.minusAssign
|
import tachiyomi.core.preference.minusAssign
|
||||||
import tachiyomi.core.preference.plusAssign
|
import tachiyomi.core.preference.plusAssign
|
||||||
|
@ -107,19 +108,19 @@ object Migrations {
|
||||||
}
|
}
|
||||||
if (oldVersion < 44) {
|
if (oldVersion < 44) {
|
||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
val oldMangaSortingMode = prefs.getInt(libraryPreferences.libraryMangaSortingMode().key(), 0)
|
val oldMangaSortingMode = prefs.getInt(libraryPreferences.mangaSortingMode().key(), 0)
|
||||||
|
|
||||||
if (oldMangaSortingMode == 5) { // SOURCE = 5
|
if (oldMangaSortingMode == 5) { // SOURCE = 5
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(libraryPreferences.libraryMangaSortingMode().key(), 0) // ALPHABETICAL = 0
|
putInt(libraryPreferences.mangaSortingMode().key(), 0) // ALPHABETICAL = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val oldAnimeSortingMode = prefs.getInt(libraryPreferences.libraryAnimeSortingMode().key(), 0)
|
val oldAnimeSortingMode = prefs.getInt(libraryPreferences.animeSortingMode().key(), 0)
|
||||||
|
|
||||||
if (oldAnimeSortingMode == 5) { // SOURCE = 5
|
if (oldAnimeSortingMode == 5) { // SOURCE = 5
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(libraryPreferences.libraryAnimeSortingMode().key(), 0) // ALPHABETICAL = 0
|
putInt(libraryPreferences.animeSortingMode().key(), 0) // ALPHABETICAL = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,9 +195,9 @@ object Migrations {
|
||||||
}
|
}
|
||||||
if (oldVersion < 61) {
|
if (oldVersion < 61) {
|
||||||
// Handle removed every 1 or 2 hour library updates
|
// Handle removed every 1 or 2 hour library updates
|
||||||
val updateInterval = libraryPreferences.libraryUpdateInterval().get()
|
val updateInterval = libraryPreferences.autoUpdateInterval().get()
|
||||||
if (updateInterval == 1 || updateInterval == 2) {
|
if (updateInterval == 1 || updateInterval == 2) {
|
||||||
libraryPreferences.libraryUpdateInterval().set(3)
|
libraryPreferences.autoUpdateInterval().set(3)
|
||||||
MangaLibraryUpdateJob.setupTask(context, 3)
|
MangaLibraryUpdateJob.setupTask(context, 3)
|
||||||
AnimeLibraryUpdateJob.setupTask(context, 3)
|
AnimeLibraryUpdateJob.setupTask(context, 3)
|
||||||
}
|
}
|
||||||
|
@ -207,8 +208,8 @@ object Migrations {
|
||||||
AnimeLibraryUpdateJob.setupTask(context)
|
AnimeLibraryUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
if (oldVersion < 64) {
|
if (oldVersion < 64) {
|
||||||
val oldMangaSortingMode = prefs.getInt(libraryPreferences.libraryMangaSortingMode().key(), 0)
|
val oldMangaSortingMode = prefs.getInt(libraryPreferences.mangaSortingMode().key(), 0)
|
||||||
val oldAnimeSortingMode = prefs.getInt(libraryPreferences.libraryAnimeSortingMode().key(), 0)
|
val oldAnimeSortingMode = prefs.getInt(libraryPreferences.animeSortingMode().key(), 0)
|
||||||
val oldSortingDirection = prefs.getBoolean("library_sorting_ascending", true)
|
val oldSortingDirection = prefs.getBoolean("library_sorting_ascending", true)
|
||||||
|
|
||||||
val newMangaSortingMode = when (oldMangaSortingMode) {
|
val newMangaSortingMode = when (oldMangaSortingMode) {
|
||||||
|
@ -241,14 +242,14 @@ object Migrations {
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs.edit(commit = true) {
|
prefs.edit(commit = true) {
|
||||||
remove(libraryPreferences.libraryMangaSortingMode().key())
|
remove(libraryPreferences.mangaSortingMode().key())
|
||||||
remove(libraryPreferences.libraryAnimeSortingMode().key())
|
remove(libraryPreferences.animeSortingMode().key())
|
||||||
remove("library_sorting_ascending")
|
remove("library_sorting_ascending")
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putString(libraryPreferences.libraryMangaSortingMode().key(), newMangaSortingMode)
|
putString(libraryPreferences.mangaSortingMode().key(), newMangaSortingMode)
|
||||||
putString(libraryPreferences.libraryAnimeSortingMode().key(), newAnimeSortingMode)
|
putString(libraryPreferences.animeSortingMode().key(), newAnimeSortingMode)
|
||||||
putString("library_sorting_ascending", newSortingDirection)
|
putString("library_sorting_ascending", newSortingDirection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,9 +260,9 @@ object Migrations {
|
||||||
}
|
}
|
||||||
if (oldVersion < 71) {
|
if (oldVersion < 71) {
|
||||||
// Handle removed every 3, 4, 6, and 8 hour library updates
|
// Handle removed every 3, 4, 6, and 8 hour library updates
|
||||||
val updateInterval = libraryPreferences.libraryUpdateInterval().get()
|
val updateInterval = libraryPreferences.autoUpdateInterval().get()
|
||||||
if (updateInterval in listOf(3, 4, 6, 8)) {
|
if (updateInterval in listOf(3, 4, 6, 8)) {
|
||||||
libraryPreferences.libraryUpdateInterval().set(12)
|
libraryPreferences.autoUpdateInterval().set(12)
|
||||||
MangaLibraryUpdateJob.setupTask(context, 12)
|
MangaLibraryUpdateJob.setupTask(context, 12)
|
||||||
AnimeLibraryUpdateJob.setupTask(context, 12)
|
AnimeLibraryUpdateJob.setupTask(context, 12)
|
||||||
}
|
}
|
||||||
|
@ -269,7 +270,7 @@ object Migrations {
|
||||||
if (oldVersion < 72) {
|
if (oldVersion < 72) {
|
||||||
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
|
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
|
||||||
if (!oldUpdateOngoingOnly) {
|
if (!oldUpdateOngoingOnly) {
|
||||||
libraryPreferences.libraryUpdateItemRestriction() -= ENTRY_NON_COMPLETED
|
libraryPreferences.autoUpdateItemRestrictions() -= ENTRY_NON_COMPLETED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 75) {
|
if (oldVersion < 75) {
|
||||||
|
@ -294,29 +295,29 @@ object Migrations {
|
||||||
if (oldVersion < 81) {
|
if (oldVersion < 81) {
|
||||||
// Handle renamed enum values
|
// Handle renamed enum values
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
val newMangaSortingMode = when (val oldSortingMode = prefs.getString(libraryPreferences.libraryMangaSortingMode().key(), "ALPHABETICAL")) {
|
val newMangaSortingMode = when (val oldSortingMode = prefs.getString(libraryPreferences.mangaSortingMode().key(), "ALPHABETICAL")) {
|
||||||
"LAST_CHECKED" -> "LAST_MANGA_UPDATE"
|
"LAST_CHECKED" -> "LAST_MANGA_UPDATE"
|
||||||
"UNREAD" -> "UNREAD_COUNT"
|
"UNREAD" -> "UNREAD_COUNT"
|
||||||
"DATE_FETCHED" -> "CHAPTER_FETCH_DATE"
|
"DATE_FETCHED" -> "CHAPTER_FETCH_DATE"
|
||||||
else -> oldSortingMode
|
else -> oldSortingMode
|
||||||
}
|
}
|
||||||
val newAnimeSortingMode = when (val oldSortingMode = prefs.getString(libraryPreferences.libraryAnimeSortingMode().key(), "ALPHABETICAL")) {
|
val newAnimeSortingMode = when (val oldSortingMode = prefs.getString(libraryPreferences.animeSortingMode().key(), "ALPHABETICAL")) {
|
||||||
"LAST_CHECKED" -> "LAST_MANGA_UPDATE"
|
"LAST_CHECKED" -> "LAST_MANGA_UPDATE"
|
||||||
"UNREAD" -> "UNREAD_COUNT"
|
"UNREAD" -> "UNREAD_COUNT"
|
||||||
"DATE_FETCHED" -> "CHAPTER_FETCH_DATE"
|
"DATE_FETCHED" -> "CHAPTER_FETCH_DATE"
|
||||||
else -> oldSortingMode
|
else -> oldSortingMode
|
||||||
}
|
}
|
||||||
putString(libraryPreferences.libraryMangaSortingMode().key(), newMangaSortingMode)
|
putString(libraryPreferences.mangaSortingMode().key(), newMangaSortingMode)
|
||||||
putString(libraryPreferences.libraryAnimeSortingMode().key(), newAnimeSortingMode)
|
putString(libraryPreferences.animeSortingMode().key(), newAnimeSortingMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 82) {
|
if (oldVersion < 82) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
val mangasort = prefs.getString(libraryPreferences.libraryMangaSortingMode().key(), null) ?: return@edit
|
val mangasort = prefs.getString(libraryPreferences.mangaSortingMode().key(), null) ?: return@edit
|
||||||
val animesort = prefs.getString(libraryPreferences.libraryAnimeSortingMode().key(), null) ?: return@edit
|
val animesort = prefs.getString(libraryPreferences.animeSortingMode().key(), null) ?: return@edit
|
||||||
val direction = prefs.getString("library_sorting_ascending", "ASCENDING")!!
|
val direction = prefs.getString("library_sorting_ascending", "ASCENDING")!!
|
||||||
putString(libraryPreferences.libraryMangaSortingMode().key(), "$mangasort,$direction")
|
putString(libraryPreferences.mangaSortingMode().key(), "$mangasort,$direction")
|
||||||
putString(libraryPreferences.libraryAnimeSortingMode().key(), "$animesort,$direction")
|
putString(libraryPreferences.animeSortingMode().key(), "$animesort,$direction")
|
||||||
remove("library_sorting_ascending")
|
remove("library_sorting_ascending")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,6 +453,12 @@ object Migrations {
|
||||||
readerPreferences.longStripSplitWebtoon().set(false)
|
readerPreferences.longStripSplitWebtoon().set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 105) {
|
||||||
|
val pref = libraryPreferences.autoUpdateDeviceRestrictions()
|
||||||
|
if (pref.isSet() && "battery_not_low" in pref.get()) {
|
||||||
|
pref.getAndSet { it - "battery_not_low" }
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
|
import androidx.work.Constraints
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
@ -78,6 +79,10 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||||
s.toInt(16)
|
s.toInt(16)
|
||||||
}
|
}
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
|
val constraints = Constraints(
|
||||||
|
requiresBatteryNotLow = true,
|
||||||
|
)
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<BackupCreateJob>(
|
val request = PeriodicWorkRequestBuilder<BackupCreateJob>(
|
||||||
interval.toLong(),
|
interval.toLong(),
|
||||||
TimeUnit.HOURS,
|
TimeUnit.HOURS,
|
||||||
|
@ -86,6 +91,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||||
)
|
)
|
||||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10.minutes.toJavaDuration())
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10.minutes.toJavaDuration())
|
||||||
.addTag(TAG_AUTO)
|
.addTag(TAG_AUTO)
|
||||||
|
.setConstraints(constraints)
|
||||||
.setInputData(
|
.setInputData(
|
||||||
workDataOf(
|
workDataOf(
|
||||||
IS_AUTO_BACKUP_KEY to true,
|
IS_AUTO_BACKUP_KEY to true,
|
||||||
|
|
|
@ -162,10 +162,10 @@ class BackupManager(
|
||||||
UniFile.fromUri(context, uri)
|
UniFile.fromUri(context, uri)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
?: throw Exception("Couldn't create backup file")
|
?: throw Exception(context.getString(R.string.create_backup_file_error))
|
||||||
|
|
||||||
if (!file.isFile) {
|
if (!file.isFile) {
|
||||||
throw IllegalStateException("Failed to get handle on file")
|
throw IllegalStateException("Failed to get handle on a backup file")
|
||||||
}
|
}
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.download.anime
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.core.util.mapNotNullKeys
|
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
import eu.kanade.tachiyomi.util.size
|
import eu.kanade.tachiyomi.util.size
|
||||||
|
@ -334,21 +333,23 @@ class AnimeDownloadCache(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
|
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
|
||||||
.associate { it.name to SourceDirectory(it) }
|
|
||||||
.mapNotNullKeys { entry ->
|
|
||||||
sources.find {
|
|
||||||
provider.getSourceDirName(it).equals(entry.key, ignoreCase = true)
|
|
||||||
}?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
rootDownloadsDir.sourceDirs = sourceDirs
|
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
|
||||||
|
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||||
|
.mapNotNull { dir ->
|
||||||
|
val sourceId = sourceMap[dir.name!!.lowercase()]
|
||||||
|
sourceId?.let { it to SourceDirectory(dir) }
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
|
||||||
|
rootDownloadsDir.sourceDirs = sourceDirs as ConcurrentHashMap<Long, SourceDirectory>
|
||||||
|
|
||||||
sourceDirs.values
|
sourceDirs.values
|
||||||
.map { sourceDir ->
|
.map { sourceDir ->
|
||||||
async {
|
async {
|
||||||
val animeDirs = sourceDir.dir.listFiles().orEmpty()
|
val animeDirs = sourceDir.dir.listFiles().orEmpty()
|
||||||
.filterNot { it.name.isNullOrBlank() }
|
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||||
.associate { it.name!! to AnimeDirectory(it) }
|
.associate { it.name!! to AnimeDirectory(it) }
|
||||||
|
|
||||||
sourceDir.animeDirs = ConcurrentHashMap(animeDirs)
|
sourceDir.animeDirs = ConcurrentHashMap(animeDirs)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.core.util.mapNotNullKeys
|
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import eu.kanade.tachiyomi.source.MangaSource
|
import eu.kanade.tachiyomi.source.MangaSource
|
||||||
import eu.kanade.tachiyomi.util.size
|
import eu.kanade.tachiyomi.util.size
|
||||||
|
@ -361,14 +360,16 @@ class MangaDownloadCache(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
|
||||||
|
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirLock.withLock {
|
||||||
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
|
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
|
||||||
.associate { it.name to SourceDirectory(it) }
|
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||||
.mapNotNullKeys { entry ->
|
.mapNotNull { dir ->
|
||||||
sources.find {
|
val sourceId = sourceMap[dir.name!!.lowercase()]
|
||||||
provider.getSourceDirName(it).equals(entry.key, ignoreCase = true)
|
sourceId?.let { it to SourceDirectory(dir) }
|
||||||
}?.id
|
|
||||||
}
|
}
|
||||||
|
.toMap()
|
||||||
|
|
||||||
rootDownloadsDir.sourceDirs = sourceDirs
|
rootDownloadsDir.sourceDirs = sourceDirs
|
||||||
|
|
||||||
|
@ -376,7 +377,7 @@ class MangaDownloadCache(
|
||||||
.map { sourceDir ->
|
.map { sourceDir ->
|
||||||
async {
|
async {
|
||||||
val mangaDirs = sourceDir.dir.listFiles().orEmpty()
|
val mangaDirs = sourceDir.dir.listFiles().orEmpty()
|
||||||
.filterNot { it.name.isNullOrBlank() }
|
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||||
.associate { it.name!! to MangaDirectory(it) }
|
.associate { it.name!! to MangaDirectory(it) }
|
||||||
|
|
||||||
sourceDir.mangaDirs = ConcurrentHashMap(mangaDirs)
|
sourceDir.mangaDirs = ConcurrentHashMap(mangaDirs)
|
||||||
|
|
|
@ -64,7 +64,6 @@ import tachiyomi.domain.items.episode.model.Episode
|
||||||
import tachiyomi.domain.items.episode.model.NoEpisodesException
|
import tachiyomi.domain.items.episode.model.NoEpisodesException
|
||||||
import tachiyomi.domain.library.anime.LibraryAnime
|
import tachiyomi.domain.library.anime.LibraryAnime
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_BATTERY_NOT_LOW
|
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_NETWORK_NOT_METERED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_NETWORK_NOT_METERED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI
|
||||||
|
@ -113,7 +112,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
if (tags.contains(WORK_NAME_AUTO)) {
|
if (tags.contains(WORK_NAME_AUTO)) {
|
||||||
val preferences = Injekt.get<LibraryPreferences>()
|
val preferences = Injekt.get<LibraryPreferences>()
|
||||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||||
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
@ -134,7 +133,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
|
|
||||||
// If this is a chapter update, set the last update time to now
|
// If this is a chapter update, set the last update time to now
|
||||||
if (target == Target.EPISODES) {
|
if (target == Target.EPISODES) {
|
||||||
libraryPreferences.libraryUpdateLastTimestamp().set(Date().time)
|
libraryPreferences.lastUpdatedTimestamp().set(Date().time)
|
||||||
}
|
}
|
||||||
|
|
||||||
val categoryId = inputData.getLong(KEY_CATEGORY, -1L)
|
val categoryId = inputData.getLong(KEY_CATEGORY, -1L)
|
||||||
|
@ -181,14 +180,14 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val listToUpdate = if (categoryId != -1L) {
|
val listToUpdate = if (categoryId != -1L) {
|
||||||
libraryAnime.filter { it.category == categoryId }
|
libraryAnime.filter { it.category == categoryId }
|
||||||
} else {
|
} else {
|
||||||
val categoriesToUpdate = libraryPreferences.animeLibraryUpdateCategories().get().map { it.toLong() }
|
val categoriesToUpdate = libraryPreferences.animeUpdateCategories().get().map { it.toLong() }
|
||||||
val includedAnime = if (categoriesToUpdate.isNotEmpty()) {
|
val includedAnime = if (categoriesToUpdate.isNotEmpty()) {
|
||||||
libraryAnime.filter { it.category in categoriesToUpdate }
|
libraryAnime.filter { it.category in categoriesToUpdate }
|
||||||
} else {
|
} else {
|
||||||
libraryAnime
|
libraryAnime
|
||||||
}
|
}
|
||||||
|
|
||||||
val categoriesToExclude = libraryPreferences.animeLibraryUpdateCategoriesExclude().get().map { it.toLong() }
|
val categoriesToExclude = libraryPreferences.animeUpdateCategoriesExclude().get().map { it.toLong() }
|
||||||
val excludedAnimeIds = if (categoriesToExclude.isNotEmpty()) {
|
val excludedAnimeIds = if (categoriesToExclude.isNotEmpty()) {
|
||||||
libraryAnime.filter { it.category in categoriesToExclude }.map { it.anime.id }
|
libraryAnime.filter { it.category in categoriesToExclude }.map { it.anime.id }
|
||||||
} else {
|
} else {
|
||||||
|
@ -229,7 +228,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val skippedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
val skippedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
||||||
val fetchWindow = setAnimeFetchInterval.getWindow(ZonedDateTime.now())
|
val fetchWindow = setAnimeFetchInterval.getWindow(ZonedDateTime.now())
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
|
@ -558,13 +557,13 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
prefInterval: Int? = null,
|
prefInterval: Int? = null,
|
||||||
) {
|
) {
|
||||||
val preferences = Injekt.get<LibraryPreferences>()
|
val preferences = Injekt.get<LibraryPreferences>()
|
||||||
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
val interval = prefInterval ?: preferences.autoUpdateInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||||
val constraints = Constraints(
|
val constraints = Constraints(
|
||||||
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED },
|
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED },
|
||||||
requiresCharging = DEVICE_CHARGING in restrictions,
|
requiresCharging = DEVICE_CHARGING in restrictions,
|
||||||
requiresBatteryNotLow = DEVICE_BATTERY_NOT_LOW in restrictions,
|
requiresBatteryNotLow = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<AnimeLibraryUpdateJob>(
|
val request = PeriodicWorkRequestBuilder<AnimeLibraryUpdateJob>(
|
||||||
|
|
|
@ -64,7 +64,6 @@ import tachiyomi.domain.items.chapter.model.Chapter
|
||||||
import tachiyomi.domain.items.chapter.model.NoChaptersException
|
import tachiyomi.domain.items.chapter.model.NoChaptersException
|
||||||
import tachiyomi.domain.library.manga.LibraryManga
|
import tachiyomi.domain.library.manga.LibraryManga
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_BATTERY_NOT_LOW
|
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_NETWORK_NOT_METERED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_NETWORK_NOT_METERED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI
|
||||||
|
@ -113,7 +112,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
if (tags.contains(WORK_NAME_AUTO)) {
|
if (tags.contains(WORK_NAME_AUTO)) {
|
||||||
val preferences = Injekt.get<LibraryPreferences>()
|
val preferences = Injekt.get<LibraryPreferences>()
|
||||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||||
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||||
return Result.retry()
|
return Result.retry()
|
||||||
}
|
}
|
||||||
|
@ -134,7 +133,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
|
|
||||||
// If this is a chapter update, set the last update time to now
|
// If this is a chapter update, set the last update time to now
|
||||||
if (target == Target.CHAPTERS) {
|
if (target == Target.CHAPTERS) {
|
||||||
libraryPreferences.libraryUpdateLastTimestamp().set(Date().time)
|
libraryPreferences.lastUpdatedTimestamp().set(Date().time)
|
||||||
}
|
}
|
||||||
|
|
||||||
val categoryId = inputData.getLong(KEY_CATEGORY, -1L)
|
val categoryId = inputData.getLong(KEY_CATEGORY, -1L)
|
||||||
|
@ -181,14 +180,14 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val listToUpdate = if (categoryId != -1L) {
|
val listToUpdate = if (categoryId != -1L) {
|
||||||
libraryManga.filter { it.category == categoryId }
|
libraryManga.filter { it.category == categoryId }
|
||||||
} else {
|
} else {
|
||||||
val categoriesToUpdate = libraryPreferences.mangaLibraryUpdateCategories().get().map { it.toLong() }
|
val categoriesToUpdate = libraryPreferences.mangaUpdateCategories().get().map { it.toLong() }
|
||||||
val includedManga = if (categoriesToUpdate.isNotEmpty()) {
|
val includedManga = if (categoriesToUpdate.isNotEmpty()) {
|
||||||
libraryManga.filter { it.category in categoriesToUpdate }
|
libraryManga.filter { it.category in categoriesToUpdate }
|
||||||
} else {
|
} else {
|
||||||
libraryManga
|
libraryManga
|
||||||
}
|
}
|
||||||
|
|
||||||
val categoriesToExclude = libraryPreferences.mangaLibraryUpdateCategoriesExclude().get().map { it.toLong() }
|
val categoriesToExclude = libraryPreferences.mangaUpdateCategoriesExclude().get().map { it.toLong() }
|
||||||
val excludedMangaIds = if (categoriesToExclude.isNotEmpty()) {
|
val excludedMangaIds = if (categoriesToExclude.isNotEmpty()) {
|
||||||
libraryManga.filter { it.category in categoriesToExclude }.map { it.manga.id }
|
libraryManga.filter { it.category in categoriesToExclude }.map { it.manga.id }
|
||||||
} else {
|
} else {
|
||||||
|
@ -229,7 +228,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
||||||
val fetchWindow = setMangaFetchInterval.getWindow(ZonedDateTime.now())
|
val fetchWindow = setMangaFetchInterval.getWindow(ZonedDateTime.now())
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
|
@ -557,13 +556,13 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
prefInterval: Int? = null,
|
prefInterval: Int? = null,
|
||||||
) {
|
) {
|
||||||
val preferences = Injekt.get<LibraryPreferences>()
|
val preferences = Injekt.get<LibraryPreferences>()
|
||||||
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
val interval = prefInterval ?: preferences.autoUpdateInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||||
val constraints = Constraints(
|
val constraints = Constraints(
|
||||||
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED },
|
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED },
|
||||||
requiresCharging = DEVICE_CHARGING in restrictions,
|
requiresCharging = DEVICE_CHARGING in restrictions,
|
||||||
requiresBatteryNotLow = DEVICE_BATTERY_NOT_LOW in restrictions,
|
requiresBatteryNotLow = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<MangaLibraryUpdateJob>(
|
val request = PeriodicWorkRequestBuilder<MangaLibraryUpdateJob>(
|
||||||
|
|
|
@ -17,9 +17,12 @@ import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
|
||||||
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
|
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import tachiyomi.domain.track.anime.model.AnimeTrack as DomainAnimeTrack
|
import tachiyomi.domain.track.anime.model.AnimeTrack as DomainAnimeTrack
|
||||||
|
|
||||||
|
private val insertTrack: InsertAnimeTrack by injectLazy()
|
||||||
|
|
||||||
interface AnimeTrackService {
|
interface AnimeTrackService {
|
||||||
|
|
||||||
// Common functions
|
// Common functions
|
||||||
|
@ -63,7 +66,7 @@ interface AnimeTrackService {
|
||||||
|
|
||||||
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
||||||
|
|
||||||
Injekt.get<InsertAnimeTrack>().await(track)
|
insertTrack.await(track)
|
||||||
|
|
||||||
// Update episode progress if newer episodes marked seen locally
|
// Update episode progress if newer episodes marked seen locally
|
||||||
if (hasSeenEpisodes) {
|
if (hasSeenEpisodes) {
|
||||||
|
@ -71,7 +74,7 @@ interface AnimeTrackService {
|
||||||
.sortedBy { it.episodeNumber }
|
.sortedBy { it.episodeNumber }
|
||||||
.takeWhile { it.seen }
|
.takeWhile { it.seen }
|
||||||
.lastOrNull()
|
.lastOrNull()
|
||||||
?.episodeNumber?.toDouble() ?: -1.0
|
?.episodeNumber ?: -1.0
|
||||||
|
|
||||||
if (latestLocalSeenEpisodeNumber > track.lastEpisodeSeen) {
|
if (latestLocalSeenEpisodeNumber > track.lastEpisodeSeen) {
|
||||||
track = track.copy(
|
track = track.copy(
|
||||||
|
@ -123,6 +126,7 @@ interface AnimeTrackService {
|
||||||
track.last_episode_seen = episodeNumber.toFloat()
|
track.last_episode_seen = episodeNumber.toFloat()
|
||||||
if (track.total_episodes != 0 && track.last_episode_seen.toInt() == track.total_episodes) {
|
if (track.total_episodes != 0 && track.last_episode_seen.toInt() == track.total_episodes) {
|
||||||
track.status = getCompletionStatus()
|
track.status = getCompletionStatus()
|
||||||
|
track.finished_watching_date = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
withIOContext { updateRemote(track) }
|
withIOContext { updateRemote(track) }
|
||||||
}
|
}
|
||||||
|
@ -147,7 +151,7 @@ interface AnimeTrackService {
|
||||||
try {
|
try {
|
||||||
update(track)
|
update(track)
|
||||||
track.toDomainTrack(idRequired = false)?.let {
|
track.toDomainTrack(idRequired = false)?.let {
|
||||||
Injekt.get<InsertAnimeTrack>().await(it)
|
insertTrack.await(it)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=${track.id}" }
|
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=${track.id}" }
|
||||||
|
|
|
@ -17,9 +17,12 @@ import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
|
||||||
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
|
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import tachiyomi.domain.track.manga.model.MangaTrack as DomainTrack
|
import tachiyomi.domain.track.manga.model.MangaTrack as DomainTrack
|
||||||
|
|
||||||
|
private val insertTrack: InsertMangaTrack by injectLazy()
|
||||||
|
|
||||||
interface MangaTrackService {
|
interface MangaTrackService {
|
||||||
|
|
||||||
// Common functions
|
// Common functions
|
||||||
|
@ -63,7 +66,7 @@ interface MangaTrackService {
|
||||||
|
|
||||||
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
||||||
|
|
||||||
Injekt.get<InsertMangaTrack>().await(track)
|
insertTrack.await(track)
|
||||||
|
|
||||||
// Update chapter progress if newer chapters marked read locally
|
// Update chapter progress if newer chapters marked read locally
|
||||||
if (hasReadChapters) {
|
if (hasReadChapters) {
|
||||||
|
@ -71,7 +74,7 @@ interface MangaTrackService {
|
||||||
.sortedBy { it.chapterNumber }
|
.sortedBy { it.chapterNumber }
|
||||||
.takeWhile { it.read }
|
.takeWhile { it.read }
|
||||||
.lastOrNull()
|
.lastOrNull()
|
||||||
?.chapterNumber?.toDouble() ?: -1.0
|
?.chapterNumber ?: -1.0
|
||||||
|
|
||||||
if (latestLocalReadChapterNumber > track.lastChapterRead) {
|
if (latestLocalReadChapterNumber > track.lastChapterRead) {
|
||||||
track = track.copy(
|
track = track.copy(
|
||||||
|
@ -123,6 +126,7 @@ interface MangaTrackService {
|
||||||
track.last_chapter_read = chapterNumber.toFloat()
|
track.last_chapter_read = chapterNumber.toFloat()
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read.toInt() == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read.toInt() == track.total_chapters) {
|
||||||
track.status = getCompletionStatus()
|
track.status = getCompletionStatus()
|
||||||
|
track.finished_reading_date = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
withIOContext { updateRemote(track) }
|
withIOContext { updateRemote(track) }
|
||||||
}
|
}
|
||||||
|
@ -147,7 +151,7 @@ interface MangaTrackService {
|
||||||
try {
|
try {
|
||||||
update(track)
|
update(track)
|
||||||
track.toDomainTrack(idRequired = false)?.let {
|
track.toDomainTrack(idRequired = false)?.let {
|
||||||
Injekt.get<InsertMangaTrack>().await(it)
|
insertTrack.await(it)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=${track.id}" }
|
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=${track.id}" }
|
||||||
|
|
|
@ -4,7 +4,6 @@ import androidx.annotation.CallSuper
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.domain.base.BasePreferences
|
|
||||||
import eu.kanade.domain.track.service.TrackPreferences
|
import eu.kanade.domain.track.service.TrackPreferences
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
@ -12,7 +11,6 @@ import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
abstract class TrackService(val id: Long) {
|
abstract class TrackService(val id: Long) {
|
||||||
|
|
||||||
val preferences: BasePreferences by injectLazy()
|
|
||||||
val trackPreferences: TrackPreferences by injectLazy()
|
val trackPreferences: TrackPreferences by injectLazy()
|
||||||
val networkService: NetworkHelper by injectLazy()
|
val networkService: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,10 @@ class AnimeExtensionManager(
|
||||||
fun getAppIconForSource(sourceId: Long): Drawable? {
|
fun getAppIconForSource(sourceId: Long): Drawable? {
|
||||||
val pkgName = _installedAnimeExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
|
val pkgName = _installedAnimeExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
|
||||||
if (pkgName != null) {
|
if (pkgName != null) {
|
||||||
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
|
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
|
||||||
|
AnimeExtensionLoader.getAnimeExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
|
||||||
|
.loadIcon(context.packageManager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -333,6 +336,7 @@ class AnimeExtensionManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPackageUninstalled(pkgName: String) {
|
override fun onPackageUninstalled(pkgName: String) {
|
||||||
|
AnimeExtensionLoader.uninstallPrivateExtension(context, pkgName)
|
||||||
unregisterAnimeExtension(pkgName)
|
unregisterAnimeExtension(pkgName)
|
||||||
updatePendingUpdatesCount()
|
updatePendingUpdatesCount()
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ sealed class AnimeExtension {
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
val isObsolete: Boolean = false,
|
val isObsolete: Boolean = false,
|
||||||
val isUnofficial: Boolean = false,
|
val isUnofficial: Boolean = false,
|
||||||
|
val isShared: Boolean,
|
||||||
) : AnimeExtension()
|
) : AnimeExtension()
|
||||||
|
|
||||||
data class Available(
|
data class Available(
|
||||||
|
|
|
@ -4,6 +4,9 @@ import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
@ -27,7 +30,7 @@ internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
|
||||||
* Registers this broadcast receiver
|
* Registers this broadcast receiver
|
||||||
*/
|
*/
|
||||||
fun register(context: Context) {
|
fun register(context: Context) {
|
||||||
context.registerReceiver(this, filter)
|
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,6 +41,9 @@ internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
|
||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
|
addAction(ACTION_EXTENSION_ADDED)
|
||||||
|
addAction(ACTION_EXTENSION_REPLACED)
|
||||||
|
addAction(ACTION_EXTENSION_REMOVED)
|
||||||
addDataScheme("package")
|
addDataScheme("package")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +55,7 @@ internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
|
||||||
if (intent == null) return
|
if (intent == null) return
|
||||||
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_PACKAGE_ADDED -> {
|
Intent.ACTION_PACKAGE_ADDED, ACTION_EXTENSION_ADDED -> {
|
||||||
if (isReplacing(intent)) return
|
if (isReplacing(intent)) return
|
||||||
|
|
||||||
launchNow {
|
launchNow {
|
||||||
|
@ -61,7 +67,7 @@ internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
Intent.ACTION_PACKAGE_REPLACED, ACTION_EXTENSION_REPLACED -> {
|
||||||
launchNow {
|
launchNow {
|
||||||
when (val result = getExtensionFromIntent(context, intent)) {
|
when (val result = getExtensionFromIntent(context, intent)) {
|
||||||
is AnimeLoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
is AnimeLoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
||||||
|
@ -71,7 +77,7 @@ internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
Intent.ACTION_PACKAGE_REMOVED, ACTION_EXTENSION_REMOVED -> {
|
||||||
if (isReplacing(intent)) return
|
if (isReplacing(intent)) return
|
||||||
|
|
||||||
val pkgName = getPackageNameFromIntent(intent)
|
val pkgName = getPackageNameFromIntent(intent)
|
||||||
|
@ -127,4 +133,30 @@ internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
|
||||||
fun onExtensionUntrusted(extension: AnimeExtension.Untrusted)
|
fun onExtensionUntrusted(extension: AnimeExtension.Untrusted)
|
||||||
fun onPackageUninstalled(pkgName: String)
|
fun onPackageUninstalled(pkgName: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ACTION_EXTENSION_ADDED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_ADDED"
|
||||||
|
private const val ACTION_EXTENSION_REPLACED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_REPLACED"
|
||||||
|
private const val ACTION_EXTENSION_REMOVED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_REMOVED"
|
||||||
|
|
||||||
|
fun notifyAdded(context: Context, pkgName: String) {
|
||||||
|
notify(context, pkgName, ACTION_EXTENSION_ADDED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyReplaced(context: Context, pkgName: String) {
|
||||||
|
notify(context, pkgName, ACTION_EXTENSION_REPLACED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyRemoved(context: Context, pkgName: String) {
|
||||||
|
notify(context, pkgName, ACTION_EXTENSION_REMOVED)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notify(context: Context, pkgName: String, action: String) {
|
||||||
|
Intent(action).apply {
|
||||||
|
data = Uri.parse("package:$pkgName")
|
||||||
|
`package` = context.packageName
|
||||||
|
context.sendBroadcast(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,11 @@ import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
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.AnimeExtensionManager
|
||||||
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 eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -157,6 +159,35 @@ internal class AnimeExtensionInstaller(private val context: Context) {
|
||||||
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
BasePreferences.ExtensionInstaller.PRIVATE -> {
|
||||||
|
val extensionManager = Injekt.get<AnimeExtensionManager>()
|
||||||
|
val tempFile = File(context.cacheDir, "temp_$downloadId")
|
||||||
|
|
||||||
|
if (tempFile.exists() && !tempFile.delete()) {
|
||||||
|
// Unlikely but just in case
|
||||||
|
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
tempFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AnimeExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
|
||||||
|
extensionManager.updateInstallStep(downloadId, InstallStep.Installed)
|
||||||
|
} else {
|
||||||
|
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
|
||||||
|
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val intent =
|
val intent =
|
||||||
AnimeExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
AnimeExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
||||||
|
@ -180,10 +211,15 @@ internal class AnimeExtensionInstaller(private val context: Context) {
|
||||||
* @param pkgName The package name of the extension to uninstall
|
* @param pkgName The package name of the extension to uninstall
|
||||||
*/
|
*/
|
||||||
fun uninstallApk(pkgName: String) {
|
fun uninstallApk(pkgName: String) {
|
||||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
if (context.isPackageInstalled(pkgName)) {
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
@Suppress("DEPRECATION")
|
||||||
|
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
||||||
context.startActivity(intent)
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(intent)
|
||||||
|
} else {
|
||||||
|
AnimeExtensionLoader.uninstallPrivateExtension(context, pkgName)
|
||||||
|
AnimeExtensionInstallReceiver.notifyRemoved(context, pkgName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.anime.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -14,12 +15,13 @@ import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that handles the loading of the extensions installed in the system.
|
* Class that handles the loading of the extensions installed in the system.
|
||||||
|
@ -41,12 +43,11 @@ internal object AnimeExtensionLoader {
|
||||||
const val LIB_VERSION_MIN = 12
|
const val LIB_VERSION_MIN = 12
|
||||||
const val LIB_VERSION_MAX = 15
|
const val LIB_VERSION_MAX = 15
|
||||||
|
|
||||||
private val PACKAGE_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
@Suppress("DEPRECATION")
|
||||||
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNING_CERTIFICATES
|
private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or
|
||||||
} else {
|
PackageManager.GET_META_DATA or
|
||||||
@Suppress("DEPRECATION")
|
PackageManager.GET_SIGNATURES or
|
||||||
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0)
|
||||||
}
|
|
||||||
|
|
||||||
// jmir1's key
|
// jmir1's key
|
||||||
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"
|
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"
|
||||||
|
@ -56,8 +57,57 @@ internal object AnimeExtensionLoader {
|
||||||
*/
|
*/
|
||||||
var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get()
|
var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get()
|
||||||
|
|
||||||
|
private const val PRIVATE_EXTENSION_EXTENSION = "ext"
|
||||||
|
|
||||||
|
private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
|
||||||
|
|
||||||
|
fun installPrivateExtensionFile(context: Context, file: File): Boolean {
|
||||||
|
val extension = context.packageManager.getPackageArchiveInfo(file.absolutePath, PACKAGE_FLAGS)
|
||||||
|
?.takeIf { isPackageAnExtension(it) } ?: return false
|
||||||
|
val currentExtension = getAnimeExtensionPackageInfoFromPkgName(context, extension.packageName)
|
||||||
|
|
||||||
|
if (currentExtension != null) {
|
||||||
|
if (PackageInfoCompat.getLongVersionCode(extension) <
|
||||||
|
PackageInfoCompat.getLongVersionCode(currentExtension)
|
||||||
|
) {
|
||||||
|
logcat(LogPriority.ERROR) { "Installed extension version is higher. Downgrading is not allowed." }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionSignatures = getSignatures(extension)
|
||||||
|
if (extensionSignatures.isNullOrEmpty()) {
|
||||||
|
logcat(LogPriority.ERROR) { "Extension to be installed is not signed." }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extensionSignatures.containsAll(getSignatures(currentExtension)!!)) {
|
||||||
|
logcat(LogPriority.ERROR) { "Installed extension signature is not matched." }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val target = File(getPrivateExtensionDir(context), "${extension.packageName}.$PRIVATE_EXTENSION_EXTENSION")
|
||||||
|
return try {
|
||||||
|
file.copyTo(target, overwrite = true)
|
||||||
|
if (currentExtension != null) {
|
||||||
|
AnimeExtensionInstallReceiver.notifyReplaced(context, extension.packageName)
|
||||||
|
} else {
|
||||||
|
AnimeExtensionInstallReceiver.notifyAdded(context, extension.packageName)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to copy extension file." }
|
||||||
|
target.delete()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstallPrivateExtension(context: Context, pkgName: String) {
|
||||||
|
File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION").delete()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a list of all the installed extensions initialized concurrently.
|
* Return a list of all the available extensions initialized concurrently.
|
||||||
*
|
*
|
||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
*/
|
*/
|
||||||
|
@ -70,16 +120,43 @@ internal object AnimeExtensionLoader {
|
||||||
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
||||||
}
|
}
|
||||||
|
|
||||||
val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
|
val sharedExtPkgs = installedPkgs
|
||||||
|
.asSequence()
|
||||||
|
.filter { isPackageAnExtension(it) }
|
||||||
|
.map { AnimeExtensionInfo(packageInfo = it, isShared = true) }
|
||||||
|
|
||||||
|
val privateExtPkgs = getPrivateExtensionDir(context)
|
||||||
|
.listFiles()
|
||||||
|
?.asSequence()
|
||||||
|
?.filter { it.isFile && it.extension == PRIVATE_EXTENSION_EXTENSION }
|
||||||
|
?.mapNotNull {
|
||||||
|
val path = it.absolutePath
|
||||||
|
pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
|
||||||
|
?.apply { applicationInfo.fixBasePaths(path) }
|
||||||
|
}
|
||||||
|
?.filter { isPackageAnExtension(it) }
|
||||||
|
?.map { AnimeExtensionInfo(packageInfo = it, isShared = false) }
|
||||||
|
?: emptySequence()
|
||||||
|
|
||||||
|
val extPkgs = (sharedExtPkgs + privateExtPkgs)
|
||||||
|
// Remove duplicates. Shared takes priority than private by default
|
||||||
|
.distinctBy { it.packageInfo.packageName }
|
||||||
|
// Compare version number
|
||||||
|
.mapNotNull { sharedPkg ->
|
||||||
|
val privatePkg = privateExtPkgs
|
||||||
|
.singleOrNull { it.packageInfo.packageName == sharedPkg.packageInfo.packageName }
|
||||||
|
selectExtensionPackage(sharedPkg, privatePkg)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
|
||||||
if (extPkgs.isEmpty()) return emptyList()
|
if (extPkgs.isEmpty()) return emptyList()
|
||||||
|
|
||||||
// Load each extension concurrently and wait for completion
|
// Load each extension concurrently and wait for completion
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
val deferred = extPkgs.map {
|
val deferred = extPkgs.map {
|
||||||
async { loadExtension(context, it.packageName, it) }
|
async { loadExtension(context, it) }
|
||||||
}
|
}
|
||||||
deferred.map { it.await() }
|
deferred.awaitAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,37 +165,62 @@ internal object AnimeExtensionLoader {
|
||||||
* contains the required feature flag before trying to load it.
|
* contains the required feature flag before trying to load it.
|
||||||
*/
|
*/
|
||||||
fun loadExtensionFromPkgName(context: Context, pkgName: String): AnimeLoadResult {
|
fun loadExtensionFromPkgName(context: Context, pkgName: String): AnimeLoadResult {
|
||||||
val pkgInfo = try {
|
val extensionPackage = getAnimeExtensionInfoFromPkgName(context, pkgName)
|
||||||
|
if (extensionPackage == null) {
|
||||||
|
logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" }
|
||||||
|
return AnimeLoadResult.Error
|
||||||
|
}
|
||||||
|
return loadExtension(context, extensionPackage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAnimeExtensionPackageInfoFromPkgName(context: Context, pkgName: String): PackageInfo? {
|
||||||
|
return getAnimeExtensionInfoFromPkgName(context, pkgName)?.packageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAnimeExtensionInfoFromPkgName(context: Context, pkgName: String): AnimeExtensionInfo? {
|
||||||
|
val privateExtensionFile = File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION")
|
||||||
|
val privatePkg = if (privateExtensionFile.isFile) {
|
||||||
|
context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS)
|
||||||
|
?.takeIf { isPackageAnExtension(it) }
|
||||||
|
?.let {
|
||||||
|
it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath)
|
||||||
|
AnimeExtensionInfo(
|
||||||
|
packageInfo = it,
|
||||||
|
isShared = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val sharedPkg = try {
|
||||||
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
||||||
|
.takeIf { isPackageAnExtension(it) }
|
||||||
|
?.let {
|
||||||
|
AnimeExtensionInfo(
|
||||||
|
packageInfo = it,
|
||||||
|
isShared = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (error: PackageManager.NameNotFoundException) {
|
} catch (error: PackageManager.NameNotFoundException) {
|
||||||
// Unlikely, but the package may have been uninstalled at this point
|
null
|
||||||
logcat(LogPriority.ERROR, error)
|
|
||||||
return AnimeLoadResult.Error
|
|
||||||
}
|
}
|
||||||
if (!isPackageAnExtension(pkgInfo)) {
|
|
||||||
logcat(LogPriority.WARN) { "Tried to load a package that wasn't a extension ($pkgName)" }
|
return selectExtensionPackage(sharedPkg, privatePkg)
|
||||||
return AnimeLoadResult.Error
|
|
||||||
}
|
|
||||||
return loadExtension(context, pkgName, pkgInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads an extension given its package name.
|
* Loads an extension
|
||||||
*
|
*
|
||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
* @param pkgName The package name of the extension to load.
|
* @param extensionInfo The extension to load.
|
||||||
* @param pkgInfo The package info of the extension.
|
|
||||||
*/
|
*/
|
||||||
private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): AnimeLoadResult {
|
private fun loadExtension(context: Context, extensionInfo: AnimeExtensionInfo): AnimeLoadResult {
|
||||||
val pkgManager = context.packageManager
|
val pkgManager = context.packageManager
|
||||||
|
|
||||||
val appInfo = try {
|
val pkgInfo = extensionInfo.packageInfo
|
||||||
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
val appInfo = pkgInfo.applicationInfo
|
||||||
} catch (error: PackageManager.NameNotFoundException) {
|
val pkgName = pkgInfo.packageName
|
||||||
// Unlikely, but the package may have been uninstalled at this point
|
|
||||||
logcat(LogPriority.ERROR, error)
|
|
||||||
return AnimeLoadResult.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Aniyomi: ")
|
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Aniyomi: ")
|
||||||
val versionName = pkgInfo.versionName
|
val versionName = pkgInfo.versionName
|
||||||
|
@ -139,13 +241,19 @@ internal object AnimeExtensionLoader {
|
||||||
return AnimeLoadResult.Error
|
return AnimeLoadResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
val signatureHash = getSignatureHash(context, pkgInfo)
|
val signatures = getSignatures(pkgInfo)
|
||||||
|
if (signatures.isNullOrEmpty()) {
|
||||||
if (signatureHash == null) {
|
|
||||||
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
|
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
|
||||||
return AnimeLoadResult.Error
|
return AnimeLoadResult.Error
|
||||||
} else if (signatureHash !in trustedSignatures) {
|
} else if (!hasTrustedSignature(signatures)) {
|
||||||
val extension = AnimeExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
|
val extension = AnimeExtension.Untrusted(
|
||||||
|
extName,
|
||||||
|
pkgName,
|
||||||
|
versionName,
|
||||||
|
versionCode,
|
||||||
|
libVersion,
|
||||||
|
signatures.last(),
|
||||||
|
)
|
||||||
logcat(LogPriority.WARN, message = { "Extension $pkgName isn't trusted" })
|
logcat(LogPriority.WARN, message = { "Extension $pkgName isn't trusted" })
|
||||||
return AnimeLoadResult.Untrusted(extension)
|
return AnimeLoadResult.Untrusted(extension)
|
||||||
}
|
}
|
||||||
|
@ -205,12 +313,35 @@ internal object AnimeExtensionLoader {
|
||||||
hasChangelog = hasChangelog,
|
hasChangelog = hasChangelog,
|
||||||
sources = sources,
|
sources = sources,
|
||||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||||
isUnofficial = signatureHash != officialSignature,
|
isUnofficial = !isOfficiallySigned(signatures),
|
||||||
icon = context.getApplicationIcon(pkgName),
|
icon = appInfo.loadIcon(pkgManager),
|
||||||
|
isShared = extensionInfo.isShared,
|
||||||
)
|
)
|
||||||
return AnimeLoadResult.Success(extension)
|
return AnimeLoadResult.Success(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose which extension package to use based on version code
|
||||||
|
*
|
||||||
|
* @param shared extension installed to system
|
||||||
|
* @param private extension installed to data directory
|
||||||
|
*/
|
||||||
|
private fun selectExtensionPackage(shared: AnimeExtensionInfo?, private: AnimeExtensionInfo?): AnimeExtensionInfo? {
|
||||||
|
when {
|
||||||
|
private == null && shared != null -> return shared
|
||||||
|
shared == null && private != null -> return private
|
||||||
|
shared == null && private == null -> return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (PackageInfoCompat.getLongVersionCode(shared!!.packageInfo) >=
|
||||||
|
PackageInfoCompat.getLongVersionCode(private!!.packageInfo)
|
||||||
|
) {
|
||||||
|
shared
|
||||||
|
} else {
|
||||||
|
private
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the given package is an extension.
|
* Returns true if the given package is an extension.
|
||||||
*
|
*
|
||||||
|
@ -221,12 +352,50 @@ internal object AnimeExtensionLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the signature hash of the package or null if it's not signed.
|
* Returns the signatures of the package or null if it's not signed.
|
||||||
*
|
*
|
||||||
* @param pkgInfo The package info of the application.
|
* @param pkgInfo The package info of the application.
|
||||||
|
* @return List SHA256 digest of the signatures
|
||||||
*/
|
*/
|
||||||
private fun getSignatureHash(context: Context, pkgInfo: PackageInfo): String? {
|
private fun getSignatures(pkgInfo: PackageInfo): List<String>? {
|
||||||
val signatures = PackageInfoCompat.getSignatures(context.packageManager, pkgInfo.packageName)
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
return signatures.firstOrNull()?.let { Hash.sha256(it.toByteArray()) }
|
val signingInfo = pkgInfo.signingInfo
|
||||||
|
if (signingInfo.hasMultipleSigners()) {
|
||||||
|
signingInfo.apkContentsSigners
|
||||||
|
} else {
|
||||||
|
signingInfo.signingCertificateHistory
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
pkgInfo.signatures
|
||||||
|
}
|
||||||
|
?.map { Hash.sha256(it.toByteArray()) }
|
||||||
|
?.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasTrustedSignature(signatures: List<String>): Boolean {
|
||||||
|
return trustedSignatures.any { signatures.contains(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isOfficiallySigned(signatures: List<String>): Boolean {
|
||||||
|
return signatures.all { it == officialSignature }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On Android 13+ the ApplicationInfo generated by getPackageArchiveInfo doesn't
|
||||||
|
* have sourceDir which breaks assets loading (used for getting icon here).
|
||||||
|
*/
|
||||||
|
private fun ApplicationInfo.fixBasePaths(apkPath: String) {
|
||||||
|
if (sourceDir == null) {
|
||||||
|
sourceDir = apkPath
|
||||||
|
}
|
||||||
|
if (publicSourceDir == null) {
|
||||||
|
publicSourceDir = apkPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AnimeExtensionInfo(
|
||||||
|
val packageInfo: PackageInfo,
|
||||||
|
val isShared: Boolean,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,10 @@ class MangaExtensionManager(
|
||||||
fun getAppIconForSource(sourceId: Long): Drawable? {
|
fun getAppIconForSource(sourceId: Long): Drawable? {
|
||||||
val pkgName = _installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
|
val pkgName = _installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
|
||||||
if (pkgName != null) {
|
if (pkgName != null) {
|
||||||
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
|
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
|
||||||
|
MangaExtensionLoader.getMangaExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
|
||||||
|
.loadIcon(context.packageManager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -333,6 +336,7 @@ class MangaExtensionManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPackageUninstalled(pkgName: String) {
|
override fun onPackageUninstalled(pkgName: String) {
|
||||||
|
MangaExtensionLoader.uninstallPrivateExtension(context, pkgName)
|
||||||
unregisterExtension(pkgName)
|
unregisterExtension(pkgName)
|
||||||
updatePendingUpdatesCount()
|
updatePendingUpdatesCount()
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ sealed class MangaExtension {
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
val isObsolete: Boolean = false,
|
val isObsolete: Boolean = false,
|
||||||
val isUnofficial: Boolean = false,
|
val isUnofficial: Boolean = false,
|
||||||
|
val isShared: Boolean,
|
||||||
) : MangaExtension()
|
) : MangaExtension()
|
||||||
|
|
||||||
data class Available(
|
data class Available(
|
||||||
|
|
|
@ -4,6 +4,9 @@ import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
@ -27,7 +30,7 @@ internal class MangaExtensionInstallReceiver(private val listener: Listener) :
|
||||||
* Registers this broadcast receiver
|
* Registers this broadcast receiver
|
||||||
*/
|
*/
|
||||||
fun register(context: Context) {
|
fun register(context: Context) {
|
||||||
context.registerReceiver(this, filter)
|
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,6 +41,9 @@ internal class MangaExtensionInstallReceiver(private val listener: Listener) :
|
||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
|
addAction(ACTION_EXTENSION_ADDED)
|
||||||
|
addAction(ACTION_EXTENSION_REPLACED)
|
||||||
|
addAction(ACTION_EXTENSION_REMOVED)
|
||||||
addDataScheme("package")
|
addDataScheme("package")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +55,7 @@ internal class MangaExtensionInstallReceiver(private val listener: Listener) :
|
||||||
if (intent == null) return
|
if (intent == null) return
|
||||||
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_PACKAGE_ADDED -> {
|
Intent.ACTION_PACKAGE_ADDED, ACTION_EXTENSION_ADDED -> {
|
||||||
if (isReplacing(intent)) return
|
if (isReplacing(intent)) return
|
||||||
|
|
||||||
launchNow {
|
launchNow {
|
||||||
|
@ -61,7 +67,7 @@ internal class MangaExtensionInstallReceiver(private val listener: Listener) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
Intent.ACTION_PACKAGE_REPLACED, ACTION_EXTENSION_REPLACED -> {
|
||||||
launchNow {
|
launchNow {
|
||||||
when (val result = getExtensionFromIntent(context, intent)) {
|
when (val result = getExtensionFromIntent(context, intent)) {
|
||||||
is MangaLoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
is MangaLoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
||||||
|
@ -71,7 +77,7 @@ internal class MangaExtensionInstallReceiver(private val listener: Listener) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
Intent.ACTION_PACKAGE_REMOVED, ACTION_EXTENSION_REMOVED -> {
|
||||||
if (isReplacing(intent)) return
|
if (isReplacing(intent)) return
|
||||||
|
|
||||||
val pkgName = getPackageNameFromIntent(intent)
|
val pkgName = getPackageNameFromIntent(intent)
|
||||||
|
@ -127,4 +133,30 @@ internal class MangaExtensionInstallReceiver(private val listener: Listener) :
|
||||||
fun onExtensionUntrusted(extension: MangaExtension.Untrusted)
|
fun onExtensionUntrusted(extension: MangaExtension.Untrusted)
|
||||||
fun onPackageUninstalled(pkgName: String)
|
fun onPackageUninstalled(pkgName: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ACTION_EXTENSION_ADDED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_ADDED"
|
||||||
|
private const val ACTION_EXTENSION_REPLACED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_REPLACED"
|
||||||
|
private const val ACTION_EXTENSION_REMOVED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_REMOVED"
|
||||||
|
|
||||||
|
fun notifyAdded(context: Context, pkgName: String) {
|
||||||
|
notify(context, pkgName, ACTION_EXTENSION_ADDED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyReplaced(context: Context, pkgName: String) {
|
||||||
|
notify(context, pkgName, ACTION_EXTENSION_REPLACED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyRemoved(context: Context, pkgName: String) {
|
||||||
|
notify(context, pkgName, ACTION_EXTENSION_REMOVED)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notify(context: Context, pkgName: String, action: String) {
|
||||||
|
Intent(action).apply {
|
||||||
|
data = Uri.parse("package:$pkgName")
|
||||||
|
`package` = context.packageName
|
||||||
|
context.sendBroadcast(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,11 @@ import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
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.MangaExtensionManager
|
||||||
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 eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -157,6 +159,35 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
BasePreferences.ExtensionInstaller.PRIVATE -> {
|
||||||
|
val extensionManager = Injekt.get<MangaExtensionManager>()
|
||||||
|
val tempFile = File(context.cacheDir, "temp_$downloadId")
|
||||||
|
|
||||||
|
if (tempFile.exists() && !tempFile.delete()) {
|
||||||
|
// Unlikely but just in case
|
||||||
|
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
tempFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MangaExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
|
||||||
|
extensionManager.updateInstallStep(downloadId, InstallStep.Installed)
|
||||||
|
} else {
|
||||||
|
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
|
||||||
|
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val intent =
|
val intent =
|
||||||
MangaExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
MangaExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
||||||
|
@ -180,10 +211,15 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
* @param pkgName The package name of the extension to uninstall
|
* @param pkgName The package name of the extension to uninstall
|
||||||
*/
|
*/
|
||||||
fun uninstallApk(pkgName: String) {
|
fun uninstallApk(pkgName: String) {
|
||||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
if (context.isPackageInstalled(pkgName)) {
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
@Suppress("DEPRECATION")
|
||||||
|
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
||||||
context.startActivity(intent)
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(intent)
|
||||||
|
} else {
|
||||||
|
MangaExtensionLoader.uninstallPrivateExtension(context, pkgName)
|
||||||
|
MangaExtensionInstallReceiver.notifyRemoved(context, pkgName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,10 +2,12 @@ package eu.kanade.tachiyomi.extension.manga.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
|
import androidx.core.content.pm.PackageInfoCompat.getSignatures
|
||||||
import dalvik.system.PathClassLoader
|
import dalvik.system.PathClassLoader
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
|
@ -14,15 +16,27 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.MangaSource
|
import eu.kanade.tachiyomi.source.MangaSource
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that handles the loading of the extensions installed in the system.
|
* Class that handles the loading of the extensions. Supports two kinds of extensions:
|
||||||
|
*
|
||||||
|
* 1. Shared extension: This extension is installed to the system with package
|
||||||
|
* installer, so other variants of Tachiyomi/Aniyomi and its forks can also use this extension.
|
||||||
|
*
|
||||||
|
* 2. Private extension: This extension is put inside private data directory of the
|
||||||
|
* running app, so this extension can only be used by the running app and not shared
|
||||||
|
* with other apps.
|
||||||
|
*
|
||||||
|
* When both kinds of extensions are installed with a same package name, shared
|
||||||
|
* extension will be used unless the version codes are different. In that case the
|
||||||
|
* one with higher version code will be used.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("PackageManagerGetSignatures")
|
@SuppressLint("PackageManagerGetSignatures")
|
||||||
internal object MangaExtensionLoader {
|
internal object MangaExtensionLoader {
|
||||||
|
@ -41,12 +55,11 @@ internal object MangaExtensionLoader {
|
||||||
const val LIB_VERSION_MIN = 1.2
|
const val LIB_VERSION_MIN = 1.2
|
||||||
const val LIB_VERSION_MAX = 1.5
|
const val LIB_VERSION_MAX = 1.5
|
||||||
|
|
||||||
private val PACKAGE_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
@Suppress("DEPRECATION")
|
||||||
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNING_CERTIFICATES
|
private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or
|
||||||
} else {
|
PackageManager.GET_META_DATA or
|
||||||
@Suppress("DEPRECATION")
|
PackageManager.GET_SIGNATURES or
|
||||||
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0)
|
||||||
}
|
|
||||||
|
|
||||||
// inorichi's key
|
// inorichi's key
|
||||||
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||||
|
@ -56,8 +69,57 @@ internal object MangaExtensionLoader {
|
||||||
*/
|
*/
|
||||||
var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get()
|
var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get()
|
||||||
|
|
||||||
|
private const val PRIVATE_EXTENSION_EXTENSION = "ext"
|
||||||
|
|
||||||
|
private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
|
||||||
|
|
||||||
|
fun installPrivateExtensionFile(context: Context, file: File): Boolean {
|
||||||
|
val extension = context.packageManager.getPackageArchiveInfo(file.absolutePath, PACKAGE_FLAGS)
|
||||||
|
?.takeIf { isPackageAnExtension(it) } ?: return false
|
||||||
|
val currentExtension = getMangaExtensionPackageInfoFromPkgName(context, extension.packageName)
|
||||||
|
|
||||||
|
if (currentExtension != null) {
|
||||||
|
if (PackageInfoCompat.getLongVersionCode(extension) <
|
||||||
|
PackageInfoCompat.getLongVersionCode(currentExtension)
|
||||||
|
) {
|
||||||
|
logcat(LogPriority.ERROR) { "Installed extension version is higher. Downgrading is not allowed." }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionSignatures = getSignatures(extension)
|
||||||
|
if (extensionSignatures.isNullOrEmpty()) {
|
||||||
|
logcat(LogPriority.ERROR) { "Extension to be installed is not signed." }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extensionSignatures.containsAll(getSignatures(currentExtension)!!)) {
|
||||||
|
logcat(LogPriority.ERROR) { "Installed extension signature is not matched." }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val target = File(getPrivateExtensionDir(context), "${extension.packageName}.$PRIVATE_EXTENSION_EXTENSION")
|
||||||
|
return try {
|
||||||
|
file.copyTo(target, overwrite = true)
|
||||||
|
if (currentExtension != null) {
|
||||||
|
MangaExtensionInstallReceiver.notifyReplaced(context, extension.packageName)
|
||||||
|
} else {
|
||||||
|
MangaExtensionInstallReceiver.notifyAdded(context, extension.packageName)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to copy extension file." }
|
||||||
|
target.delete()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstallPrivateExtension(context: Context, pkgName: String) {
|
||||||
|
File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION").delete()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a list of all the installed extensions initialized concurrently.
|
* Return a list of all the available extensions initialized concurrently.
|
||||||
*
|
*
|
||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
*/
|
*/
|
||||||
|
@ -70,16 +132,43 @@ internal object MangaExtensionLoader {
|
||||||
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
||||||
}
|
}
|
||||||
|
|
||||||
val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
|
val sharedExtPkgs = installedPkgs
|
||||||
|
.asSequence()
|
||||||
|
.filter { isPackageAnExtension(it) }
|
||||||
|
.map { MangaExtensionInfo(packageInfo = it, isShared = true) }
|
||||||
|
|
||||||
|
val privateExtPkgs = getPrivateExtensionDir(context)
|
||||||
|
.listFiles()
|
||||||
|
?.asSequence()
|
||||||
|
?.filter { it.isFile && it.extension == PRIVATE_EXTENSION_EXTENSION }
|
||||||
|
?.mapNotNull {
|
||||||
|
val path = it.absolutePath
|
||||||
|
pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
|
||||||
|
?.apply { applicationInfo.fixBasePaths(path) }
|
||||||
|
}
|
||||||
|
?.filter { isPackageAnExtension(it) }
|
||||||
|
?.map { MangaExtensionInfo(packageInfo = it, isShared = false) }
|
||||||
|
?: emptySequence()
|
||||||
|
|
||||||
|
val extPkgs = (sharedExtPkgs + privateExtPkgs)
|
||||||
|
// Remove duplicates. Shared takes priority than private by default
|
||||||
|
.distinctBy { it.packageInfo.packageName }
|
||||||
|
// Compare version number
|
||||||
|
.mapNotNull { sharedPkg ->
|
||||||
|
val privatePkg = privateExtPkgs
|
||||||
|
.singleOrNull { it.packageInfo.packageName == sharedPkg.packageInfo.packageName }
|
||||||
|
selectExtensionPackage(sharedPkg, privatePkg)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
|
||||||
if (extPkgs.isEmpty()) return emptyList()
|
if (extPkgs.isEmpty()) return emptyList()
|
||||||
|
|
||||||
// Load each extension concurrently and wait for completion
|
// Load each extension concurrently and wait for completion
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
val deferred = extPkgs.map {
|
val deferred = extPkgs.map {
|
||||||
async { loadMangaExtension(context, it.packageName, it) }
|
async { loadMangaExtension(context, it) }
|
||||||
}
|
}
|
||||||
deferred.map { it.await() }
|
deferred.awaitAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,37 +177,61 @@ internal object MangaExtensionLoader {
|
||||||
* contains the required feature flag before trying to load it.
|
* contains the required feature flag before trying to load it.
|
||||||
*/
|
*/
|
||||||
fun loadMangaExtensionFromPkgName(context: Context, pkgName: String): MangaLoadResult {
|
fun loadMangaExtensionFromPkgName(context: Context, pkgName: String): MangaLoadResult {
|
||||||
val pkgInfo = try {
|
val extensionPackage = getMangaExtensionInfoFromPkgName(context, pkgName)
|
||||||
|
if (extensionPackage == null) {
|
||||||
|
logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" }
|
||||||
|
return MangaLoadResult.Error
|
||||||
|
}
|
||||||
|
return loadMangaExtension(context, extensionPackage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMangaExtensionPackageInfoFromPkgName(context: Context, pkgName: String): PackageInfo? {
|
||||||
|
return getMangaExtensionInfoFromPkgName(context, pkgName)?.packageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMangaExtensionInfoFromPkgName(context: Context, pkgName: String): MangaExtensionInfo? {
|
||||||
|
val privateExtensionFile = File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION")
|
||||||
|
val privatePkg = if (privateExtensionFile.isFile) {
|
||||||
|
context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS)
|
||||||
|
?.takeIf { isPackageAnExtension(it) }
|
||||||
|
?.let {
|
||||||
|
it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath)
|
||||||
|
MangaExtensionInfo(
|
||||||
|
packageInfo = it,
|
||||||
|
isShared = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val sharedPkg = try {
|
||||||
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
||||||
|
.takeIf { isPackageAnExtension(it) }
|
||||||
|
?.let {
|
||||||
|
MangaExtensionInfo(
|
||||||
|
packageInfo = it,
|
||||||
|
isShared = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (error: PackageManager.NameNotFoundException) {
|
} catch (error: PackageManager.NameNotFoundException) {
|
||||||
// Unlikely, but the package may have been uninstalled at this point
|
null
|
||||||
logcat(LogPriority.ERROR, error)
|
|
||||||
return MangaLoadResult.Error
|
|
||||||
}
|
}
|
||||||
if (!isPackageAnExtension(pkgInfo)) {
|
|
||||||
logcat(LogPriority.WARN) { "Tried to load a package that wasn't a extension ($pkgName)" }
|
return selectExtensionPackage(sharedPkg, privatePkg)
|
||||||
return MangaLoadResult.Error
|
|
||||||
}
|
|
||||||
return loadMangaExtension(context, pkgName, pkgInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads an extension given its package name.
|
* Loads an extension
|
||||||
*
|
*
|
||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
* @param pkgName The package name of the extension to load.
|
* @param extensionInfo The extension to load.
|
||||||
* @param pkgInfo The package info of the extension.
|
|
||||||
*/
|
*/
|
||||||
private fun loadMangaExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): MangaLoadResult {
|
private fun loadMangaExtension(context: Context, extensionInfo: MangaExtensionInfo): MangaLoadResult {
|
||||||
val pkgManager = context.packageManager
|
val pkgManager = context.packageManager
|
||||||
|
val pkgInfo = extensionInfo.packageInfo
|
||||||
val appInfo = try {
|
val appInfo = pkgInfo.applicationInfo
|
||||||
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
val pkgName = pkgInfo.packageName
|
||||||
} catch (error: PackageManager.NameNotFoundException) {
|
|
||||||
// Unlikely, but the package may have been uninstalled at this point
|
|
||||||
logcat(LogPriority.ERROR, error)
|
|
||||||
return MangaLoadResult.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
||||||
val versionName = pkgInfo.versionName
|
val versionName = pkgInfo.versionName
|
||||||
|
@ -139,13 +252,19 @@ internal object MangaExtensionLoader {
|
||||||
return MangaLoadResult.Error
|
return MangaLoadResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
val signatureHash = getSignatureHash(context, pkgInfo)
|
val signatures = getSignatures(pkgInfo)
|
||||||
|
if (signatures.isNullOrEmpty()) {
|
||||||
if (signatureHash == null) {
|
|
||||||
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
|
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
|
||||||
return MangaLoadResult.Error
|
return MangaLoadResult.Error
|
||||||
} else if (signatureHash !in trustedSignatures) {
|
} else if (!hasTrustedSignature(signatures)) {
|
||||||
val extension = MangaExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
|
val extension = MangaExtension.Untrusted(
|
||||||
|
extName,
|
||||||
|
pkgName,
|
||||||
|
versionName,
|
||||||
|
versionCode,
|
||||||
|
libVersion,
|
||||||
|
signatures.last(),
|
||||||
|
)
|
||||||
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
|
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
|
||||||
return MangaLoadResult.Untrusted(extension)
|
return MangaLoadResult.Untrusted(extension)
|
||||||
}
|
}
|
||||||
|
@ -205,12 +324,35 @@ internal object MangaExtensionLoader {
|
||||||
hasChangelog = hasChangelog,
|
hasChangelog = hasChangelog,
|
||||||
sources = sources,
|
sources = sources,
|
||||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||||
isUnofficial = signatureHash != officialSignature,
|
isUnofficial = !isOfficiallySigned(signatures),
|
||||||
icon = context.getApplicationIcon(pkgName),
|
icon = appInfo.loadIcon(pkgManager),
|
||||||
|
isShared = extensionInfo.isShared,
|
||||||
)
|
)
|
||||||
return MangaLoadResult.Success(extension)
|
return MangaLoadResult.Success(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose which extension package to use based on version code
|
||||||
|
*
|
||||||
|
* @param shared extension installed to system
|
||||||
|
* @param private extension installed to data directory
|
||||||
|
*/
|
||||||
|
private fun selectExtensionPackage(shared: MangaExtensionInfo?, private: MangaExtensionInfo?): MangaExtensionInfo? {
|
||||||
|
when {
|
||||||
|
private == null && shared != null -> return shared
|
||||||
|
shared == null && private != null -> return private
|
||||||
|
shared == null && private == null -> return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (PackageInfoCompat.getLongVersionCode(shared!!.packageInfo) >=
|
||||||
|
PackageInfoCompat.getLongVersionCode(private!!.packageInfo)
|
||||||
|
) {
|
||||||
|
shared
|
||||||
|
} else {
|
||||||
|
private
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the given package is an extension.
|
* Returns true if the given package is an extension.
|
||||||
*
|
*
|
||||||
|
@ -221,12 +363,50 @@ internal object MangaExtensionLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the signature hash of the package or null if it's not signed.
|
* Returns the signatures of the package or null if it's not signed.
|
||||||
*
|
*
|
||||||
* @param pkgInfo The package info of the application.
|
* @param pkgInfo The package info of the application.
|
||||||
|
* @return List SHA256 digest of the signatures
|
||||||
*/
|
*/
|
||||||
private fun getSignatureHash(context: Context, pkgInfo: PackageInfo): String? {
|
private fun getSignatures(pkgInfo: PackageInfo): List<String>? {
|
||||||
val signatures = PackageInfoCompat.getSignatures(context.packageManager, pkgInfo.packageName)
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
return signatures.firstOrNull()?.let { Hash.sha256(it.toByteArray()) }
|
val signingInfo = pkgInfo.signingInfo
|
||||||
|
if (signingInfo.hasMultipleSigners()) {
|
||||||
|
signingInfo.apkContentsSigners
|
||||||
|
} else {
|
||||||
|
signingInfo.signingCertificateHistory
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
pkgInfo.signatures
|
||||||
|
}
|
||||||
|
?.map { Hash.sha256(it.toByteArray()) }
|
||||||
|
?.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasTrustedSignature(signatures: List<String>): Boolean {
|
||||||
|
return trustedSignatures.any { signatures.contains(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isOfficiallySigned(signatures: List<String>): Boolean {
|
||||||
|
return signatures.all { it == officialSignature }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On Android 13+ the ApplicationInfo generated by getPackageArchiveInfo doesn't
|
||||||
|
* have sourceDir which breaks assets loading (used for getting icon here).
|
||||||
|
*/
|
||||||
|
private fun ApplicationInfo.fixBasePaths(apkPath: String) {
|
||||||
|
if (sourceDir == null) {
|
||||||
|
sourceDir = apkPath
|
||||||
|
}
|
||||||
|
if (publicSourceDir == null) {
|
||||||
|
publicSourceDir = apkPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class MangaExtensionInfo(
|
||||||
|
val packageInfo: PackageInfo,
|
||||||
|
val isShared: Boolean,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,7 +147,7 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() {
|
||||||
sourceScreen.forEach { pref ->
|
sourceScreen.forEach { pref ->
|
||||||
pref.isIconSpaceReserved = false
|
pref.isIconSpaceReserved = false
|
||||||
pref.isSingleLineTitle = false
|
pref.isSingleLineTitle = false
|
||||||
if (pref is DialogPreference) {
|
if (pref is DialogPreference && pref.dialogTitle.isNullOrEmpty()) {
|
||||||
pref.dialogTitle = pref.title
|
pref.dialogTitle = pref.title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -147,7 +147,7 @@ class MangaSourcePreferencesFragment : PreferenceFragmentCompat() {
|
||||||
sourceScreen.forEach { pref ->
|
sourceScreen.forEach { pref ->
|
||||||
pref.isIconSpaceReserved = false
|
pref.isIconSpaceReserved = false
|
||||||
pref.isSingleLineTitle = false
|
pref.isSingleLineTitle = false
|
||||||
if (pref is DialogPreference) {
|
if (pref is DialogPreference && pref.dialogTitle.isNullOrEmpty()) {
|
||||||
pref.dialogTitle = pref.title
|
pref.dialogTitle = pref.title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package eu.kanade.tachiyomi.ui.main
|
package eu.kanade.tachiyomi.ui.deeplink.anime
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
|
||||||
class DeepLinkAnimeActivity : Activity() {
|
class DeepLinkAnimeActivity : Activity() {
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.deeplink.anime
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.GlobalAnimeSearchScreen
|
||||||
|
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreen
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
|
class DeepLinkAnimeScreen(
|
||||||
|
val query: String = "",
|
||||||
|
) : Screen() {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
|
val screenModel = rememberScreenModel {
|
||||||
|
DeepLinkAnimeScreenModel(query = query)
|
||||||
|
}
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
Scaffold(
|
||||||
|
topBar = { scrollBehavior ->
|
||||||
|
AppBar(
|
||||||
|
title = stringResource(R.string.action_search_hint),
|
||||||
|
navigateUp = navigator::pop,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { contentPadding ->
|
||||||
|
when (state) {
|
||||||
|
is DeepLinkAnimeScreenModel.State.Loading -> {
|
||||||
|
LoadingScreen(Modifier.padding(contentPadding))
|
||||||
|
}
|
||||||
|
is DeepLinkAnimeScreenModel.State.NoResults -> {
|
||||||
|
navigator.replace(GlobalAnimeSearchScreen(query))
|
||||||
|
}
|
||||||
|
is DeepLinkAnimeScreenModel.State.Result -> {
|
||||||
|
navigator.replace(
|
||||||
|
AnimeScreen(
|
||||||
|
(state as DeepLinkAnimeScreenModel.State.Result).anime.id,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.deeplink.anime
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import eu.kanade.domain.entries.anime.model.toDomainAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.ResolvableAnimeSource
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import tachiyomi.core.util.lang.launchIO
|
||||||
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
|
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class DeepLinkAnimeScreenModel(
|
||||||
|
query: String = "",
|
||||||
|
private val sourceManager: AnimeSourceManager = Injekt.get(),
|
||||||
|
) : StateScreenModel<DeepLinkAnimeScreenModel.State>(State.Loading) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
coroutineScope.launchIO {
|
||||||
|
val anime = sourceManager.getCatalogueSources()
|
||||||
|
.filterIsInstance<ResolvableAnimeSource>()
|
||||||
|
.filter { it.canResolveUri(query) }
|
||||||
|
.firstNotNullOfOrNull { it.getAnime(query)?.toDomainAnime(it.id) }
|
||||||
|
|
||||||
|
mutableState.update {
|
||||||
|
if (anime == null) {
|
||||||
|
State.NoResults
|
||||||
|
} else {
|
||||||
|
State.Result(anime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface State {
|
||||||
|
@Immutable
|
||||||
|
data object Loading : State
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data object NoResults : State
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class Result(val anime: Anime) : State
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
package eu.kanade.tachiyomi.ui.main
|
package eu.kanade.tachiyomi.ui.deeplink.manga
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
|
||||||
class DeepLinkMangaActivity : Activity() {
|
class DeepLinkMangaActivity : Activity() {
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.deeplink.manga
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.GlobalMangaSearchScreen
|
||||||
|
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreen
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
|
class DeepLinkMangaScreen(
|
||||||
|
val query: String = "",
|
||||||
|
) : Screen() {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
|
val screenModel = rememberScreenModel {
|
||||||
|
DeepLinkMangaScreenModel(query = query)
|
||||||
|
}
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
Scaffold(
|
||||||
|
topBar = { scrollBehavior ->
|
||||||
|
AppBar(
|
||||||
|
title = stringResource(R.string.action_search_hint),
|
||||||
|
navigateUp = navigator::pop,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { contentPadding ->
|
||||||
|
when (state) {
|
||||||
|
is DeepLinkMangaScreenModel.State.Loading -> {
|
||||||
|
LoadingScreen(Modifier.padding(contentPadding))
|
||||||
|
}
|
||||||
|
is DeepLinkMangaScreenModel.State.NoResults -> {
|
||||||
|
navigator.replace(GlobalMangaSearchScreen(query))
|
||||||
|
}
|
||||||
|
is DeepLinkMangaScreenModel.State.Result -> {
|
||||||
|
navigator.replace(
|
||||||
|
MangaScreen(
|
||||||
|
(state as DeepLinkMangaScreenModel.State.Result).manga.id,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.deeplink.manga
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import eu.kanade.domain.entries.manga.model.toDomainManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.ResolvableMangaSource
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import tachiyomi.core.util.lang.launchIO
|
||||||
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
|
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class DeepLinkMangaScreenModel(
|
||||||
|
query: String = "",
|
||||||
|
private val sourceManager: MangaSourceManager = Injekt.get(),
|
||||||
|
) : StateScreenModel<DeepLinkMangaScreenModel.State>(State.Loading) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
coroutineScope.launchIO {
|
||||||
|
val manga = sourceManager.getCatalogueSources()
|
||||||
|
.filterIsInstance<ResolvableMangaSource>()
|
||||||
|
.filter { it.canResolveUri(query) }
|
||||||
|
.firstNotNullOfOrNull { it.getManga(query)?.toDomainManga(it.id) }
|
||||||
|
|
||||||
|
mutableState.update {
|
||||||
|
if (manga == null) {
|
||||||
|
State.NoResults
|
||||||
|
} else {
|
||||||
|
State.Result(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface State {
|
||||||
|
@Immutable
|
||||||
|
data object Loading : State
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data object NoResults : State
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class Result(val manga: Manga) : State
|
||||||
|
}
|
||||||
|
}
|
|
@ -134,7 +134,7 @@ class AnimeScreenModel(
|
||||||
|
|
||||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||||
|
|
||||||
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
|
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateItemRestrictions().get()
|
||||||
|
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
||||||
private val selectedEpisodeIds: HashSet<Long> = HashSet()
|
private val selectedEpisodeIds: HashSet<Long> = HashSet()
|
||||||
|
|
|
@ -130,7 +130,7 @@ class MangaScreenModel(
|
||||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||||
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
||||||
|
|
||||||
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
|
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateItemRestrictions().get()
|
||||||
|
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
||||||
private val selectedChapterIds: HashSet<Long> = HashSet()
|
private val selectedChapterIds: HashSet<Long> = HashSet()
|
||||||
|
|
|
@ -526,7 +526,7 @@ class AnimeLibraryScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDisplayMode(): PreferenceMutableState<LibraryDisplayMode> {
|
fun getDisplayMode(): PreferenceMutableState<LibraryDisplayMode> {
|
||||||
return libraryPreferences.libraryDisplayMode().asState(coroutineScope)
|
return libraryPreferences.displayMode().asState(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
|
fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
|
||||||
|
|
|
@ -168,7 +168,7 @@ object AnimeLibraryTab : Tab() {
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.searchQuery.isNullOrEmpty() && !state.hasActiveFilters && state.isLibraryEmpty -> {
|
state.searchQuery.isNullOrEmpty() && !state.hasActiveFilters && state.isLibraryEmpty -> {
|
||||||
val handler = LocalUriHandler.current
|
val handler = LocalUriHandler.current
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
|
|
|
@ -520,7 +520,7 @@ class MangaLibraryScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDisplayMode(): PreferenceMutableState<LibraryDisplayMode> {
|
fun getDisplayMode(): PreferenceMutableState<LibraryDisplayMode> {
|
||||||
return libraryPreferences.libraryDisplayMode().asState(coroutineScope)
|
return libraryPreferences.displayMode().asState(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
|
fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
|
||||||
|
|
|
@ -165,7 +165,7 @@ object MangaLibraryTab : Tab() {
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.searchQuery.isNullOrEmpty() && !state.hasActiveFilters && state.isLibraryEmpty -> {
|
state.searchQuery.isNullOrEmpty() && !state.hasActiveFilters && state.isLibraryEmpty -> {
|
||||||
val handler = LocalUriHandler.current
|
val handler = LocalUriHandler.current
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
|
|
|
@ -83,6 +83,7 @@ import eu.kanade.tachiyomi.ui.browse.anime.source.browse.BrowseAnimeSourceScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.GlobalAnimeSearchScreen
|
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.GlobalAnimeSearchScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.manga.source.browse.BrowseMangaSourceScreen
|
import eu.kanade.tachiyomi.ui.browse.manga.source.browse.BrowseMangaSourceScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.GlobalMangaSearchScreen
|
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.GlobalMangaSearchScreen
|
||||||
|
import eu.kanade.tachiyomi.ui.deeplink.manga.DeepLinkMangaScreen
|
||||||
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreen
|
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreen
|
||||||
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreen
|
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreen
|
||||||
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
||||||
|
@ -449,6 +450,7 @@ class MainActivity : BaseActivity() {
|
||||||
if (!query.isNullOrEmpty()) {
|
if (!query.isNullOrEmpty()) {
|
||||||
navigator.popUntilRoot()
|
navigator.popUntilRoot()
|
||||||
navigator.push(GlobalMangaSearchScreen(query))
|
navigator.push(GlobalMangaSearchScreen(query))
|
||||||
|
navigator.push(DeepLinkMangaScreen(query))
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
@ -385,11 +385,14 @@ class ReaderActivity : BaseActivity() {
|
||||||
|
|
||||||
binding.pageNumber.setComposeContent {
|
binding.pageNumber.setComposeContent {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
val showPageNumber by viewModel.readerPreferences.showPageNumber().collectAsState()
|
||||||
|
|
||||||
PageIndicatorText(
|
if (!state.menuVisible && showPageNumber) {
|
||||||
currentPage = state.currentPage,
|
PageIndicatorText(
|
||||||
totalPages = state.totalPages,
|
currentPage = state.currentPage,
|
||||||
)
|
totalPages = state.totalPages,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.readerMenuBottom.setComposeContent {
|
binding.readerMenuBottom.setComposeContent {
|
||||||
|
@ -557,10 +560,6 @@ class ReaderActivity : BaseActivity() {
|
||||||
bottomAnimation.applySystemAnimatorScale(this)
|
bottomAnimation.applySystemAnimatorScale(this)
|
||||||
binding.readerMenuBottom.startAnimation(bottomAnimation)
|
binding.readerMenuBottom.startAnimation(bottomAnimation)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readerPreferences.showPageNumber().get()) {
|
|
||||||
config?.setPageNumberVisibility(false)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (readerPreferences.fullscreen().get()) {
|
if (readerPreferences.fullscreen().get()) {
|
||||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
|
@ -583,10 +582,6 @@ class ReaderActivity : BaseActivity() {
|
||||||
bottomAnimation.applySystemAnimatorScale(this)
|
bottomAnimation.applySystemAnimatorScale(this)
|
||||||
binding.readerMenuBottom.startAnimation(bottomAnimation)
|
binding.readerMenuBottom.startAnimation(bottomAnimation)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readerPreferences.showPageNumber().get()) {
|
|
||||||
config?.setPageNumberVisibility(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -639,9 +634,8 @@ class ReaderActivity : BaseActivity() {
|
||||||
|
|
||||||
private fun showReadingModeToast(mode: Int) {
|
private fun showReadingModeToast(mode: Int) {
|
||||||
try {
|
try {
|
||||||
val strings = resources.getStringArray(R.array.viewers_selector)
|
|
||||||
readingModeToast?.cancel()
|
readingModeToast?.cancel()
|
||||||
readingModeToast = toast(strings[mode])
|
readingModeToast = toast(ReadingModeType.fromPreference(mode).stringRes)
|
||||||
} catch (e: ArrayIndexOutOfBoundsException) {
|
} catch (e: ArrayIndexOutOfBoundsException) {
|
||||||
logcat(LogPriority.ERROR) { "Unknown reading mode: $mode" }
|
logcat(LogPriority.ERROR) { "Unknown reading mode: $mode" }
|
||||||
}
|
}
|
||||||
|
@ -895,10 +889,6 @@ class ReaderActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
readerPreferences.showPageNumber().changes()
|
|
||||||
.onEach(::setPageNumberVisibility)
|
|
||||||
.launchIn(lifecycleScope)
|
|
||||||
|
|
||||||
readerPreferences.trueColor().changes()
|
readerPreferences.trueColor().changes()
|
||||||
.onEach(::setTrueColor)
|
.onEach(::setTrueColor)
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
@ -948,13 +938,6 @@ class ReaderActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the visibility of the bottom page indicator according to [visible].
|
|
||||||
*/
|
|
||||||
fun setPageNumberVisibility(visible: Boolean) {
|
|
||||||
binding.pageNumber.isVisible = visible
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the 32-bit color mode according to [enabled].
|
* Sets the 32-bit color mode according to [enabled].
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -5,14 +5,14 @@ import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
enum class OrientationType(val prefValue: Int, val flag: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) {
|
enum class OrientationType(val flag: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) {
|
||||||
DEFAULT(0, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.label_default, R.drawable.ic_screen_rotation_24dp, 0x00000000),
|
DEFAULT(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.label_default, R.drawable.ic_screen_rotation_24dp, 0x00000000),
|
||||||
FREE(1, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, R.drawable.ic_screen_rotation_24dp, 0x00000008),
|
FREE(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, R.drawable.ic_screen_rotation_24dp, 0x00000008),
|
||||||
PORTRAIT(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000010),
|
PORTRAIT(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000010),
|
||||||
LANDSCAPE(3, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_landscape, R.drawable.ic_stay_current_landscape_24dp, 0x00000018),
|
LANDSCAPE(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_landscape, R.drawable.ic_stay_current_landscape_24dp, 0x00000018),
|
||||||
LOCKED_PORTRAIT(4, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp, 0x00000020),
|
LOCKED_PORTRAIT(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp, 0x00000020),
|
||||||
LOCKED_LANDSCAPE(5, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp, 0x00000028),
|
LOCKED_LANDSCAPE(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp, 0x00000028),
|
||||||
REVERSE_PORTRAIT(6, ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, R.string.rotation_reverse_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000030),
|
REVERSE_PORTRAIT(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, R.string.rotation_reverse_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000030),
|
||||||
;
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -10,13 +10,13 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
||||||
|
|
||||||
enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) {
|
enum class ReadingModeType(@StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) {
|
||||||
DEFAULT(0, R.string.label_default, R.drawable.ic_reader_default_24dp, 0x00000000),
|
DEFAULT(R.string.label_default, R.drawable.ic_reader_default_24dp, 0x00000000),
|
||||||
LEFT_TO_RIGHT(1, R.string.left_to_right_viewer, R.drawable.ic_reader_ltr_24dp, 0x00000001),
|
LEFT_TO_RIGHT(R.string.left_to_right_viewer, R.drawable.ic_reader_ltr_24dp, 0x00000001),
|
||||||
RIGHT_TO_LEFT(2, R.string.right_to_left_viewer, R.drawable.ic_reader_rtl_24dp, 0x00000002),
|
RIGHT_TO_LEFT(R.string.right_to_left_viewer, R.drawable.ic_reader_rtl_24dp, 0x00000002),
|
||||||
VERTICAL(3, R.string.vertical_viewer, R.drawable.ic_reader_vertical_24dp, 0x00000003),
|
VERTICAL(R.string.vertical_viewer, R.drawable.ic_reader_vertical_24dp, 0x00000003),
|
||||||
WEBTOON(4, R.string.webtoon_viewer, R.drawable.ic_reader_webtoon_24dp, 0x00000004),
|
WEBTOON(R.string.webtoon_viewer, R.drawable.ic_reader_webtoon_24dp, 0x00000004),
|
||||||
CONTINUOUS_VERTICAL(5, R.string.vertical_plus_viewer, R.drawable.ic_reader_continuous_vertical_24dp, 0x00000005),
|
CONTINUOUS_VERTICAL(R.string.vertical_plus_viewer, R.drawable.ic_reader_continuous_vertical_24dp, 0x00000005),
|
||||||
;
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -88,14 +88,14 @@ class AnimeStatsScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getGlobalUpdateItemCount(libraryAnime: List<LibraryAnime>): Int {
|
private fun getGlobalUpdateItemCount(libraryAnime: List<LibraryAnime>): Int {
|
||||||
val includedCategories = preferences.animeLibraryUpdateCategories().get().map { it.toLong() }
|
val includedCategories = preferences.animeUpdateCategories().get().map { it.toLong() }
|
||||||
val includedAnime = if (includedCategories.isNotEmpty()) {
|
val includedAnime = if (includedCategories.isNotEmpty()) {
|
||||||
libraryAnime.filter { it.category in includedCategories }
|
libraryAnime.filter { it.category in includedCategories }
|
||||||
} else {
|
} else {
|
||||||
libraryAnime
|
libraryAnime
|
||||||
}
|
}
|
||||||
|
|
||||||
val excludedCategories = preferences.animeLibraryUpdateCategoriesExclude().get().map { it.toLong() }
|
val excludedCategories = preferences.animeUpdateCategoriesExclude().get().map { it.toLong() }
|
||||||
val excludedMangaIds = if (excludedCategories.isNotEmpty()) {
|
val excludedMangaIds = if (excludedCategories.isNotEmpty()) {
|
||||||
libraryAnime.fastMapNotNull { anime ->
|
libraryAnime.fastMapNotNull { anime ->
|
||||||
anime.id.takeIf { anime.category in excludedCategories }
|
anime.id.takeIf { anime.category in excludedCategories }
|
||||||
|
@ -104,7 +104,7 @@ class AnimeStatsScreenModel(
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val updateRestrictions = preferences.libraryUpdateItemRestriction().get()
|
val updateRestrictions = preferences.autoUpdateItemRestrictions().get()
|
||||||
return includedAnime
|
return includedAnime
|
||||||
.fastFilterNot { it.anime.id in excludedMangaIds }
|
.fastFilterNot { it.anime.id in excludedMangaIds }
|
||||||
.fastDistinctBy { it.anime.id }
|
.fastDistinctBy { it.anime.id }
|
||||||
|
|
|
@ -88,14 +88,14 @@ class MangaStatsScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getGlobalUpdateItemCount(libraryManga: List<LibraryManga>): Int {
|
private fun getGlobalUpdateItemCount(libraryManga: List<LibraryManga>): Int {
|
||||||
val includedCategories = preferences.mangaLibraryUpdateCategories().get().map { it.toLong() }
|
val includedCategories = preferences.mangaUpdateCategories().get().map { it.toLong() }
|
||||||
val includedManga = if (includedCategories.isNotEmpty()) {
|
val includedManga = if (includedCategories.isNotEmpty()) {
|
||||||
libraryManga.filter { it.category in includedCategories }
|
libraryManga.filter { it.category in includedCategories }
|
||||||
} else {
|
} else {
|
||||||
libraryManga
|
libraryManga
|
||||||
}
|
}
|
||||||
|
|
||||||
val excludedCategories = preferences.mangaLibraryUpdateCategoriesExclude().get().map { it.toLong() }
|
val excludedCategories = preferences.mangaUpdateCategoriesExclude().get().map { it.toLong() }
|
||||||
val excludedMangaIds = if (excludedCategories.isNotEmpty()) {
|
val excludedMangaIds = if (excludedCategories.isNotEmpty()) {
|
||||||
libraryManga.fastMapNotNull { manga ->
|
libraryManga.fastMapNotNull { manga ->
|
||||||
manga.id.takeIf { manga.category in excludedCategories }
|
manga.id.takeIf { manga.category in excludedCategories }
|
||||||
|
@ -104,7 +104,7 @@ class MangaStatsScreenModel(
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val updateRestrictions = preferences.libraryUpdateItemRestriction().get()
|
val updateRestrictions = preferences.autoUpdateItemRestrictions().get()
|
||||||
return includedManga
|
return includedManga
|
||||||
.fastFilterNot { it.manga.id in excludedMangaIds }
|
.fastFilterNot { it.manga.id in excludedMangaIds }
|
||||||
.fastDistinctBy { it.manga.id }
|
.fastDistinctBy { it.manga.id }
|
||||||
|
|
|
@ -66,7 +66,7 @@ class AnimeUpdatesScreenModel(
|
||||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||||
val events: Flow<Event> = _events.receiveAsFlow()
|
val events: Flow<Event> = _events.receiveAsFlow()
|
||||||
|
|
||||||
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope)
|
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(coroutineScope)
|
||||||
|
|
||||||
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ class MangaUpdatesScreenModel(
|
||||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||||
val events: Flow<Event> = _events.receiveAsFlow()
|
val events: Flow<Event> = _events.receiveAsFlow()
|
||||||
|
|
||||||
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope)
|
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(coroutineScope)
|
||||||
|
|
||||||
// First and last selected index in list
|
// First and last selected index in list
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||||
|
|
|
@ -1,24 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string-array name="viewers_selector">
|
|
||||||
<item>@string/label_default</item>
|
|
||||||
<item>@string/left_to_right_viewer</item>
|
|
||||||
<item>@string/right_to_left_viewer</item>
|
|
||||||
<item>@string/vertical_viewer</item>
|
|
||||||
<item>@string/webtoon_viewer</item>
|
|
||||||
<item>@string/vertical_plus_viewer</item>
|
|
||||||
</string-array>
|
|
||||||
|
|
||||||
<string-array name="rotation_type">
|
|
||||||
<item>@string/label_default</item>
|
|
||||||
<item>@string/rotation_free</item>
|
|
||||||
<item>@string/rotation_portrait</item>
|
|
||||||
<item>@string/rotation_landscape</item>
|
|
||||||
<item>@string/rotation_force_portrait</item>
|
|
||||||
<item>@string/rotation_force_landscape</item>
|
|
||||||
<item>@string/rotation_reverse_portrait</item>
|
|
||||||
</string-array>
|
|
||||||
|
|
||||||
<string-array name="playback_options">
|
<string-array name="playback_options">
|
||||||
<item>@string/playback_options_speed</item>
|
<item>@string/playback_options_speed</item>
|
||||||
<item>@string/playback_options_quality</item>
|
<item>@string/playback_options_quality</item>
|
||||||
|
|
|
@ -143,4 +143,11 @@ fun <T> decodeFromJsonResponse(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception that handles HTTP codes considered not successful by OkHttp.
|
||||||
|
* Use it to have a standardized error message in the app across the extensions.
|
||||||
|
*
|
||||||
|
* @since extensions-lib 1.5
|
||||||
|
* @param code [Int] the HTTP status code
|
||||||
|
*/
|
||||||
class HttpException(val code: Int) : IllegalStateException("HTTP error $code")
|
class HttpException(val code: Int) : IllegalStateException("HTTP error $code")
|
||||||
|
|
|
@ -58,7 +58,7 @@ class ChapterRepositoryImpl(
|
||||||
read = chapterUpdate.read,
|
read = chapterUpdate.read,
|
||||||
bookmark = chapterUpdate.bookmark,
|
bookmark = chapterUpdate.bookmark,
|
||||||
lastPageRead = chapterUpdate.lastPageRead,
|
lastPageRead = chapterUpdate.lastPageRead,
|
||||||
chapterNumber = chapterUpdate.chapterNumber?.toDouble(),
|
chapterNumber = chapterUpdate.chapterNumber,
|
||||||
sourceOrder = chapterUpdate.sourceOrder,
|
sourceOrder = chapterUpdate.sourceOrder,
|
||||||
dateFetch = chapterUpdate.dateFetch,
|
dateFetch = chapterUpdate.dateFetch,
|
||||||
dateUpload = chapterUpdate.dateUpload,
|
dateUpload = chapterUpdate.dateUpload,
|
||||||
|
|
|
@ -60,7 +60,7 @@ class EpisodeRepositoryImpl(
|
||||||
bookmark = episodeUpdate.bookmark,
|
bookmark = episodeUpdate.bookmark,
|
||||||
lastSecondSeen = episodeUpdate.lastSecondSeen,
|
lastSecondSeen = episodeUpdate.lastSecondSeen,
|
||||||
totalSeconds = episodeUpdate.totalSeconds,
|
totalSeconds = episodeUpdate.totalSeconds,
|
||||||
episodeNumber = episodeUpdate.episodeNumber?.toDouble(),
|
episodeNumber = episodeUpdate.episodeNumber,
|
||||||
sourceOrder = episodeUpdate.sourceOrder,
|
sourceOrder = episodeUpdate.sourceOrder,
|
||||||
dateFetch = episodeUpdate.dateFetch,
|
dateFetch = episodeUpdate.dateFetch,
|
||||||
dateUpload = episodeUpdate.dateUpload,
|
dateUpload = episodeUpdate.dateUpload,
|
||||||
|
|
|
@ -21,10 +21,25 @@ data class GithubRelease(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GitHubAssets(@SerialName("browser_download_url") val downloadLink: String)
|
data class GitHubAssets(@SerialName("browser_download_url") val downloadLink: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression that matches a mention to a valid GitHub username, like it's
|
||||||
|
* done in GitHub Flavored Markdown. It follows these constraints:
|
||||||
|
*
|
||||||
|
* - Alphanumeric with single hyphens (no consecutive hyphens)
|
||||||
|
* - Cannot begin or end with a hyphen
|
||||||
|
* - Max length of 39 characters
|
||||||
|
*
|
||||||
|
* Reference: https://stackoverflow.com/a/30281147
|
||||||
|
*/
|
||||||
|
val gitHubUsernameMentionRegex =
|
||||||
|
"""\B@([a-z0-9](?:-(?=[a-z0-9])|[a-z0-9]){0,38}(?<=[a-z0-9]))""".toRegex(RegexOption.IGNORE_CASE)
|
||||||
|
|
||||||
val releaseMapper: (GithubRelease) -> Release = {
|
val releaseMapper: (GithubRelease) -> Release = {
|
||||||
Release(
|
Release(
|
||||||
it.version,
|
it.version,
|
||||||
it.info,
|
it.info.replace(gitHubUsernameMentionRegex) { mention ->
|
||||||
|
"[${mention.value}](https://github.com/${mention.value.substring(1)})"
|
||||||
|
},
|
||||||
it.releaseLink,
|
it.releaseLink,
|
||||||
it.assets.map(GitHubAssets::downloadLink),
|
it.assets.map(GitHubAssets::downloadLink),
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,7 +14,7 @@ class CreateAnimeCategoryWithName(
|
||||||
|
|
||||||
private val initialFlags: Long
|
private val initialFlags: Long
|
||||||
get() {
|
get() {
|
||||||
val sort = preferences.libraryAnimeSortingMode().get()
|
val sort = preferences.animeSortingMode().get()
|
||||||
return sort.type.flag or sort.direction.flag
|
return sort.type.flag or sort.direction.flag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ class ResetAnimeCategoryFlags(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await() {
|
suspend fun await() {
|
||||||
val sort = preferences.libraryAnimeSortingMode().get()
|
val sort = preferences.animeSortingMode().get()
|
||||||
categoryRepository.updateAllAnimeCategoryFlags(sort.type + sort.direction)
|
categoryRepository.updateAllAnimeCategoryFlags(sort.type + sort.direction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,6 @@ class SetAnimeDisplayMode(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun await(display: LibraryDisplayMode) {
|
fun await(display: LibraryDisplayMode) {
|
||||||
preferences.libraryDisplayMode().set(display)
|
preferences.displayMode().set(display)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class SetSortModeForAnimeCategory(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
preferences.libraryAnimeSortingMode().set(AnimeLibrarySort(type, direction))
|
preferences.animeSortingMode().set(AnimeLibrarySort(type, direction))
|
||||||
categoryRepository.updateAllAnimeCategoryFlags(flags)
|
categoryRepository.updateAllAnimeCategoryFlags(flags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ class CreateMangaCategoryWithName(
|
||||||
|
|
||||||
private val initialFlags: Long
|
private val initialFlags: Long
|
||||||
get() {
|
get() {
|
||||||
val sort = preferences.libraryMangaSortingMode().get()
|
val sort = preferences.mangaSortingMode().get()
|
||||||
return sort.type.flag or sort.direction.flag
|
return sort.type.flag or sort.direction.flag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ class ResetMangaCategoryFlags(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await() {
|
suspend fun await() {
|
||||||
val sort = preferences.libraryMangaSortingMode().get()
|
val sort = preferences.mangaSortingMode().get()
|
||||||
categoryRepository.updateAllMangaCategoryFlags(sort.type + sort.direction)
|
categoryRepository.updateAllMangaCategoryFlags(sort.type + sort.direction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,6 @@ class SetMangaDisplayMode(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun await(display: LibraryDisplayMode) {
|
fun await(display: LibraryDisplayMode) {
|
||||||
preferences.libraryDisplayMode().set(display)
|
preferences.displayMode().set(display)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class SetSortModeForMangaCategory(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
preferences.libraryMangaSortingMode().set(MangaLibrarySort(type, direction))
|
preferences.mangaSortingMode().set(MangaLibrarySort(type, direction))
|
||||||
categoryRepository.updateAllMangaCategoryFlags(flags)
|
categoryRepository.updateAllMangaCategoryFlags(flags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,39 +20,38 @@ class LibraryPreferences(
|
||||||
fun isDefaultHomeTabLibraryManga() =
|
fun isDefaultHomeTabLibraryManga() =
|
||||||
preferenceStore.getBoolean("default_home_tab_library", false)
|
preferenceStore.getBoolean("default_home_tab_library", false)
|
||||||
|
|
||||||
fun libraryDisplayMode() = preferenceStore.getObject(
|
fun displayMode() = preferenceStore.getObject(
|
||||||
"pref_display_mode_library",
|
"pref_display_mode_library",
|
||||||
LibraryDisplayMode.default,
|
LibraryDisplayMode.default,
|
||||||
LibraryDisplayMode.Serializer::serialize,
|
LibraryDisplayMode.Serializer::serialize,
|
||||||
LibraryDisplayMode.Serializer::deserialize,
|
LibraryDisplayMode.Serializer::deserialize,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun libraryMangaSortingMode() = preferenceStore.getObject(
|
fun mangaSortingMode() = preferenceStore.getObject(
|
||||||
"library_sorting_mode",
|
"library_sorting_mode",
|
||||||
MangaLibrarySort.default,
|
MangaLibrarySort.default,
|
||||||
MangaLibrarySort.Serializer::serialize,
|
MangaLibrarySort.Serializer::serialize,
|
||||||
MangaLibrarySort.Serializer::deserialize,
|
MangaLibrarySort.Serializer::deserialize,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun libraryAnimeSortingMode() = preferenceStore.getObject(
|
fun animeSortingMode() = preferenceStore.getObject(
|
||||||
"animelib_sorting_mode",
|
"animelib_sorting_mode",
|
||||||
AnimeLibrarySort.default,
|
AnimeLibrarySort.default,
|
||||||
AnimeLibrarySort.Serializer::serialize,
|
AnimeLibrarySort.Serializer::serialize,
|
||||||
AnimeLibrarySort.Serializer::deserialize,
|
AnimeLibrarySort.Serializer::deserialize,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun libraryUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0)
|
fun lastUpdatedTimestamp() = preferenceStore.getLong("library_update_last_timestamp", 0L)
|
||||||
|
fun autoUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0)
|
||||||
|
|
||||||
fun libraryUpdateLastTimestamp() = preferenceStore.getLong("library_update_last_timestamp", 0L)
|
fun autoUpdateDeviceRestrictions() = preferenceStore.getStringSet(
|
||||||
|
|
||||||
fun libraryUpdateDeviceRestriction() = preferenceStore.getStringSet(
|
|
||||||
"library_update_restriction",
|
"library_update_restriction",
|
||||||
setOf(
|
setOf(
|
||||||
DEVICE_ONLY_ON_WIFI,
|
DEVICE_ONLY_ON_WIFI,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun libraryUpdateItemRestriction() = preferenceStore.getStringSet(
|
fun autoUpdateItemRestrictions() = preferenceStore.getStringSet(
|
||||||
"library_update_manga_restriction",
|
"library_update_manga_restriction",
|
||||||
setOf(
|
setOf(
|
||||||
ENTRY_HAS_UNVIEWED,
|
ENTRY_HAS_UNVIEWED,
|
||||||
|
@ -172,16 +171,16 @@ class LibraryPreferences(
|
||||||
fun lastUsedAnimeCategory() = preferenceStore.getInt("last_used_anime_category", 0)
|
fun lastUsedAnimeCategory() = preferenceStore.getInt("last_used_anime_category", 0)
|
||||||
fun lastUsedMangaCategory() = preferenceStore.getInt("last_used_category", 0)
|
fun lastUsedMangaCategory() = preferenceStore.getInt("last_used_category", 0)
|
||||||
|
|
||||||
fun animeLibraryUpdateCategories() =
|
fun animeUpdateCategories() =
|
||||||
preferenceStore.getStringSet("animelib_update_categories", emptySet())
|
preferenceStore.getStringSet("animelib_update_categories", emptySet())
|
||||||
|
|
||||||
fun mangaLibraryUpdateCategories() =
|
fun mangaUpdateCategories() =
|
||||||
preferenceStore.getStringSet("library_update_categories", emptySet())
|
preferenceStore.getStringSet("library_update_categories", emptySet())
|
||||||
|
|
||||||
fun animeLibraryUpdateCategoriesExclude() =
|
fun animeUpdateCategoriesExclude() =
|
||||||
preferenceStore.getStringSet("animelib_update_categories_exclude", emptySet())
|
preferenceStore.getStringSet("animelib_update_categories_exclude", emptySet())
|
||||||
|
|
||||||
fun mangaLibraryUpdateCategoriesExclude() =
|
fun mangaUpdateCategoriesExclude() =
|
||||||
preferenceStore.getStringSet("library_update_categories_exclude", emptySet())
|
preferenceStore.getStringSet("library_update_categories_exclude", emptySet())
|
||||||
|
|
||||||
// Mixture Item
|
// Mixture Item
|
||||||
|
@ -291,7 +290,6 @@ class LibraryPreferences(
|
||||||
const val DEVICE_ONLY_ON_WIFI = "wifi"
|
const val DEVICE_ONLY_ON_WIFI = "wifi"
|
||||||
const val DEVICE_NETWORK_NOT_METERED = "network_not_metered"
|
const val DEVICE_NETWORK_NOT_METERED = "network_not_metered"
|
||||||
const val DEVICE_CHARGING = "ac"
|
const val DEVICE_CHARGING = "ac"
|
||||||
const val DEVICE_BATTERY_NOT_LOW = "battery_not_low"
|
|
||||||
|
|
||||||
const val ENTRY_NON_COMPLETED = "manga_ongoing"
|
const val ENTRY_NON_COMPLETED = "manga_ongoing"
|
||||||
const val ENTRY_HAS_UNVIEWED = "manga_fully_read"
|
const val ENTRY_HAS_UNVIEWED = "manga_fully_read"
|
||||||
|
|
|
@ -12,7 +12,7 @@ class StubAnimeSource(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
) : AnimeSource {
|
) : AnimeSource {
|
||||||
|
|
||||||
val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
private val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
||||||
|
|
||||||
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
||||||
throw AnimeSourceNotInstalledException()
|
throw AnimeSourceNotInstalledException()
|
||||||
|
|
|
@ -13,13 +13,13 @@ class StubMangaSource(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
) : MangaSource {
|
) : MangaSource {
|
||||||
|
|
||||||
val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
private val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||||
throw SourceNotInstalledException()
|
throw SourceNotInstalledException()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
return Observable.error(SourceNotInstalledException())
|
return Observable.error(SourceNotInstalledException())
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ class StubMangaSource(
|
||||||
throw SourceNotInstalledException()
|
throw SourceNotInstalledException()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
return Observable.error(SourceNotInstalledException())
|
return Observable.error(SourceNotInstalledException())
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ class StubMangaSource(
|
||||||
throw SourceNotInstalledException()
|
throw SourceNotInstalledException()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
return Observable.error(SourceNotInstalledException())
|
return Observable.error(SourceNotInstalledException())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,10 @@ import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.parallel.Execution
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||||
import tachiyomi.domain.items.episode.model.Episode
|
import tachiyomi.domain.items.episode.model.Episode
|
||||||
import java.time.Duration
|
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.toJavaDuration
|
||||||
|
|
||||||
@Execution(ExecutionMode.CONCURRENT)
|
@Execution(ExecutionMode.CONCURRENT)
|
||||||
class SetAnimeFetchIntervalTest {
|
class SetAnimeFetchIntervalTest {
|
||||||
|
@ -22,49 +24,34 @@ class SetAnimeFetchIntervalTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
|
fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = (1..2).map {
|
||||||
(1..1).forEach {
|
episodeWithTime(episode, 10.hours)
|
||||||
val duration = Duration.ofHours(10)
|
|
||||||
val newEpisode = episodeAddTime(episode, duration)
|
|
||||||
episodes.add(newEpisode)
|
|
||||||
}
|
}
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 7 when 5 episodes in 1 day`() {
|
fun `calculateInterval returns 7 when 5 episodes in 1 day`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = (1..5).map {
|
||||||
(1..5).forEach {
|
episodeWithTime(episode, 10.hours)
|
||||||
val duration = Duration.ofHours(10)
|
|
||||||
val newEpisode = episodeAddTime(episode, duration)
|
|
||||||
episodes.add(newEpisode)
|
|
||||||
}
|
}
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 7 when 7 episodes in 48 hours, 2 day`() {
|
fun `calculateInterval returns 7 when 7 episodes in 48 hours, 2 day`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = (1..2).map {
|
||||||
(1..2).forEach {
|
episodeWithTime(episode, 24.hours)
|
||||||
val duration = Duration.ofHours(24L)
|
} + (1..5).map {
|
||||||
val newEpisode = episodeAddTime(episode, duration)
|
episodeWithTime(episode, 48.hours)
|
||||||
episodes.add(newEpisode)
|
|
||||||
}
|
|
||||||
(1..5).forEach {
|
|
||||||
val duration = Duration.ofHours(48L)
|
|
||||||
val newEpisode = episodeAddTime(episode, duration)
|
|
||||||
episodes.add(newEpisode)
|
|
||||||
}
|
}
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns default of 1 day when interval less than 1`() {
|
fun `calculateInterval returns default of 1 day when interval less than 1`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = (1..5).map {
|
||||||
(1..5).forEach {
|
episodeWithTime(episode, (15 * it).hours)
|
||||||
val duration = Duration.ofHours(15L * it)
|
|
||||||
val newEpisode = episodeAddTime(episode, duration)
|
|
||||||
episodes.add(newEpisode)
|
|
||||||
}
|
}
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
@ -72,61 +59,46 @@ class SetAnimeFetchIntervalTest {
|
||||||
// Normal interval calculation
|
// Normal interval calculation
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 episodes in 120 hours, 5 days`() {
|
fun `calculateInterval returns 1 when 5 episodes in 120 hours, 5 days`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = (1..5).map {
|
||||||
(1..5).forEach {
|
episodeWithTime(episode, (24 * it).hours)
|
||||||
val duration = Duration.ofHours(24L * it)
|
|
||||||
val newEpisode = episodeAddTime(episode, duration)
|
|
||||||
episodes.add(newEpisode)
|
|
||||||
}
|
}
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 2 when 5 episodes in 240 hours, 10 days`() {
|
fun `calculateInterval returns 2 when 5 episodes in 240 hours, 10 days`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = (1..5).map {
|
||||||
(1..5).forEach {
|
episodeWithTime(episode, (48 * it).hours)
|
||||||
val duration = Duration.ofHours(48L * it)
|
|
||||||
val newEpisode = episodeAddTime(episode, duration)
|
|
||||||
episodes.add(newEpisode)
|
|
||||||
}
|
}
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 2
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns floored value when interval is decimal`() {
|
fun `calculateInterval returns floored value when interval is decimal`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = (1..5).map {
|
||||||
(1..5).forEach {
|
episodeWithTime(episode, (25 * it).hours)
|
||||||
val duration = Duration.ofHours(25L * it)
|
|
||||||
val newEpisode = episodeAddTime(episode, duration)
|
|
||||||
episodes.add(newEpisode)
|
|
||||||
}
|
}
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 episodes in 215 hours, 5 days`() {
|
fun `calculateInterval returns 1 when 5 episodes in 215 hours, 5 days`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = (1..5).map {
|
||||||
(1..5).forEach {
|
episodeWithTime(episode, (43 * it).hours)
|
||||||
val duration = Duration.ofHours(43L * it)
|
|
||||||
val newEpisode = episodeAddTime(episode, duration)
|
|
||||||
episodes.add(newEpisode)
|
|
||||||
}
|
}
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
|
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = (1..5).map {
|
||||||
(1..5).forEach {
|
episodeWithTime(episode, (25 * it).hours).copy(dateUpload = 0L)
|
||||||
val duration = Duration.ofHours(25L * it)
|
|
||||||
val newEpisode = episodeAddTime(episode, duration).copy(dateUpload = 0L)
|
|
||||||
episodes.add(newEpisode)
|
|
||||||
}
|
}
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun episodeAddTime(episode: Episode, duration: Duration): Episode {
|
private fun episodeWithTime(episode: Episode, duration: Duration): Episode {
|
||||||
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000
|
||||||
return episode.copy(dateFetch = newTime, dateUpload = newTime)
|
return episode.copy(dateFetch = newTime, dateUpload = newTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,10 @@ import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.parallel.Execution
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||||
import tachiyomi.domain.items.chapter.model.Chapter
|
import tachiyomi.domain.items.chapter.model.Chapter
|
||||||
import java.time.Duration
|
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.toJavaDuration
|
||||||
|
|
||||||
@Execution(ExecutionMode.CONCURRENT)
|
@Execution(ExecutionMode.CONCURRENT)
|
||||||
class SetMangaFetchIntervalTest {
|
class SetMangaFetchIntervalTest {
|
||||||
|
@ -22,49 +24,34 @@ class SetMangaFetchIntervalTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
|
fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = (1..2).map {
|
||||||
(1..1).forEach {
|
chapterWithTime(chapter, 10.hours)
|
||||||
val duration = Duration.ofHours(10)
|
|
||||||
val newChapter = chapterAddTime(chapter, duration)
|
|
||||||
chapters.add(newChapter)
|
|
||||||
}
|
}
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 7 when 5 chapters in 1 day`() {
|
fun `calculateInterval returns 7 when 5 chapters in 1 day`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = (1..5).map {
|
||||||
(1..5).forEach {
|
chapterWithTime(chapter, 10.hours)
|
||||||
val duration = Duration.ofHours(10)
|
|
||||||
val newChapter = chapterAddTime(chapter, duration)
|
|
||||||
chapters.add(newChapter)
|
|
||||||
}
|
}
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 7 when 7 chapters in 48 hours, 2 day`() {
|
fun `calculateInterval returns 7 when 7 chapters in 48 hours, 2 day`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = (1..2).map {
|
||||||
(1..2).forEach {
|
chapterWithTime(chapter, 24.hours)
|
||||||
val duration = Duration.ofHours(24L)
|
} + (1..5).map {
|
||||||
val newChapter = chapterAddTime(chapter, duration)
|
chapterWithTime(chapter, 48.hours)
|
||||||
chapters.add(newChapter)
|
|
||||||
}
|
|
||||||
(1..5).forEach {
|
|
||||||
val duration = Duration.ofHours(48L)
|
|
||||||
val newChapter = chapterAddTime(chapter, duration)
|
|
||||||
chapters.add(newChapter)
|
|
||||||
}
|
}
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns default of 1 day when interval less than 1`() {
|
fun `calculateInterval returns default of 1 day when interval less than 1`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = (1..5).map {
|
||||||
(1..5).forEach {
|
chapterWithTime(chapter, (15 * it).hours)
|
||||||
val duration = Duration.ofHours(15L * it)
|
|
||||||
val newChapter = chapterAddTime(chapter, duration)
|
|
||||||
chapters.add(newChapter)
|
|
||||||
}
|
}
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
@ -72,61 +59,46 @@ class SetMangaFetchIntervalTest {
|
||||||
// Normal interval calculation
|
// Normal interval calculation
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 chapters in 120 hours, 5 days`() {
|
fun `calculateInterval returns 1 when 5 chapters in 120 hours, 5 days`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = (1..5).map {
|
||||||
(1..5).forEach {
|
chapterWithTime(chapter, (24 * it).hours)
|
||||||
val duration = Duration.ofHours(24L * it)
|
|
||||||
val newChapter = chapterAddTime(chapter, duration)
|
|
||||||
chapters.add(newChapter)
|
|
||||||
}
|
}
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 2 when 5 chapters in 240 hours, 10 days`() {
|
fun `calculateInterval returns 2 when 5 chapters in 240 hours, 10 days`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = (1..5).map {
|
||||||
(1..5).forEach {
|
chapterWithTime(chapter, (48 * it).hours)
|
||||||
val duration = Duration.ofHours(48L * it)
|
|
||||||
val newChapter = chapterAddTime(chapter, duration)
|
|
||||||
chapters.add(newChapter)
|
|
||||||
}
|
}
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns floored value when interval is decimal`() {
|
fun `calculateInterval returns floored value when interval is decimal`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = (1..5).map {
|
||||||
(1..5).forEach {
|
chapterWithTime(chapter, (25 * it).hours)
|
||||||
val duration = Duration.ofHours(25L * it)
|
|
||||||
val newChapter = chapterAddTime(chapter, duration)
|
|
||||||
chapters.add(newChapter)
|
|
||||||
}
|
}
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 chapters in 215 hours, 5 days`() {
|
fun `calculateInterval returns 1 when 5 chapters in 215 hours, 5 days`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = (1..5).map {
|
||||||
(1..5).forEach {
|
chapterWithTime(chapter, (43 * it).hours)
|
||||||
val duration = Duration.ofHours(43L * it)
|
|
||||||
val newChapter = chapterAddTime(chapter, duration)
|
|
||||||
chapters.add(newChapter)
|
|
||||||
}
|
}
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
|
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = (1..5).map {
|
||||||
(1..5).forEach {
|
chapterWithTime(chapter, (25 * it).hours).copy(dateUpload = 0L)
|
||||||
val duration = Duration.ofHours(25L * it)
|
|
||||||
val newChapter = chapterAddTime(chapter, duration).copy(dateUpload = 0L)
|
|
||||||
chapters.add(newChapter)
|
|
||||||
}
|
}
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
|
private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter {
|
||||||
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000
|
||||||
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,4 @@ kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.defaults.buildfeatures.buildconfig=true
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
android.nonTransitiveRClass=false
|
android.nonTransitiveRClass=false
|
||||||
android.nonFinalResIds=false
|
#android.nonFinalResIds=false
|
||||||
|
|
||||||
android.experimental.useDefaultDebugSigningConfigForProfileableBuildtypes=true
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
[versions]
|
[versions]
|
||||||
agp_version = "8.0.2"
|
agp_version = "8.1.1"
|
||||||
lifecycle_version = "2.6.1"
|
lifecycle_version = "2.6.1"
|
||||||
paging_version = "3.2.0"
|
paging_version = "3.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" }
|
gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" }
|
||||||
|
|
||||||
annotation = "androidx.annotation:annotation:1.7.0-alpha03"
|
annotation = "androidx.annotation:annotation:1.7.0-rc01"
|
||||||
appcompat = "androidx.appcompat:appcompat:1.6.1"
|
appcompat = "androidx.appcompat:appcompat:1.6.1"
|
||||||
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||||
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
|
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||||
corektx = "androidx.core:core-ktx:1.12.0-beta01"
|
corektx = "androidx.core:core-ktx:1.12.0-rc01"
|
||||||
splashscreen = "androidx.core:core-splashscreen:1.0.1"
|
splashscreen = "androidx.core:core-splashscreen:1.0.1"
|
||||||
recyclerview = "androidx.recyclerview:recyclerview:1.3.1"
|
recyclerview = "androidx.recyclerview:recyclerview:1.3.1"
|
||||||
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
||||||
|
@ -28,7 +28,7 @@ guava = "com.google.guava:guava:32.1.2-android"
|
||||||
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
|
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
|
||||||
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
|
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
|
||||||
|
|
||||||
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0-beta02"
|
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0-beta04"
|
||||||
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01"
|
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01"
|
||||||
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01"
|
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01"
|
||||||
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04"
|
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[versions]
|
[versions]
|
||||||
compiler = "1.5.1"
|
compiler = "1.5.2"
|
||||||
compose-bom = "2023.07.00-alpha02"
|
compose-bom = "2023.09.00-alpha02"
|
||||||
accompanist = "0.31.5-beta"
|
accompanist = "0.33.1-alpha"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
activity = "androidx.activity:activity-compose:1.7.2"
|
activity = "androidx.activity:activity-compose:1.7.2"
|
||||||
|
|
|
@ -12,7 +12,6 @@ richtext = "0.17.0"
|
||||||
desugar = "com.android.tools:desugar_jdk_libs:2.0.3"
|
desugar = "com.android.tools:desugar_jdk_libs:2.0.3"
|
||||||
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
|
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
|
||||||
|
|
||||||
rxandroid = "io.reactivex:rxandroid:1.2.1"
|
|
||||||
rxjava = "io.reactivex:rxjava:1.3.8"
|
rxjava = "io.reactivex:rxjava:1.3.8"
|
||||||
flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
|
flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
|
||||||
|
|
||||||
|
@ -35,7 +34,7 @@ sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref =
|
||||||
sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" }
|
sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" }
|
||||||
sqlite-android = "com.github.requery:sqlite-android:3.42.0"
|
sqlite-android = "com.github.requery:sqlite-android:3.42.0"
|
||||||
|
|
||||||
preferencektx = "androidx.preference:preference-ktx:1.2.0"
|
preferencektx = "androidx.preference:preference-ktx:1.2.1"
|
||||||
|
|
||||||
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
|
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
|
||||||
|
|
||||||
|
@ -57,14 +56,14 @@ flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013
|
||||||
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
||||||
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
||||||
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
||||||
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.0.4"
|
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.0.6"
|
||||||
compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0"
|
compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0"
|
||||||
|
|
||||||
swipe = "me.saket.swipe:swipe:1.2.0"
|
swipe = "me.saket.swipe:swipe:1.2.0"
|
||||||
|
|
||||||
logcat = "com.squareup.logcat:logcat:0.1"
|
logcat = "com.squareup.logcat:logcat:0.1"
|
||||||
|
|
||||||
acra-http = "ch.acra:acra-http:5.11.0"
|
acra-http = "ch.acra:acra-http:5.11.1"
|
||||||
|
|
||||||
aboutLibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlib_version" }
|
aboutLibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlib_version" }
|
||||||
aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
|
aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
|
||||||
|
@ -84,7 +83,7 @@ sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref
|
||||||
|
|
||||||
junit = "org.junit.jupiter:junit-jupiter:5.10.0"
|
junit = "org.junit.jupiter:junit-jupiter:5.10.0"
|
||||||
kotest-assertions = "io.kotest:kotest-assertions-core:5.6.2"
|
kotest-assertions = "io.kotest:kotest-assertions-core:5.6.2"
|
||||||
mockk = "io.mockk:mockk:1.13.5"
|
mockk = "io.mockk:mockk:1.13.7"
|
||||||
|
|
||||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||||
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
|
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
|
||||||
|
@ -101,7 +100,6 @@ seeker = "io.github.2307vivek:seeker:1.1.1"
|
||||||
truetypeparser = "io.github.yubyf:truetypeparser-light:2.1.4"
|
truetypeparser = "io.github.yubyf:truetypeparser-light:2.1.4"
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
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"]
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -252,6 +252,7 @@
|
||||||
<item quantity="one">%d second</item>
|
<item quantity="one">%d second</item>
|
||||||
<item quantity="other">%d seconds</item>
|
<item quantity="other">%d seconds</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="licensed_manga_chapters_error">Licensed - No items to show</string>
|
||||||
<string name="no_next_episode">Next Episode not found!</string>
|
<string name="no_next_episode">Next Episode not found!</string>
|
||||||
<string name="label_storage">Storage</string>
|
<string name="label_storage">Storage</string>
|
||||||
<string name="label_history">Manga</string>
|
<string name="label_history">Manga</string>
|
||||||
|
|
|
@ -253,7 +253,6 @@
|
||||||
<string name="connected_to_wifi">Only on Wi-Fi</string>
|
<string name="connected_to_wifi">Only on Wi-Fi</string>
|
||||||
<string name="network_not_metered">Only on unmetered network</string>
|
<string name="network_not_metered">Only on unmetered network</string>
|
||||||
<string name="charging">When charging</string>
|
<string name="charging">When charging</string>
|
||||||
<string name="battery_not_low">When battery not low</string>
|
|
||||||
<string name="restrictions">Restrictions: %s</string>
|
<string name="restrictions">Restrictions: %s</string>
|
||||||
|
|
||||||
<string name="pref_library_update_manga_restriction">Skip updating entries</string>
|
<string name="pref_library_update_manga_restriction">Skip updating entries</string>
|
||||||
|
@ -313,6 +312,7 @@
|
||||||
<string name="ext_installer_packageinstaller" translatable="false">PackageInstaller</string>
|
<string name="ext_installer_packageinstaller" translatable="false">PackageInstaller</string>
|
||||||
<string name="ext_installer_shizuku" translatable="false">Shizuku</string>
|
<string name="ext_installer_shizuku" translatable="false">Shizuku</string>
|
||||||
<string name="ext_installer_shizuku_stopped">Shizuku is not running</string>
|
<string name="ext_installer_shizuku_stopped">Shizuku is not running</string>
|
||||||
|
<string name="ext_installer_private" translatable="false">Private</string>
|
||||||
<string name="ext_installer_shizuku_unavailable_dialog">Install and start Shizuku to use Shizuku as extension installer.</string>
|
<string name="ext_installer_shizuku_unavailable_dialog">Install and start Shizuku to use Shizuku as extension installer.</string>
|
||||||
|
|
||||||
<!-- Reader section -->
|
<!-- Reader section -->
|
||||||
|
@ -489,6 +489,7 @@
|
||||||
<string name="creating_backup_error">Backup failed</string>
|
<string name="creating_backup_error">Backup failed</string>
|
||||||
<string name="missing_storage_permission">Storage permissions not granted</string>
|
<string name="missing_storage_permission">Storage permissions not granted</string>
|
||||||
<string name="empty_backup_error">No library entries to back up</string>
|
<string name="empty_backup_error">No library entries to back up</string>
|
||||||
|
<string name="create_backup_file_error">Couldn\'t create a backup file</string>
|
||||||
<string name="restore_miui_warning">Backup/restore may not function properly if MIUI Optimization is disabled.</string>
|
<string name="restore_miui_warning">Backup/restore may not function properly if MIUI Optimization is disabled.</string>
|
||||||
<string name="restore_in_progress">Restore is already in progress</string>
|
<string name="restore_in_progress">Restore is already in progress</string>
|
||||||
<string name="restoring_backup">Restoring backup</string>
|
<string name="restoring_backup">Restoring backup</string>
|
||||||
|
@ -594,8 +595,6 @@
|
||||||
<!-- missing prompt after Compose rewrite #7901 -->
|
<!-- missing prompt after Compose rewrite #7901 -->
|
||||||
<string name="no_more_results">No more results</string>
|
<string name="no_more_results">No more results</string>
|
||||||
<string name="no_results_found">No results found</string>
|
<string name="no_results_found">No results found</string>
|
||||||
<!-- Do not translate "WebView" -->
|
|
||||||
<string name="http_error_hint">Check website in WebView</string>
|
|
||||||
<string name="local_source">Local source</string>
|
<string name="local_source">Local source</string>
|
||||||
<string name="other_source">Other</string>
|
<string name="other_source">Other</string>
|
||||||
<string name="last_used_source">Last used</string>
|
<string name="last_used_source">Last used</string>
|
||||||
|
@ -927,4 +926,10 @@
|
||||||
<string name="appwidget_updates_description">See your recently updated library entries</string>
|
<string name="appwidget_updates_description">See your recently updated library entries</string>
|
||||||
<string name="appwidget_unavailable_locked">Widget not available when app lock is enabled</string>
|
<string name="appwidget_unavailable_locked">Widget not available when app lock is enabled</string>
|
||||||
<string name="remove_manga">You are about to remove \"%s\" from your library</string>
|
<string name="remove_manga">You are about to remove \"%s\" from your library</string>
|
||||||
|
|
||||||
|
<!-- Common exceptions -->
|
||||||
|
<!-- Do not translate "WebView" -->
|
||||||
|
<string name="exception_http">HTTP %d, check website in WebView</string>
|
||||||
|
<string name="exception_offline">No Internet connection</string>
|
||||||
|
<string name="exception_unknown_host">Couldn\'t reach %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -243,7 +243,6 @@ fun SelectItem(
|
||||||
label = { Text(text = label) },
|
label = { Text(text = label) },
|
||||||
value = options[selectedIndex].toString(),
|
value = options[selectedIndex].toString(),
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
enabled = false,
|
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
|
@ -251,9 +250,7 @@ fun SelectItem(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
colors = ExposedDropdownMenuDefaults.textFieldColors(
|
colors = ExposedDropdownMenuDefaults.textFieldColors(),
|
||||||
disabledTextColor = MaterialTheme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ExposedDropdownMenu(
|
ExposedDropdownMenu(
|
||||||
|
|
|
@ -24,13 +24,44 @@ interface AnimeSource {
|
||||||
val lang: String
|
val lang: String
|
||||||
get() = ""
|
get() = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the updated details for a anime.
|
||||||
|
*
|
||||||
|
* @param anime the anime to update.
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
||||||
|
return fetchAnimeDetails(anime).awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the available episodes for a anime.
|
||||||
|
*
|
||||||
|
* @param anime the anime to update.
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
||||||
|
return fetchEpisodeList(anime).awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of videos a episode has. Pages should be returned
|
||||||
|
* in the expected order; the index is ignored.
|
||||||
|
*
|
||||||
|
* @param episode the episode.
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||||
|
return fetchVideoList(episode).awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated details for a anime.
|
* Returns an observable with the updated details for a anime.
|
||||||
*
|
*
|
||||||
* @param anime the anime to update.
|
* @param anime the anime to update.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
"Use the 1.x API instead",
|
"Use the non-RxJava API instead",
|
||||||
ReplaceWith("getAnimeDetails"),
|
ReplaceWith("getAnimeDetails"),
|
||||||
)
|
)
|
||||||
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = throw IllegalStateException("Not used")
|
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = throw IllegalStateException("Not used")
|
||||||
|
@ -41,7 +72,7 @@ interface AnimeSource {
|
||||||
* @param anime the anime to update.
|
* @param anime the anime to update.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
"Use the 1.x API instead",
|
"Use the non-RxJava API instead",
|
||||||
ReplaceWith("getEpisodeList"),
|
ReplaceWith("getEpisodeList"),
|
||||||
)
|
)
|
||||||
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> = throw IllegalStateException("Not used")
|
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> = throw IllegalStateException("Not used")
|
||||||
|
@ -53,33 +84,8 @@ interface AnimeSource {
|
||||||
* @param episode the episode.
|
* @param episode the episode.
|
||||||
*/
|
*/
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
"Use the 1.x API instead",
|
"Use the non-RxJava API instead",
|
||||||
ReplaceWith("getVideoList"),
|
ReplaceWith("getVideoList"),
|
||||||
)
|
)
|
||||||
fun fetchVideoList(episode: SEpisode): Observable<List<Video>> = Observable.empty()
|
fun fetchVideoList(episode: SEpisode): Observable<List<Video>> = Observable.empty()
|
||||||
|
|
||||||
/**
|
|
||||||
* [1.x API] Get the updated details for a anime.
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
|
||||||
return fetchAnimeDetails(anime).awaitSingle()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [1.x API] Get all the available episodes for a anime.
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
|
||||||
return fetchEpisodeList(anime).awaitSingle()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [1.x API] Get the list of videos a episode has. Videos should be returned
|
|
||||||
* in the expected order; the index is ignored.
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
suspend fun getVideoList(episode: SEpisode): List<Video> {
|
|
||||||
return fetchVideoList(episode).awaitSingle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,15 +50,16 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
open val versionId = 1
|
open val versionId = 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
* ID of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||||
* of the MD5 of the string: sourcename/language/versionId
|
* of the MD5 of the string `"${name.lowercase()}/$lang/$versionId"`.
|
||||||
* Note the generated id sets the sign bit to 0.
|
*
|
||||||
|
* The ID is generated by the [generateId] function, which can be reused if needed
|
||||||
|
* to generate outdated IDs for cases where the source name or language needs to
|
||||||
|
* be changed but migrations can be avoided.
|
||||||
|
*
|
||||||
|
* Note: the generated ID sets the sign bit to `0`.
|
||||||
*/
|
*/
|
||||||
override val id by lazy {
|
override val id by lazy { generateId(name, lang, versionId) }
|
||||||
val key = "${name.lowercase()}/$lang/$versionId"
|
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
|
||||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Headers used for requests.
|
* Headers used for requests.
|
||||||
|
@ -71,6 +72,28 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
open val client: OkHttpClient
|
open val client: OkHttpClient
|
||||||
get() = network.client
|
get() = network.client
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique ID for the source based on the provided [name], [lang] and
|
||||||
|
* [versionId]. It will use the first 16 characters (64 bits) of the MD5 of the string
|
||||||
|
* `"${name.lowercase()}/$lang/$versionId"`.
|
||||||
|
*
|
||||||
|
* Note: the generated ID sets the sign bit to `0`.
|
||||||
|
*
|
||||||
|
* Can be used to generate outdated IDs, such as when the source name or language
|
||||||
|
* needs to be changed but migrations can be avoided.
|
||||||
|
*
|
||||||
|
* @since extensions-lib 1.5
|
||||||
|
* @param name [String] the name of the source
|
||||||
|
* @param lang [String] the language of the source
|
||||||
|
* @param versionId [Int] the version ID of the source
|
||||||
|
* @return a unique ID for the source
|
||||||
|
*/
|
||||||
|
protected fun generateId(name: String, lang: String, versionId: Int): Long {
|
||||||
|
val key = "${name.lowercase()}/$lang/$versionId"
|
||||||
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
|
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||||
*/
|
*/
|
||||||
|
@ -222,7 +245,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
episodeListParse(response)
|
episodeListParse(response)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Observable.error(Exception("Licensed - No episodes to show"))
|
Observable.error(Exception(LicensedEntryItemsException()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,3 +455,5 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
*/
|
*/
|
||||||
override fun getFilterList() = AnimeFilterList()
|
override fun getFilterList() = AnimeFilterList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LicensedEntryItemsException : Exception("Licensed - No items to show")
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue