Merge branch 'master' into sync-part-final

This commit is contained in:
KaiserBh 2023-08-27 12:59:10 +10:00 committed by GitHub
commit d2290107d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 395 additions and 230 deletions

View File

@ -23,7 +23,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi" applicationId = "eu.kanade.tachiyomi"
versionCode = 104 versionCode = 105
versionName = "0.14.6" versionName = "0.14.6"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
@ -194,7 +194,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

View File

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

View File

@ -11,8 +11,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 *; }

View File

@ -65,10 +65,10 @@
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".ui.main.DeepLinkActivity" android:name=".ui.deeplink.DeepLinkActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay" android:theme="@android:style/Theme.NoDisplay"
android:label="@string/action_global_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" />

View File

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

View File

@ -16,6 +16,7 @@ import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.interactor.ToggleSourcePin
import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.interactor.TrackChapter import eu.kanade.domain.track.interactor.TrackChapter
import tachiyomi.data.category.CategoryRepositoryImpl import tachiyomi.data.category.CategoryRepositoryImpl
import tachiyomi.data.chapter.ChapterRepositoryImpl import tachiyomi.data.chapter.ChapterRepositoryImpl
@ -113,6 +114,7 @@ class DomainModule : InjektModule {
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) } addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { TrackChapter(get(), get(), get(), get()) } addFactory { TrackChapter(get(), get(), get(), get()) }
addFactory { RefreshTracks(get(), get(), get(), get()) }
addFactory { DeleteTrack(get()) } addFactory { DeleteTrack(get()) }
addFactory { GetTracksPerManga(get()) } addFactory { GetTracksPerManga(get()) }
addFactory { GetTracks(get()) } addFactory { GetTracks(get()) }
@ -125,7 +127,7 @@ class DomainModule : InjektModule {
addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() } addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) } addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) }
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) } addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get(), get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { GetHistory(get()) } addFactory { GetHistory(get()) }

View File

@ -1,11 +1,12 @@
package eu.kanade.domain.chapter.interactor package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.model.toChapterUpdate import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track import tachiyomi.domain.track.model.Track
@ -13,14 +14,22 @@ import tachiyomi.domain.track.model.Track
class SyncChaptersWithTrackServiceTwoWay( class SyncChaptersWithTrackServiceTwoWay(
private val updateChapter: UpdateChapter, private val updateChapter: UpdateChapter,
private val insertTrack: InsertTrack, private val insertTrack: InsertTrack,
private val getChapterByMangaId: GetChapterByMangaId,
) { ) {
suspend fun await( suspend fun await(
chapters: List<Chapter>, mangaId: Long,
remoteTrack: Track, remoteTrack: Track,
service: TrackService, service: TrackService,
) { ) {
val sortedChapters = chapters.sortedBy { it.chapterNumber } if (service !is EnhancedTrackService) {
return
}
val sortedChapters = getChapterByMangaId.await(mangaId)
.sortedBy { it.chapterNumber }
.filter { it.isRecognizedNumber }
val chapterUpdates = sortedChapters val chapterUpdates = sortedChapters
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read } .filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
.map { it.copy(read = true).toChapterUpdate() } .map { it.copy(read = true).toChapterUpdate() }

View File

@ -0,0 +1,43 @@
package eu.kanade.domain.track.interactor
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.track.TrackManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
class RefreshTracks(
private val getTracks: GetTracks,
private val trackManager: TrackManager,
private val insertTrack: InsertTrack,
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay,
) {
suspend fun await(mangaId: Long) {
supervisorScope {
getTracks.await(mangaId)
.map { track ->
async {
val service = trackManager.getService(track.syncId)
if (service != null && service.isLoggedIn) {
try {
val updatedTrack = service.refresh(track.toDbTrack())
insertTrack.await(updatedTrack.toDomainTrack()!!)
syncChaptersWithTrackServiceTwoWay.await(mangaId, track, service)
} catch (e: Throwable) {
// Ignore errors and continue
logcat(LogPriority.ERROR, e)
}
}
}
}
.awaitAll()
}
}
}

View File

@ -24,33 +24,31 @@ class TrackChapter(
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) = coroutineScope { suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) = coroutineScope {
launchNonCancellable { launchNonCancellable {
val tracks = getTracks.await(mangaId) val tracks = getTracks.await(mangaId)
if (tracks.isEmpty()) return@launchNonCancellable if (tracks.isEmpty()) return@launchNonCancellable
tracks.mapNotNull { track -> tracks.mapNotNull { track ->
val service = trackManager.getService(track.syncId) val service = trackManager.getService(track.syncId)
if (service != null && service.isLogged && chapterNumber > track.lastChapterRead) { if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
val updatedTrack = track.copy(lastChapterRead = chapterNumber) return@mapNotNull null
}
async { val updatedTrack = track.copy(lastChapterRead = chapterNumber)
runCatching { async {
try { runCatching {
service.update(updatedTrack.toDbTrack(), true) try {
insertTrack.await(updatedTrack) service.update(updatedTrack.toDbTrack(), true)
} catch (e: Exception) { insertTrack.await(updatedTrack)
delayedTrackingStore.addItem(updatedTrack) } catch (e: Exception) {
DelayedTrackingUpdateJob.setupTask(context) delayedTrackingStore.addItem(updatedTrack)
throw e DelayedTrackingUpdateJob.setupTask(context)
} throw e
} }
} }
} else {
null
} }
} }
.awaitAll() .awaitAll()
.mapNotNull { it.exceptionOrNull() } .mapNotNull { it.exceptionOrNull() }
.forEach { logcat(LogPriority.INFO, it) } .forEach { logcat(LogPriority.WARN, it) }
} }
} }
} }

