mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Merge branch 'master' into sync-part-final
This commit is contained in:
		| @@ -65,10 +65,10 @@ | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".ui.main.DeepLinkActivity" | ||||
|             android:name=".ui.deeplink.DeepLinkActivity" | ||||
|             android:launchMode="singleTask" | ||||
|             android:theme="@android:style/Theme.NoDisplay" | ||||
|             android:label="@string/action_global_search" | ||||
|             android:label="@string/action_search" | ||||
|             android:exported="true"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEARCH" /> | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| package eu.kanade.core.util | ||||
|  | ||||
| import androidx.compose.ui.util.fastForEach | ||||
| import java.util.concurrent.ConcurrentHashMap | ||||
| import kotlin.contracts.ExperimentalContracts | ||||
| import kotlin.contracts.contract | ||||
|  | ||||
| @@ -20,15 +19,6 @@ fun <T : R, R : Any> List<T>.insertSeparators( | ||||
|     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) { | ||||
|     if (shouldAdd) { | ||||
|         add(value) | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import eu.kanade.domain.source.interactor.SetMigrateSorting | ||||
| import eu.kanade.domain.source.interactor.ToggleLanguage | ||||
| import eu.kanade.domain.source.interactor.ToggleSource | ||||
| import eu.kanade.domain.source.interactor.ToggleSourcePin | ||||
| import eu.kanade.domain.track.interactor.RefreshTracks | ||||
| import eu.kanade.domain.track.interactor.TrackChapter | ||||
| import tachiyomi.data.category.CategoryRepositoryImpl | ||||
| import tachiyomi.data.chapter.ChapterRepositoryImpl | ||||
| @@ -113,6 +114,7 @@ class DomainModule : InjektModule { | ||||
|  | ||||
|         addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) } | ||||
|         addFactory { TrackChapter(get(), get(), get(), get()) } | ||||
|         addFactory { RefreshTracks(get(), get(), get(), get()) } | ||||
|         addFactory { DeleteTrack(get()) } | ||||
|         addFactory { GetTracksPerManga(get()) } | ||||
|         addFactory { GetTracks(get()) } | ||||
| @@ -125,7 +127,7 @@ class DomainModule : InjektModule { | ||||
|         addFactory { SetReadStatus(get(), get(), get(), get()) } | ||||
|         addFactory { ShouldUpdateDbChapter() } | ||||
|         addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) } | ||||
|         addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) } | ||||
|         addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } | ||||
|         addFactory { GetHistory(get()) } | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| package eu.kanade.domain.chapter.interactor | ||||
|  | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.tachiyomi.data.track.EnhancedTrackService | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.util.system.logcat | ||||
| import tachiyomi.domain.chapter.interactor.GetChapterByMangaId | ||||
| import tachiyomi.domain.chapter.interactor.UpdateChapter | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.chapter.model.toChapterUpdate | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
| import tachiyomi.domain.track.model.Track | ||||
| @@ -13,14 +14,22 @@ import tachiyomi.domain.track.model.Track | ||||
| class SyncChaptersWithTrackServiceTwoWay( | ||||
|     private val updateChapter: UpdateChapter, | ||||
|     private val insertTrack: InsertTrack, | ||||
|     private val getChapterByMangaId: GetChapterByMangaId, | ||||
| ) { | ||||
|  | ||||
|     suspend fun await( | ||||
|         chapters: List<Chapter>, | ||||
|         mangaId: Long, | ||||
|         remoteTrack: Track, | ||||
|         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 | ||||
|             .filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read } | ||||
|             .map { it.copy(read = true).toChapterUpdate() } | ||||
|   | ||||
| @@ -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() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -24,33 +24,31 @@ class TrackChapter( | ||||
|     suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) = coroutineScope { | ||||
|         launchNonCancellable { | ||||
|             val tracks = getTracks.await(mangaId) | ||||
|  | ||||
|             if (tracks.isEmpty()) return@launchNonCancellable | ||||
|  | ||||
|             tracks.mapNotNull { track -> | ||||
|                 val service = trackManager.getService(track.syncId) | ||||
|                 if (service != null && service.isLogged && chapterNumber > track.lastChapterRead) { | ||||
|                     val updatedTrack = track.copy(lastChapterRead = chapterNumber) | ||||
|                 if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) { | ||||
|                     return@mapNotNull null | ||||
|                 } | ||||
|  | ||||
|                     async { | ||||
|                         runCatching { | ||||
|                             try { | ||||
|                                 service.update(updatedTrack.toDbTrack(), true) | ||||
|                                 insertTrack.await(updatedTrack) | ||||
|                             } catch (e: Exception) { | ||||
|                                 delayedTrackingStore.addItem(updatedTrack) | ||||
|                                 DelayedTrackingUpdateJob.setupTask(context) | ||||
|                                 throw e | ||||
|                             } | ||||
|                 val updatedTrack = track.copy(lastChapterRead = chapterNumber) | ||||
|                 async { | ||||
|                     runCatching { | ||||
|                         try { | ||||
|                             service.update(updatedTrack.toDbTrack(), true) | ||||
|                             insertTrack.await(updatedTrack) | ||||
|                         } catch (e: Exception) { | ||||
|                             delayedTrackingStore.addItem(updatedTrack) | ||||
|                             DelayedTrackingUpdateJob.setupTask(context) | ||||
|                             throw e | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     null | ||||
|                 } | ||||
|             } | ||||
|                 .awaitAll() | ||||
|                 .mapNotNull { it.exceptionOrNull() } | ||||
|                 .forEach { logcat(LogPriority.INFO, it) } | ||||
|                 .forEach { logcat(LogPriority.WARN, it) } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -48,7 +48,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) | ||||
|                 .forEach { track -> | ||||
|                     try { | ||||
|                         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}" } | ||||
|                             service.update(track.toDbTrack(), true) | ||||
|                             insertTrack.await(track) | ||||
|   | ||||
| @@ -76,7 +76,7 @@ fun ExtensionScreen( | ||||
|         enabled = !state.isLoading, | ||||
|     ) { | ||||
|         when { | ||||
|             state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) | ||||
|             state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) | ||||
|             state.isEmpty -> { | ||||
|                 val msg = if (!searchQuery.isNullOrEmpty()) { | ||||
|                     R.string.no_results_found | ||||
|   | ||||
| @@ -51,7 +51,7 @@ fun MigrateSourceScreen( | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     when { | ||||
|         state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) | ||||
|         state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) | ||||
|         state.isEmpty -> EmptyScreen( | ||||
|             textResource = R.string.information_empty_library, | ||||
|             modifier = Modifier.padding(contentPadding), | ||||
|   | ||||
| @@ -47,7 +47,7 @@ fun SourcesScreen( | ||||
|     onLongClickItem: (Source) -> Unit, | ||||
| ) { | ||||
|     when { | ||||
|         state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) | ||||
|         state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) | ||||
|         state.isEmpty -> EmptyScreen( | ||||
|             textResource = R.string.source_empty_screen, | ||||
|             modifier = Modifier.padding(contentPadding), | ||||
|   | ||||
| @@ -65,7 +65,7 @@ fun HistoryScreen( | ||||
|     ) { contentPadding -> | ||||
|         state.list.let { | ||||
|             if (it == null) { | ||||
|                 LoadingScreen(modifier = Modifier.padding(contentPadding)) | ||||
|                 LoadingScreen(Modifier.padding(contentPadding)) | ||||
|             } else if (it.isEmpty()) { | ||||
|                 val msg = if (!state.searchQuery.isNullOrEmpty()) { | ||||
|                     R.string.no_results_found | ||||
|   | ||||
| @@ -180,7 +180,7 @@ private val displayModes = listOf( | ||||
| private fun ColumnScope.DisplayPage( | ||||
|     screenModel: LibrarySettingsScreenModel, | ||||
| ) { | ||||
|     val displayMode by screenModel.libraryPreferences.libraryDisplayMode().collectAsState() | ||||
|     val displayMode by screenModel.libraryPreferences.displayMode().collectAsState() | ||||
|     SettingsChipRow(R.string.action_display_mode) { | ||||
|         displayModes.map { (titleRes, mode) -> | ||||
|             FilterChip( | ||||
|   | ||||
| @@ -164,7 +164,7 @@ internal fun PreferenceItem( | ||||
|                     TrackingPreferenceWidget( | ||||
|                         service = this, | ||||
|                         checked = uName.isNotEmpty(), | ||||
|                         onClick = { if (isLogged) item.logout() else item.login() }, | ||||
|                         onClick = { if (isLoggedIn) item.logout() else item.login() }, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -31,7 +31,6 @@ import tachiyomi.domain.category.interactor.GetCategories | ||||
| import tachiyomi.domain.category.interactor.ResetCategoryFlags | ||||
| import tachiyomi.domain.category.model.Category | ||||
| 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_NETWORK_NOT_METERED | ||||
| import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI | ||||
| @@ -123,14 +122,14 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|     ): Preference.PreferenceGroup { | ||||
|         val context = LocalContext.current | ||||
|  | ||||
|         val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval() | ||||
|         val libraryUpdateCategoriesPref = libraryPreferences.libraryUpdateCategories() | ||||
|         val libraryUpdateCategoriesExcludePref = libraryPreferences.libraryUpdateCategoriesExclude() | ||||
|         val autoUpdateIntervalPref = libraryPreferences.autoUpdateInterval() | ||||
|         val autoUpdateCategoriesPref = libraryPreferences.updateCategories() | ||||
|         val autoUpdateCategoriesExcludePref = libraryPreferences.updateCategoriesExclude() | ||||
|  | ||||
|         val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState() | ||||
|         val autoUpdateInterval by autoUpdateIntervalPref.collectAsState() | ||||
|  | ||||
|         val included by libraryUpdateCategoriesPref.collectAsState() | ||||
|         val excluded by libraryUpdateCategoriesExcludePref.collectAsState() | ||||
|         val included by autoUpdateCategoriesPref.collectAsState() | ||||
|         val excluded by autoUpdateCategoriesExcludePref.collectAsState() | ||||
|         var showCategoriesDialog by rememberSaveable { mutableStateOf(false) } | ||||
|         if (showCategoriesDialog) { | ||||
|             TriStateListDialog( | ||||
| @@ -142,8 +141,8 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|                 itemLabel = { it.visualName }, | ||||
|                 onDismissRequest = { showCategoriesDialog = false }, | ||||
|                 onValueChanged = { newIncluded, newExcluded -> | ||||
|                     libraryUpdateCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet()) | ||||
|                     libraryUpdateCategoriesExcludePref.set(newExcluded.map { it.id.toString() }.toSet()) | ||||
|                     autoUpdateCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet()) | ||||
|                     autoUpdateCategoriesExcludePref.set(newExcluded.map { it.id.toString() }.toSet()) | ||||
|                     showCategoriesDialog = false | ||||
|                 }, | ||||
|             ) | ||||
| @@ -153,7 +152,7 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|             title = stringResource(R.string.pref_category_library_update), | ||||
|             preferenceItems = listOf( | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     pref = libraryUpdateIntervalPref, | ||||
|                     pref = autoUpdateIntervalPref, | ||||
|                     title = stringResource(R.string.pref_library_update_interval), | ||||
|                     entries = mapOf( | ||||
|                         0 to stringResource(R.string.update_never), | ||||
| @@ -169,15 +168,14 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|                     }, | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.MultiSelectListPreference( | ||||
|                     pref = libraryPreferences.libraryUpdateDeviceRestriction(), | ||||
|                     enabled = libraryUpdateInterval > 0, | ||||
|                     pref = libraryPreferences.autoUpdateDeviceRestrictions(), | ||||
|                     enabled = autoUpdateInterval > 0, | ||||
|                     title = stringResource(R.string.pref_library_update_restriction), | ||||
|                     subtitle = stringResource(R.string.restrictions), | ||||
|                     entries = mapOf( | ||||
|                         DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi), | ||||
|                         DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered), | ||||
|                         DEVICE_CHARGING to stringResource(R.string.charging), | ||||
|                         DEVICE_BATTERY_NOT_LOW to stringResource(R.string.battery_not_low), | ||||
|                     ), | ||||
|                     onValueChanged = { | ||||
|                         // 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), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.MultiSelectListPreference( | ||||
|                     pref = libraryPreferences.libraryUpdateMangaRestriction(), | ||||
|                     pref = libraryPreferences.autoUpdateMangaRestrictions(), | ||||
|                     title = stringResource(R.string.pref_library_update_manga_restriction), | ||||
|                     entries = mapOf( | ||||
|                         MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read), | ||||
|   | ||||
| @@ -81,7 +81,7 @@ fun UpdateScreen( | ||||
|         snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, | ||||
|     ) { contentPadding -> | ||||
|         when { | ||||
|             state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) | ||||
|             state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) | ||||
|             state.items.isEmpty() -> EmptyScreen( | ||||
|                 textResource = R.string.information_no_recent, | ||||
|                 modifier = Modifier.padding(contentPadding), | ||||
|   | ||||
| @@ -4,16 +4,26 @@ import android.content.Context | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.network.HttpException | ||||
| import eu.kanade.tachiyomi.source.online.LicensedMangaChaptersException | ||||
| import eu.kanade.tachiyomi.util.system.isOnline | ||||
| import tachiyomi.data.source.NoResultsException | ||||
| import tachiyomi.domain.source.model.SourceNotInstalledException | ||||
| import java.net.UnknownHostException | ||||
|  | ||||
| context(Context) | ||||
| val Throwable.formattedMessage: String | ||||
|     get() { | ||||
|         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 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) | ||||
|         } | ||||
|         return when (val className = this::class.simpleName) { | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.util.system.workManager | ||||
| import tachiyomi.core.preference.PreferenceStore | ||||
| import tachiyomi.core.preference.TriState | ||||
| import tachiyomi.core.preference.getAndSet | ||||
| import tachiyomi.core.preference.getEnum | ||||
| import tachiyomi.core.preference.minusAssign | ||||
| import tachiyomi.core.preference.plusAssign | ||||
| @@ -101,11 +102,11 @@ object Migrations { | ||||
|             } | ||||
|             if (oldVersion < 44) { | ||||
|                 // 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 | ||||
|                     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 | ||||
|                 // v52: switched from scraping to WebView | ||||
|                 // v53: switched from WebView to OAuth | ||||
|                 if (trackManager.myAnimeList.isLogged) { | ||||
|                 if (trackManager.myAnimeList.isLoggedIn) { | ||||
|                     trackManager.myAnimeList.logout() | ||||
|                     context.toast(R.string.myanimelist_relogin) | ||||
|                 } | ||||
| @@ -180,14 +181,14 @@ object Migrations { | ||||
|             } | ||||
|             if (oldVersion < 61) { | ||||
|                 // Handle removed every 1 or 2 hour library updates | ||||
|                 val updateInterval = libraryPreferences.libraryUpdateInterval().get() | ||||
|                 val updateInterval = libraryPreferences.autoUpdateInterval().get() | ||||
|                 if (updateInterval == 1 || updateInterval == 2) { | ||||
|                     libraryPreferences.libraryUpdateInterval().set(3) | ||||
|                     libraryPreferences.autoUpdateInterval().set(3) | ||||
|                     LibraryUpdateJob.setupTask(context, 3) | ||||
|                 } | ||||
|             } | ||||
|             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 newSortingMode = when (oldSortingMode) { | ||||
| @@ -208,12 +209,12 @@ object Migrations { | ||||
|                 } | ||||
|  | ||||
|                 prefs.edit(commit = true) { | ||||
|                     remove(libraryPreferences.librarySortingMode().key()) | ||||
|                     remove(libraryPreferences.sortingMode().key()) | ||||
|                     remove("library_sorting_ascending") | ||||
|                 } | ||||
|  | ||||
|                 prefs.edit { | ||||
|                     putString(libraryPreferences.librarySortingMode().key(), newSortingMode) | ||||
|                     putString(libraryPreferences.sortingMode().key(), newSortingMode) | ||||
|                     putString("library_sorting_ascending", newSortingDirection) | ||||
|                 } | ||||
|             } | ||||
| @@ -224,16 +225,16 @@ object Migrations { | ||||
|             } | ||||
|             if (oldVersion < 71) { | ||||
|                 // 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)) { | ||||
|                     libraryPreferences.libraryUpdateInterval().set(12) | ||||
|                     libraryPreferences.autoUpdateInterval().set(12) | ||||
|                     LibraryUpdateJob.setupTask(context, 12) | ||||
|                 } | ||||
|             } | ||||
|             if (oldVersion < 72) { | ||||
|                 val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true) | ||||
|                 if (!oldUpdateOngoingOnly) { | ||||
|                     libraryPreferences.libraryUpdateMangaRestriction() -= MANGA_NON_COMPLETED | ||||
|                     libraryPreferences.autoUpdateMangaRestrictions() -= MANGA_NON_COMPLETED | ||||
|                 } | ||||
|             } | ||||
|             if (oldVersion < 75) { | ||||
| @@ -258,20 +259,20 @@ object Migrations { | ||||
|             if (oldVersion < 81) { | ||||
|                 // Handle renamed enum values | ||||
|                 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" | ||||
|                         "UNREAD" -> "UNREAD_COUNT" | ||||
|                         "DATE_FETCHED" -> "CHAPTER_FETCH_DATE" | ||||
|                         else -> oldSortingMode | ||||
|                     } | ||||
|                     putString(libraryPreferences.librarySortingMode().key(), newSortingMode) | ||||
|                     putString(libraryPreferences.sortingMode().key(), newSortingMode) | ||||
|                 } | ||||
|             } | ||||
|             if (oldVersion < 82) { | ||||
|                 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")!! | ||||
|                     putString(libraryPreferences.librarySortingMode().key(), "$sort,$direction") | ||||
|                     putString(libraryPreferences.sortingMode().key(), "$sort,$direction") | ||||
|                     remove("library_sorting_ascending") | ||||
|                 } | ||||
|             } | ||||
| @@ -368,6 +369,12 @@ object Migrations { | ||||
|                     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 | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import android.content.Context | ||||
| import android.net.Uri | ||||
| import androidx.core.net.toUri | ||||
| import androidx.work.BackoffPolicy | ||||
| import androidx.work.Constraints | ||||
| import androidx.work.CoroutineWorker | ||||
| import androidx.work.ExistingPeriodicWorkPolicy | ||||
| import androidx.work.ExistingWorkPolicy | ||||
| @@ -76,6 +77,10 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete | ||||
|             val backupPreferences = Injekt.get<BackupPreferences>() | ||||
|             val interval = prefInterval ?: backupPreferences.backupInterval().get() | ||||
|             if (interval > 0) { | ||||
|                 val constraints = Constraints( | ||||
|                     requiresBatteryNotLow = true, | ||||
|                 ) | ||||
|  | ||||
|                 val request = PeriodicWorkRequestBuilder<BackupCreateJob>( | ||||
|                     interval.toLong(), | ||||
|                     TimeUnit.HOURS, | ||||
| @@ -84,6 +89,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete | ||||
|                 ) | ||||
|                     .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10.minutes.toJavaDuration()) | ||||
|                     .addTag(TAG_AUTO) | ||||
|                     .setConstraints(constraints) | ||||
|                     .setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true)) | ||||
|                     .build() | ||||
|  | ||||
|   | ||||
| @@ -51,7 +51,7 @@ class BackupFileValidator( | ||||
|             .distinct() | ||||
|         val missingTrackers = trackers | ||||
|             .mapNotNull { trackManager.getService(it.toLong()) } | ||||
|             .filter { !it.isLogged } | ||||
|             .filter { !it.isLoggedIn } | ||||
|             .map { context.getString(it.nameRes()) } | ||||
|             .sorted() | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import android.content.Context | ||||
| import android.net.Uri | ||||
| import androidx.core.net.toUri | ||||
| import com.hippo.unifile.UniFile | ||||
| import eu.kanade.core.util.mapNotNullKeys | ||||
| import eu.kanade.tachiyomi.extension.ExtensionManager | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import kotlinx.coroutines.CancellationException | ||||
| @@ -327,14 +326,16 @@ class DownloadCache( | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } | ||||
|  | ||||
|             rootDownloadsDirLock.withLock { | ||||
|                 val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty() | ||||
|                     .associate { it.name to SourceDirectory(it) } | ||||
|                     .mapNotNullKeys { entry -> | ||||
|                         sources.find { | ||||
|                             provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) | ||||
|                         }?.id | ||||
|                     .filter { it.isDirectory && !it.name.isNullOrBlank() } | ||||
|                     .mapNotNull { dir -> | ||||
|                         val sourceId = sourceMap[dir.name!!.lowercase()] | ||||
|                         sourceId?.let { it to SourceDirectory(dir) } | ||||
|                     } | ||||
|                     .toMap() | ||||
|  | ||||
|                 rootDownloadsDir.sourceDirs = sourceDirs | ||||
|  | ||||
| @@ -342,7 +343,7 @@ class DownloadCache( | ||||
|                     .map { sourceDir -> | ||||
|                         async { | ||||
|                             sourceDir.mangaDirs = sourceDir.dir.listFiles().orEmpty() | ||||
|                                 .filterNot { it.name.isNullOrBlank() } | ||||
|                                 .filter { it.isDirectory && !it.name.isNullOrBlank() } | ||||
|                                 .associate { it.name!! to MangaDirectory(it) } | ||||
|  | ||||
|                             sourceDir.mangaDirs.values.forEach { mangaDir -> | ||||
|   | ||||
| @@ -15,19 +15,14 @@ import androidx.work.WorkQuery | ||||
| import androidx.work.WorkerParameters | ||||
| import androidx.work.workDataOf | ||||
| 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.model.copyFrom | ||||
| import eu.kanade.domain.manga.model.toSManga | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.domain.track.model.toDomainTrack | ||||
| import eu.kanade.domain.track.interactor.RefreshTracks | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| 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.model.SManga | ||||
| import eu.kanade.tachiyomi.source.model.UpdateStrategy | ||||
| @@ -44,7 +39,6 @@ import kotlinx.coroutines.awaitAll | ||||
| import kotlinx.coroutines.coroutineScope | ||||
| import kotlinx.coroutines.ensureActive | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import kotlinx.coroutines.supervisorScope | ||||
| import kotlinx.coroutines.sync.Semaphore | ||||
| import kotlinx.coroutines.sync.withPermit | ||||
| import logcat.LogPriority | ||||
| @@ -53,13 +47,11 @@ import tachiyomi.core.util.lang.withIOContext | ||||
| import tachiyomi.core.util.system.logcat | ||||
| import tachiyomi.domain.category.interactor.GetCategories | ||||
| import tachiyomi.domain.category.model.Category | ||||
| import tachiyomi.domain.chapter.interactor.GetChapterByMangaId | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.chapter.model.NoChaptersException | ||||
| import tachiyomi.domain.download.service.DownloadPreferences | ||||
| import tachiyomi.domain.library.model.LibraryManga | ||||
| 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_NETWORK_NOT_METERED | ||||
| 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.source.model.SourceNotInstalledException | ||||
| 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.api.get | ||||
| import java.io.File | ||||
| @@ -93,17 +83,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|     private val downloadPreferences: DownloadPreferences = Injekt.get() | ||||
|     private val libraryPreferences: LibraryPreferences = Injekt.get() | ||||
|     private val downloadManager: DownloadManager = Injekt.get() | ||||
|     private val trackManager: TrackManager = Injekt.get() | ||||
|     private val coverCache: CoverCache = Injekt.get() | ||||
|     private val getLibraryManga: GetLibraryManga = Injekt.get() | ||||
|     private val getManga: GetManga = Injekt.get() | ||||
|     private val updateManga: UpdateManga = Injekt.get() | ||||
|     private val getChapterByMangaId: GetChapterByMangaId = Injekt.get() | ||||
|     private val getCategories: GetCategories = Injekt.get() | ||||
|     private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get() | ||||
|     private val getTracks: GetTracks = Injekt.get() | ||||
|     private val insertTrack: InsertTrack = Injekt.get() | ||||
|     private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get() | ||||
|     private val refreshTracks: RefreshTracks = Injekt.get() | ||||
|     private val setFetchInterval: SetFetchInterval = Injekt.get() | ||||
|  | ||||
|     private val notifier = LibraryUpdateNotifier(context) | ||||
| @@ -113,7 +99,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|     override suspend fun doWork(): Result { | ||||
|         if (tags.contains(WORK_NAME_AUTO)) { | ||||
|             val preferences = Injekt.get<LibraryPreferences>() | ||||
|             val restrictions = preferences.libraryUpdateDeviceRestriction().get() | ||||
|             val restrictions = preferences.autoUpdateDeviceRestrictions().get() | ||||
|             if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) { | ||||
|                 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 (target == Target.CHAPTERS) { | ||||
|             libraryPreferences.libraryUpdateLastTimestamp().set(Date().time) | ||||
|             libraryPreferences.lastUpdatedTimestamp().set(Date().time) | ||||
|         } | ||||
|  | ||||
|         val categoryId = inputData.getLong(KEY_CATEGORY, -1L) | ||||
| @@ -181,14 +167,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|         val listToUpdate = if (categoryId != -1L) { | ||||
|             libraryManga.filter { it.category == categoryId } | ||||
|         } else { | ||||
|             val categoriesToUpdate = libraryPreferences.libraryUpdateCategories().get().map { it.toLong() } | ||||
|             val categoriesToUpdate = libraryPreferences.updateCategories().get().map { it.toLong() } | ||||
|             val includedManga = if (categoriesToUpdate.isNotEmpty()) { | ||||
|                 libraryManga.filter { it.category in categoriesToUpdate } | ||||
|             } else { | ||||
|                 libraryManga | ||||
|             } | ||||
|  | ||||
|             val categoriesToExclude = libraryPreferences.libraryUpdateCategoriesExclude().get().map { it.toLong() } | ||||
|             val categoriesToExclude = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() } | ||||
|             val excludedMangaIds = if (categoriesToExclude.isNotEmpty()) { | ||||
|                 libraryManga.filter { it.category in categoriesToExclude }.map { it.manga.id } | ||||
|             } else { | ||||
| @@ -229,7 +215,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|         val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>() | ||||
|         val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>() | ||||
|         val hasDownloads = AtomicBoolean(false) | ||||
|         val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get() | ||||
|         val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get() | ||||
|         val fetchWindow = setFetchInterval.getWindow(ZonedDateTime.now()) | ||||
|  | ||||
|         coroutineScope { | ||||
| @@ -297,8 +283,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|                                     } | ||||
|  | ||||
|                                     if (libraryPreferences.autoUpdateTrackers().get()) { | ||||
|                                         val loggedServices = trackManager.services.filter { it.isLogged } | ||||
|                                         updateTrackings(manga, loggedServices) | ||||
|                                         refreshTracks.await(manga.id) | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
| @@ -418,49 +403,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|     private suspend fun updateTrackings() { | ||||
|         coroutineScope { | ||||
|             var progressCount = 0 | ||||
|             val loggedServices = trackManager.services.filter { it.isLogged } | ||||
|  | ||||
|             mangaToUpdate.forEach { libraryManga -> | ||||
|                 val manga = libraryManga.manga | ||||
|  | ||||
|                 ensureActive() | ||||
|  | ||||
|                 val manga = libraryManga.manga | ||||
|                 notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size) | ||||
|  | ||||
|                 // Update the tracking details. | ||||
|                 updateTrackings(manga, loggedServices) | ||||
|                 refreshTracks.await(manga.id) | ||||
|             } | ||||
|  | ||||
|             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( | ||||
|         updatingManga: CopyOnWriteArrayList<Manga>, | ||||
|         completed: AtomicInteger, | ||||
| @@ -558,13 +513,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|             prefInterval: Int? = null, | ||||
|         ) { | ||||
|             val preferences = Injekt.get<LibraryPreferences>() | ||||
|             val interval = prefInterval ?: preferences.libraryUpdateInterval().get() | ||||
|             val interval = prefInterval ?: preferences.autoUpdateInterval().get() | ||||
|             if (interval > 0) { | ||||
|                 val restrictions = preferences.libraryUpdateDeviceRestriction().get() | ||||
|                 val restrictions = preferences.autoUpdateDeviceRestrictions().get() | ||||
|                 val constraints = Constraints( | ||||
|                     requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED }, | ||||
|                     requiresCharging = DEVICE_CHARGING in restrictions, | ||||
|                     requiresBatteryNotLow = DEVICE_BATTERY_NOT_LOW in restrictions, | ||||
|                     requiresBatteryNotLow = true, | ||||
|                 ) | ||||
|  | ||||
|                 val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>( | ||||
|   | ||||
| @@ -39,5 +39,5 @@ class TrackManager(context: Context) { | ||||
|  | ||||
|     fun getService(id: Long) = services.find { it.id == id } | ||||
|  | ||||
|     fun hasLoggedServices() = services.any { it.isLogged } | ||||
|     fun hasLoggedServices() = services.any { it.isLoggedIn } | ||||
| } | ||||
|   | ||||
| @@ -33,6 +33,7 @@ abstract class TrackService(val id: Long) { | ||||
|     val trackPreferences: TrackPreferences by injectLazy() | ||||
|     val networkService: NetworkHelper by injectLazy() | ||||
|     private val insertTrack: InsertTrack by injectLazy() | ||||
|     private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay by injectLazy() | ||||
|  | ||||
|     open val client: OkHttpClient | ||||
|         get() = networkService.client | ||||
| @@ -89,7 +90,7 @@ abstract class TrackService(val id: Long) { | ||||
|         trackPreferences.setTrackCredentials(this, "", "") | ||||
|     } | ||||
|  | ||||
|     open val isLogged: Boolean | ||||
|     open val isLoggedIn: Boolean | ||||
|         get() = getUsername().isNotEmpty() && | ||||
|             getPassword().isNotEmpty() | ||||
|  | ||||
| @@ -101,6 +102,7 @@ abstract class TrackService(val id: Long) { | ||||
|         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) { | ||||
|         item.manga_id = mangaId | ||||
|         try { | ||||
| @@ -113,6 +115,7 @@ abstract class TrackService(val id: Long) { | ||||
|  | ||||
|                 insertTrack.await(track) | ||||
|  | ||||
|                 // TODO: merge into SyncChaptersWithTrackServiceTwoWay? | ||||
|                 // Update chapter progress if newer chapters marked read locally | ||||
|                 if (hasReadChapters) { | ||||
|                     val latestLocalReadChapterNumber = allChapters | ||||
| @@ -144,9 +147,7 @@ abstract class TrackService(val id: Long) { | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (this is EnhancedTrackService) { | ||||
|                     Injekt.get<SyncChaptersWithTrackServiceTwoWay>().await(allChapters, track, this@TrackService) | ||||
|                 } | ||||
|                 syncChaptersWithTrackServiceTwoWay.await(mangaId, track, this@TrackService) | ||||
|             } | ||||
|         } catch (e: Throwable) { | ||||
|             withUIContext { Injekt.get<Application>().toast(e.message) } | ||||
|   | ||||
| @@ -45,7 +45,6 @@ import tachiyomi.core.util.system.logcat | ||||
| import tachiyomi.domain.category.interactor.GetCategories | ||||
| import tachiyomi.domain.category.interactor.SetMangaCategories | ||||
| import tachiyomi.domain.category.model.Category | ||||
| import tachiyomi.domain.chapter.interactor.GetChapterByMangaId | ||||
| import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags | ||||
| import tachiyomi.domain.library.service.LibraryPreferences | ||||
| import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga | ||||
| @@ -72,7 +71,6 @@ class BrowseSourceScreenModel( | ||||
|     private val getRemoteManga: GetRemoteManga = Injekt.get(), | ||||
|     private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), | ||||
|     private val getCategories: GetCategories = Injekt.get(), | ||||
|     private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), | ||||
|     private val setMangaCategories: SetMangaCategories = Injekt.get(), | ||||
|     private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(), | ||||
|     private val getManga: GetManga = Injekt.get(), | ||||
| @@ -82,7 +80,7 @@ class BrowseSourceScreenModel( | ||||
|     private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(), | ||||
| ) : 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) | ||||
|  | ||||
| @@ -299,8 +297,7 @@ class BrowseSourceScreenModel( | ||||
|                         (service as TrackService).bind(track) | ||||
|                         insertTrack.await(track.toDomainTrack()!!) | ||||
|  | ||||
|                         val chapters = getChapterByMangaId.await(manga.id) | ||||
|                         syncChaptersWithTrackServiceTwoWay.await(chapters, track.toDomainTrack()!!, service) | ||||
|                         syncChaptersWithTrackServiceTwoWay.await(manga.id, track.toDomainTrack()!!, service) | ||||
|                     } | ||||
|                 } catch (e: Exception) { | ||||
|                     logcat(LogPriority.WARN, e) { "Could not match manga: ${manga.title} with service $service" } | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| package eu.kanade.tachiyomi.ui.main | ||||
| package eu.kanade.tachiyomi.ui.deeplink | ||||
| 
 | ||||
| import android.app.Activity | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| 
 | ||||
| class DeepLinkActivity : Activity() { | ||||
| 
 | ||||
| @@ -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, | ||||
|                         ), | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
| @@ -366,7 +366,7 @@ class LibraryScreenModel( | ||||
|      * @return map of track id with the filter value | ||||
|      */ | ||||
|     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()) { | ||||
|             val prefFlows = loggedServices | ||||
|                 .map { libraryPreferences.filterTracking(it.id.toInt()).changes() } | ||||
| @@ -519,7 +519,7 @@ class LibraryScreenModel( | ||||
|     } | ||||
|  | ||||
|     fun getDisplayMode(): PreferenceMutableState<LibraryDisplayMode> { | ||||
|         return libraryPreferences.libraryDisplayMode().asState(coroutineScope) | ||||
|         return libraryPreferences.displayMode().asState(coroutineScope) | ||||
|     } | ||||
|  | ||||
|     fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> { | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class LibrarySettingsScreenModel( | ||||
| ) : ScreenModel { | ||||
|  | ||||
|     val trackServices | ||||
|         get() = trackManager.services.filter { it.isLogged } | ||||
|         get() = trackManager.services.filter { it.isLoggedIn } | ||||
|  | ||||
|     fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriState>) { | ||||
|         preference(libraryPreferences).getAndSet { | ||||
|   | ||||
| @@ -148,7 +148,7 @@ object LibraryTab : Tab { | ||||
|             snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, | ||||
|         ) { contentPadding -> | ||||
|             when { | ||||
|                 state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) | ||||
|                 state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) | ||||
|                 state.searchQuery.isNullOrEmpty() && !state.hasActiveFilters && state.isLibraryEmpty -> { | ||||
|                     val handler = LocalUriHandler.current | ||||
|                     EmptyScreen( | ||||
|   | ||||
| @@ -71,6 +71,7 @@ import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi | ||||
| import eu.kanade.tachiyomi.ui.base.activity.BaseActivity | ||||
| import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen | ||||
| 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.manga.MangaScreen | ||||
| 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) | ||||
|                 if (!query.isNullOrEmpty()) { | ||||
|                     navigator.popUntilRoot() | ||||
|                     navigator.push(GlobalSearchScreen(query)) | ||||
|                     navigator.push(DeepLinkScreen(query)) | ||||
|                 } | ||||
|                 null | ||||
|             } | ||||
|   | ||||
| @@ -105,7 +105,7 @@ class MangaScreenModel( | ||||
|     private val successState: 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? | ||||
|         get() = successState?.manga | ||||
| @@ -128,7 +128,7 @@ class MangaScreenModel( | ||||
|     val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get())) | ||||
|     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 selectedChapterIds: HashSet<Long> = HashSet() | ||||
|   | ||||
| @@ -71,7 +71,6 @@ import tachiyomi.core.util.lang.withIOContext | ||||
| import tachiyomi.core.util.lang.withUIContext | ||||
| import tachiyomi.core.util.system.logcat | ||||
| import tachiyomi.domain.manga.interactor.GetManga | ||||
| import tachiyomi.domain.manga.interactor.GetMangaWithChapters | ||||
| import tachiyomi.domain.source.service.SourceManager | ||||
| import tachiyomi.domain.track.interactor.DeleteTrack | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| @@ -218,8 +217,7 @@ data class TrackInfoDialogHomeScreen( | ||||
|  | ||||
|         private suspend fun refreshTrackers() { | ||||
|             val insertTrack = Injekt.get<InsertTrack>() | ||||
|             val getMangaWithChapters = Injekt.get<GetMangaWithChapters>() | ||||
|             val syncTwoWayService = Injekt.get<SyncChaptersWithTrackServiceTwoWay>() | ||||
|             val syncChaptersWithTrackServiceTwoWay = Injekt.get<SyncChaptersWithTrackServiceTwoWay>() | ||||
|             val context = Injekt.get<Application>() | ||||
|  | ||||
|             try { | ||||
| @@ -229,11 +227,7 @@ data class TrackInfoDialogHomeScreen( | ||||
|                         val track = trackItem.track ?: continue | ||||
|                         val domainTrack = trackItem.service.refresh(track.toDbTrack()).toDomainTrack() ?: continue | ||||
|                         insertTrack.await(domainTrack) | ||||
|  | ||||
|                         if (trackItem.service is EnhancedTrackService) { | ||||
|                             val allChapters = getMangaWithChapters.awaitChapters(mangaId) | ||||
|                             syncTwoWayService.await(allChapters, domainTrack, trackItem.service) | ||||
|                         } | ||||
|                         syncChaptersWithTrackServiceTwoWay.await(mangaId, domainTrack, trackItem.service) | ||||
|                     } catch (e: Exception) { | ||||
|                         logcat( | ||||
|                             LogPriority.ERROR, | ||||
| @@ -257,7 +251,7 @@ data class TrackInfoDialogHomeScreen( | ||||
|         } | ||||
|  | ||||
|         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) | ||||
|             return loggedServices | ||||
|                 // Map to TrackItem | ||||
|   | ||||
| @@ -36,7 +36,7 @@ class StatsScreenModel( | ||||
|     private val trackManager: TrackManager = Injekt.get(), | ||||
| ) : 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 { | ||||
|         coroutineScope.launchIO { | ||||
| @@ -87,14 +87,14 @@ class StatsScreenModel( | ||||
|     } | ||||
|  | ||||
|     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()) { | ||||
|             libraryManga.filter { it.category in includedCategories } | ||||
|         } else { | ||||
|             libraryManga | ||||
|         } | ||||
|  | ||||
|         val excludedCategories = preferences.libraryUpdateCategoriesExclude().get().map { it.toLong() } | ||||
|         val excludedCategories = preferences.updateCategoriesExclude().get().map { it.toLong() } | ||||
|         val excludedMangaIds = if (excludedCategories.isNotEmpty()) { | ||||
|             libraryManga.fastMapNotNull { manga -> | ||||
|                 manga.id.takeIf { manga.category in excludedCategories } | ||||
| @@ -103,7 +103,7 @@ class StatsScreenModel( | ||||
|             emptyList() | ||||
|         } | ||||
|  | ||||
|         val updateRestrictions = preferences.libraryUpdateMangaRestriction().get() | ||||
|         val updateRestrictions = preferences.autoUpdateMangaRestrictions().get() | ||||
|         return includedManga | ||||
|             .fastFilterNot { it.manga.id in excludedMangaIds } | ||||
|             .fastDistinctBy { it.manga.id } | ||||
|   | ||||
| @@ -64,7 +64,7 @@ class UpdatesScreenModel( | ||||
|     private val _events: Channel<Event> = Channel(Int.MAX_VALUE) | ||||
|     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 | ||||
|     private val selectedPositions: Array<Int> = arrayOf(-1, -1) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user