View File

@ -48,7 +48,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
.forEach { track -> .forEach { track ->
try { try {
val service = trackManager.getService(track.syncId) val service = trackManager.getService(track.syncId)
if (service != null && service.isLogged) { if (service != null && service.isLoggedIn) {
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.id}, last chapter read: ${track.lastChapterRead}" } logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.id}, last chapter read: ${track.lastChapterRead}" }
service.update(track.toDbTrack(), true) service.update(track.toDbTrack(), true)
insertTrack.await(track) insertTrack.await(track)

View File

@ -76,7 +76,7 @@ fun ExtensionScreen(
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

View File

@ -51,7 +51,7 @@ fun MigrateSourceScreen(
) { ) {
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),

View File

@ -47,7 +47,7 @@ fun SourcesScreen(
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),

View File

@ -65,7 +65,7 @@ fun HistoryScreen(
) { contentPadding -> ) { contentPadding ->
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 (!state.searchQuery.isNullOrEmpty()) { val msg = if (!state.searchQuery.isNullOrEmpty()) {
R.string.no_results_found R.string.no_results_found

View File

@ -180,7 +180,7 @@ private val displayModes = listOf(
private fun ColumnScope.DisplayPage( private fun ColumnScope.DisplayPage(
screenModel: LibrarySettingsScreenModel, screenModel: LibrarySettingsScreenModel,
) { ) {
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(

View File

@ -164,7 +164,7 @@ internal fun PreferenceItem(
TrackingPreferenceWidget( TrackingPreferenceWidget(
service = this, service = this,
checked = uName.isNotEmpty(), checked = uName.isNotEmpty(),
onClick = { if (isLogged) item.logout() else item.login() }, onClick = { if (isLoggedIn) item.logout() else item.login() },
) )
} }
} }

View File

@ -31,7 +31,6 @@ import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.ResetCategoryFlags import tachiyomi.domain.category.interactor.ResetCategoryFlags
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_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
@ -123,14 +122,14 @@ object SettingsLibraryScreen : SearchableSettings {
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
val context = LocalContext.current val context = LocalContext.current
val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval() val autoUpdateIntervalPref = libraryPreferences.autoUpdateInterval()
val libraryUpdateCategoriesPref = libraryPreferences.libraryUpdateCategories() val autoUpdateCategoriesPref = libraryPreferences.updateCategories()
val libraryUpdateCategoriesExcludePref = libraryPreferences.libraryUpdateCategoriesExclude() val autoUpdateCategoriesExcludePref = libraryPreferences.updateCategoriesExclude()
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState() val autoUpdateInterval by autoUpdateIntervalPref.collectAsState()
val included by libraryUpdateCategoriesPref.collectAsState() val included by autoUpdateCategoriesPref.collectAsState()
val excluded by libraryUpdateCategoriesExcludePref.collectAsState() val excluded by autoUpdateCategoriesExcludePref.collectAsState()
var showCategoriesDialog by rememberSaveable { mutableStateOf(false) } var showCategoriesDialog by rememberSaveable { mutableStateOf(false) }
if (showCategoriesDialog) { if (showCategoriesDialog) {
TriStateListDialog( TriStateListDialog(
@ -142,8 +141,8 @@ object SettingsLibraryScreen : SearchableSettings {
itemLabel = { it.visualName }, itemLabel = { it.visualName },
onDismissRequest = { showCategoriesDialog = false }, onDismissRequest = { showCategoriesDialog = 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(newExcluded.map { it.id.toString() }.toSet()) autoUpdateCategoriesExcludePref.set(newExcluded.map { it.id.toString() }.toSet())
showCategoriesDialog = false showCategoriesDialog = false
}, },
) )
@ -153,7 +152,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),
@ -169,15 +168,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(
DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi), DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered), DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered),
DEVICE_CHARGING to stringResource(R.string.charging), DEVICE_CHARGING to stringResource(R.string.charging),
DEVICE_BATTERY_NOT_LOW to stringResource(R.string.battery_not_low),
), ),
onValueChanged = { onValueChanged = {
// Post to event looper to allow the preference to be updated. // Post to event looper to allow the preference to be updated.
@ -206,7 +204,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.libraryUpdateMangaRestriction(), pref = libraryPreferences.autoUpdateMangaRestrictions(),
title = stringResource(R.string.pref_library_update_manga_restriction), title = stringResource(R.string.pref_library_update_manga_restriction),
entries = mapOf( entries = mapOf(
MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read), MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),

View File

@ -81,7 +81,7 @@ fun UpdateScreen(
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.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),

View File

@ -4,16 +4,26 @@ import android.content.Context
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.source.online.LicensedMangaChaptersException import eu.kanade.tachiyomi.source.online.LicensedMangaChaptersException
import eu.kanade.tachiyomi.util.system.isOnline
import tachiyomi.data.source.NoResultsException import tachiyomi.data.source.NoResultsException
import tachiyomi.domain.source.model.SourceNotInstalledException import tachiyomi.domain.source.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 NoResultsException -> return getString(R.string.no_results_found) is NoResultsException -> return getString(R.string.no_results_found)
is SourceNotInstalledException -> return getString(R.string.loader_not_implemented_error) is SourceNotInstalledException -> return getString(R.string.loader_not_implemented_error)
is HttpException -> return "$message: ${getString(R.string.http_error_hint)}"
is LicensedMangaChaptersException -> return getString(R.string.licensed_manga_chapters_error) is LicensedMangaChaptersException -> return getString(R.string.licensed_manga_chapters_error)
} }
return when (val className = this::class.simpleName) { return when (val className = this::class.simpleName) {

View File

@ -20,6 +20,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
@ -101,11 +102,11 @@ 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 oldSortingMode = prefs.getInt(libraryPreferences.librarySortingMode().key(), 0) val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0)
if (oldSortingMode == 5) { // SOURCE = 5 if (oldSortingMode == 5) { // SOURCE = 5
prefs.edit { prefs.edit {
putInt(libraryPreferences.librarySortingMode().key(), 0) // ALPHABETICAL = 0 putInt(libraryPreferences.sortingMode().key(), 0) // ALPHABETICAL = 0
} }
} }
} }
@ -134,7 +135,7 @@ object Migrations {
// Force MAL log out due to login flow change // Force MAL log out due to login flow change
// v52: switched from scraping to WebView // v52: switched from scraping to WebView
// v53: switched from WebView to OAuth // v53: switched from WebView to OAuth
if (trackManager.myAnimeList.isLogged) { if (trackManager.myAnimeList.isLoggedIn) {
trackManager.myAnimeList.logout() trackManager.myAnimeList.logout()
context.toast(R.string.myanimelist_relogin) context.toast(R.string.myanimelist_relogin)
} }
@ -180,14 +181,14 @@ 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)
LibraryUpdateJob.setupTask(context, 3) LibraryUpdateJob.setupTask(context, 3)
} }
} }
if (oldVersion < 64) { if (oldVersion < 64) {
val oldSortingMode = prefs.getInt(libraryPreferences.librarySortingMode().key(), 0) val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0)
val oldSortingDirection = prefs.getBoolean("library_sorting_ascending", true) val oldSortingDirection = prefs.getBoolean("library_sorting_ascending", true)
val newSortingMode = when (oldSortingMode) { val newSortingMode = when (oldSortingMode) {
@ -208,12 +209,12 @@ object Migrations {
} }
prefs.edit(commit = true) { prefs.edit(commit = true) {
remove(libraryPreferences.librarySortingMode().key()) remove(libraryPreferences.sortingMode().key())
remove("library_sorting_ascending") remove("library_sorting_ascending")
} }
prefs.edit { prefs.edit {
putString(libraryPreferences.librarySortingMode().key(), newSortingMode) putString(libraryPreferences.sortingMode().key(), newSortingMode)
putString("library_sorting_ascending", newSortingDirection) putString("library_sorting_ascending", newSortingDirection)
} }
} }
@ -224,16 +225,16 @@ 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)
LibraryUpdateJob.setupTask(context, 12) LibraryUpdateJob.setupTask(context, 12)
} }
} }
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.libraryUpdateMangaRestriction() -= MANGA_NON_COMPLETED libraryPreferences.autoUpdateMangaRestrictions() -= MANGA_NON_COMPLETED
} }
} }
if (oldVersion < 75) { if (oldVersion < 75) {
@ -258,20 +259,20 @@ object Migrations {
if (oldVersion < 81) { if (oldVersion < 81) {
// Handle renamed enum values // Handle renamed enum values
prefs.edit { prefs.edit {
val newSortingMode = when (val oldSortingMode = prefs.getString(libraryPreferences.librarySortingMode().key(), "ALPHABETICAL")) { val newSortingMode = when (val oldSortingMode = prefs.getString(libraryPreferences.sortingMode().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.librarySortingMode().key(), newSortingMode) putString(libraryPreferences.sortingMode().key(), newSortingMode)
} }
} }
if (oldVersion < 82) { if (oldVersion < 82) {
prefs.edit { prefs.edit {
val sort = prefs.getString(libraryPreferences.librarySortingMode().key(), null) ?: return@edit val sort = prefs.getString(libraryPreferences.sortingMode().key(), null) ?: return@edit
val direction = prefs.getString("library_sorting_ascending", "ASCENDING")!! val direction = prefs.getString("library_sorting_ascending", "ASCENDING")!!
putString(libraryPreferences.librarySortingMode().key(), "$sort,$direction") putString(libraryPreferences.sortingMode().key(), "$sort,$direction")
remove("library_sorting_ascending") remove("library_sorting_ascending")
} }
} }
@ -368,6 +369,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
} }

View File

@ -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
@ -76,6 +77,10 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
val backupPreferences = Injekt.get<BackupPreferences>() val backupPreferences = Injekt.get<BackupPreferences>()
val interval = prefInterval ?: backupPreferences.backupInterval().get() val interval = prefInterval ?: backupPreferences.backupInterval().get()
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,
@ -84,6 +89,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(workDataOf(IS_AUTO_BACKUP_KEY to true)) .setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true))
.build() .build()

View File

@ -51,7 +51,7 @@ class BackupFileValidator(
.distinct() .distinct()
val missingTrackers = trackers val missingTrackers = trackers
.mapNotNull { trackManager.getService(it.toLong()) } .mapNotNull { trackManager.getService(it.toLong()) }
.filter { !it.isLogged } .filter { !it.isLoggedIn }
.map { context.getString(it.nameRes()) } .map { context.getString(it.nameRes()) }
.sorted() .sorted()

View File

@ -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.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@ -327,14 +326,16 @@ class DownloadCache(
} }
} }
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
@ -342,7 +343,7 @@ class DownloadCache(
.map { sourceDir -> .map { sourceDir ->
async { async {
sourceDir.mangaDirs = sourceDir.dir.listFiles().orEmpty() sourceDir.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.values.forEach { mangaDir -> sourceDir.mangaDirs.values.forEach { mangaDir ->

View File

@ -15,19 +15,14 @@ import androidx.work.WorkQuery
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.copyFrom import eu.kanade.domain.manga.model.copyFrom
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
@ -44,7 +39,6 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority import logcat.LogPriority
@ -53,13 +47,11 @@ import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.model.NoChaptersException import tachiyomi.domain.chapter.model.NoChaptersException
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.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
@ -74,8 +66,6 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.toMangaUpdate import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.model.SourceNotInstalledException import tachiyomi.domain.source.model.SourceNotInstalledException
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -93,17 +83,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private val downloadPreferences: DownloadPreferences = Injekt.get() private val downloadPreferences: DownloadPreferences = Injekt.get()
private val libraryPreferences: LibraryPreferences = Injekt.get() private val libraryPreferences: LibraryPreferences = Injekt.get()
private val downloadManager: DownloadManager = Injekt.get() private val downloadManager: DownloadManager = Injekt.get()
private val trackManager: TrackManager = Injekt.get()
private val coverCache: CoverCache = Injekt.get() private val coverCache: CoverCache = Injekt.get()
private val getLibraryManga: GetLibraryManga = Injekt.get() private val getLibraryManga: GetLibraryManga = Injekt.get()
private val getManga: GetManga = Injekt.get() private val getManga: GetManga = Injekt.get()
private val updateManga: UpdateManga = Injekt.get() private val updateManga: UpdateManga = Injekt.get()
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get()
private val getCategories: GetCategories = Injekt.get() private val getCategories: GetCategories = Injekt.get()
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get() private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
private val getTracks: GetTracks = Injekt.get() private val refreshTracks: RefreshTracks = Injekt.get()
private val insertTrack: InsertTrack = Injekt.get()
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get()
private val setFetchInterval: SetFetchInterval = Injekt.get() private val setFetchInterval: SetFetchInterval = Injekt.get()
private val notifier = LibraryUpdateNotifier(context) private val notifier = LibraryUpdateNotifier(context)
@ -113,7 +99,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
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 +120,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
// 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 +167,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
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.libraryUpdateCategories().get().map { it.toLong() } val categoriesToUpdate = libraryPreferences.updateCategories().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.libraryUpdateCategoriesExclude().get().map { it.toLong() } val categoriesToExclude = libraryPreferences.updateCategoriesExclude().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 +215,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
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.libraryUpdateMangaRestriction().get() val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
val fetchWindow = setFetchInterval.getWindow(ZonedDateTime.now()) val fetchWindow = setFetchInterval.getWindow(ZonedDateTime.now())
coroutineScope { coroutineScope {
@ -297,8 +283,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
} }
if (libraryPreferences.autoUpdateTrackers().get()) { if (libraryPreferences.autoUpdateTrackers().get()) {
val loggedServices = trackManager.services.filter { it.isLogged } refreshTracks.await(manga.id)
updateTrackings(manga, loggedServices)
} }
} }
} }
@ -418,49 +403,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateTrackings() { private suspend fun updateTrackings() {
coroutineScope { coroutineScope {
var progressCount = 0 var progressCount = 0
val loggedServices = trackManager.services.filter { it.isLogged }
mangaToUpdate.forEach { libraryManga -> mangaToUpdate.forEach { libraryManga ->
val manga = libraryManga.manga
ensureActive() ensureActive()
val manga = libraryManga.manga
notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size) notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size)
refreshTracks.await(manga.id)
// Update the tracking details.
updateTrackings(manga, loggedServices)
} }
notifier.cancelProgressNotification() notifier.cancelProgressNotification()
} }
} }
private suspend fun updateTrackings(manga: Manga, loggedServices: List<TrackService>) {
getTracks.await(manga.id)
.map { track ->
supervisorScope {
async {
val service = trackManager.getService(track.syncId)
if (service != null && service in loggedServices) {
try {
val updatedTrack = service.refresh(track.toDbTrack())
insertTrack.await(updatedTrack.toDomainTrack()!!)
if (service is EnhancedTrackService) {
val chapters = getChapterByMangaId.await(manga.id)
syncChaptersWithTrackServiceTwoWay.await(chapters, track, service)
}
} catch (e: Throwable) {
// Ignore errors and continue
logcat(LogPriority.ERROR, e)
}
}
}
}
}
.awaitAll()
}
private suspend fun withUpdateNotification( private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>, updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger, completed: AtomicInteger,
@ -558,13 +513,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
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<LibraryUpdateJob>( val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(

View File

@ -39,5 +39,5 @@ class TrackManager(context: Context) {
fun getService(id: Long) = services.find { it.id == id } fun getService(id: Long) = services.find { it.id == id }
fun hasLoggedServices() = services.any { it.isLogged } fun hasLoggedServices() = services.any { it.isLoggedIn }
} }

View File

@ -33,6 +33,7 @@ abstract class TrackService(val id: Long) {
val trackPreferences: TrackPreferences by injectLazy() val trackPreferences: TrackPreferences by injectLazy()
val networkService: NetworkHelper by injectLazy() val networkService: NetworkHelper by injectLazy()
private val insertTrack: InsertTrack by injectLazy() private val insertTrack: InsertTrack by injectLazy()
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay by injectLazy()
open val client: OkHttpClient open val client: OkHttpClient
get() = networkService.client get() = networkService.client
@ -89,7 +90,7 @@ abstract class TrackService(val id: Long) {
trackPreferences.setTrackCredentials(this, "", "") trackPreferences.setTrackCredentials(this, "", "")
} }
open val isLogged: Boolean open val isLoggedIn: Boolean
get() = getUsername().isNotEmpty() && get() = getUsername().isNotEmpty() &&
getPassword().isNotEmpty() getPassword().isNotEmpty()
@ -101,6 +102,7 @@ abstract class TrackService(val id: Long) {
trackPreferences.setTrackCredentials(this, username, password) trackPreferences.setTrackCredentials(this, username, password)
} }
// TODO: move this to an interactor, and update all trackers based on common data
suspend fun registerTracking(item: Track, mangaId: Long) { suspend fun registerTracking(item: Track, mangaId: Long) {
item.manga_id = mangaId item.manga_id = mangaId
try { try {
@ -113,6 +115,7 @@ abstract class TrackService(val id: Long) {
insertTrack.await(track) insertTrack.await(track)
// TODO: merge into SyncChaptersWithTrackServiceTwoWay?
// Update chapter progress if newer chapters marked read locally // Update chapter progress if newer chapters marked read locally
if (hasReadChapters) { if (hasReadChapters) {
val latestLocalReadChapterNumber = allChapters val latestLocalReadChapterNumber = allChapters
@ -144,9 +147,7 @@ abstract class TrackService(val id: Long) {
} }
} }
if (this is EnhancedTrackService) { syncChaptersWithTrackServiceTwoWay.await(mangaId, track, this@TrackService)
Injekt.get<SyncChaptersWithTrackServiceTwoWay>().await(allChapters, track, this@TrackService)
}
} }
} catch (e: Throwable) { } catch (e: Throwable) {
withUIContext { Injekt.get<Application>().toast(e.message) } withUIContext { Injekt.get<Application>().toast(e.message) }

View File

@ -45,7 +45,6 @@ import tachiyomi.core.util.system.logcat
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
@ -72,7 +71,6 @@ class BrowseSourceScreenModel(
private val getRemoteManga: GetRemoteManga = Injekt.get(), private val getRemoteManga: GetRemoteManga = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(), private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
@ -82,7 +80,7 @@ class BrowseSourceScreenModel(
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(), private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
) : StateScreenModel<BrowseSourceScreenModel.State>(State(Listing.valueOf(listingQuery))) { ) : StateScreenModel<BrowseSourceScreenModel.State>(State(Listing.valueOf(listingQuery))) {
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } } private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLoggedIn } }
var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope) var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope)
@ -299,8 +297,7 @@ class BrowseSourceScreenModel(
(service as TrackService).bind(track) (service as TrackService).bind(track)
insertTrack.await(track.toDomainTrack()!!) insertTrack.await(track.toDomainTrack()!!)
val chapters = getChapterByMangaId.await(manga.id) syncChaptersWithTrackServiceTwoWay.await(manga.id, track.toDomainTrack()!!, service)
syncChaptersWithTrackServiceTwoWay.await(chapters, track.toDomainTrack()!!, service)
} }
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Could not match manga: ${manga.title} with service $service" } logcat(LogPriority.WARN, e) { "Could not match manga: ${manga.title} with service $service" }

View File

@ -1,8 +1,9 @@
package eu.kanade.tachiyomi.ui.main package eu.kanade.tachiyomi.ui.deeplink
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 DeepLinkActivity : Activity() { class DeepLinkActivity : Activity() {

View File

@ -0,0 +1,59 @@
package eu.kanade.tachiyomi.ui.deeplink
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.source.globalsearch.GlobalSearchScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.LoadingScreen
class DeepLinkScreen(
val query: String = "",
) : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel {
DeepLinkScreenModel(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 DeepLinkScreenModel.State.Loading -> {
LoadingScreen(Modifier.padding(contentPadding))
}
is DeepLinkScreenModel.State.NoResults -> {
navigator.replace(GlobalSearchScreen(query))
}
is DeepLinkScreenModel.State.Result -> {
navigator.replace(
MangaScreen(
(state as DeepLinkScreenModel.State.Result).manga.id,
true,
),
)
}
}
}
}
}

View File

@ -0,0 +1,47 @@
package eu.kanade.tachiyomi.ui.deeplink
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.tachiyomi.source.online.ResolvableSource
import kotlinx.coroutines.flow.update
import tachiyomi.core.util.lang.launchIO
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class DeepLinkScreenModel(
query: String = "",
private val sourceManager: SourceManager = Injekt.get(),
) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) {
init {
coroutineScope.launchIO {
val manga = sourceManager.getCatalogueSources()
.filterIsInstance<ResolvableSource>()
.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
}
}

View File

@ -366,7 +366,7 @@ class LibraryScreenModel(
* @return map of track id with the filter value * @return map of track id with the filter value
*/ */
private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> { private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> {
val loggedServices = trackManager.services.filter { it.isLogged } val loggedServices = trackManager.services.filter { it.isLoggedIn }
return if (loggedServices.isNotEmpty()) { return if (loggedServices.isNotEmpty()) {
val prefFlows = loggedServices val prefFlows = loggedServices
.map { libraryPreferences.filterTracking(it.id.toInt()).changes() } .map { libraryPreferences.filterTracking(it.id.toInt()).changes() }
@ -519,7 +519,7 @@ class LibraryScreenModel(
} }
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> {

View File

@ -26,7 +26,7 @@ class LibrarySettingsScreenModel(
) : ScreenModel { ) : ScreenModel {
val trackServices val trackServices
get() = trackManager.services.filter { it.isLogged } get() = trackManager.services.filter { it.isLoggedIn }
fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriState>) { fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriState>) {
preference(libraryPreferences).getAndSet { preference(libraryPreferences).getAndSet {

View File

@ -148,7 +148,7 @@ object LibraryTab : 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(

View File

@ -71,6 +71,7 @@ import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import eu.kanade.tachiyomi.ui.deeplink.DeepLinkScreen
import eu.kanade.tachiyomi.ui.home.HomeScreen import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
@ -409,7 +410,7 @@ class MainActivity : BaseActivity() {
val query = intent.getStringExtra(SearchManager.QUERY) ?: intent.getStringExtra(Intent.EXTRA_TEXT) val query = intent.getStringExtra(SearchManager.QUERY) ?: intent.getStringExtra(Intent.EXTRA_TEXT)
if (!query.isNullOrEmpty()) { if (!query.isNullOrEmpty()) {
navigator.popUntilRoot() navigator.popUntilRoot()
navigator.push(GlobalSearchScreen(query)) navigator.push(DeepLinkScreen(query))
} }
null null
} }

View File

@ -105,7 +105,7 @@ class MangaScreenModel(
private val successState: State.Success? private val successState: State.Success?
get() = state.value as? State.Success get() = state.value as? State.Success
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } private val loggedServices by lazy { trackManager.services.filter { it.isLoggedIn } }
val manga: Manga? val manga: Manga?
get() = successState?.manga get() = successState?.manga
@ -128,7 +128,7 @@ class MangaScreenModel(
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get())) val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
private val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope) private val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
val isUpdateIntervalEnabled = LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateMangaRestriction().get() val isUpdateIntervalEnabled = LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateMangaRestrictions().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()

View File

@ -71,7 +71,6 @@ import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.DeleteTrack import tachiyomi.domain.track.interactor.DeleteTrack
import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.GetTracks
@ -218,8 +217,7 @@ data class TrackInfoDialogHomeScreen(
private suspend fun refreshTrackers() { private suspend fun refreshTrackers() {
val insertTrack = Injekt.get<InsertTrack>() val insertTrack = Injekt.get<InsertTrack>()
val getMangaWithChapters = Injekt.get<GetMangaWithChapters>() val syncChaptersWithTrackServiceTwoWay = Injekt.get<SyncChaptersWithTrackServiceTwoWay>()
val syncTwoWayService = Injekt.get<SyncChaptersWithTrackServiceTwoWay>()
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
try { try {
@ -229,11 +227,7 @@ data class TrackInfoDialogHomeScreen(
val track = trackItem.track ?: continue val track = trackItem.track ?: continue
val domainTrack = trackItem.service.refresh(track.toDbTrack()).toDomainTrack() ?: continue val domainTrack = trackItem.service.refresh(track.toDbTrack()).toDomainTrack() ?: continue
insertTrack.await(domainTrack) insertTrack.await(domainTrack)
syncChaptersWithTrackServiceTwoWay.await(mangaId, domainTrack, trackItem.service)
if (trackItem.service is EnhancedTrackService) {
val allChapters = getMangaWithChapters.awaitChapters(mangaId)
syncTwoWayService.await(allChapters, domainTrack, trackItem.service)
}
} catch (e: Exception) { } catch (e: Exception) {
logcat( logcat(
LogPriority.ERROR, LogPriority.ERROR,
@ -257,7 +251,7 @@ data class TrackInfoDialogHomeScreen(
} }
private fun List<Track>.mapToTrackItem(): List<TrackItem> { private fun List<Track>.mapToTrackItem(): List<TrackItem> {
val loggedServices = Injekt.get<TrackManager>().services.filter { it.isLogged } val loggedServices = Injekt.get<TrackManager>().services.filter { it.isLoggedIn }
val source = Injekt.get<SourceManager>().getOrStub(sourceId) val source = Injekt.get<SourceManager>().getOrStub(sourceId)
return loggedServices return loggedServices
// Map to TrackItem // Map to TrackItem

View File

@ -36,7 +36,7 @@ class StatsScreenModel(
private val trackManager: TrackManager = Injekt.get(), private val trackManager: TrackManager = Injekt.get(),
) : StateScreenModel<StatsScreenState>(StatsScreenState.Loading) { ) : StateScreenModel<StatsScreenState>(StatsScreenState.Loading) {
private val loggedServices by lazy { trackManager.services.fastFilter { it.isLogged } } private val loggedServices by lazy { trackManager.services.fastFilter { it.isLoggedIn } }
init { init {
coroutineScope.launchIO { coroutineScope.launchIO {
@ -87,14 +87,14 @@ class StatsScreenModel(
} }
private fun getGlobalUpdateItemCount(libraryManga: List<LibraryManga>): Int { private fun getGlobalUpdateItemCount(libraryManga: List<LibraryManga>): Int {
val includedCategories = preferences.libraryUpdateCategories().get().map { it.toLong() } val includedCategories = preferences.updateCategories().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.libraryUpdateCategoriesExclude().get().map { it.toLong() } val excludedCategories = preferences.updateCategoriesExclude().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 }
@ -103,7 +103,7 @@ class StatsScreenModel(
emptyList() emptyList()
} }
val updateRestrictions = preferences.libraryUpdateMangaRestriction().get() val updateRestrictions = preferences.autoUpdateMangaRestrictions().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 }

View File

@ -64,7 +64,7 @@ class UpdatesScreenModel(
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)

View File

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

View File

@ -14,7 +14,7 @@ class CreateCategoryWithName(
private val initialFlags: Long private val initialFlags: Long
get() { get() {
val sort = preferences.librarySortingMode().get() val sort = preferences.sortingMode().get()
return sort.type.flag or sort.direction.flag return sort.type.flag or sort.direction.flag
} }

View File

@ -10,7 +10,7 @@ class ResetCategoryFlags(
) { ) {
suspend fun await() { suspend fun await() {
val sort = preferences.librarySortingMode().get() val sort = preferences.sortingMode().get()
categoryRepository.updateAllFlags(sort.type + sort.direction) categoryRepository.updateAllFlags(sort.type + sort.direction)
} }
} }

View File

@ -8,6 +8,6 @@ class SetDisplayMode(
) { ) {
fun await(display: LibraryDisplayMode) { fun await(display: LibraryDisplayMode) {
preferences.libraryDisplayMode().set(display) preferences.displayMode().set(display)
} }
} }

View File

@ -23,7 +23,7 @@ class SetSortModeForCategory(
), ),
) )
} else { } else {
preferences.librarySortingMode().set(LibrarySort(type, direction)) preferences.sortingMode().set(LibrarySort(type, direction))
categoryRepository.updateAllFlags(flags) categoryRepository.updateAllFlags(flags)
} }
} }

View File

@ -11,24 +11,24 @@ class LibraryPreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun libraryDisplayMode() = preferenceStore.getObject("pref_display_mode_library", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize) fun displayMode() = preferenceStore.getObject("pref_display_mode_library", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize)
fun librarySortingMode() = preferenceStore.getObject("library_sorting_mode", LibrarySort.default, LibrarySort.Serializer::serialize, LibrarySort.Serializer::deserialize) fun sortingMode() = preferenceStore.getObject("library_sorting_mode", LibrarySort.default, LibrarySort.Serializer::serialize, LibrarySort.Serializer::deserialize)
fun portraitColumns() = preferenceStore.getInt("pref_library_columns_portrait_key", 0) fun portraitColumns() = preferenceStore.getInt("pref_library_columns_portrait_key", 0)
fun landscapeColumns() = preferenceStore.getInt("pref_library_columns_landscape_key", 0) fun landscapeColumns() = preferenceStore.getInt("pref_library_columns_landscape_key", 0)
fun libraryUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0) fun lastUpdatedTimestamp() = preferenceStore.getLong("library_update_last_timestamp", 0L)
fun libraryUpdateLastTimestamp() = preferenceStore.getLong("library_update_last_timestamp", 0L) fun autoUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0)
fun libraryUpdateDeviceRestriction() = preferenceStore.getStringSet( fun autoUpdateDeviceRestrictions() = preferenceStore.getStringSet(
"library_update_restriction", "library_update_restriction",
setOf( setOf(
DEVICE_ONLY_ON_WIFI, DEVICE_ONLY_ON_WIFI,
), ),
) )
fun libraryUpdateMangaRestriction() = preferenceStore.getStringSet( fun autoUpdateMangaRestrictions() = preferenceStore.getStringSet(
"library_update_manga_restriction", "library_update_manga_restriction",
setOf( setOf(
MANGA_HAS_UNREAD, MANGA_HAS_UNREAD,
@ -95,9 +95,9 @@ class LibraryPreferences(
fun categorizedDisplaySettings() = preferenceStore.getBoolean("categorized_display", false) fun categorizedDisplaySettings() = preferenceStore.getBoolean("categorized_display", false)
fun libraryUpdateCategories() = preferenceStore.getStringSet("library_update_categories", emptySet()) fun updateCategories() = preferenceStore.getStringSet("library_update_categories", emptySet())
fun libraryUpdateCategoriesExclude() = preferenceStore.getStringSet("library_update_categories_exclude", emptySet()) fun updateCategoriesExclude() = preferenceStore.getStringSet("library_update_categories_exclude", emptySet())
// endregion // endregion
@ -148,7 +148,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 MANGA_NON_COMPLETED = "manga_ongoing" const val MANGA_NON_COMPLETED = "manga_ongoing"
const val MANGA_HAS_UNREAD = "manga_fully_read" const val MANGA_HAS_UNREAD = "manga_fully_read"

View File

@ -6,20 +6,19 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import rx.Observable import rx.Observable
@Suppress("OverridingDeprecatedMember")
class StubSource( class StubSource(
override val id: Long, override val id: Long,
override val lang: String, override val lang: String,
override val name: String, override val name: String,
) : Source { ) : Source {
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 +27,7 @@ class StubSource(
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 +36,7 @@ class StubSource(
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())
} }

View File

@ -25,4 +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

View File

@ -1,12 +1,12 @@
[versions] [versions]
agp_version = "8.1.0" 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-beta01" 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"
@ -27,7 +27,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-beta03" 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"

View File

@ -1,7 +1,7 @@
[versions] [versions]
compiler = "1.5.1" compiler = "1.5.2"
compose-bom = "2023.09.00-alpha02" compose-bom = "2023.09.00-alpha02"
accompanist = "0.33.0-alpha" accompanist = "0.33.1-alpha"
[libraries] [libraries]
activity = "androidx.activity:activity-compose:1.7.2" activity = "androidx.activity:activity-compose:1.7.2"

View File

@ -13,7 +13,6 @@ 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"
google-services-gradle = "com.google.gms:google-services:4.3.15" google-services-gradle = "com.google.gms:google-services:4.3.15"
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"
@ -97,7 +96,6 @@ google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1"
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0" kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
[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"]

View File

@ -254,7 +254,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>
@ -646,8 +645,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="licensed_manga_chapters_error">Licensed - No chapters to show</string> <string name="licensed_manga_chapters_error">Licensed - No chapters to show</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>
@ -987,4 +984,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>

View File

@ -24,13 +24,44 @@ interface Source {
val lang: String val lang: String
get() = "" get() = ""
/**
* Get the updated details for a manga.
*
* @param manga the manga to update.
*/
@Suppress("DEPRECATION")
suspend fun getMangaDetails(manga: SManga): SManga {
return fetchMangaDetails(manga).awaitSingle()
}
/**
* Get all the available chapters for a manga.
*
* @param manga the manga to update.
*/
@Suppress("DEPRECATION")
suspend fun getChapterList(manga: SManga): List<SChapter> {
return fetchChapterList(manga).awaitSingle()
}
/**
* Get the list of pages a chapter has. Pages should be returned
* in the expected order; the index is ignored.
*
* @param chapter the chapter.
*/
@Suppress("DEPRECATION")
suspend fun getPageList(chapter: SChapter): List<Page> {
return fetchPageList(chapter).awaitSingle()
}
/** /**
* Returns an observable with the updated details for a manga. * Returns an observable with the updated details for a manga.
* *
* @param manga the manga to update. * @param manga the manga to update.
*/ */
@Deprecated( @Deprecated(
"Use the 1.x API instead", "Use the non-RxJava API instead",
ReplaceWith("getMangaDetails"), ReplaceWith("getMangaDetails"),
) )
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used") fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
@ -41,7 +72,7 @@ interface Source {
* @param manga the manga to update. * @param manga the manga to update.
*/ */
@Deprecated( @Deprecated(
"Use the 1.x API instead", "Use the non-RxJava API instead",
ReplaceWith("getChapterList"), ReplaceWith("getChapterList"),
) )
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used") fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
@ -53,33 +84,8 @@ interface Source {
* @param chapter the chapter. * @param chapter the chapter.
*/ */
@Deprecated( @Deprecated(
"Use the 1.x API instead", "Use the non-RxJava API instead",
ReplaceWith("getPageList"), ReplaceWith("getPageList"),
) )
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty() fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty()
/**
* [1.x API] Get the updated details for a manga.
*/
@Suppress("DEPRECATION")
suspend fun getMangaDetails(manga: SManga): SManga {
return fetchMangaDetails(manga).awaitSingle()
}
/**
* [1.x API] Get all the available chapters for a manga.
*/
@Suppress("DEPRECATION")
suspend fun getChapterList(manga: SManga): List<SChapter> {
return fetchChapterList(manga).awaitSingle()
}
/**
* [1.x API] Get the list of pages a chapter has. Pages should be returned
* in the expected order; the index is ignored.
*/
@Suppress("DEPRECATION")
suspend fun getPageList(chapter: SChapter): List<Page> {
return fetchPageList(chapter).awaitSingle()
}
} }

View File

@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SManga
/**
* A source that may handle opening an SManga for a given URI.
*
* @since extensions-lib 1.5
*/
interface ResolvableSource : Source {
/**
* Whether this source may potentially handle the given URI.
*
* @since extensions-lib 1.5
*/
fun canResolveUri(uri: String): Boolean
/**
* Called if canHandleUri is true. Returns the corresponding SManga, if possible.
*
* @since extensions-lib 1.5
*/
suspend fun getManga(uri: String): SManga?
}