mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Merge branch 'master' into sync-part-1
This commit is contained in:
		| @@ -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.TrackChapter | ||||
| import tachiyomi.data.category.CategoryRepositoryImpl | ||||
| import tachiyomi.data.chapter.ChapterRepositoryImpl | ||||
| import tachiyomi.data.history.HistoryRepositoryImpl | ||||
| @@ -109,6 +110,7 @@ class DomainModule : InjektModule { | ||||
|         addFactory { GetApplicationRelease(get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) } | ||||
|         addFactory { TrackChapter(get(), get(), get(), get()) } | ||||
|         addFactory { DeleteTrack(get()) } | ||||
|         addFactory { GetTracksPerManga(get()) } | ||||
|         addFactory { GetTracks(get()) } | ||||
|   | ||||
| @@ -7,9 +7,9 @@ import eu.kanade.tachiyomi.ui.reader.setting.OrientationType | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType | ||||
| import tachiyomi.core.metadata.comicinfo.ComicInfo | ||||
| import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus | ||||
| import tachiyomi.core.preference.TriState | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.TriStateFilter | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| @@ -20,19 +20,19 @@ val Manga.readingModeType: Long | ||||
| val Manga.orientationType: Long | ||||
|     get() = viewerFlags and OrientationType.MASK.toLong() | ||||
|  | ||||
| val Manga.downloadedFilter: TriStateFilter | ||||
| val Manga.downloadedFilter: TriState | ||||
|     get() { | ||||
|         if (forceDownloaded()) return TriStateFilter.ENABLED_IS | ||||
|         if (forceDownloaded()) return TriState.ENABLED_IS | ||||
|         return when (downloadedFilterRaw) { | ||||
|             Manga.CHAPTER_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS | ||||
|             Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT | ||||
|             else -> TriStateFilter.DISABLED | ||||
|             Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS | ||||
|             Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT | ||||
|             else -> TriState.DISABLED | ||||
|         } | ||||
|     } | ||||
| fun Manga.chaptersFiltered(): Boolean { | ||||
|     return unreadFilter != TriStateFilter.DISABLED || | ||||
|         downloadedFilter != TriStateFilter.DISABLED || | ||||
|         bookmarkedFilter != TriStateFilter.DISABLED | ||||
|     return unreadFilter != TriState.DISABLED || | ||||
|         downloadedFilter != TriState.DISABLED || | ||||
|         bookmarkedFilter != TriState.DISABLED | ||||
| } | ||||
| fun Manga.forceDownloaded(): Boolean { | ||||
|     return favorite && Injekt.get<BasePreferences>().downloadedOnly().get() | ||||
|   | ||||
| @@ -0,0 +1,56 @@ | ||||
| package eu.kanade.domain.track.interactor | ||||
|  | ||||
| import android.content.Context | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.domain.track.service.DelayedTrackingUpdateJob | ||||
| import eu.kanade.domain.track.store.DelayedTrackingStore | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import kotlinx.coroutines.async | ||||
| import kotlinx.coroutines.awaitAll | ||||
| import kotlinx.coroutines.coroutineScope | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.util.lang.launchNonCancellable | ||||
| import tachiyomi.core.util.system.logcat | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
|  | ||||
| class TrackChapter( | ||||
|     private val getTracks: GetTracks, | ||||
|     private val trackManager: TrackManager, | ||||
|     private val insertTrack: InsertTrack, | ||||
|     private val delayedTrackingStore: DelayedTrackingStore, | ||||
| ) { | ||||
|  | ||||
|     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) | ||||
|  | ||||
|                     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) } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -2,6 +2,7 @@ package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| @@ -19,7 +20,6 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchState | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.presentation.core.components.LazyColumn | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.State | ||||
| import eu.kanade.presentation.browse.components.GlobalSearchCardRow | ||||
| @@ -14,7 +15,6 @@ import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchState | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.presentation.core.components.LazyColumn | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
|  | ||||
| @Composable | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package eu.kanade.presentation.browse.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| @@ -12,7 +13,6 @@ import eu.kanade.presentation.library.components.MangaListItem | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.MangaCover | ||||
| import tachiyomi.presentation.core.components.LazyColumn | ||||
| import tachiyomi.presentation.core.util.plus | ||||
|  | ||||
| @Composable | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package eu.kanade.presentation.category | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.LazyListState | ||||
| import androidx.compose.foundation.lazy.itemsIndexed | ||||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||||
| @@ -16,7 +17,6 @@ import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.category.CategoryScreenState | ||||
| import tachiyomi.domain.category.model.Category | ||||
| import tachiyomi.presentation.core.components.LazyColumn | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.components.material.topSmallPaddingValues | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import androidx.compose.foundation.text.KeyboardOptions | ||||
| import androidx.compose.material.TextFieldDefaults | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.ArrowBack | ||||
| import androidx.compose.material.icons.outlined.ArrowForward | ||||
| import androidx.compose.material.icons.outlined.Close | ||||
| import androidx.compose.material.icons.outlined.MoreVert | ||||
| import androidx.compose.material.icons.outlined.Search | ||||
| @@ -38,12 +39,14 @@ import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.graphics.SolidColor | ||||
| import androidx.compose.ui.graphics.vector.ImageVector | ||||
| import androidx.compose.ui.platform.LocalFocusManager | ||||
| import androidx.compose.ui.platform.LocalLayoutDirection | ||||
| import androidx.compose.ui.platform.LocalSoftwareKeyboardController | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.input.ImeAction | ||||
| import androidx.compose.ui.text.input.VisualTransformation | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.LayoutDirection | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import eu.kanade.tachiyomi.R | ||||
| @@ -62,7 +65,7 @@ fun AppBar( | ||||
|     subtitle: String? = null, | ||||
|     // Up button | ||||
|     navigateUp: (() -> Unit)? = null, | ||||
|     navigationIcon: ImageVector = Icons.Outlined.ArrowBack, | ||||
|     navigationIcon: ImageVector? = null, | ||||
|     // Menu | ||||
|     actions: @Composable RowScope.() -> Unit = {}, | ||||
|     // Action mode | ||||
| @@ -107,7 +110,7 @@ fun AppBar( | ||||
|     titleContent: @Composable () -> Unit, | ||||
|     // Up button | ||||
|     navigateUp: (() -> Unit)? = null, | ||||
|     navigationIcon: ImageVector = Icons.Outlined.ArrowBack, | ||||
|     navigationIcon: ImageVector? = null, | ||||
|     // Menu | ||||
|     actions: @Composable RowScope.() -> Unit = {}, | ||||
|     // Action mode | ||||
| @@ -131,10 +134,7 @@ fun AppBar( | ||||
|                 } else { | ||||
|                     navigateUp?.let { | ||||
|                         IconButton(onClick = it) { | ||||
|                             Icon( | ||||
|                                 imageVector = navigationIcon, | ||||
|                                 contentDescription = stringResource(R.string.abc_action_bar_up_description), | ||||
|                             ) | ||||
|                             UpIcon(navigationIcon) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| @@ -360,6 +360,16 @@ fun SearchToolbar( | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun UpIcon(navigationIcon: ImageVector? = null) { | ||||
|     val icon = navigationIcon | ||||
|         ?: if (LocalLayoutDirection.current == LayoutDirection.Ltr) Icons.Outlined.ArrowBack else Icons.Outlined.ArrowForward | ||||
|     Icon( | ||||
|         imageVector = icon, | ||||
|         contentDescription = stringResource(R.string.abc_action_bar_up_description), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| sealed interface AppBar { | ||||
|     sealed interface AppBarAction | ||||
|  | ||||
|   | ||||
| @@ -1,128 +0,0 @@ | ||||
| package eu.kanade.presentation.components | ||||
|  | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.ContentAlpha | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.rounded.CheckBox | ||||
| import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank | ||||
| import androidx.compose.material.icons.rounded.DisabledByDefault | ||||
| import androidx.compose.material3.DropdownMenuItem | ||||
| import androidx.compose.material3.ExposedDropdownMenuBox | ||||
| import androidx.compose.material3.ExposedDropdownMenuDefaults | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.OutlinedTextField | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.unit.dp | ||||
| import tachiyomi.domain.manga.model.TriStateFilter | ||||
| import tachiyomi.presentation.core.components.SettingsItemsPaddings | ||||
|  | ||||
| @Composable | ||||
| fun TriStateItem( | ||||
|     label: String, | ||||
|     state: TriStateFilter, | ||||
|     enabled: Boolean = true, | ||||
|     onClick: ((TriStateFilter) -> Unit)?, | ||||
| ) { | ||||
|     Row( | ||||
|         modifier = Modifier | ||||
|             .clickable( | ||||
|                 enabled = enabled && onClick != null, | ||||
|                 onClick = { | ||||
|                     when (state) { | ||||
|                         TriStateFilter.DISABLED -> onClick?.invoke(TriStateFilter.ENABLED_IS) | ||||
|                         TriStateFilter.ENABLED_IS -> onClick?.invoke(TriStateFilter.ENABLED_NOT) | ||||
|                         TriStateFilter.ENABLED_NOT -> onClick?.invoke(TriStateFilter.DISABLED) | ||||
|                     } | ||||
|                 }, | ||||
|             ) | ||||
|             .fillMaxWidth() | ||||
|             .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical), | ||||
|         verticalAlignment = Alignment.CenterVertically, | ||||
|         horizontalArrangement = Arrangement.spacedBy(24.dp), | ||||
|     ) { | ||||
|         val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled | ||||
|  | ||||
|         Icon( | ||||
|             imageVector = when (state) { | ||||
|                 TriStateFilter.DISABLED -> Icons.Rounded.CheckBoxOutlineBlank | ||||
|                 TriStateFilter.ENABLED_IS -> Icons.Rounded.CheckBox | ||||
|                 TriStateFilter.ENABLED_NOT -> Icons.Rounded.DisabledByDefault | ||||
|             }, | ||||
|             contentDescription = null, | ||||
|             tint = if (!enabled || state == TriStateFilter.DISABLED) { | ||||
|                 MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha) | ||||
|             } else { | ||||
|                 when (onClick) { | ||||
|                     null -> MaterialTheme.colorScheme.onSurface.copy(alpha = ContentAlpha.disabled) | ||||
|                     else -> MaterialTheme.colorScheme.primary | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|         Text( | ||||
|             text = label, | ||||
|             color = MaterialTheme.colorScheme.onSurface.copy(alpha = stateAlpha), | ||||
|             style = MaterialTheme.typography.bodyMedium, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun SelectItem( | ||||
|     label: String, | ||||
|     options: Array<out Any?>, | ||||
|     selectedIndex: Int, | ||||
|     onSelect: (Int) -> Unit, | ||||
| ) { | ||||
|     var expanded by remember { mutableStateOf(false) } | ||||
|  | ||||
|     ExposedDropdownMenuBox( | ||||
|         expanded = expanded, | ||||
|         onExpandedChange = { expanded = !expanded }, | ||||
|     ) { | ||||
|         OutlinedTextField( | ||||
|             modifier = Modifier | ||||
|                 .menuAnchor() | ||||
|                 .fillMaxWidth() | ||||
|                 .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical), | ||||
|             label = { Text(text = label) }, | ||||
|             value = options[selectedIndex].toString(), | ||||
|             onValueChange = {}, | ||||
|             readOnly = true, | ||||
|             singleLine = true, | ||||
|             trailingIcon = { | ||||
|                 ExposedDropdownMenuDefaults.TrailingIcon( | ||||
|                     expanded = expanded, | ||||
|                 ) | ||||
|             }, | ||||
|             colors = ExposedDropdownMenuDefaults.textFieldColors(), | ||||
|         ) | ||||
|  | ||||
|         ExposedDropdownMenu( | ||||
|             modifier = Modifier.exposedDropdownSize(matchTextFieldWidth = true), | ||||
|             expanded = expanded, | ||||
|             onDismissRequest = { expanded = false }, | ||||
|         ) { | ||||
|             options.forEachIndexed { index, text -> | ||||
|                 DropdownMenuItem( | ||||
|                     text = { Text(text.toString()) }, | ||||
|                     onClick = { | ||||
|                         onSelect(index) | ||||
|                         expanded = false | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.ColumnScope | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.wrapContentSize | ||||
| import androidx.compose.foundation.pager.PagerState | ||||
| import androidx.compose.foundation.pager.rememberPagerState | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.MoreVert | ||||
| @@ -42,13 +43,13 @@ fun TabbedDialog( | ||||
|     onDismissRequest: () -> Unit, | ||||
|     tabTitles: List<String>, | ||||
|     tabOverflowMenuContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null, | ||||
|     pagerState: PagerState = rememberPagerState { tabTitles.size }, | ||||
|     content: @Composable (Int) -> Unit, | ||||
| ) { | ||||
|     AdaptiveSheet( | ||||
|         onDismissRequest = onDismissRequest, | ||||
|     ) { | ||||
|         val scope = rememberCoroutineScope() | ||||
|         val pagerState = rememberPagerState { tabTitles.size } | ||||
|  | ||||
|         Column { | ||||
|             Row { | ||||
|   | ||||
| @@ -14,21 +14,21 @@ import androidx.compose.ui.platform.LocalConfiguration | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import eu.kanade.presentation.components.TabbedDialog | ||||
| import eu.kanade.presentation.components.TabbedDialogPaddings | ||||
| import eu.kanade.presentation.components.TriStateItem | ||||
| import eu.kanade.presentation.util.collectAsState | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel | ||||
| import tachiyomi.core.preference.TriState | ||||
| import tachiyomi.domain.category.model.Category | ||||
| import tachiyomi.domain.library.model.LibraryDisplayMode | ||||
| import tachiyomi.domain.library.model.LibrarySort | ||||
| import tachiyomi.domain.library.model.sort | ||||
| import tachiyomi.domain.library.service.LibraryPreferences | ||||
| import tachiyomi.domain.manga.model.TriStateFilter | ||||
| import tachiyomi.presentation.core.components.CheckboxItem | ||||
| import tachiyomi.presentation.core.components.HeadingItem | ||||
| import tachiyomi.presentation.core.components.RadioItem | ||||
| import tachiyomi.presentation.core.components.SliderItem | ||||
| import tachiyomi.presentation.core.components.SortItem | ||||
| import tachiyomi.presentation.core.components.TriStateItem | ||||
|  | ||||
| @Composable | ||||
| fun LibrarySettingsDialog( | ||||
| @@ -74,7 +74,7 @@ private fun ColumnScope.FilterPage( | ||||
|     TriStateItem( | ||||
|         label = stringResource(R.string.label_downloaded), | ||||
|         state = if (downloadedOnly) { | ||||
|             TriStateFilter.ENABLED_IS | ||||
|             TriState.ENABLED_IS | ||||
|         } else { | ||||
|             filterDownloaded | ||||
|         }, | ||||
|   | ||||
| @@ -27,20 +27,20 @@ import eu.kanade.domain.manga.model.downloadedFilter | ||||
| import eu.kanade.domain.manga.model.forceDownloaded | ||||
| import eu.kanade.presentation.components.TabbedDialog | ||||
| import eu.kanade.presentation.components.TabbedDialogPaddings | ||||
| import eu.kanade.presentation.components.TriStateItem | ||||
| import eu.kanade.tachiyomi.R | ||||
| import tachiyomi.core.preference.TriState | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.TriStateFilter | ||||
| import tachiyomi.presentation.core.components.RadioItem | ||||
| import tachiyomi.presentation.core.components.SortItem | ||||
| import tachiyomi.presentation.core.components.TriStateItem | ||||
|  | ||||
| @Composable | ||||
| fun ChapterSettingsDialog( | ||||
|     onDismissRequest: () -> Unit, | ||||
|     manga: Manga? = null, | ||||
|     onDownloadFilterChanged: (TriStateFilter) -> Unit, | ||||
|     onUnreadFilterChanged: (TriStateFilter) -> Unit, | ||||
|     onBookmarkedFilterChanged: (TriStateFilter) -> Unit, | ||||
|     onDownloadFilterChanged: (TriState) -> Unit, | ||||
|     onUnreadFilterChanged: (TriState) -> Unit, | ||||
|     onBookmarkedFilterChanged: (TriState) -> Unit, | ||||
|     onSortModeChanged: (Long) -> Unit, | ||||
|     onDisplayModeChanged: (Long) -> Unit, | ||||
|     onSetAsDefault: (applyToExistingManga: Boolean) -> Unit, | ||||
| @@ -78,11 +78,11 @@ fun ChapterSettingsDialog( | ||||
|             when (page) { | ||||
|                 0 -> { | ||||
|                     FilterPage( | ||||
|                         downloadFilter = manga?.downloadedFilter ?: TriStateFilter.DISABLED, | ||||
|                         downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED, | ||||
|                         onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { manga?.forceDownloaded() == true }, | ||||
|                         unreadFilter = manga?.unreadFilter ?: TriStateFilter.DISABLED, | ||||
|                         unreadFilter = manga?.unreadFilter ?: TriState.DISABLED, | ||||
|                         onUnreadFilterChanged = onUnreadFilterChanged, | ||||
|                         bookmarkedFilter = manga?.bookmarkedFilter ?: TriStateFilter.DISABLED, | ||||
|                         bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED, | ||||
|                         onBookmarkedFilterChanged = onBookmarkedFilterChanged, | ||||
|                     ) | ||||
|                 } | ||||
| @@ -106,12 +106,12 @@ fun ChapterSettingsDialog( | ||||
|  | ||||
| @Composable | ||||
| private fun ColumnScope.FilterPage( | ||||
|     downloadFilter: TriStateFilter, | ||||
|     onDownloadFilterChanged: ((TriStateFilter) -> Unit)?, | ||||
|     unreadFilter: TriStateFilter, | ||||
|     onUnreadFilterChanged: (TriStateFilter) -> Unit, | ||||
|     bookmarkedFilter: TriStateFilter, | ||||
|     onBookmarkedFilterChanged: (TriStateFilter) -> Unit, | ||||
|     downloadFilter: TriState, | ||||
|     onDownloadFilterChanged: ((TriState) -> Unit)?, | ||||
|     unreadFilter: TriState, | ||||
|     onUnreadFilterChanged: (TriState) -> Unit, | ||||
|     bookmarkedFilter: TriState, | ||||
|     onBookmarkedFilterChanged: (TriState) -> Unit, | ||||
| ) { | ||||
|     TriStateItem( | ||||
|         label = stringResource(R.string.label_downloaded), | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.only | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.systemBars | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.LazyListScope | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||||
| @@ -69,7 +70,6 @@ import tachiyomi.domain.chapter.service.missingChaptersCount | ||||
| import tachiyomi.domain.library.service.LibraryPreferences | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.source.model.StubSource | ||||
| import tachiyomi.presentation.core.components.LazyColumn | ||||
| import tachiyomi.presentation.core.components.TwoPanelBox | ||||
| import tachiyomi.presentation.core.components.VerticalFastScroller | ||||
| import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton | ||||
| @@ -87,8 +87,8 @@ fun MangaScreen( | ||||
|     dateRelativeTime: Int, | ||||
|     dateFormat: DateFormat, | ||||
|     isTabletUi: Boolean, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     onBackClicked: () -> Unit, | ||||
|     onChapterClicked: (Chapter) -> Unit, | ||||
|     onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, | ||||
| @@ -141,8 +141,8 @@ fun MangaScreen( | ||||
|             snackbarHostState = snackbarHostState, | ||||
|             dateRelativeTime = dateRelativeTime, | ||||
|             dateFormat = dateFormat, | ||||
|             chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|             chapterSwipeStartAction = chapterSwipeStartAction, | ||||
|             chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|             onBackClicked = onBackClicked, | ||||
|             onChapterClicked = onChapterClicked, | ||||
|             onDownloadChapter = onDownloadChapter, | ||||
| @@ -175,8 +175,8 @@ fun MangaScreen( | ||||
|             state = state, | ||||
|             snackbarHostState = snackbarHostState, | ||||
|             dateRelativeTime = dateRelativeTime, | ||||
|             chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|             chapterSwipeStartAction = chapterSwipeStartAction, | ||||
|             chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|             dateFormat = dateFormat, | ||||
|             onBackClicked = onBackClicked, | ||||
|             onChapterClicked = onChapterClicked, | ||||
| @@ -214,8 +214,8 @@ private fun MangaScreenSmallImpl( | ||||
|     snackbarHostState: SnackbarHostState, | ||||
|     dateRelativeTime: Int, | ||||
|     dateFormat: DateFormat, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     onBackClicked: () -> Unit, | ||||
|     onChapterClicked: (Chapter) -> Unit, | ||||
|     onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, | ||||
| @@ -258,7 +258,7 @@ private fun MangaScreenSmallImpl( | ||||
| ) { | ||||
|     val chapterListState = rememberLazyListState() | ||||
|  | ||||
|     val chapters = remember(state) { state.processedChapters.toList() } | ||||
|     val chapters = remember(state) { state.processedChapters } | ||||
|  | ||||
|     val internalOnBackPressed = { | ||||
|         if (chapters.fastAny { it.selected }) { | ||||
| @@ -320,7 +320,7 @@ private fun MangaScreenSmallImpl( | ||||
|             ) { | ||||
|                 ExtendedFloatingActionButton( | ||||
|                     text = { | ||||
|                         val id = if (chapters.fastAny { it.chapter.read }) { | ||||
|                         val id = if (state.chapters.fastAny { it.chapter.read }) { | ||||
|                             R.string.action_resume | ||||
|                         } else { | ||||
|                             R.string.action_start | ||||
| @@ -421,8 +421,8 @@ private fun MangaScreenSmallImpl( | ||||
|                         chapters = chapters, | ||||
|                         dateRelativeTime = dateRelativeTime, | ||||
|                         dateFormat = dateFormat, | ||||
|                         chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|                         chapterSwipeStartAction = chapterSwipeStartAction, | ||||
|                         chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|                         onChapterClicked = onChapterClicked, | ||||
|                         onDownloadChapter = onDownloadChapter, | ||||
|                         onChapterSelected = onChapterSelected, | ||||
| @@ -440,8 +440,8 @@ fun MangaScreenLargeImpl( | ||||
|     snackbarHostState: SnackbarHostState, | ||||
|     dateRelativeTime: Int, | ||||
|     dateFormat: DateFormat, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     onBackClicked: () -> Unit, | ||||
|     onChapterClicked: (Chapter) -> Unit, | ||||
|     onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, | ||||
| @@ -485,7 +485,7 @@ fun MangaScreenLargeImpl( | ||||
|     val layoutDirection = LocalLayoutDirection.current | ||||
|     val density = LocalDensity.current | ||||
|  | ||||
|     val chapters = remember(state) { state.processedChapters.toList() } | ||||
|     val chapters = remember(state) { state.processedChapters } | ||||
|  | ||||
|     val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() | ||||
|     var topBarHeight by remember { mutableIntStateOf(0) } | ||||
| @@ -555,7 +555,7 @@ fun MangaScreenLargeImpl( | ||||
|                 ) { | ||||
|                     ExtendedFloatingActionButton( | ||||
|                         text = { | ||||
|                             val id = if (chapters.fastAny { it.chapter.read }) { | ||||
|                             val id = if (state.chapters.fastAny { it.chapter.read }) { | ||||
|                                 R.string.action_resume | ||||
|                             } else { | ||||
|                                 R.string.action_start | ||||
| @@ -641,8 +641,8 @@ fun MangaScreenLargeImpl( | ||||
|                                 chapters = chapters, | ||||
|                                 dateRelativeTime = dateRelativeTime, | ||||
|                                 dateFormat = dateFormat, | ||||
|                                 chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|                                 chapterSwipeStartAction = chapterSwipeStartAction, | ||||
|                                 chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|                                 onChapterClicked = onChapterClicked, | ||||
|                                 onDownloadChapter = onDownloadChapter, | ||||
|                                 onChapterSelected = onChapterSelected, | ||||
| @@ -703,8 +703,8 @@ private fun LazyListScope.sharedChapterItems( | ||||
|     chapters: List<ChapterItem>, | ||||
|     dateRelativeTime: Int, | ||||
|     dateFormat: DateFormat, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     onChapterClicked: (Chapter) -> Unit, | ||||
|     onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, | ||||
|     onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, | ||||
| @@ -751,8 +751,8 @@ private fun LazyListScope.sharedChapterItems( | ||||
|             downloadIndicatorEnabled = chapters.fastAll { !it.selected }, | ||||
|             downloadStateProvider = { chapterItem.downloadState }, | ||||
|             downloadProgressProvider = { chapterItem.downloadProgress }, | ||||
|             chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|             chapterSwipeStartAction = chapterSwipeStartAction, | ||||
|             chapterSwipeEndAction = chapterSwipeEndAction, | ||||
|             onLongClick = { | ||||
|                 onChapterSelected(chapterItem, !chapterItem.selected, true, true) | ||||
|                 haptic.performHapticFeedback(HapticFeedbackType.LongPress) | ||||
|   | ||||
| @@ -1,20 +1,12 @@ | ||||
| package eu.kanade.presentation.manga.components | ||||
|  | ||||
| import androidx.compose.animation.core.animateFloatAsState | ||||
| import androidx.compose.animation.core.tween | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.combinedClickable | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.sizeIn | ||||
| import androidx.compose.material.DismissDirection | ||||
| import androidx.compose.material.DismissValue | ||||
| import androidx.compose.material.SwipeToDismiss | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Bookmark | ||||
| import androidx.compose.material.icons.filled.Circle | ||||
| @@ -25,7 +17,6 @@ import androidx.compose.material.icons.outlined.Done | ||||
| import androidx.compose.material.icons.outlined.Download | ||||
| import androidx.compose.material.icons.outlined.FileDownloadOff | ||||
| import androidx.compose.material.icons.outlined.RemoveDone | ||||
| import androidx.compose.material.rememberDismissState | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.LocalContentColor | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| @@ -37,14 +28,18 @@ import androidx.compose.runtime.CompositionLocalProvider | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableIntStateOf | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.runtime.snapshotFlow | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.alpha | ||||
| import androidx.compose.ui.draw.clipToBounds | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.graphics.vector.ImageVector | ||||
| import androidx.compose.ui.hapticfeedback.HapticFeedbackType | ||||
| import androidx.compose.ui.platform.LocalDensity | ||||
| import androidx.compose.ui.platform.LocalHapticFeedback | ||||
| import androidx.compose.ui.platform.LocalViewConfiguration | ||||
| import androidx.compose.ui.platform.ViewConfiguration | ||||
| import androidx.compose.ui.res.stringResource | ||||
| @@ -53,10 +48,13 @@ import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import me.saket.swipe.SwipeableActionsBox | ||||
| import me.saket.swipe.rememberSwipeableActionsState | ||||
| import tachiyomi.domain.library.service.LibraryPreferences | ||||
| import tachiyomi.presentation.core.components.material.ReadItemAlpha | ||||
| import tachiyomi.presentation.core.components.material.SecondaryItemAlpha | ||||
| import tachiyomi.presentation.core.util.selectedBackground | ||||
| import kotlin.math.absoluteValue | ||||
|  | ||||
| @Composable | ||||
| fun MangaChapterListItem( | ||||
| @@ -71,13 +69,19 @@ fun MangaChapterListItem( | ||||
|     downloadIndicatorEnabled: Boolean, | ||||
|     downloadStateProvider: () -> Download.State, | ||||
|     downloadProgressProvider: () -> Int, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, | ||||
|     onLongClick: () -> Unit, | ||||
|     onClick: () -> Unit, | ||||
|     onDownloadClick: ((ChapterDownloadAction) -> Unit)?, | ||||
|     onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, | ||||
| ) { | ||||
|     val haptic = LocalHapticFeedback.current | ||||
|     val density = LocalDensity.current | ||||
|  | ||||
|     val textAlpha = if (read) ReadItemAlpha else 1f | ||||
|     val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha | ||||
|  | ||||
|     // Increase touch slop of swipe action to reduce accidental trigger | ||||
|     val configuration = LocalViewConfiguration.current | ||||
|     CompositionLocalProvider( | ||||
| @@ -85,247 +89,188 @@ fun MangaChapterListItem( | ||||
|             override val touchSlop: Float = configuration.touchSlop * 3f | ||||
|         }, | ||||
|     ) { | ||||
|         val textAlpha = if (read) ReadItemAlpha else 1f | ||||
|         val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha | ||||
|  | ||||
|         val chapterSwipeStartEnabled = chapterSwipeStartAction != LibraryPreferences.ChapterSwipeAction.Disabled | ||||
|         val chapterSwipeEndEnabled = chapterSwipeEndAction != LibraryPreferences.ChapterSwipeAction.Disabled | ||||
|  | ||||
|         val dismissState = rememberDismissState() | ||||
|         val dismissDirections = remember { mutableSetOf<DismissDirection>() } | ||||
|         var lastDismissDirection: DismissDirection? by remember { mutableStateOf(null) } | ||||
|         if (lastDismissDirection == null) { | ||||
|             if (chapterSwipeStartEnabled) { | ||||
|                 dismissDirections.add(DismissDirection.EndToStart) | ||||
|             } | ||||
|             if (chapterSwipeEndEnabled) { | ||||
|                 dismissDirections.add(DismissDirection.StartToEnd) | ||||
|             } | ||||
|         } | ||||
|         val animateDismissContentAlpha by animateFloatAsState( | ||||
|             label = "animateDismissContentAlpha", | ||||
|             targetValue = if (lastDismissDirection != null) 1f else 0f, | ||||
|             animationSpec = tween(durationMillis = if (lastDismissDirection != null) 500 else 0), | ||||
|             finishedListener = { | ||||
|                 lastDismissDirection = null | ||||
|             }, | ||||
|         val start = getSwipeAction( | ||||
|             action = chapterSwipeStartAction, | ||||
|             read = read, | ||||
|             bookmark = bookmark, | ||||
|             downloadState = downloadStateProvider(), | ||||
|             background = MaterialTheme.colorScheme.primaryContainer, | ||||
|             onSwipe = { onChapterSwipe(chapterSwipeStartAction) }, | ||||
|         ) | ||||
|         val dismissContentAlpha = if (lastDismissDirection != null) animateDismissContentAlpha else 1f | ||||
|         val backgroundColor = if (chapterSwipeEndEnabled && (dismissState.dismissDirection == DismissDirection.StartToEnd || lastDismissDirection == DismissDirection.StartToEnd)) { | ||||
|             MaterialTheme.colorScheme.primary | ||||
|         } else if (chapterSwipeStartEnabled && (dismissState.dismissDirection == DismissDirection.EndToStart || lastDismissDirection == DismissDirection.EndToStart)) { | ||||
|             MaterialTheme.colorScheme.primary | ||||
|         } else { | ||||
|             Color.Unspecified | ||||
|         val end = getSwipeAction( | ||||
|             action = chapterSwipeEndAction, | ||||
|             read = read, | ||||
|             bookmark = bookmark, | ||||
|             downloadState = downloadStateProvider(), | ||||
|             background = MaterialTheme.colorScheme.primaryContainer, | ||||
|             onSwipe = { onChapterSwipe(chapterSwipeEndAction) }, | ||||
|         ) | ||||
|  | ||||
|         val swipeableActionsState = rememberSwipeableActionsState() | ||||
|         LaunchedEffect(Unit) { | ||||
|             // Haptic effect when swipe over threshold | ||||
|             val swipeActionThresholdPx = with(density) { swipeActionThreshold.toPx() } | ||||
|             snapshotFlow { swipeableActionsState.offset.value.absoluteValue > swipeActionThresholdPx } | ||||
|                 .collect { if (it) haptic.performHapticFeedback(HapticFeedbackType.LongPress) } | ||||
|         } | ||||
|  | ||||
|         LaunchedEffect(dismissState.currentValue) { | ||||
|             when (dismissState.currentValue) { | ||||
|                 DismissValue.DismissedToEnd -> { | ||||
|                     lastDismissDirection = DismissDirection.StartToEnd | ||||
|                     val dismissDirectionsCopy = dismissDirections.toSet() | ||||
|                     dismissDirections.clear() | ||||
|                     onChapterSwipe(chapterSwipeEndAction) | ||||
|                     dismissState.snapTo(DismissValue.Default) | ||||
|                     dismissDirections.addAll(dismissDirectionsCopy) | ||||
|                 } | ||||
|                 DismissValue.DismissedToStart -> { | ||||
|                     lastDismissDirection = DismissDirection.EndToStart | ||||
|                     val dismissDirectionsCopy = dismissDirections.toSet() | ||||
|                     dismissDirections.clear() | ||||
|                     onChapterSwipe(chapterSwipeStartAction) | ||||
|                     dismissState.snapTo(DismissValue.Default) | ||||
|                     dismissDirections.addAll(dismissDirectionsCopy) | ||||
|                 } | ||||
|                 DismissValue.Default -> { } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         SwipeToDismiss( | ||||
|             state = dismissState, | ||||
|             directions = dismissDirections, | ||||
|             background = { | ||||
|                 Box( | ||||
|                     modifier = Modifier | ||||
|                         .fillMaxSize() | ||||
|                         .background(backgroundColor), | ||||
|         SwipeableActionsBox( | ||||
|             modifier = Modifier.clipToBounds(), | ||||
|             state = swipeableActionsState, | ||||
|             startActions = listOfNotNull(start), | ||||
|             endActions = listOfNotNull(end), | ||||
|             swipeThreshold = swipeActionThreshold, | ||||
|             backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest, | ||||
|         ) { | ||||
|             Row( | ||||
|                 modifier = modifier | ||||
|                     .selectedBackground(selected) | ||||
|                     .combinedClickable( | ||||
|                         onClick = onClick, | ||||
|                         onLongClick = onLongClick, | ||||
|                     ) | ||||
|                     .padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp), | ||||
|             ) { | ||||
|                 Column( | ||||
|                     modifier = Modifier.weight(1f), | ||||
|                     verticalArrangement = Arrangement.spacedBy(6.dp), | ||||
|                 ) { | ||||
|                     if (dismissState.dismissDirection in dismissDirections) { | ||||
|                         val downloadState = downloadStateProvider() | ||||
|                         SwipeBackgroundIcon( | ||||
|                             modifier = Modifier | ||||
|                                 .padding(start = 16.dp) | ||||
|                                 .align(Alignment.CenterStart) | ||||
|                                 .alpha( | ||||
|                                     if (dismissState.dismissDirection == DismissDirection.StartToEnd) 1f else 0f, | ||||
|                                 ), | ||||
|                             tint = contentColorFor(backgroundColor), | ||||
|                             swipeAction = chapterSwipeEndAction, | ||||
|                             read = read, | ||||
|                             bookmark = bookmark, | ||||
|                             downloadState = downloadState, | ||||
|                         ) | ||||
|                         SwipeBackgroundIcon( | ||||
|                             modifier = Modifier | ||||
|                                 .padding(end = 16.dp) | ||||
|                                 .align(Alignment.CenterEnd) | ||||
|                                 .alpha( | ||||
|                                     if (dismissState.dismissDirection == DismissDirection.EndToStart) 1f else 0f, | ||||
|                                 ), | ||||
|                             tint = contentColorFor(backgroundColor), | ||||
|                             swipeAction = chapterSwipeStartAction, | ||||
|                             read = read, | ||||
|                             bookmark = bookmark, | ||||
|                             downloadState = downloadState, | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             dismissContent = { | ||||
|                 Row( | ||||
|                     modifier = modifier | ||||
|                         .background( | ||||
|                             MaterialTheme.colorScheme.background.copy(dismissContentAlpha), | ||||
|                         ) | ||||
|                         .selectedBackground(selected) | ||||
|                         .alpha(dismissContentAlpha) | ||||
|                         .combinedClickable( | ||||
|                             onClick = onClick, | ||||
|                             onLongClick = onLongClick, | ||||
|                         ) | ||||
|                         .padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp), | ||||
|                 ) { | ||||
|                     Column( | ||||
|                         modifier = Modifier.weight(1f), | ||||
|                         verticalArrangement = Arrangement.spacedBy(6.dp), | ||||
|                     Row( | ||||
|                         horizontalArrangement = Arrangement.spacedBy(2.dp), | ||||
|                         verticalAlignment = Alignment.CenterVertically, | ||||
|                     ) { | ||||
|                         Row( | ||||
|                             horizontalArrangement = Arrangement.spacedBy(2.dp), | ||||
|                             verticalAlignment = Alignment.CenterVertically, | ||||
|                         ) { | ||||
|                             var textHeight by remember { mutableIntStateOf(0) } | ||||
|                             if (!read) { | ||||
|                                 Icon( | ||||
|                                     imageVector = Icons.Filled.Circle, | ||||
|                                     contentDescription = stringResource(R.string.unread), | ||||
|                                     modifier = Modifier | ||||
|                                         .height(8.dp) | ||||
|                                         .padding(end = 4.dp), | ||||
|                                     tint = MaterialTheme.colorScheme.primary, | ||||
|                                 ) | ||||
|                             } | ||||
|                             if (bookmark) { | ||||
|                                 Icon( | ||||
|                                     imageVector = Icons.Filled.Bookmark, | ||||
|                                     contentDescription = stringResource(R.string.action_filter_bookmarked), | ||||
|                                     modifier = Modifier | ||||
|                                         .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), | ||||
|                                     tint = MaterialTheme.colorScheme.primary, | ||||
|                                 ) | ||||
|                             } | ||||
|                             Text( | ||||
|                                 text = title, | ||||
|                                 style = MaterialTheme.typography.bodyMedium, | ||||
|                                 color = LocalContentColor.current.copy(alpha = textAlpha), | ||||
|                                 maxLines = 1, | ||||
|                                 overflow = TextOverflow.Ellipsis, | ||||
|                                 onTextLayout = { textHeight = it.size.height }, | ||||
|                         var textHeight by remember { mutableIntStateOf(0) } | ||||
|                         if (!read) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Filled.Circle, | ||||
|                                 contentDescription = stringResource(R.string.unread), | ||||
|                                 modifier = Modifier | ||||
|                                     .height(8.dp) | ||||
|                                     .padding(end = 4.dp), | ||||
|                                 tint = MaterialTheme.colorScheme.primary, | ||||
|                             ) | ||||
|                         } | ||||
|                         if (bookmark) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Filled.Bookmark, | ||||
|                                 contentDescription = stringResource(R.string.action_filter_bookmarked), | ||||
|                                 modifier = Modifier | ||||
|                                     .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), | ||||
|                                 tint = MaterialTheme.colorScheme.primary, | ||||
|                             ) | ||||
|                         } | ||||
|                         Text( | ||||
|                             text = title, | ||||
|                             style = MaterialTheme.typography.bodyMedium, | ||||
|                             color = LocalContentColor.current.copy(alpha = textAlpha), | ||||
|                             maxLines = 1, | ||||
|                             overflow = TextOverflow.Ellipsis, | ||||
|                             onTextLayout = { textHeight = it.size.height }, | ||||
|                         ) | ||||
|                     } | ||||
|  | ||||
|                         Row { | ||||
|                             ProvideTextStyle( | ||||
|                                 value = MaterialTheme.typography.bodyMedium.copy( | ||||
|                                     fontSize = 12.sp, | ||||
|                                     color = LocalContentColor.current.copy(alpha = textSubtitleAlpha), | ||||
|                                 ), | ||||
|                             ) { | ||||
|                                 if (date != null) { | ||||
|                                     Text( | ||||
|                                         text = date, | ||||
|                                         maxLines = 1, | ||||
|                                         overflow = TextOverflow.Ellipsis, | ||||
|                                     ) | ||||
|                                     if (readProgress != null || scanlator != null) DotSeparatorText() | ||||
|                                 } | ||||
|                                 if (readProgress != null) { | ||||
|                                     Text( | ||||
|                                         text = readProgress, | ||||
|                                         maxLines = 1, | ||||
|                                         overflow = TextOverflow.Ellipsis, | ||||
|                                         modifier = Modifier.alpha(ReadItemAlpha), | ||||
|                                     ) | ||||
|                                     if (scanlator != null) DotSeparatorText() | ||||
|                                 } | ||||
|                                 if (scanlator != null) { | ||||
|                                     Text( | ||||
|                                         text = scanlator, | ||||
|                                         maxLines = 1, | ||||
|                                         overflow = TextOverflow.Ellipsis, | ||||
|                                     ) | ||||
|                                 } | ||||
|                     Row { | ||||
|                         ProvideTextStyle( | ||||
|                             value = MaterialTheme.typography.bodyMedium.copy( | ||||
|                                 fontSize = 12.sp, | ||||
|                                 color = LocalContentColor.current.copy(alpha = textSubtitleAlpha), | ||||
|                             ), | ||||
|                         ) { | ||||
|                             if (date != null) { | ||||
|                                 Text( | ||||
|                                     text = date, | ||||
|                                     maxLines = 1, | ||||
|                                     overflow = TextOverflow.Ellipsis, | ||||
|                                 ) | ||||
|                                 if (readProgress != null || scanlator != null) DotSeparatorText() | ||||
|                             } | ||||
|                             if (readProgress != null) { | ||||
|                                 Text( | ||||
|                                     text = readProgress, | ||||
|                                     maxLines = 1, | ||||
|                                     overflow = TextOverflow.Ellipsis, | ||||
|                                     modifier = Modifier.alpha(ReadItemAlpha), | ||||
|                                 ) | ||||
|                                 if (scanlator != null) DotSeparatorText() | ||||
|                             } | ||||
|                             if (scanlator != null) { | ||||
|                                 Text( | ||||
|                                     text = scanlator, | ||||
|                                     maxLines = 1, | ||||
|                                     overflow = TextOverflow.Ellipsis, | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (onDownloadClick != null) { | ||||
|                         ChapterDownloadIndicator( | ||||
|                             enabled = downloadIndicatorEnabled, | ||||
|                             modifier = Modifier.padding(start = 4.dp), | ||||
|                             downloadStateProvider = downloadStateProvider, | ||||
|                             downloadProgressProvider = downloadProgressProvider, | ||||
|                             onClick = onDownloadClick, | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|                 if (onDownloadClick != null) { | ||||
|                     ChapterDownloadIndicator( | ||||
|                         enabled = downloadIndicatorEnabled, | ||||
|                         modifier = Modifier.padding(start = 4.dp), | ||||
|                         downloadStateProvider = downloadStateProvider, | ||||
|                         downloadProgressProvider = downloadProgressProvider, | ||||
|                         onClick = onDownloadClick, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun SwipeBackgroundIcon( | ||||
|     modifier: Modifier = Modifier, | ||||
|     tint: Color, | ||||
|     swipeAction: LibraryPreferences.ChapterSwipeAction, | ||||
| private fun getSwipeAction( | ||||
|     action: LibraryPreferences.ChapterSwipeAction, | ||||
|     read: Boolean, | ||||
|     bookmark: Boolean, | ||||
|     downloadState: Download.State, | ||||
| ) { | ||||
|     val imageVector = when (swipeAction) { | ||||
|         LibraryPreferences.ChapterSwipeAction.ToggleRead -> { | ||||
|             if (!read) { | ||||
|                 Icons.Outlined.Done | ||||
|             } else { | ||||
|                 Icons.Outlined.RemoveDone | ||||
|             } | ||||
|         } | ||||
|         LibraryPreferences.ChapterSwipeAction.ToggleBookmark -> { | ||||
|             if (!bookmark) { | ||||
|                 Icons.Outlined.BookmarkAdd | ||||
|             } else { | ||||
|                 Icons.Outlined.BookmarkRemove | ||||
|             } | ||||
|         } | ||||
|         LibraryPreferences.ChapterSwipeAction.Download -> { | ||||
|             when (downloadState) { | ||||
|                 Download.State.NOT_DOWNLOADED, | ||||
|                 Download.State.ERROR, | ||||
|                 -> { Icons.Outlined.Download } | ||||
|                 Download.State.QUEUE, | ||||
|                 Download.State.DOWNLOADING, | ||||
|                 -> { Icons.Outlined.FileDownloadOff } | ||||
|                 Download.State.DOWNLOADED -> { Icons.Outlined.Delete } | ||||
|             } | ||||
|         } | ||||
|     background: Color, | ||||
|     onSwipe: () -> Unit, | ||||
| ): me.saket.swipe.SwipeAction? { | ||||
|     return when (action) { | ||||
|         LibraryPreferences.ChapterSwipeAction.ToggleRead -> swipeAction( | ||||
|             icon = if (!read) Icons.Outlined.Done else Icons.Outlined.RemoveDone, | ||||
|             background = background, | ||||
|             isUndo = read, | ||||
|             onSwipe = onSwipe, | ||||
|         ) | ||||
|         LibraryPreferences.ChapterSwipeAction.ToggleBookmark -> swipeAction( | ||||
|             icon = if (!bookmark) Icons.Outlined.BookmarkAdd else Icons.Outlined.BookmarkRemove, | ||||
|             background = background, | ||||
|             isUndo = bookmark, | ||||
|             onSwipe = onSwipe, | ||||
|         ) | ||||
|         LibraryPreferences.ChapterSwipeAction.Download -> swipeAction( | ||||
|             icon = when (downloadState) { | ||||
|                 Download.State.NOT_DOWNLOADED, Download.State.ERROR -> Icons.Outlined.Download | ||||
|                 Download.State.QUEUE, Download.State.DOWNLOADING -> Icons.Outlined.FileDownloadOff | ||||
|                 Download.State.DOWNLOADED -> Icons.Outlined.Delete | ||||
|             }, | ||||
|             background = background, | ||||
|             onSwipe = onSwipe, | ||||
|         ) | ||||
|         LibraryPreferences.ChapterSwipeAction.Disabled -> null | ||||
|     } | ||||
|     imageVector?.let { | ||||
|         Icon( | ||||
|             modifier = modifier, | ||||
|             imageVector = imageVector, | ||||
|             tint = tint, | ||||
|             contentDescription = null, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| private fun swipeAction( | ||||
|     onSwipe: () -> Unit, | ||||
|     icon: ImageVector, | ||||
|     background: Color, | ||||
|     isUndo: Boolean = false, | ||||
| ): me.saket.swipe.SwipeAction { | ||||
|     return me.saket.swipe.SwipeAction( | ||||
|         icon = { | ||||
|             Icon( | ||||
|                 modifier = Modifier.padding(16.dp), | ||||
|                 imageVector = icon, | ||||
|                 tint = contentColorFor(background), | ||||
|                 contentDescription = null, | ||||
|             ) | ||||
|         }, | ||||
|         background = background, | ||||
|         onSwipe = onSwipe, | ||||
|         isUndo = isUndo, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| private val swipeActionThreshold = 56.dp | ||||
|   | ||||
| @@ -2,13 +2,11 @@ package eu.kanade.presentation.manga.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.ArrowBack | ||||
| import androidx.compose.material.icons.outlined.Close | ||||
| import androidx.compose.material.icons.outlined.Download | ||||
| import androidx.compose.material.icons.outlined.FilterList | ||||
| import androidx.compose.material.icons.outlined.FlipToBack | ||||
| import androidx.compose.material.icons.outlined.SelectAll | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.LocalContentColor | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| @@ -29,6 +27,7 @@ import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.AppBarActions | ||||
| import eu.kanade.presentation.components.DownloadDropdownMenu | ||||
| import eu.kanade.presentation.components.UpIcon | ||||
| import eu.kanade.presentation.manga.DownloadAction | ||||
| import eu.kanade.tachiyomi.R | ||||
| import tachiyomi.presentation.core.theme.active | ||||
| @@ -67,10 +66,7 @@ fun MangaToolbar( | ||||
|             }, | ||||
|             navigationIcon = { | ||||
|                 IconButton(onClick = onBackClicked) { | ||||
|                     Icon( | ||||
|                         imageVector = if (isActionMode) Icons.Outlined.Close else Icons.Outlined.ArrowBack, | ||||
|                         contentDescription = stringResource(R.string.abc_action_bar_up_description), | ||||
|                     ) | ||||
|                     UpIcon(Icons.Outlined.Close.takeIf { isActionMode }) | ||||
|                 } | ||||
|             }, | ||||
|             actions = { | ||||
|   | ||||
| @@ -2,15 +2,12 @@ package eu.kanade.presentation.more.settings | ||||
|  | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.foundation.layout.RowScope | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.ArrowBack | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TopAppBar | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.presentation.components.UpIcon | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
|  | ||||
| @Composable | ||||
| @@ -27,10 +24,7 @@ fun PreferenceScaffold( | ||||
|                 navigationIcon = { | ||||
|                     if (onBackPressed != null) { | ||||
|                         IconButton(onClick = onBackPressed) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Outlined.ArrowBack, | ||||
|                                 contentDescription = stringResource(R.string.abc_action_bar_up_description), | ||||
|                             ) | ||||
|                             UpIcon() | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|   | ||||
| @@ -28,6 +28,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.presentation.more.settings.Preference | ||||
| import eu.kanade.presentation.more.settings.screen.advanced.ClearDatabaseScreen | ||||
| import eu.kanade.presentation.more.settings.screen.debug.DebugInfoScreen | ||||
| import eu.kanade.presentation.util.collectAsState | ||||
| import eu.kanade.tachiyomi.R | ||||
|   | ||||
| @@ -281,8 +281,8 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|             title = stringResource(R.string.pref_chapter_swipe), | ||||
|             preferenceItems = listOf( | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     pref = libraryPreferences.swipeEndAction(), | ||||
|                     title = stringResource(R.string.pref_chapter_swipe_end), | ||||
|                     pref = libraryPreferences.swipeToStartAction(), | ||||
|                     title = stringResource(R.string.pref_chapter_swipe_start), | ||||
|                     entries = mapOf( | ||||
|                         LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.disabled), | ||||
|                         LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark), | ||||
| @@ -291,8 +291,8 @@ object SettingsLibraryScreen : SearchableSettings { | ||||
|                     ), | ||||
|                 ), | ||||
|                 Preference.PreferenceItem.ListPreference( | ||||
|                     pref = libraryPreferences.swipeStartAction(), | ||||
|                     title = stringResource(R.string.pref_chapter_swipe_start), | ||||
|                     pref = libraryPreferences.swipeToEndAction(), | ||||
|                     title = stringResource(R.string.pref_chapter_swipe_end), | ||||
|                     entries = mapOf( | ||||
|                         LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.disabled), | ||||
|                         LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark), | ||||
|   | ||||
| @@ -4,11 +4,11 @@ import androidx.annotation.StringRes | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.isSystemInDarkTheme | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.itemsIndexed | ||||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.ArrowBack | ||||
| import androidx.compose.material.icons.outlined.ChromeReaderMode | ||||
| import androidx.compose.material.icons.outlined.Code | ||||
| import androidx.compose.material.icons.outlined.CollectionsBookmark | ||||
| @@ -20,7 +20,6 @@ import androidx.compose.material.icons.outlined.Search | ||||
| import androidx.compose.material.icons.outlined.Security | ||||
| import androidx.compose.material.icons.outlined.SettingsBackupRestore | ||||
| import androidx.compose.material.icons.outlined.Sync | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.LocalContentColor | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| @@ -45,11 +44,12 @@ import cafe.adriel.voyager.navigator.Navigator | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.AppBarActions | ||||
| import eu.kanade.presentation.components.UpIcon | ||||
| import eu.kanade.presentation.more.settings.screen.about.AboutScreen | ||||
| import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget | ||||
| import eu.kanade.presentation.util.LocalBackPress | ||||
| import eu.kanade.presentation.util.Screen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import tachiyomi.presentation.core.components.LazyColumn | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import cafe.adriel.voyager.core.screen.Screen as VoyagerScreen | ||||
|  | ||||
| @@ -94,10 +94,7 @@ object SettingsMainScreen : Screen() { | ||||
|                     }, | ||||
|                     navigationIcon = { | ||||
|                         IconButton(onClick = backPress::invoke) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Outlined.ArrowBack, | ||||
|                                 contentDescription = stringResource(R.string.abc_action_bar_up_description), | ||||
|                             ) | ||||
|                             UpIcon() | ||||
|                         } | ||||
|                     }, | ||||
|                     actions = { | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package eu.kanade.presentation.more.settings.screen | ||||
|  | ||||
| import android.content.res.Resources | ||||
| import androidx.compose.animation.Crossfade | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Column | ||||
| @@ -17,7 +16,6 @@ import androidx.compose.foundation.text.BasicTextField | ||||
| import androidx.compose.foundation.text.KeyboardActions | ||||
| import androidx.compose.foundation.text.KeyboardOptions | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.ArrowBack | ||||
| import androidx.compose.material.icons.outlined.Close | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| @@ -40,19 +38,21 @@ import androidx.compose.ui.focus.FocusRequester | ||||
| import androidx.compose.ui.focus.focusRequester | ||||
| import androidx.compose.ui.graphics.SolidColor | ||||
| import androidx.compose.ui.platform.LocalFocusManager | ||||
| import androidx.compose.ui.platform.LocalLayoutDirection | ||||
| import androidx.compose.ui.platform.LocalSoftwareKeyboardController | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.input.ImeAction | ||||
| import androidx.compose.ui.text.input.TextFieldValue | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.LayoutDirection | ||||
| import androidx.compose.ui.unit.dp | ||||
| import cafe.adriel.voyager.navigator.LocalNavigator | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import eu.kanade.presentation.components.UpIcon | ||||
| import eu.kanade.presentation.more.settings.Preference | ||||
| import eu.kanade.presentation.util.Screen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.system.isLTR | ||||
| import tachiyomi.presentation.core.components.material.Divider | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
| @@ -97,11 +97,7 @@ class SettingsSearchScreen : Screen() { | ||||
|                             val canPop = remember { navigator.canPop } | ||||
|                             if (canPop) { | ||||
|                                 IconButton(onClick = navigator::pop) { | ||||
|                                     Icon( | ||||
|                                         imageVector = Icons.Outlined.ArrowBack, | ||||
|                                         contentDescription = stringResource(R.string.abc_action_bar_up_description), | ||||
|                                         tint = MaterialTheme.colorScheme.onSurfaceVariant, | ||||
|                                     ) | ||||
|                                     UpIcon() | ||||
|                                 } | ||||
|                             } | ||||
|                         }, | ||||
| @@ -169,6 +165,8 @@ private fun SearchResult( | ||||
| ) { | ||||
|     if (searchKey.isEmpty()) return | ||||
|  | ||||
|     val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr | ||||
|  | ||||
|     val index = getIndex() | ||||
|     val result by produceState<List<SearchResultItem>?>(initialValue = null, searchKey) { | ||||
|         value = index.asSequence() | ||||
| @@ -204,7 +202,7 @@ private fun SearchResult( | ||||
|                         SearchResultItem( | ||||
|                             route = settingsData.route, | ||||
|                             title = p.title, | ||||
|                             breadcrumbs = getLocalizedBreadcrumb(path = settingsData.title, node = categoryTitle), | ||||
|                             breadcrumbs = getLocalizedBreadcrumb(path = settingsData.title, node = categoryTitle, isLtr = isLtr), | ||||
|                             highlightKey = p.title, | ||||
|                         ) | ||||
|                     } | ||||
| @@ -269,11 +267,11 @@ private fun getIndex() = settingScreens | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| private fun getLocalizedBreadcrumb(path: String, node: String?): String { | ||||
| private fun getLocalizedBreadcrumb(path: String, node: String?, isLtr: Boolean): String { | ||||
|     return if (node == null) { | ||||
|         path | ||||
|     } else { | ||||
|         if (Resources.getSystem().isLTR) { | ||||
|         if (isLtr) { | ||||
|             // This locale reads left to right. | ||||
|             "$path > $node" | ||||
|         } else { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| package eu.kanade.presentation.more.settings.screen | ||||
| package eu.kanade.presentation.more.settings.screen.about | ||||
| 
 | ||||
| import android.content.Context | ||||
| import androidx.compose.animation.AnimatedVisibility | ||||
| @@ -1,4 +1,4 @@ | ||||
| package eu.kanade.presentation.more.settings.screen | ||||
| package eu.kanade.presentation.more.settings.screen.about | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.padding | ||||
| @@ -1,4 +1,4 @@ | ||||
| package eu.kanade.presentation.more.settings.screen | ||||
| package eu.kanade.presentation.more.settings.screen.about | ||||
| 
 | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| @@ -1,4 +1,4 @@ | ||||
| package eu.kanade.presentation.more.settings.screen | ||||
| package eu.kanade.presentation.more.settings.screen.advanced | ||||
| 
 | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Column | ||||
| @@ -12,7 +12,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import eu.kanade.presentation.more.settings.Preference | ||||
| import eu.kanade.presentation.more.settings.PreferenceScaffold | ||||
| import eu.kanade.presentation.more.settings.screen.AboutScreen | ||||
| import eu.kanade.presentation.more.settings.screen.about.AboutScreen | ||||
| import eu.kanade.presentation.util.Screen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.system.DeviceUtil | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import android.content.Context | ||||
| import androidx.compose.foundation.horizontalScroll | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.rememberScrollState | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.ArrowBack | ||||
| @@ -39,7 +40,6 @@ import eu.kanade.tachiyomi.util.system.workManager | ||||
| import kotlinx.coroutines.flow.SharingStarted | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.stateIn | ||||
| import tachiyomi.presentation.core.components.LazyColumn | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.util.plus | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.defaultMinSize | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.itemsIndexed | ||||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||||
| import androidx.compose.material.icons.Icons | ||||
| @@ -28,7 +29,6 @@ import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.tachiyomi.R | ||||
| import tachiyomi.presentation.core.components.LazyColumn | ||||
| import tachiyomi.presentation.core.components.material.Divider | ||||
| import tachiyomi.presentation.core.util.isScrolledToEnd | ||||
| import tachiyomi.presentation.core.util.isScrolledToStart | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package eu.kanade.presentation.more.stats | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.CollectionsBookmark | ||||
| @@ -19,7 +20,6 @@ import eu.kanade.presentation.more.stats.components.StatsSection | ||||
| import eu.kanade.presentation.more.stats.data.StatsData | ||||
| import eu.kanade.presentation.util.toDurationString | ||||
| import eu.kanade.tachiyomi.R | ||||
| import tachiyomi.presentation.core.components.LazyColumn | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import java.util.Locale | ||||
| import kotlin.time.DurationUnit | ||||
|   | ||||
| @@ -0,0 +1,158 @@ | ||||
| package eu.kanade.presentation.reader.settings | ||||
|  | ||||
| import android.os.Build | ||||
| import androidx.compose.foundation.layout.ColumnScope | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.core.graphics.alpha | ||||
| import androidx.core.graphics.blue | ||||
| import androidx.core.graphics.green | ||||
| import androidx.core.graphics.red | ||||
| import eu.kanade.presentation.util.collectAsState | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel | ||||
| import tachiyomi.core.preference.getAndSet | ||||
| import tachiyomi.presentation.core.components.CheckboxItem | ||||
| import tachiyomi.presentation.core.components.SelectItem | ||||
| import tachiyomi.presentation.core.components.SliderItem | ||||
|  | ||||
| @Composable | ||||
| internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) { | ||||
|     val colorFilterModes = buildList { | ||||
|         addAll( | ||||
|             listOf( | ||||
|                 R.string.label_default, | ||||
|                 R.string.filter_mode_multiply, | ||||
|                 R.string.filter_mode_screen, | ||||
|             ), | ||||
|         ) | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { | ||||
|             addAll( | ||||
|                 listOf( | ||||
|                     R.string.filter_mode_overlay, | ||||
|                     R.string.filter_mode_lighten, | ||||
|                     R.string.filter_mode_darken, | ||||
|                 ), | ||||
|             ) | ||||
|         } | ||||
|     }.map { stringResource(it) } | ||||
|  | ||||
|     val customBrightness by screenModel.preferences.customBrightness().collectAsState() | ||||
|     CheckboxItem( | ||||
|         label = stringResource(R.string.pref_custom_brightness), | ||||
|         checked = customBrightness, | ||||
|         onClick = { | ||||
|             screenModel.togglePreference(ReaderPreferences::customBrightness) | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Sets the brightness of the screen. Range is [-75, 100]. | ||||
|      * From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness. | ||||
|      * From 1 to 100 it sets that value as brightness. | ||||
|      * 0 sets system brightness and hides the overlay. | ||||
|      */ | ||||
|     if (customBrightness) { | ||||
|         val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState() | ||||
|         SliderItem( | ||||
|             label = stringResource(R.string.pref_custom_brightness), | ||||
|             min = -75, | ||||
|             max = 100, | ||||
|             value = customBrightnessValue, | ||||
|             valueText = customBrightnessValue.toString(), | ||||
|             onChange = { screenModel.preferences.customBrightnessValue().set(it) }, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     val colorFilter by screenModel.preferences.colorFilter().collectAsState() | ||||
|     CheckboxItem( | ||||
|         label = stringResource(R.string.pref_custom_color_filter), | ||||
|         checked = colorFilter, | ||||
|         onClick = { | ||||
|             screenModel.togglePreference(ReaderPreferences::colorFilter) | ||||
|         }, | ||||
|     ) | ||||
|     if (colorFilter) { | ||||
|         val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState() | ||||
|         SliderItem( | ||||
|             label = stringResource(R.string.color_filter_r_value), | ||||
|             max = 255, | ||||
|             value = colorFilterValue.red, | ||||
|             valueText = colorFilterValue.red.toString(), | ||||
|             onChange = { newRValue -> | ||||
|                 screenModel.preferences.colorFilterValue().getAndSet { | ||||
|                     getColorValue(it, newRValue, RED_MASK, 16) | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|         SliderItem( | ||||
|             label = stringResource(R.string.color_filter_g_value), | ||||
|             max = 255, | ||||
|             value = colorFilterValue.green, | ||||
|             valueText = colorFilterValue.green.toString(), | ||||
|             onChange = { newGValue -> | ||||
|                 screenModel.preferences.colorFilterValue().getAndSet { | ||||
|                     getColorValue(it, newGValue, GREEN_MASK, 8) | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|         SliderItem( | ||||
|             label = stringResource(R.string.color_filter_b_value), | ||||
|             max = 255, | ||||
|             value = colorFilterValue.blue, | ||||
|             valueText = colorFilterValue.blue.toString(), | ||||
|             onChange = { newBValue -> | ||||
|                 screenModel.preferences.colorFilterValue().getAndSet { | ||||
|                     getColorValue(it, newBValue, BLUE_MASK, 0) | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|         SliderItem( | ||||
|             label = stringResource(R.string.color_filter_a_value), | ||||
|             max = 255, | ||||
|             value = colorFilterValue.alpha, | ||||
|             valueText = colorFilterValue.alpha.toString(), | ||||
|             onChange = { newAValue -> | ||||
|                 screenModel.preferences.colorFilterValue().getAndSet { | ||||
|                     getColorValue(it, newAValue, ALPHA_MASK, 24) | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState() | ||||
|         SelectItem( | ||||
|             label = stringResource(R.string.pref_color_filter_mode), | ||||
|             options = colorFilterModes.toTypedArray(), | ||||
|             selectedIndex = colorFilterMode, | ||||
|         ) { | ||||
|             screenModel.preferences.colorFilterMode().set(it) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     val grayscale by screenModel.preferences.grayscale().collectAsState() | ||||
|     CheckboxItem( | ||||
|         label = stringResource(R.string.pref_grayscale), | ||||
|         checked = grayscale, | ||||
|         onClick = { | ||||
|             screenModel.togglePreference(ReaderPreferences::grayscale) | ||||
|         }, | ||||
|     ) | ||||
|     val invertedColors by screenModel.preferences.invertedColors().collectAsState() | ||||
|     CheckboxItem( | ||||
|         label = stringResource(R.string.pref_inverted_colors), | ||||
|         checked = invertedColors, | ||||
|         onClick = { | ||||
|             screenModel.togglePreference(ReaderPreferences::invertedColors) | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| private fun getColorValue(currentColor: Int, color: Int, mask: Long, bitShift: Int): Int { | ||||
|     return (color shl bitShift) or (currentColor and mask.inv().toInt()) | ||||
| } | ||||
| private const val ALPHA_MASK: Long = 0xFF000000 | ||||
| private const val RED_MASK: Long = 0x00FF0000 | ||||
| private const val GREEN_MASK: Long = 0x0000FF00 | ||||
| private const val BLUE_MASK: Long = 0x000000FF | ||||
| @@ -0,0 +1,96 @@ | ||||
| package eu.kanade.presentation.reader.settings | ||||
|  | ||||
| import androidx.compose.foundation.layout.ColumnScope | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import eu.kanade.presentation.util.collectAsState | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel | ||||
| import tachiyomi.presentation.core.components.CheckboxItem | ||||
| import tachiyomi.presentation.core.components.HeadingItem | ||||
| import tachiyomi.presentation.core.components.RadioItem | ||||
|  | ||||
| @Composable | ||||
| internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) { | ||||
|     // TODO: show this in a nicer way | ||||
|     HeadingItem(stringResource(R.string.pref_reader_theme)) | ||||
|     val readerTheme by screenModel.preferences.readerTheme().collectAsState() | ||||
|     listOf( | ||||
|         R.string.black_background to 1, | ||||
|         R.string.gray_background to 2, | ||||
|         R.string.white_background to 0, | ||||
|         R.string.automatic_background to 3, | ||||
|     ).map { (titleRes, theme) -> | ||||
|         RadioItem( | ||||
|             label = stringResource(titleRes), | ||||
|             selected = readerTheme == theme, | ||||
|             onClick = { screenModel.preferences.readerTheme().set(theme) }, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     val showPageNumber by screenModel.preferences.showPageNumber().collectAsState() | ||||
|     CheckboxItem( | ||||
|         label = stringResource(R.string.pref_show_page_number), | ||||
|         checked = showPageNumber, | ||||
|         onClick = { | ||||
|             screenModel.togglePreference(ReaderPreferences::showPageNumber) | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     val fullscreen by screenModel.preferences.fullscreen().collectAsState() | ||||
|     CheckboxItem( | ||||
|         label = stringResource(R.string.pref_fullscreen), | ||||
|         checked = fullscreen, | ||||
|         onClick = { | ||||
|             screenModel.togglePreference(ReaderPreferences::fullscreen) | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     // TODO: hide if there's no cutout | ||||
|     val cutoutShort by screenModel.preferences.cutoutShort().collectAsState() | ||||
|     CheckboxItem( | ||||
|         label = stringResource(R.string.pref_cutout_short), | ||||
|         checked = cutoutShort, | ||||
|         onClick = { | ||||
|             screenModel.togglePreference(ReaderPreferences::cutoutShort) | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     val keepScreenOn by screenModel.preferences.keepScreenOn().collectAsState() | ||||
|     CheckboxItem( | ||||
|         label = stringResource(R.string.pref_keep_screen_on), | ||||
|         checked = keepScreenOn, | ||||
|         onClick = { | ||||
|             screenModel.togglePreference(ReaderPreferences::keepScreenOn) | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     val readWithLongTap by screenModel.preferences.readWithLongTap().collectAsState() | ||||
|     CheckboxItem( | ||||
|         label = stringResource(R.string.pref_read_with_long_tap), | ||||
|         checked = readWithLongTap, | ||||
|         onClick = { | ||||
|             screenModel.togglePreference(ReaderPreferences::readWithLongTap) | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     val alwaysShowChapterTransition by screenModel.preferences.alwaysShowChapterTransition().collectAsState() | ||||
|     CheckboxItem( | ||||
|         label = stringResource(R.string.pref_always_show_chapter_transition), | ||||
|         checked = alwaysShowChapterTransition, | ||||
|         onClick = { | ||||
|             screenModel.togglePreference(ReaderPreferences::alwaysShowChapterTransition) | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     val pageTransitions by screenModel.preferences.pageTransitions().collectAsState() | ||||
|     CheckboxItem( | ||||
|         label = stringResource(R.string.pref_page_transitions), | ||||
|         checked = pageTransitions, | ||||
|         onClick = { | ||||
|             screenModel.togglePreference(ReaderPreferences::pageTransitions) | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| package eu.kanade.presentation.reader.settings | ||||
|  | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.pager.rememberPagerState | ||||
| import androidx.compose.foundation.rememberScrollState | ||||
| import androidx.compose.foundation.verticalScroll | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalView | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.window.DialogWindowProvider | ||||
| import eu.kanade.presentation.components.TabbedDialog | ||||
| import eu.kanade.presentation.components.TabbedDialogPaddings | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel | ||||
|  | ||||
| @Composable | ||||
| fun ReaderSettingsDialog( | ||||
|     onDismissRequest: () -> Unit, | ||||
|     onShowMenus: () -> Unit, | ||||
|     onHideMenus: () -> Unit, | ||||
|     screenModel: ReaderSettingsScreenModel, | ||||
| ) { | ||||
|     // TODO: undimming doesn't seem to work | ||||
|     val window = (LocalView.current.parent as? DialogWindowProvider)?.window | ||||
|  | ||||
|     val tabTitles = listOf( | ||||
|         stringResource(R.string.pref_category_reading_mode), | ||||
|         stringResource(R.string.pref_category_general), | ||||
|         stringResource(R.string.custom_filter), | ||||
|     ) | ||||
|     val pagerState = rememberPagerState { tabTitles.size } | ||||
|  | ||||
|     LaunchedEffect(pagerState.currentPage) { | ||||
|         if (pagerState.currentPage == 2) { | ||||
|             window?.setDimAmount(0f) | ||||
|             onHideMenus() | ||||
|         } else { | ||||
|             window?.setDimAmount(0.75f) | ||||
|             onShowMenus() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     TabbedDialog( | ||||
|         onDismissRequest = { | ||||
|             onDismissRequest() | ||||
|             onShowMenus() | ||||
|         }, | ||||
|         tabTitles = tabTitles, | ||||
|         pagerState = pagerState, | ||||
|     ) { page -> | ||||
|         Column( | ||||
|             modifier = Modifier | ||||
|                 .padding(vertical = TabbedDialogPaddings.Vertical) | ||||
|                 .verticalScroll(rememberScrollState()), | ||||
|         ) { | ||||
|             when (page) { | ||||
|                 0 -> ReadingModePage(screenModel) | ||||
|                 1 -> GeneralPage(screenModel) | ||||
|                 2 -> ColorFilterPage(screenModel) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,10 @@ | ||||
| package eu.kanade.presentation.reader.settings | ||||
|  | ||||
| import androidx.compose.foundation.layout.ColumnScope | ||||
| import androidx.compose.runtime.Composable | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel | ||||
|  | ||||
| @Composable | ||||
| internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) { | ||||
|     // TODO | ||||
| } | ||||
| @@ -28,7 +28,6 @@ import eu.kanade.domain.ui.UiPreferences | ||||
| import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode | ||||
| import eu.kanade.tachiyomi.crash.CrashActivity | ||||
| import eu.kanade.tachiyomi.crash.GlobalExceptionHandler | ||||
| import eu.kanade.tachiyomi.data.cache.ChapterCache | ||||
| import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher | ||||
| import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer | ||||
| import eu.kanade.tachiyomi.data.coil.MangaKeyer | ||||
| @@ -54,7 +53,6 @@ import org.acra.ktx.initAcra | ||||
| import org.acra.sender.HttpSender | ||||
| import org.conscrypt.Conscrypt | ||||
| import tachiyomi.core.util.system.logcat | ||||
| import tachiyomi.domain.library.service.LibraryPreferences | ||||
| import tachiyomi.presentation.widget.TachiyomiWidgetManager | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| @@ -64,11 +62,9 @@ import java.security.Security | ||||
| class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { | ||||
|  | ||||
|     private val basePreferences: BasePreferences by injectLazy() | ||||
|     private val libraryPreferences: LibraryPreferences by injectLazy() | ||||
|     private val networkPreferences: NetworkPreferences by injectLazy() | ||||
|  | ||||
|     private val disableIncognitoReceiver = DisableIncognitoReceiver() | ||||
|     private val chapterCache: ChapterCache by injectLazy() | ||||
|  | ||||
|     @SuppressLint("LaunchActivityFromNotification") | ||||
|     override fun onCreate() { | ||||
| @@ -172,10 +168,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { | ||||
|  | ||||
|     override fun onStop(owner: LifecycleOwner) { | ||||
|         SecureActivityDelegate.onApplicationStopped() | ||||
|  | ||||
|         if (libraryPreferences.autoClearChapterCache().get()) { | ||||
|             chapterCache.clear() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun getPackageName(): String { | ||||
|   | ||||
| @@ -21,12 +21,11 @@ import eu.kanade.tachiyomi.util.system.isReleaseBuildType | ||||
| 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.getEnum | ||||
| import tachiyomi.domain.backup.service.BackupPreferences | ||||
| import tachiyomi.domain.library.service.LibraryPreferences | ||||
| import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED | ||||
| import tachiyomi.domain.manga.model.TriStateFilter | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.io.File | ||||
|  | ||||
| object Migrations { | ||||
| @@ -350,12 +349,12 @@ object Migrations { | ||||
|                         remove(key) | ||||
|  | ||||
|                         val newValue = when (pref.get()) { | ||||
|                             1 -> TriStateFilter.ENABLED_IS | ||||
|                             2 -> TriStateFilter.ENABLED_NOT | ||||
|                             else -> TriStateFilter.DISABLED | ||||
|                             1 -> TriState.ENABLED_IS | ||||
|                             2 -> TriState.ENABLED_NOT | ||||
|                             else -> TriState.DISABLED | ||||
|                         } | ||||
|  | ||||
|                         preferenceStore.getEnum("${key}_v2", TriStateFilter.DISABLED).set(newValue) | ||||
|                         preferenceStore.getEnum("${key}_v2", TriState.DISABLED).set(newValue) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -20,7 +20,7 @@ data class BackupChapter( | ||||
|     // chapterNumber is called number is 1.x | ||||
|     @ProtoNumber(9) var chapterNumber: Float = 0F, | ||||
|     @ProtoNumber(10) var sourceOrder: Long = 0, | ||||
|     @ProtoNumber(11) var lastModifiedAt: Long? = null, | ||||
|     @ProtoNumber(11) var lastModifiedAt: Long = 0, | ||||
| ) { | ||||
|     fun toChapterImpl(): Chapter { | ||||
|         return Chapter.create().copy( | ||||
| @@ -39,7 +39,7 @@ data class BackupChapter( | ||||
|     } | ||||
| } | ||||
|  | ||||
| val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Float, source_order: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long? -> | ||||
| val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Float, source_order: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long -> | ||||
|     BackupChapter( | ||||
|         url = url, | ||||
|         name = name, | ||||
|   | ||||
| @@ -39,7 +39,8 @@ data class BackupManga( | ||||
|     @ProtoNumber(103) var viewer_flags: Int? = null, | ||||
|     @ProtoNumber(104) var history: List<BackupHistory> = emptyList(), | ||||
|     @ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE, | ||||
|     @ProtoNumber(106) var lastModifiedAt: Long? = 0, | ||||
|     @ProtoNumber(106) var lastModifiedAt: Long = 0, | ||||
|     @ProtoNumber(107) var favoriteModifiedAt: Long? = null, | ||||
| ) { | ||||
|     fun getMangaImpl(): Manga { | ||||
|         return Manga.create().copy( | ||||
| @@ -58,6 +59,7 @@ data class BackupManga( | ||||
|             chapterFlags = this@BackupManga.chapterFlags.toLong(), | ||||
|             updateStrategy = this@BackupManga.updateStrategy, | ||||
|             lastModifiedAt = this@BackupManga.lastModifiedAt, | ||||
|             favoriteModifiedAt = this@BackupManga.favoriteModifiedAt, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
| @@ -92,6 +94,7 @@ data class BackupManga( | ||||
|                 chapterFlags = manga.chapterFlags.toInt(), | ||||
|                 updateStrategy = manga.updateStrategy, | ||||
|                 lastModifiedAt = manga.lastModifiedAt, | ||||
|                 favoriteModifiedAt = manga.favoriteModifiedAt, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -20,7 +20,7 @@ interface Chapter : SChapter, Serializable { | ||||
|  | ||||
|     var source_order: Int | ||||
|  | ||||
|     var last_modified: Long? | ||||
|     var last_modified: Long | ||||
| } | ||||
|  | ||||
| fun Chapter.toDomainChapter(): DomainChapter? { | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class ChapterImpl : Chapter { | ||||
|  | ||||
|     override var source_order: Int = 0 | ||||
|  | ||||
|     override var last_modified: Long? = null | ||||
|     override var last_modified: Long = 0 | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|   | ||||
| @@ -7,9 +7,6 @@ import android.view.View | ||||
| import androidx.appcompat.view.ContextThemeWrapper | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.ArrowBack | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TopAppBar | ||||
| @@ -21,7 +18,6 @@ import androidx.compose.runtime.saveable.rememberSaveable | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.viewinterop.AndroidView | ||||
| import androidx.core.os.bundleOf | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| @@ -38,6 +34,7 @@ import androidx.preference.forEach | ||||
| import androidx.preference.getOnBindEditTextListener | ||||
| import cafe.adriel.voyager.navigator.LocalNavigator | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import eu.kanade.presentation.components.UpIcon | ||||
| import eu.kanade.presentation.util.Screen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore | ||||
| @@ -62,10 +59,7 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() { | ||||
|                     title = { Text(text = Injekt.get<SourceManager>().getOrStub(sourceId).toString()) }, | ||||
|                     navigationIcon = { | ||||
|                         IconButton(onClick = navigator::pop) { | ||||
|                             Icon( | ||||
|                                 imageVector = Icons.Outlined.ArrowBack, | ||||
|                                 contentDescription = stringResource(R.string.abc_action_bar_up_description), | ||||
|                             ) | ||||
|                             UpIcon() | ||||
|                         } | ||||
|                     }, | ||||
|                     scrollBehavior = it, | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.migration | ||||
| import eu.kanade.domain.manga.model.hasCustomCover | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.data.download.DownloadCache | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| @@ -12,15 +13,18 @@ import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| object MigrationFlags { | ||||
|  | ||||
|     private const val CHAPTERS = 0b0001 | ||||
|     private const val CATEGORIES = 0b0010 | ||||
|     private const val TRACK = 0b0100 | ||||
|     private const val CUSTOM_COVER = 0b1000 | ||||
|     private const val CHAPTERS = 0b00001 | ||||
|     private const val CATEGORIES = 0b00010 | ||||
|     private const val TRACK = 0b00100 | ||||
|     private const val CUSTOM_COVER = 0b01000 | ||||
|     private const val DELETE_DOWNLOADED = 0b10000 | ||||
|  | ||||
|     private val coverCache: CoverCache by injectLazy() | ||||
|     private val getTracks: GetTracks = Injekt.get() | ||||
|     private val downloadCache: DownloadCache by injectLazy() | ||||
|  | ||||
|     val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER) | ||||
|     val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER, DELETE_DOWNLOADED) | ||||
|     private var enableFlags = emptyList<Int>().toMutableList() | ||||
|  | ||||
|     fun hasChapters(value: Int): Boolean { | ||||
|         return value and CHAPTERS != 0 | ||||
| @@ -38,23 +42,36 @@ object MigrationFlags { | ||||
|         return value and CUSTOM_COVER != 0 | ||||
|     } | ||||
|  | ||||
|     fun hasDeleteDownloaded(value: Int): Boolean { | ||||
|         return value and DELETE_DOWNLOADED != 0 | ||||
|     } | ||||
|  | ||||
|     fun getEnabledFlagsPositions(value: Int): List<Int> { | ||||
|         return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null } | ||||
|     } | ||||
|  | ||||
|     fun getFlagsFromPositions(positions: Array<Int>): Int { | ||||
|         return positions.fold(0) { accumulated, position -> accumulated or (1 shl position) } | ||||
|         val fold = positions.fold(0) { accumulated, position -> accumulated or enableFlags[position] } | ||||
|         enableFlags.clear() | ||||
|         return fold | ||||
|     } | ||||
|  | ||||
|     fun titles(manga: Manga?): Array<Int> { | ||||
|         enableFlags.add(CHAPTERS) | ||||
|         enableFlags.add(CATEGORIES) | ||||
|         val titles = arrayOf(R.string.chapters, R.string.categories).toMutableList() | ||||
|         if (manga != null) { | ||||
|             if (runBlocking { getTracks.await(manga.id) }.isNotEmpty()) { | ||||
|                 titles.add(R.string.track) | ||||
|                 enableFlags.add(TRACK) | ||||
|             } | ||||
|  | ||||
|             if (manga.hasCustomCover(coverCache)) { | ||||
|                 titles.add(R.string.custom_cover) | ||||
|                 enableFlags.add(CUSTOM_COVER) | ||||
|             } | ||||
|             if (downloadCache.getDownloadCount(manga) > 0) { | ||||
|                 titles.add(R.string.delete_downloaded) | ||||
|                 enableFlags.add(DELETE_DOWNLOADED) | ||||
|             } | ||||
|         } | ||||
|         return titles.toTypedArray() | ||||
|   | ||||
| @@ -34,6 +34,7 @@ import eu.kanade.domain.manga.model.hasCustomCover | ||||
| import eu.kanade.domain.manga.model.toSManga | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.track.EnhancedTrackService | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| @@ -161,6 +162,7 @@ internal fun MigrateDialog( | ||||
|  | ||||
| internal class MigrateDialogScreenModel( | ||||
|     private val sourceManager: SourceManager = Injekt.get(), | ||||
|     private val downloadManager: DownloadManager = Injekt.get(), | ||||
|     private val updateManga: UpdateManga = Injekt.get(), | ||||
|     private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), | ||||
|     private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), | ||||
| @@ -219,6 +221,7 @@ internal class MigrateDialogScreenModel( | ||||
|         val migrateCategories = MigrationFlags.hasCategories(flags) | ||||
|         val migrateTracks = MigrationFlags.hasTracks(flags) | ||||
|         val migrateCustomCover = MigrationFlags.hasCustomCover(flags) | ||||
|         val deleteDownloaded = MigrationFlags.hasDeleteDownloaded(flags) | ||||
|  | ||||
|         try { | ||||
|             syncChaptersWithSource.await(sourceChapters, newManga, newSource) | ||||
| @@ -283,6 +286,13 @@ internal class MigrateDialogScreenModel( | ||||
|             insertTrack.awaitAll(tracks) | ||||
|         } | ||||
|  | ||||
|         // Delete downloaded | ||||
|         if (deleteDownloaded) { | ||||
|             if (oldSource != null) { | ||||
|                 downloadManager.deleteManga(oldManga, oldSource) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (replace) { | ||||
|             updateManga.await(MangaUpdate(oldManga.id, favorite = false, dateAdded = 0)) | ||||
|         } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.Spacer | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material3.LocalTextStyle | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| @@ -15,18 +16,17 @@ import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.components.AdaptiveSheet | ||||
| import eu.kanade.presentation.components.SelectItem | ||||
| import eu.kanade.presentation.components.TriStateItem | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.source.model.Filter | ||||
| import eu.kanade.tachiyomi.source.model.FilterList | ||||
| import tachiyomi.domain.manga.model.TriStateFilter | ||||
| import tachiyomi.core.preference.TriState | ||||
| import tachiyomi.presentation.core.components.CheckboxItem | ||||
| import tachiyomi.presentation.core.components.CollapsibleBox | ||||
| import tachiyomi.presentation.core.components.HeadingItem | ||||
| import tachiyomi.presentation.core.components.LazyColumn | ||||
| import tachiyomi.presentation.core.components.SelectItem | ||||
| import tachiyomi.presentation.core.components.SortItem | ||||
| import tachiyomi.presentation.core.components.TextItem | ||||
| import tachiyomi.presentation.core.components.TriStateItem | ||||
| import tachiyomi.presentation.core.components.material.Button | ||||
| import tachiyomi.presentation.core.components.material.Divider | ||||
|  | ||||
| @@ -164,19 +164,19 @@ private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| private fun Int.toTriStateFilter(): TriStateFilter { | ||||
| private fun Int.toTriStateFilter(): TriState { | ||||
|     return when (this) { | ||||
|         Filter.TriState.STATE_IGNORE -> TriStateFilter.DISABLED | ||||
|         Filter.TriState.STATE_INCLUDE -> TriStateFilter.ENABLED_IS | ||||
|         Filter.TriState.STATE_EXCLUDE -> TriStateFilter.ENABLED_NOT | ||||
|         Filter.TriState.STATE_IGNORE -> TriState.DISABLED | ||||
|         Filter.TriState.STATE_INCLUDE -> TriState.ENABLED_IS | ||||
|         Filter.TriState.STATE_EXCLUDE -> TriState.ENABLED_NOT | ||||
|         else -> throw IllegalStateException("Unknown TriState state: $this") | ||||
|     } | ||||
| } | ||||
|  | ||||
| private fun TriStateFilter.toTriStateInt(): Int { | ||||
| private fun TriState.toTriStateInt(): Int { | ||||
|     return when (this) { | ||||
|         TriStateFilter.DISABLED -> Filter.TriState.STATE_IGNORE | ||||
|         TriStateFilter.ENABLED_IS -> Filter.TriState.STATE_INCLUDE | ||||
|         TriStateFilter.ENABLED_NOT -> Filter.TriState.STATE_EXCLUDE | ||||
|         TriState.DISABLED -> Filter.TriState.STATE_IGNORE | ||||
|         TriState.ENABLED_IS -> Filter.TriState.STATE_INCLUDE | ||||
|         TriState.ENABLED_NOT -> Filter.TriState.STATE_EXCLUDE | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import kotlinx.coroutines.flow.update | ||||
| import tachiyomi.core.preference.CheckboxState | ||||
| import tachiyomi.core.preference.TriState | ||||
| import tachiyomi.core.util.lang.launchIO | ||||
| import tachiyomi.core.util.lang.launchNonCancellable | ||||
| import tachiyomi.core.util.lang.withIOContext | ||||
| @@ -57,7 +58,6 @@ import tachiyomi.domain.library.service.LibraryPreferences | ||||
| import tachiyomi.domain.manga.interactor.GetLibraryManga | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.MangaUpdate | ||||
| import tachiyomi.domain.manga.model.TriStateFilter | ||||
| import tachiyomi.domain.manga.model.applyFilter | ||||
| import tachiyomi.domain.source.service.SourceManager | ||||
| import tachiyomi.domain.track.interactor.GetTracksPerManga | ||||
| @@ -153,7 +153,7 @@ class LibraryScreenModel( | ||||
|                     prefs.filterBookmarked, | ||||
|                     prefs.filterCompleted, | ||||
|                 ) + trackFilter.values | ||||
|                 ).any { it != TriStateFilter.DISABLED } | ||||
|                 ).any { it != TriState.DISABLED } | ||||
|         } | ||||
|             .distinctUntilChanged() | ||||
|             .onEach { | ||||
| @@ -169,12 +169,12 @@ class LibraryScreenModel( | ||||
|      */ | ||||
|     private suspend fun LibraryMap.applyFilters( | ||||
|         trackMap: Map<Long, List<Long>>, | ||||
|         loggedInTrackServices: Map<Long, TriStateFilter>, | ||||
|         loggedInTrackServices: Map<Long, TriState>, | ||||
|     ): LibraryMap { | ||||
|         val prefs = getLibraryItemPreferencesFlow().first() | ||||
|         val downloadedOnly = prefs.globalFilterDownloaded | ||||
|         val filterDownloaded = | ||||
|             if (downloadedOnly) TriStateFilter.ENABLED_IS else prefs.filterDownloaded | ||||
|             if (downloadedOnly) TriState.ENABLED_IS else prefs.filterDownloaded | ||||
|         val filterUnread = prefs.filterUnread | ||||
|         val filterStarted = prefs.filterStarted | ||||
|         val filterBookmarked = prefs.filterBookmarked | ||||
| @@ -182,8 +182,8 @@ class LibraryScreenModel( | ||||
|  | ||||
|         val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty() | ||||
|  | ||||
|         val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_NOT) it.key else null } | ||||
|         val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_IS) it.key else null } | ||||
|         val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null } | ||||
|         val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null } | ||||
|         val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty() | ||||
|  | ||||
|         val filterFnDownloaded: (LibraryItem) -> Boolean = { | ||||
| @@ -308,11 +308,11 @@ class LibraryScreenModel( | ||||
|                     localBadge = it[1] as Boolean, | ||||
|                     languageBadge = it[2] as Boolean, | ||||
|                     globalFilterDownloaded = it[3] as Boolean, | ||||
|                     filterDownloaded = it[4] as TriStateFilter, | ||||
|                     filterUnread = it[5] as TriStateFilter, | ||||
|                     filterStarted = it[6] as TriStateFilter, | ||||
|                     filterBookmarked = it[7] as TriStateFilter, | ||||
|                     filterCompleted = it[8] as TriStateFilter, | ||||
|                     filterDownloaded = it[4] as TriState, | ||||
|                     filterUnread = it[5] as TriState, | ||||
|                     filterStarted = it[6] as TriState, | ||||
|                     filterBookmarked = it[7] as TriState, | ||||
|                     filterCompleted = it[8] as TriState, | ||||
|                 ) | ||||
|             }, | ||||
|         ) | ||||
| @@ -365,7 +365,7 @@ class LibraryScreenModel( | ||||
|      * | ||||
|      * @return map of track id with the filter value | ||||
|      */ | ||||
|     private fun getTrackingFilterFlow(): Flow<Map<Long, TriStateFilter>> { | ||||
|     private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> { | ||||
|         val loggedServices = trackManager.services.filter { it.isLogged } | ||||
|         return if (loggedServices.isNotEmpty()) { | ||||
|             val prefFlows = loggedServices | ||||
| @@ -670,11 +670,11 @@ class LibraryScreenModel( | ||||
|         val languageBadge: Boolean, | ||||
|  | ||||
|         val globalFilterDownloaded: Boolean, | ||||
|         val filterDownloaded: TriStateFilter, | ||||
|         val filterUnread: TriStateFilter, | ||||
|         val filterStarted: TriStateFilter, | ||||
|         val filterBookmarked: TriStateFilter, | ||||
|         val filterCompleted: TriStateFilter, | ||||
|         val filterDownloaded: TriState, | ||||
|         val filterUnread: TriState, | ||||
|         val filterStarted: TriState, | ||||
|         val filterBookmarked: TriState, | ||||
|         val filterCompleted: TriState, | ||||
|     ) | ||||
|  | ||||
|     @Immutable | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.util.preference.toggle | ||||
| import tachiyomi.core.preference.Preference | ||||
| import tachiyomi.core.preference.TriState | ||||
| import tachiyomi.core.preference.getAndSet | ||||
| import tachiyomi.core.util.lang.launchIO | ||||
| import tachiyomi.domain.category.interactor.SetDisplayMode | ||||
| @@ -14,7 +15,6 @@ import tachiyomi.domain.category.model.Category | ||||
| import tachiyomi.domain.library.model.LibraryDisplayMode | ||||
| import tachiyomi.domain.library.model.LibrarySort | ||||
| import tachiyomi.domain.library.service.LibraryPreferences | ||||
| import tachiyomi.domain.manga.model.TriStateFilter | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| @@ -33,7 +33,7 @@ class LibrarySettingsScreenModel( | ||||
|         preference(libraryPreferences).toggle() | ||||
|     } | ||||
|  | ||||
|     fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriStateFilter>) { | ||||
|     fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriState>) { | ||||
|         preference(libraryPreferences).getAndSet { | ||||
|             it.next() | ||||
|         } | ||||
|   | ||||
| @@ -63,6 +63,7 @@ import eu.kanade.presentation.util.collectAsState | ||||
| import eu.kanade.tachiyomi.BuildConfig | ||||
| import eu.kanade.tachiyomi.Migrations | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.cache.ChapterCache | ||||
| import eu.kanade.tachiyomi.data.download.DownloadCache | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationReceiver | ||||
| import eu.kanade.tachiyomi.data.updater.AppUpdateChecker | ||||
| @@ -105,6 +106,7 @@ class MainActivity : BaseActivity() { | ||||
|     private val preferences: BasePreferences by injectLazy() | ||||
|  | ||||
|     private val downloadCache: DownloadCache by injectLazy() | ||||
|     private val chapterCache: ChapterCache by injectLazy() | ||||
|  | ||||
|     // To be checked by splash screen. If true then splash screen will be removed. | ||||
|     var ready = false | ||||
| @@ -112,12 +114,14 @@ class MainActivity : BaseActivity() { | ||||
|     private var navigator: Navigator? = null | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         val isLaunch = savedInstanceState == null | ||||
|  | ||||
|         // Prevent splash screen showing up on configuration changes | ||||
|         val splashScreen = if (savedInstanceState == null) installSplashScreen() else null | ||||
|         val splashScreen = if (isLaunch) installSplashScreen() else null | ||||
|  | ||||
|         super.onCreate(savedInstanceState) | ||||
|  | ||||
|         val didMigration = if (savedInstanceState == null) { | ||||
|         val didMigration = if (isLaunch) { | ||||
|             Migrations.upgrade( | ||||
|                 context = applicationContext, | ||||
|                 basePreferences = preferences, | ||||
| @@ -149,7 +153,7 @@ class MainActivity : BaseActivity() { | ||||
|             val downloadOnly by preferences.downloadedOnly().collectAsState() | ||||
|             val indexing by downloadCache.isInitializing.collectAsState() | ||||
|  | ||||
|             // Set statusbar color considering the top app state banner | ||||
|             // Set status bar color considering the top app state banner | ||||
|             val systemUiController = rememberSystemUiController() | ||||
|             val isSystemInDarkTheme = isSystemInDarkTheme() | ||||
|             val statusBarBackgroundColor = when { | ||||
| @@ -189,7 +193,7 @@ class MainActivity : BaseActivity() { | ||||
|                 LaunchedEffect(navigator) { | ||||
|                     this@MainActivity.navigator = navigator | ||||
|  | ||||
|                     if (savedInstanceState == null) { | ||||
|                     if (isLaunch) { | ||||
|                         // Set start screen | ||||
|                         handleIntentAction(intent, navigator) | ||||
|  | ||||
| @@ -267,6 +271,10 @@ class MainActivity : BaseActivity() { | ||||
|             elapsed <= SPLASH_MIN_DURATION || (!ready && elapsed <= SPLASH_MAX_DURATION) | ||||
|         } | ||||
|         setSplashScreenExitAnimation(splashScreen) | ||||
|  | ||||
|         if (isLaunch && libraryPreferences.autoClearChapterCache().get()) { | ||||
|             chapterCache.clear() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onProvideAssistContent(outContent: AssistContent) { | ||||
| @@ -279,7 +287,7 @@ class MainActivity : BaseActivity() { | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     fun HandleOnNewIntent(context: Context, navigator: Navigator) { | ||||
|     private fun HandleOnNewIntent(context: Context, navigator: Navigator) { | ||||
|         LaunchedEffect(Unit) { | ||||
|             callbackFlow<Intent> { | ||||
|                 val componentActivity = context as ComponentActivity | ||||
|   | ||||
| @@ -101,8 +101,8 @@ class MangaScreen( | ||||
|             dateRelativeTime = screenModel.relativeTime, | ||||
|             dateFormat = screenModel.dateFormat, | ||||
|             isTabletUi = isTabletUi(), | ||||
|             chapterSwipeEndAction = screenModel.chapterSwipeEndAction, | ||||
|             chapterSwipeStartAction = screenModel.chapterSwipeStartAction, | ||||
|             chapterSwipeEndAction = screenModel.chapterSwipeEndAction, | ||||
|             onBackClicked = navigator::pop, | ||||
|             onChapterClicked = { openChapter(context, it) }, | ||||
|             onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() }, | ||||
|   | ||||
| @@ -46,6 +46,7 @@ import kotlinx.coroutines.isActive | ||||
| import kotlinx.coroutines.launch | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.preference.CheckboxState | ||||
| import tachiyomi.core.preference.TriState | ||||
| import tachiyomi.core.preference.mapAsCheckboxState | ||||
| import tachiyomi.core.util.lang.launchIO | ||||
| import tachiyomi.core.util.lang.launchNonCancellable | ||||
| @@ -67,7 +68,6 @@ import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga | ||||
| import tachiyomi.domain.manga.interactor.GetMangaWithChapters | ||||
| import tachiyomi.domain.manga.interactor.SetMangaChapterFlags | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.TriStateFilter | ||||
| import tachiyomi.domain.manga.model.applyFilter | ||||
| import tachiyomi.domain.source.service.SourceManager | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| @@ -119,11 +119,11 @@ class MangaInfoScreenModel( | ||||
|     private val allChapters: List<ChapterItem>? | ||||
|         get() = successState?.chapters | ||||
|  | ||||
|     private val filteredChapters: Sequence<ChapterItem>? | ||||
|     private val filteredChapters: List<ChapterItem>? | ||||
|         get() = successState?.processedChapters | ||||
|  | ||||
|     val chapterSwipeEndAction = libraryPreferences.swipeEndAction().get() | ||||
|     val chapterSwipeStartAction = libraryPreferences.swipeStartAction().get() | ||||
|     val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get() | ||||
|     val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get() | ||||
|  | ||||
|     val relativeTime by uiPreferences.relativeTime().asState(coroutineScope) | ||||
|     val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get())) | ||||
| @@ -576,7 +576,7 @@ class MangaInfoScreenModel( | ||||
|     } | ||||
|  | ||||
|     private fun getUnreadChapters(): List<Chapter> { | ||||
|         val chapterItems = if (skipFiltered) filteredChapters.orEmpty().toList() else allChapters.orEmpty() | ||||
|         val chapterItems = if (skipFiltered) filteredChapters.orEmpty() else allChapters.orEmpty() | ||||
|         return chapterItems | ||||
|             .filter { (chapter, dlStatus) -> !chapter.read && dlStatus == Download.State.NOT_DOWNLOADED } | ||||
|             .map { it.chapter } | ||||
| @@ -664,7 +664,7 @@ class MangaInfoScreenModel( | ||||
|  | ||||
|     fun markPreviousChapterRead(pointer: Chapter) { | ||||
|         val successState = successState ?: return | ||||
|         val chapters = filteredChapters.orEmpty().map { it.chapter }.toList() | ||||
|         val chapters = filteredChapters.orEmpty().map { it.chapter } | ||||
|         val prevChapters = if (successState.manga.sortDescending()) chapters.asReversed() else chapters | ||||
|         val pointerPos = prevChapters.indexOf(pointer) | ||||
|         if (pointerPos != -1) markChaptersRead(prevChapters.take(pointerPos), true) | ||||
| @@ -743,13 +743,13 @@ class MangaInfoScreenModel( | ||||
|      * Sets the read filter and requests an UI update. | ||||
|      * @param state whether to display only unread chapters or all chapters. | ||||
|      */ | ||||
|     fun setUnreadFilter(state: TriStateFilter) { | ||||
|     fun setUnreadFilter(state: TriState) { | ||||
|         val manga = successState?.manga ?: return | ||||
|  | ||||
|         val flag = when (state) { | ||||
|             TriStateFilter.DISABLED -> Manga.SHOW_ALL | ||||
|             TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD | ||||
|             TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ | ||||
|             TriState.DISABLED -> Manga.SHOW_ALL | ||||
|             TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD | ||||
|             TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ | ||||
|         } | ||||
|         coroutineScope.launchNonCancellable { | ||||
|             setMangaChapterFlags.awaitSetUnreadFilter(manga, flag) | ||||
| @@ -760,13 +760,13 @@ class MangaInfoScreenModel( | ||||
|      * Sets the download filter and requests an UI update. | ||||
|      * @param state whether to display only downloaded chapters or all chapters. | ||||
|      */ | ||||
|     fun setDownloadedFilter(state: TriStateFilter) { | ||||
|     fun setDownloadedFilter(state: TriState) { | ||||
|         val manga = successState?.manga ?: return | ||||
|  | ||||
|         val flag = when (state) { | ||||
|             TriStateFilter.DISABLED -> Manga.SHOW_ALL | ||||
|             TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_DOWNLOADED | ||||
|             TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED | ||||
|             TriState.DISABLED -> Manga.SHOW_ALL | ||||
|             TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_DOWNLOADED | ||||
|             TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED | ||||
|         } | ||||
|  | ||||
|         coroutineScope.launchNonCancellable { | ||||
| @@ -778,13 +778,13 @@ class MangaInfoScreenModel( | ||||
|      * Sets the bookmark filter and requests an UI update. | ||||
|      * @param state whether to display only bookmarked chapters or all chapters. | ||||
|      */ | ||||
|     fun setBookmarkedFilter(state: TriStateFilter) { | ||||
|     fun setBookmarkedFilter(state: TriState) { | ||||
|         val manga = successState?.manga ?: return | ||||
|  | ||||
|         val flag = when (state) { | ||||
|             TriStateFilter.DISABLED -> Manga.SHOW_ALL | ||||
|             TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_BOOKMARKED | ||||
|             TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED | ||||
|             TriState.DISABLED -> Manga.SHOW_ALL | ||||
|             TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_BOOKMARKED | ||||
|             TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED | ||||
|         } | ||||
|  | ||||
|         coroutineScope.launchNonCancellable { | ||||
| @@ -987,8 +987,9 @@ sealed class MangaScreenState { | ||||
|         val hasPromptedToAddBefore: Boolean = false, | ||||
|     ) : MangaScreenState() { | ||||
|  | ||||
|         val processedChapters: Sequence<ChapterItem> | ||||
|             get() = chapters.applyFilters(manga) | ||||
|         val processedChapters by lazy { | ||||
|             chapters.applyFilters(manga).toList() | ||||
|         } | ||||
|  | ||||
|         val trackingAvailable: Boolean | ||||
|             get() = trackItems.isNotEmpty() | ||||
|   | ||||
| @@ -31,6 +31,7 @@ import androidx.compose.material3.CircularProgressIndicator | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.unit.dp | ||||
| @@ -51,6 +52,7 @@ import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.domain.manga.model.orientationType | ||||
| import eu.kanade.presentation.reader.ChapterNavigator | ||||
| import eu.kanade.presentation.reader.PageIndicatorText | ||||
| import eu.kanade.presentation.reader.settings.ReaderSettingsDialog | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationReceiver | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| @@ -65,8 +67,8 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.OrientationType | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderColorFilterDialog | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator | ||||
| @@ -235,13 +237,18 @@ class ReaderActivity : BaseActivity() { | ||||
|         readingModeToast?.cancel() | ||||
|     } | ||||
|  | ||||
|     override fun onPause() { | ||||
|         viewModel.flushReadTimer() | ||||
|         super.onPause() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set menu visibility again on activity resume to apply immersive mode again if needed. | ||||
|      * Helps with rotations. | ||||
|      */ | ||||
|     override fun onResume() { | ||||
|         super.onResume() | ||||
|         viewModel.setReadStartTime() | ||||
|         viewModel.restartReadTimer() | ||||
|         setMenuVisibility(viewModel.state.value.menuVisible, animate = false) | ||||
|     } | ||||
|  | ||||
| @@ -384,6 +391,8 @@ class ReaderActivity : BaseActivity() { | ||||
|  | ||||
|         binding.dialogRoot.setComposeContent { | ||||
|             val state by viewModel.state.collectAsState() | ||||
|             val settingsScreenModel = remember { ReaderSettingsScreenModel() } | ||||
|  | ||||
|             val onDismissRequest = viewModel::closeDialog | ||||
|             when (state.dialog) { | ||||
|                 is ReaderViewModel.Dialog.Loading -> { | ||||
| @@ -401,14 +410,12 @@ class ReaderActivity : BaseActivity() { | ||||
|                         }, | ||||
|                     ) | ||||
|                 } | ||||
|                 is ReaderViewModel.Dialog.ColorFilter -> { | ||||
|                     setMenuVisibility(false) | ||||
|                     ReaderColorFilterDialog( | ||||
|                         onDismissRequest = { | ||||
|                             onDismissRequest() | ||||
|                             setMenuVisibility(true) | ||||
|                         }, | ||||
|                         readerPreferences = viewModel.readerPreferences, | ||||
|                 is ReaderViewModel.Dialog.Settings -> { | ||||
|                     ReaderSettingsDialog( | ||||
|                         onDismissRequest = onDismissRequest, | ||||
|                         onShowMenus = { setMenuVisibility(true) }, | ||||
|                         onHideMenus = { setMenuVisibility(false) }, | ||||
|                         screenModel = settingsScreenModel, | ||||
|                     ) | ||||
|                 } | ||||
|                 is ReaderViewModel.Dialog.PageActions -> { | ||||
| @@ -541,7 +548,7 @@ class ReaderActivity : BaseActivity() { | ||||
|         } | ||||
|  | ||||
|         // Settings sheet | ||||
|         with(binding.actionSettings) { | ||||
|         with(binding.actionSettingsLegacy) { | ||||
|             setTooltip(R.string.action_settings) | ||||
|  | ||||
|             var readerSettingSheet: ReaderSettingsSheet? = null | ||||
| @@ -551,13 +558,11 @@ class ReaderActivity : BaseActivity() { | ||||
|                 readerSettingSheet = ReaderSettingsSheet(this@ReaderActivity).apply { show() } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Color filter sheet | ||||
|         with(binding.actionColorSettings) { | ||||
|             setTooltip(R.string.custom_filter) | ||||
|         with(binding.actionSettings) { | ||||
|             setTooltip(R.string.action_settings) | ||||
|  | ||||
|             setOnClickListener { | ||||
|                 viewModel.openColorFilterDialog() | ||||
|                 viewModel.openSettingsDialog() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -588,7 +593,7 @@ class ReaderActivity : BaseActivity() { | ||||
|      * Sets the visibility of the menu according to [visible] and with an optional parameter to | ||||
|      * [animate] the views. | ||||
|      */ | ||||
|     fun setMenuVisibility(visible: Boolean, animate: Boolean = true) { | ||||
|     private fun setMenuVisibility(visible: Boolean, animate: Boolean = true) { | ||||
|         viewModel.showMenus(visible) | ||||
|         if (visible) { | ||||
|             windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) | ||||
| @@ -793,7 +798,6 @@ class ReaderActivity : BaseActivity() { | ||||
|      * Called from the viewer whenever a [page] is marked as active. It updates the values of the | ||||
|      * bottom menu and delegates the change to the presenter. | ||||
|      */ | ||||
|     @SuppressLint("SetTextI18n") | ||||
|     fun onPageSelected(page: ReaderPage) { | ||||
|         viewModel.onPageSelected(page) | ||||
|     } | ||||
| @@ -811,7 +815,7 @@ class ReaderActivity : BaseActivity() { | ||||
|      * the viewer is reaching the beginning or end of a chapter or the transition page is active. | ||||
|      */ | ||||
|     fun requestPreloadChapter(chapter: ReaderChapter) { | ||||
|         lifecycleScope.launchIO { viewModel.preloadChapter(chapter) } | ||||
|         lifecycleScope.launchIO { viewModel.preload(chapter) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -898,7 +902,7 @@ class ReaderActivity : BaseActivity() { | ||||
|     /** | ||||
|      * Updates viewer inset depending on fullscreen reader preferences. | ||||
|      */ | ||||
|     fun updateViewerInset(fullscreen: Boolean) { | ||||
|     private fun updateViewerInset(fullscreen: Boolean) { | ||||
|         viewModel.state.value.viewer?.getView()?.applyInsetter { | ||||
|             if (!fullscreen) { | ||||
|                 type(navigationBars = true, statusBars = true) { | ||||
|   | ||||
| @@ -10,10 +10,8 @@ import eu.kanade.domain.chapter.model.toDbChapter | ||||
| import eu.kanade.domain.manga.interactor.SetMangaViewerFlags | ||||
| import eu.kanade.domain.manga.model.orientationType | ||||
| import eu.kanade.domain.manga.model.readingModeType | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.domain.track.service.DelayedTrackingUpdateJob | ||||
| import eu.kanade.domain.track.interactor.TrackChapter | ||||
| import eu.kanade.domain.track.service.TrackPreferences | ||||
| import eu.kanade.domain.track.store.DelayedTrackingStore | ||||
| import eu.kanade.tachiyomi.data.database.models.toDomainChapter | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.download.DownloadProvider | ||||
| @@ -21,10 +19,10 @@ import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.data.saver.Image | ||||
| import eu.kanade.tachiyomi.data.saver.ImageSaver | ||||
| import eu.kanade.tachiyomi.data.saver.Location | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader | ||||
| import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader | ||||
| import eu.kanade.tachiyomi.ui.reader.model.InsertPage | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| @@ -41,11 +39,8 @@ import eu.kanade.tachiyomi.util.lang.byteSize | ||||
| import eu.kanade.tachiyomi.util.lang.takeBytes | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.storage.cacheImageDir | ||||
| import eu.kanade.tachiyomi.util.system.isOnline | ||||
| import kotlinx.coroutines.CancellationException | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.async | ||||
| import kotlinx.coroutines.awaitAll | ||||
| import kotlinx.coroutines.channels.Channel | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.asStateFlow | ||||
| @@ -56,7 +51,6 @@ import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import kotlinx.coroutines.flow.receiveAsFlow | ||||
| import kotlinx.coroutines.flow.update | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.util.lang.launchIO | ||||
| @@ -75,8 +69,6 @@ import tachiyomi.domain.history.model.HistoryUpdate | ||||
| import tachiyomi.domain.manga.interactor.GetManga | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.source.service.SourceManager | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
| import tachiyomi.source.local.isLocal | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| @@ -96,12 +88,10 @@ class ReaderViewModel( | ||||
|     private val basePreferences: BasePreferences = Injekt.get(), | ||||
|     private val downloadPreferences: DownloadPreferences = Injekt.get(), | ||||
|     private val trackPreferences: TrackPreferences = Injekt.get(), | ||||
|     private val delayedTrackingStore: DelayedTrackingStore = Injekt.get(), | ||||
|     private val trackChapter: TrackChapter = Injekt.get(), | ||||
|     private val getManga: GetManga = Injekt.get(), | ||||
|     private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), | ||||
|     private val getNextChapters: GetNextChapters = Injekt.get(), | ||||
|     private val getTracks: GetTracks = Injekt.get(), | ||||
|     private val insertTrack: InsertTrack = Injekt.get(), | ||||
|     private val upsertHistory: UpsertHistory = Injekt.get(), | ||||
|     private val updateChapter: UpdateChapter = Injekt.get(), | ||||
|     private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(), | ||||
| @@ -197,13 +187,8 @@ class ReaderViewModel( | ||||
|             .map(::ReaderChapter) | ||||
|     } | ||||
|  | ||||
|     private var hasTrackers: Boolean = false | ||||
|     private val checkTrackers: (Manga) -> Unit = { manga -> | ||||
|         val tracks = runBlocking { getTracks.await(manga.id) } | ||||
|         hasTrackers = tracks.isNotEmpty() | ||||
|     } | ||||
|  | ||||
|     private val incognitoMode = preferences.incognitoMode().get() | ||||
|     private val downloadAheadAmount = downloadPreferences.autoDownloadWhileReading().get() | ||||
|  | ||||
|     init { | ||||
|         // To save state | ||||
| @@ -257,8 +242,6 @@ class ReaderViewModel( | ||||
|                     mutableState.update { it.copy(manga = manga) } | ||||
|                     if (chapterId == -1L) chapterId = initialChapterId | ||||
|  | ||||
|                     checkTrackers(manga) | ||||
|  | ||||
|                     val context = Injekt.get<Application>() | ||||
|                     val source = sourceManager.getOrStub(manga.source) | ||||
|                     loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source) | ||||
| @@ -312,12 +295,15 @@ class ReaderViewModel( | ||||
|      * Called when the user changed to the given [chapter] when changing pages from the viewer. | ||||
|      * It's used only to set this chapter as active. | ||||
|      */ | ||||
|     private suspend fun loadNewChapter(chapter: ReaderChapter) { | ||||
|     private fun loadNewChapter(chapter: ReaderChapter) { | ||||
|         val loader = loader ?: return | ||||
|  | ||||
|         logcat { "Loading ${chapter.chapter.url}" } | ||||
|         viewModelScope.launchIO { | ||||
|             logcat { "Loading ${chapter.chapter.url}" } | ||||
|  | ||||
|             flushReadTimer() | ||||
|             restartReadTimer() | ||||
|  | ||||
|         withIOContext { | ||||
|             try { | ||||
|                 loadChapter(loader, chapter) | ||||
|             } catch (e: Throwable) { | ||||
| @@ -356,7 +342,7 @@ class ReaderViewModel( | ||||
|      * Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so | ||||
|      * that the user doesn't have to wait too long to continue reading. | ||||
|      */ | ||||
|     private suspend fun preload(chapter: ReaderChapter) { | ||||
|     suspend fun preload(chapter: ReaderChapter) { | ||||
|         if (chapter.state is ReaderChapter.State.Loaded || chapter.state == ReaderChapter.State.Loading) { | ||||
|             return | ||||
|         } | ||||
| @@ -395,9 +381,7 @@ class ReaderViewModel( | ||||
|  | ||||
|     fun onViewerLoaded(viewer: Viewer?) { | ||||
|         mutableState.update { | ||||
|             it.copy( | ||||
|                 viewer = viewer, | ||||
|             ) | ||||
|             it.copy(viewer = viewer) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -412,31 +396,19 @@ class ReaderViewModel( | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         val currentChapters = state.value.viewerChapters ?: return | ||||
|         val pages = page.chapter.pages ?: return | ||||
|         val selectedChapter = page.chapter | ||||
|         val pages = selectedChapter.pages ?: return | ||||
|  | ||||
|         // Save last page read and mark as read if needed | ||||
|         saveReadingProgress() | ||||
|         mutableState.update { | ||||
|             it.copy( | ||||
|                 currentPage = page.index + 1, | ||||
|             ) | ||||
|         } | ||||
|         if (!incognitoMode) { | ||||
|             selectedChapter.chapter.last_page_read = page.index | ||||
|             if (selectedChapter.pages?.lastIndex == page.index) { | ||||
|                 selectedChapter.chapter.read = true | ||||
|                 updateTrackChapterRead(selectedChapter) | ||||
|                 deleteChapterIfNeeded(selectedChapter) | ||||
|             } | ||||
|         viewModelScope.launchNonCancellable { | ||||
|             updateChapterProgress(selectedChapter, page.index) | ||||
|         } | ||||
|  | ||||
|         if (selectedChapter != currentChapters.currChapter) { | ||||
|         if (selectedChapter != getCurrentChapter()) { | ||||
|             logcat { "Setting ${selectedChapter.chapter.url} as active" } | ||||
|             setReadStartTime() | ||||
|             viewModelScope.launch { loadNewChapter(selectedChapter) } | ||||
|             loadNewChapter(selectedChapter) | ||||
|         } | ||||
|  | ||||
|         val inDownloadRange = page.number.toDouble() / pages.size > 0.25 | ||||
|         if (inDownloadRange) { | ||||
|             downloadNextChapters() | ||||
| @@ -444,12 +416,11 @@ class ReaderViewModel( | ||||
|     } | ||||
|  | ||||
|     private fun downloadNextChapters() { | ||||
|         if (downloadAheadAmount == 0) return | ||||
|         val manga = manga ?: return | ||||
|         val amount = downloadPreferences.autoDownloadWhileReading().get() | ||||
|         if (amount == 0 || !manga.favorite) return | ||||
|  | ||||
|         // Only download ahead if current + next chapter is already downloaded too to avoid jank | ||||
|         if (getCurrentChapter()?.pageLoader?.isLocal == true) return | ||||
|         if (getCurrentChapter()?.pageLoader !is DownloadPageLoader) return | ||||
|         val nextChapter = state.value.viewerChapters?.nextChapter?.chapter ?: return | ||||
|  | ||||
|         viewModelScope.launchIO { | ||||
| @@ -466,7 +437,7 @@ class ReaderViewModel( | ||||
|                 } else { | ||||
|                     this | ||||
|                 } | ||||
|             }.take(amount) | ||||
|             }.take(downloadAheadAmount) | ||||
|  | ||||
|             downloadManager.downloadChapters( | ||||
|                 manga, | ||||
| @@ -507,40 +478,51 @@ class ReaderViewModel( | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when reader chapter is changed in reader or when activity is paused. | ||||
|      * Saves the chapter progress (last read page and whether it's read) | ||||
|      * if incognito mode isn't on. | ||||
|      */ | ||||
|     private fun saveReadingProgress() { | ||||
|     private suspend fun updateChapterProgress(readerChapter: ReaderChapter, pageIndex: Int) { | ||||
|         mutableState.update { | ||||
|             it.copy(currentPage = pageIndex + 1) | ||||
|         } | ||||
|  | ||||
|         if (!incognitoMode) { | ||||
|             readerChapter.requestedPage = pageIndex | ||||
|             readerChapter.chapter.last_page_read = pageIndex | ||||
|  | ||||
|             if (readerChapter.pages?.lastIndex == pageIndex) { | ||||
|                 readerChapter.chapter.read = true | ||||
|                 updateTrackChapterRead(readerChapter) | ||||
|                 deleteChapterIfNeeded(readerChapter) | ||||
|             } | ||||
|  | ||||
|             updateChapter.await( | ||||
|                 ChapterUpdate( | ||||
|                     id = readerChapter.chapter.id!!, | ||||
|                     read = readerChapter.chapter.read, | ||||
|                     bookmark = readerChapter.chapter.bookmark, | ||||
|                     lastPageRead = readerChapter.chapter.last_page_read.toLong(), | ||||
|                 ), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun restartReadTimer() { | ||||
|         chapterReadStartTime = Date().time | ||||
|     } | ||||
|  | ||||
|     fun flushReadTimer() { | ||||
|         getCurrentChapter()?.let { | ||||
|             viewModelScope.launchNonCancellable { | ||||
|                 saveChapterProgress(it) | ||||
|                 saveChapterHistory(it) | ||||
|                 updateHistory(it) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Saves this [readerChapter] progress (last read page and whether it's read) | ||||
|      * if incognito mode isn't on. | ||||
|      * Saves the chapter last read history if incognito mode isn't on. | ||||
|      */ | ||||
|     private suspend fun saveChapterProgress(readerChapter: ReaderChapter) { | ||||
|         if (incognitoMode) return | ||||
|  | ||||
|         val chapter = readerChapter.chapter | ||||
|         readerChapter.requestedPage = chapter.last_page_read | ||||
|         updateChapter.await( | ||||
|             ChapterUpdate( | ||||
|                 id = chapter.id!!, | ||||
|                 read = chapter.read, | ||||
|                 bookmark = chapter.bookmark, | ||||
|                 lastPageRead = chapter.last_page_read.toLong(), | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Saves this [readerChapter] last read history if incognito mode isn't on. | ||||
|      */ | ||||
|     private suspend fun saveChapterHistory(readerChapter: ReaderChapter) { | ||||
|     private suspend fun updateHistory(readerChapter: ReaderChapter) { | ||||
|         if (incognitoMode) return | ||||
|  | ||||
|         val chapterId = readerChapter.chapter.id!! | ||||
| @@ -551,17 +533,6 @@ class ReaderViewModel( | ||||
|         chapterReadStartTime = null | ||||
|     } | ||||
|  | ||||
|     fun setReadStartTime() { | ||||
|         chapterReadStartTime = Date().time | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called from the activity to preload the given [chapter]. | ||||
|      */ | ||||
|     suspend fun preloadChapter(chapter: ReaderChapter) { | ||||
|         preload(chapter) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called from the activity to load and set the next chapter as active. | ||||
|      */ | ||||
| @@ -714,8 +685,8 @@ class ReaderViewModel( | ||||
|         mutableState.update { it.copy(dialog = Dialog.PageActions(page)) } | ||||
|     } | ||||
|  | ||||
|     fun openColorFilterDialog() { | ||||
|         mutableState.update { it.copy(dialog = Dialog.ColorFilter) } | ||||
|     fun openSettingsDialog() { | ||||
|         mutableState.update { it.copy(dialog = Dialog.Settings) } | ||||
|     } | ||||
|  | ||||
|     fun closeDialog() { | ||||
| @@ -839,44 +810,14 @@ class ReaderViewModel( | ||||
|      * will run in a background thread and errors are ignored. | ||||
|      */ | ||||
|     private fun updateTrackChapterRead(readerChapter: ReaderChapter) { | ||||
|         if (incognitoMode || !hasTrackers) return | ||||
|         if (incognitoMode) return | ||||
|         if (!trackPreferences.autoUpdateTrack().get()) return | ||||
|  | ||||
|         val manga = manga ?: return | ||||
|         val chapterRead = readerChapter.chapter.chapter_number.toDouble() | ||||
|  | ||||
|         val trackManager = Injekt.get<TrackManager>() | ||||
|         val context = Injekt.get<Application>() | ||||
|  | ||||
|         viewModelScope.launchNonCancellable { | ||||
|             getTracks.await(manga.id) | ||||
|                 .mapNotNull { track -> | ||||
|                     val service = trackManager.getService(track.syncId) | ||||
|                     if (service != null && service.isLogged && chapterRead > track.lastChapterRead) { | ||||
|                         val updatedTrack = track.copy(lastChapterRead = chapterRead) | ||||
|  | ||||
|                         // We want these to execute even if the presenter is destroyed and leaks | ||||
|                         // for a while. The view can still be garbage collected. | ||||
|                         async { | ||||
|                             runCatching { | ||||
|                                 try { | ||||
|                                     if (!context.isOnline()) error("Couldn't update tracker as device is offline") | ||||
|                                     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) } | ||||
|             trackChapter.await(context, manga.id, readerChapter.chapter.chapter_number.toDouble()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -922,7 +863,7 @@ class ReaderViewModel( | ||||
|  | ||||
|     sealed class Dialog { | ||||
|         object Loading : Dialog() | ||||
|         object ColorFilter : Dialog() | ||||
|         object Settings : Dialog() | ||||
|         data class PageActions(val page: ReaderPage) : Dialog() | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,164 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.reader.setting | ||||
|  | ||||
| import android.os.Build | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.CompositionLocalProvider | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.platform.LocalView | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.window.DialogWindowProvider | ||||
| import androidx.core.graphics.alpha | ||||
| import androidx.core.graphics.blue | ||||
| import androidx.core.graphics.green | ||||
| import androidx.core.graphics.red | ||||
| import eu.kanade.presentation.components.AdaptiveSheet | ||||
| import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight | ||||
| import eu.kanade.presentation.more.settings.Preference | ||||
| import eu.kanade.presentation.more.settings.PreferenceScreen | ||||
| import eu.kanade.presentation.util.collectAsState | ||||
| import eu.kanade.tachiyomi.R | ||||
| import tachiyomi.core.preference.getAndSet | ||||
|  | ||||
| @Composable | ||||
| fun ReaderColorFilterDialog( | ||||
|     onDismissRequest: () -> Unit, | ||||
|     readerPreferences: ReaderPreferences, | ||||
| ) { | ||||
|     val colorFilterModes = buildList { | ||||
|         addAll( | ||||
|             listOf( | ||||
|                 R.string.label_default, | ||||
|                 R.string.filter_mode_multiply, | ||||
|                 R.string.filter_mode_screen, | ||||
|             ), | ||||
|         ) | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { | ||||
|             addAll( | ||||
|                 listOf( | ||||
|                     R.string.filter_mode_overlay, | ||||
|                     R.string.filter_mode_lighten, | ||||
|                     R.string.filter_mode_darken, | ||||
|                 ), | ||||
|             ) | ||||
|         } | ||||
|     }.map { stringResource(it) } | ||||
|  | ||||
|     val customBrightness by readerPreferences.customBrightness().collectAsState() | ||||
|     val customBrightnessValue by readerPreferences.customBrightnessValue().collectAsState() | ||||
|     val colorFilter by readerPreferences.colorFilter().collectAsState() | ||||
|     val colorFilterValue by readerPreferences.colorFilterValue().collectAsState() | ||||
|     val colorFilterMode by readerPreferences.colorFilterMode().collectAsState() | ||||
|  | ||||
|     AdaptiveSheet( | ||||
|         onDismissRequest = onDismissRequest, | ||||
|     ) { | ||||
|         (LocalView.current.parent as? DialogWindowProvider)?.window?.setDimAmount(0f) | ||||
|  | ||||
|         CompositionLocalProvider( | ||||
|             LocalPreferenceMinHeight provides 48.dp, | ||||
|         ) { | ||||
|             PreferenceScreen( | ||||
|                 items = listOfNotNull( | ||||
|                     Preference.PreferenceItem.SwitchPreference( | ||||
|                         pref = readerPreferences.customBrightness(), | ||||
|                         title = stringResource(R.string.pref_custom_brightness), | ||||
|                     ), | ||||
|                     /** | ||||
|                      * Sets the brightness of the screen. Range is [-75, 100]. | ||||
|                      * From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness. | ||||
|                      * From 1 to 100 it sets that value as brightness. | ||||
|                      * 0 sets system brightness and hides the overlay. | ||||
|                      */ | ||||
|                     Preference.PreferenceItem.SliderPreference( | ||||
|                         value = customBrightnessValue, | ||||
|                         title = stringResource(R.string.pref_custom_brightness), | ||||
|                         min = -75, | ||||
|                         max = 100, | ||||
|                         onValueChanged = { | ||||
|                             readerPreferences.customBrightnessValue().set(it) | ||||
|                             true | ||||
|                         }, | ||||
|                     ).takeIf { customBrightness }, | ||||
|  | ||||
|                     Preference.PreferenceItem.SwitchPreference( | ||||
|                         pref = readerPreferences.colorFilter(), | ||||
|                         title = stringResource(R.string.pref_custom_color_filter), | ||||
|                     ), | ||||
|                     Preference.PreferenceItem.SliderPreference( | ||||
|                         value = colorFilterValue.red, | ||||
|                         title = stringResource(R.string.color_filter_r_value), | ||||
|                         max = 255, | ||||
|                         onValueChanged = { newRValue -> | ||||
|                             readerPreferences.colorFilterValue().getAndSet { | ||||
|                                 getColorValue(it, newRValue, RED_MASK, 16) | ||||
|                             } | ||||
|                             true | ||||
|                         }, | ||||
|                     ).takeIf { colorFilter }, | ||||
|                     Preference.PreferenceItem.SliderPreference( | ||||
|                         value = colorFilterValue.green, | ||||
|                         title = stringResource(R.string.color_filter_g_value), | ||||
|                         max = 255, | ||||
|                         onValueChanged = { newRValue -> | ||||
|                             readerPreferences.colorFilterValue().getAndSet { | ||||
|                                 getColorValue(it, newRValue, GREEN_MASK, 8) | ||||
|                             } | ||||
|                             true | ||||
|                         }, | ||||
|                     ).takeIf { colorFilter }, | ||||
|                     Preference.PreferenceItem.SliderPreference( | ||||
|                         value = colorFilterValue.blue, | ||||
|                         title = stringResource(R.string.color_filter_b_value), | ||||
|                         max = 255, | ||||
|                         onValueChanged = { newRValue -> | ||||
|                             readerPreferences.colorFilterValue().getAndSet { | ||||
|                                 getColorValue(it, newRValue, BLUE_MASK, 0) | ||||
|                             } | ||||
|                             true | ||||
|                         }, | ||||
|                     ).takeIf { colorFilter }, | ||||
|                     Preference.PreferenceItem.SliderPreference( | ||||
|                         value = colorFilterValue.alpha, | ||||
|                         title = stringResource(R.string.color_filter_a_value), | ||||
|                         max = 255, | ||||
|                         onValueChanged = { newRValue -> | ||||
|                             readerPreferences.colorFilterValue().getAndSet { | ||||
|                                 getColorValue(it, newRValue, ALPHA_MASK, 24) | ||||
|                             } | ||||
|                             true | ||||
|                         }, | ||||
|                     ).takeIf { colorFilter }, | ||||
|                     Preference.PreferenceItem.BasicListPreference( | ||||
|                         value = colorFilterMode.toString(), | ||||
|                         title = stringResource(R.string.pref_color_filter_mode), | ||||
|                         entries = colorFilterModes | ||||
|                             .mapIndexed { index, mode -> index.toString() to mode } | ||||
|                             .toMap(), | ||||
|                         onValueChanged = { newValue -> | ||||
|                             readerPreferences.colorFilterMode().set(newValue.toInt()) | ||||
|                             true | ||||
|                         }, | ||||
|                     ).takeIf { colorFilter }, | ||||
|  | ||||
|                     Preference.PreferenceItem.SwitchPreference( | ||||
|                         pref = readerPreferences.grayscale(), | ||||
|                         title = stringResource(R.string.pref_grayscale), | ||||
|                     ), | ||||
|                     Preference.PreferenceItem.SwitchPreference( | ||||
|                         pref = readerPreferences.invertedColors(), | ||||
|                         title = stringResource(R.string.pref_inverted_colors), | ||||
|                     ), | ||||
|                 ), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private fun getColorValue(currentColor: Int, color: Int, mask: Long, bitShift: Int): Int { | ||||
|     return (color shl bitShift) or (currentColor and mask.inv().toInt()) | ||||
| } | ||||
| private const val ALPHA_MASK: Long = 0xFF000000 | ||||
| private const val RED_MASK: Long = 0x00FF0000 | ||||
| private const val GREEN_MASK: Long = 0x0000FF00 | ||||
| private const val BLUE_MASK: Long = 0x000000FF | ||||
| @@ -1,53 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.reader.setting | ||||
|  | ||||
| import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| import android.view.LayoutInflater | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.core.widget.NestedScrollView | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.databinding.ReaderGeneralSettingsBinding | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.util.preference.bindToPreference | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| /** | ||||
|  * Sheet to show reader and viewer preferences. | ||||
|  */ | ||||
| class ReaderGeneralSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : | ||||
|     NestedScrollView(context, attrs) { | ||||
|  | ||||
|     private val readerPreferences: ReaderPreferences by injectLazy() | ||||
|  | ||||
|     private val binding = ReaderGeneralSettingsBinding.inflate(LayoutInflater.from(context), this, false) | ||||
|  | ||||
|     init { | ||||
|         addView(binding.root) | ||||
|  | ||||
|         initGeneralPreferences() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Init general reader preferences. | ||||
|      */ | ||||
|     private fun initGeneralPreferences() { | ||||
|         binding.backgroundColor.bindToIntPreference(readerPreferences.readerTheme(), R.array.reader_themes_values) | ||||
|         binding.showPageNumber.bindToPreference(readerPreferences.showPageNumber()) | ||||
|         binding.fullscreen.bindToPreference(readerPreferences.fullscreen()) | ||||
|         readerPreferences.fullscreen().changes() | ||||
|             .onEach { | ||||
|                 // If the preference is explicitly disabled, that means the setting was configured since there is a cutout | ||||
|                 binding.cutoutShort.isVisible = it && ((context as ReaderActivity).hasCutout || !readerPreferences.cutoutShort().get()) | ||||
|                 binding.cutoutShort.bindToPreference(readerPreferences.cutoutShort()) | ||||
|             } | ||||
|             .launchIn((context as ReaderActivity).lifecycleScope) | ||||
|  | ||||
|         binding.keepscreen.bindToPreference(readerPreferences.keepScreenOn()) | ||||
|         binding.longTap.bindToPreference(readerPreferences.readWithLongTap()) | ||||
|         binding.alwaysShowChapterTransition.bindToPreference(readerPreferences.alwaysShowChapterTransition()) | ||||
|         binding.pageTransitions.bindToPreference(readerPreferences.pageTransitions()) | ||||
|     } | ||||
| } | ||||
| @@ -1,146 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.reader.setting | ||||
|  | ||||
| import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| import android.view.LayoutInflater | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.core.widget.NestedScrollView | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import eu.kanade.domain.manga.model.orientationType | ||||
| import eu.kanade.domain.manga.model.readingModeType | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.databinding.ReaderReadingModeSettingsBinding | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer | ||||
| import eu.kanade.tachiyomi.util.preference.bindToPreference | ||||
| import eu.kanade.tachiyomi.util.system.isReleaseBuildType | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| /** | ||||
|  * Sheet to show reader and viewer preferences. | ||||
|  */ | ||||
| class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : | ||||
|     NestedScrollView(context, attrs) { | ||||
|  | ||||
|     private val readerPreferences: ReaderPreferences by injectLazy() | ||||
|  | ||||
|     private val binding = ReaderReadingModeSettingsBinding.inflate(LayoutInflater.from(context), this, false) | ||||
|  | ||||
|     init { | ||||
|         addView(binding.root) | ||||
|  | ||||
|         initGeneralPreferences() | ||||
|  | ||||
|         when ((context as ReaderActivity).viewModel.state.value.viewer) { | ||||
|             is PagerViewer -> initPagerPreferences() | ||||
|             is WebtoonViewer -> initWebtoonPreferences() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Init general reader preferences. | ||||
|      */ | ||||
|     private fun initGeneralPreferences() { | ||||
|         binding.viewer.onItemSelectedListener = { position -> | ||||
|             val readingModeType = ReadingModeType.fromSpinner(position) | ||||
|             (context as ReaderActivity).viewModel.setMangaReadingMode(readingModeType.flagValue) | ||||
|  | ||||
|             val mangaViewer = (context as ReaderActivity).viewModel.getMangaReadingMode() | ||||
|             if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) { | ||||
|                 initWebtoonPreferences() | ||||
|             } else { | ||||
|                 initPagerPreferences() | ||||
|             } | ||||
|         } | ||||
|         binding.viewer.setSelection((context as ReaderActivity).viewModel.manga?.readingModeType?.let { ReadingModeType.fromPreference(it.toInt()).prefValue } ?: ReadingModeType.DEFAULT.prefValue) | ||||
|  | ||||
|         binding.rotationMode.onItemSelectedListener = { position -> | ||||
|             val rotationType = OrientationType.fromSpinner(position) | ||||
|             (context as ReaderActivity).viewModel.setMangaOrientationType(rotationType.flagValue) | ||||
|         } | ||||
|         binding.rotationMode.setSelection((context as ReaderActivity).viewModel.manga?.orientationType?.let { OrientationType.fromPreference(it.toInt()).prefValue } ?: OrientationType.DEFAULT.prefValue) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Init the preferences for the pager reader. | ||||
|      */ | ||||
|     private fun initPagerPreferences() { | ||||
|         binding.webtoonPrefsGroup.root.isVisible = false | ||||
|         binding.pagerPrefsGroup.root.isVisible = true | ||||
|  | ||||
|         binding.pagerPrefsGroup.tappingInverted.bindToPreference(readerPreferences.pagerNavInverted(), ReaderPreferences.TappingInvertMode::class.java) | ||||
|         binding.pagerPrefsGroup.navigatePan.bindToPreference(readerPreferences.navigateToPan()) | ||||
|  | ||||
|         binding.pagerPrefsGroup.pagerNav.bindToPreference(readerPreferences.navigationModePager()) | ||||
|         readerPreferences.navigationModePager().changes() | ||||
|             .onEach { | ||||
|                 val isTappingEnabled = it != 5 | ||||
|                 binding.pagerPrefsGroup.tappingInverted.isVisible = isTappingEnabled | ||||
|                 binding.pagerPrefsGroup.navigatePan.isVisible = isTappingEnabled | ||||
|             } | ||||
|             .launchIn((context as ReaderActivity).lifecycleScope) | ||||
|         // Makes so that landscape zoom gets hidden away when image scale type is not fit screen | ||||
|         binding.pagerPrefsGroup.scaleType.bindToPreference(readerPreferences.imageScaleType(), 1) | ||||
|         readerPreferences.imageScaleType().changes() | ||||
|             .onEach { binding.pagerPrefsGroup.landscapeZoom.isVisible = it == 1 } | ||||
|             .launchIn((context as ReaderActivity).lifecycleScope) | ||||
|         binding.pagerPrefsGroup.landscapeZoom.bindToPreference(readerPreferences.landscapeZoom()) | ||||
|  | ||||
|         binding.pagerPrefsGroup.zoomStart.bindToPreference(readerPreferences.zoomStart(), 1) | ||||
|         binding.pagerPrefsGroup.cropBorders.bindToPreference(readerPreferences.cropBorders()) | ||||
|  | ||||
|         binding.pagerPrefsGroup.dualPageSplit.bindToPreference(readerPreferences.dualPageSplitPaged()) | ||||
|         readerPreferences.dualPageSplitPaged().changes() | ||||
|             .onEach { | ||||
|                 binding.pagerPrefsGroup.dualPageInvert.isVisible = it | ||||
|                 if (it) { | ||||
|                     binding.pagerPrefsGroup.dualPageRotateToFit.isChecked = false | ||||
|                 } | ||||
|             } | ||||
|             .launchIn((context as ReaderActivity).lifecycleScope) | ||||
|         binding.pagerPrefsGroup.dualPageInvert.bindToPreference(readerPreferences.dualPageInvertPaged()) | ||||
|  | ||||
|         binding.pagerPrefsGroup.dualPageRotateToFit.bindToPreference(readerPreferences.dualPageRotateToFit()) | ||||
|         readerPreferences.dualPageRotateToFit().changes() | ||||
|             .onEach { | ||||
|                 binding.pagerPrefsGroup.dualPageRotateToFitInvert.isVisible = it | ||||
|                 if (it) { | ||||
|                     binding.pagerPrefsGroup.dualPageSplit.isChecked = false | ||||
|                 } | ||||
|             } | ||||
|             .launchIn((context as ReaderActivity).lifecycleScope) | ||||
|         binding.pagerPrefsGroup.dualPageRotateToFitInvert.bindToPreference(readerPreferences.dualPageRotateToFitInvert()) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Init the preferences for the webtoon reader. | ||||
|      */ | ||||
|     private fun initWebtoonPreferences() { | ||||
|         binding.pagerPrefsGroup.root.isVisible = false | ||||
|         binding.webtoonPrefsGroup.root.isVisible = true | ||||
|  | ||||
|         binding.webtoonPrefsGroup.tappingInverted.bindToPreference(readerPreferences.webtoonNavInverted(), ReaderPreferences.TappingInvertMode::class.java) | ||||
|  | ||||
|         binding.webtoonPrefsGroup.webtoonNav.bindToPreference(readerPreferences.navigationModeWebtoon()) | ||||
|         readerPreferences.navigationModeWebtoon().changes() | ||||
|             .onEach { binding.webtoonPrefsGroup.tappingInverted.isVisible = it != 5 } | ||||
|             .launchIn((context as ReaderActivity).lifecycleScope) | ||||
|         binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(readerPreferences.cropBordersWebtoon()) | ||||
|         binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(readerPreferences.webtoonSidePadding(), R.array.webtoon_side_padding_values) | ||||
|  | ||||
|         binding.webtoonPrefsGroup.dualPageSplit.bindToPreference(readerPreferences.dualPageSplitWebtoon()) | ||||
|         // Makes it so that dual page invert gets hidden away when dual page split is turned off | ||||
|         readerPreferences.dualPageSplitWebtoon().changes() | ||||
|             .onEach { binding.webtoonPrefsGroup.dualPageInvert.isVisible = it } | ||||
|             .launchIn((context as ReaderActivity).lifecycleScope) | ||||
|         binding.webtoonPrefsGroup.dualPageInvert.bindToPreference(readerPreferences.dualPageInvertWebtoon()) | ||||
|  | ||||
|         binding.webtoonPrefsGroup.longStripSplit.isVisible = !isReleaseBuildType | ||||
|         binding.webtoonPrefsGroup.longStripSplit.bindToPreference(readerPreferences.longStripSplitWebtoon()) | ||||
|  | ||||
|         binding.webtoonPrefsGroup.doubleTapZoom.bindToPreference(readerPreferences.webtoonDoubleTapZoomEnabled()) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| package eu.kanade.tachiyomi.ui.reader.setting | ||||
|  | ||||
| import cafe.adriel.voyager.core.model.ScreenModel | ||||
| import eu.kanade.tachiyomi.util.preference.toggle | ||||
| import tachiyomi.core.preference.Preference | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class ReaderSettingsScreenModel( | ||||
|     val preferences: ReaderPreferences = Injekt.get(), | ||||
| ) : ScreenModel { | ||||
|  | ||||
|     fun togglePreference(preference: (ReaderPreferences) -> Preference<Boolean>) { | ||||
|         preference(preferences).toggle() | ||||
|     } | ||||
| } | ||||
| @@ -1,55 +1,136 @@ | ||||
| package eu.kanade.tachiyomi.ui.reader.setting | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import com.google.android.material.bottomsheet.BottomSheetDialog | ||||
| import eu.kanade.domain.manga.model.orientationType | ||||
| import eu.kanade.domain.manga.model.readingModeType | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.databinding.CommonTabbedSheetBinding | ||||
| import eu.kanade.tachiyomi.databinding.ReaderReadingModeSettingsBinding | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.widget.ViewPagerAdapter | ||||
| import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer | ||||
| import eu.kanade.tachiyomi.util.preference.bindToPreference | ||||
| import eu.kanade.tachiyomi.util.system.isReleaseBuildType | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class ReaderSettingsSheet( | ||||
|     private val activity: ReaderActivity, | ||||
| ) : BaseBottomSheetDialog(activity) { | ||||
| ) : BottomSheetDialog(activity) { | ||||
|  | ||||
|     private val tabs = listOf( | ||||
|         ReaderReadingModeSettings(activity) to R.string.pref_category_reading_mode, | ||||
|         ReaderGeneralSettings(activity) to R.string.pref_category_general, | ||||
|     ) | ||||
|     private val readerPreferences: ReaderPreferences by injectLazy() | ||||
|  | ||||
|     private lateinit var binding: CommonTabbedSheetBinding | ||||
|  | ||||
|     override fun createView(inflater: LayoutInflater): View { | ||||
|         binding = CommonTabbedSheetBinding.inflate(activity.layoutInflater) | ||||
|  | ||||
|         val adapter = Adapter() | ||||
|         binding.pager.adapter = adapter | ||||
|         binding.tabs.setupWithViewPager(binding.pager) | ||||
|  | ||||
|         return binding.root | ||||
|     } | ||||
|     private lateinit var binding: ReaderReadingModeSettingsBinding | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|  | ||||
|         behavior.isFitToContents = false | ||||
|         behavior.halfExpandedRatio = 0.25f | ||||
|         binding = ReaderReadingModeSettingsBinding.inflate(activity.layoutInflater) | ||||
|         setContentView(binding.root) | ||||
|  | ||||
|         initGeneralPreferences() | ||||
|  | ||||
|         when (activity.viewModel.state.value.viewer) { | ||||
|             is PagerViewer -> initPagerPreferences() | ||||
|             is WebtoonViewer -> initWebtoonPreferences() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private inner class Adapter : ViewPagerAdapter() { | ||||
|     private fun initGeneralPreferences() { | ||||
|         binding.viewer.onItemSelectedListener = { position -> | ||||
|             val readingModeType = ReadingModeType.fromSpinner(position) | ||||
|             activity.viewModel.setMangaReadingMode(readingModeType.flagValue) | ||||
|  | ||||
|         override fun createView(container: ViewGroup, position: Int): View { | ||||
|             return tabs[position].first | ||||
|             val mangaViewer = activity.viewModel.getMangaReadingMode() | ||||
|             if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) { | ||||
|                 initWebtoonPreferences() | ||||
|             } else { | ||||
|                 initPagerPreferences() | ||||
|             } | ||||
|         } | ||||
|         binding.viewer.setSelection(activity.viewModel.manga?.readingModeType?.let { ReadingModeType.fromPreference(it.toInt()).prefValue } ?: ReadingModeType.DEFAULT.prefValue) | ||||
|  | ||||
|         override fun getCount(): Int { | ||||
|             return tabs.size | ||||
|         binding.rotationMode.onItemSelectedListener = { position -> | ||||
|             val rotationType = OrientationType.fromSpinner(position) | ||||
|             activity.viewModel.setMangaOrientationType(rotationType.flagValue) | ||||
|         } | ||||
|         binding.rotationMode.setSelection(activity.viewModel.manga?.orientationType?.let { OrientationType.fromPreference(it.toInt()).prefValue } ?: OrientationType.DEFAULT.prefValue) | ||||
|     } | ||||
|  | ||||
|         override fun getPageTitle(position: Int): CharSequence { | ||||
|             return activity.resources!!.getString(tabs[position].second) | ||||
|         } | ||||
|     private fun initPagerPreferences() { | ||||
|         binding.webtoonPrefsGroup.root.isVisible = false | ||||
|         binding.pagerPrefsGroup.root.isVisible = true | ||||
|  | ||||
|         binding.pagerPrefsGroup.tappingInverted.bindToPreference(readerPreferences.pagerNavInverted(), ReaderPreferences.TappingInvertMode::class.java) | ||||
|         binding.pagerPrefsGroup.navigatePan.bindToPreference(readerPreferences.navigateToPan()) | ||||
|  | ||||
|         binding.pagerPrefsGroup.pagerNav.bindToPreference(readerPreferences.navigationModePager()) | ||||
|         readerPreferences.navigationModePager().changes() | ||||
|             .onEach { | ||||
|                 val isTappingEnabled = it != 5 | ||||
|                 binding.pagerPrefsGroup.tappingInverted.isVisible = isTappingEnabled | ||||
|                 binding.pagerPrefsGroup.navigatePan.isVisible = isTappingEnabled | ||||
|             } | ||||
|             .launchIn(activity.lifecycleScope) | ||||
|         // Makes so that landscape zoom gets hidden away when image scale type is not fit screen | ||||
|         binding.pagerPrefsGroup.scaleType.bindToPreference(readerPreferences.imageScaleType(), 1) | ||||
|         readerPreferences.imageScaleType().changes() | ||||
|             .onEach { binding.pagerPrefsGroup.landscapeZoom.isVisible = it == 1 } | ||||
|             .launchIn(activity.lifecycleScope) | ||||
|         binding.pagerPrefsGroup.landscapeZoom.bindToPreference(readerPreferences.landscapeZoom()) | ||||
|  | ||||
|         binding.pagerPrefsGroup.zoomStart.bindToPreference(readerPreferences.zoomStart(), 1) | ||||
|         binding.pagerPrefsGroup.cropBorders.bindToPreference(readerPreferences.cropBorders()) | ||||
|  | ||||
|         binding.pagerPrefsGroup.dualPageSplit.bindToPreference(readerPreferences.dualPageSplitPaged()) | ||||
|         readerPreferences.dualPageSplitPaged().changes() | ||||
|             .onEach { | ||||
|                 binding.pagerPrefsGroup.dualPageInvert.isVisible = it | ||||
|                 if (it) { | ||||
|                     binding.pagerPrefsGroup.dualPageRotateToFit.isChecked = false | ||||
|                 } | ||||
|             } | ||||
|             .launchIn(activity.lifecycleScope) | ||||
|         binding.pagerPrefsGroup.dualPageInvert.bindToPreference(readerPreferences.dualPageInvertPaged()) | ||||
|  | ||||
|         binding.pagerPrefsGroup.dualPageRotateToFit.bindToPreference(readerPreferences.dualPageRotateToFit()) | ||||
|         readerPreferences.dualPageRotateToFit().changes() | ||||
|             .onEach { | ||||
|                 binding.pagerPrefsGroup.dualPageRotateToFitInvert.isVisible = it | ||||
|                 if (it) { | ||||
|                     binding.pagerPrefsGroup.dualPageSplit.isChecked = false | ||||
|                 } | ||||
|             } | ||||
|             .launchIn(activity.lifecycleScope) | ||||
|         binding.pagerPrefsGroup.dualPageRotateToFitInvert.bindToPreference(readerPreferences.dualPageRotateToFitInvert()) | ||||
|     } | ||||
|  | ||||
|     private fun initWebtoonPreferences() { | ||||
|         binding.pagerPrefsGroup.root.isVisible = false | ||||
|         binding.webtoonPrefsGroup.root.isVisible = true | ||||
|  | ||||
|         binding.webtoonPrefsGroup.tappingInverted.bindToPreference(readerPreferences.webtoonNavInverted(), ReaderPreferences.TappingInvertMode::class.java) | ||||
|  | ||||
|         binding.webtoonPrefsGroup.webtoonNav.bindToPreference(readerPreferences.navigationModeWebtoon()) | ||||
|         readerPreferences.navigationModeWebtoon().changes() | ||||
|             .onEach { binding.webtoonPrefsGroup.tappingInverted.isVisible = it != 5 } | ||||
|             .launchIn(activity.lifecycleScope) | ||||
|         binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(readerPreferences.cropBordersWebtoon()) | ||||
|         binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(readerPreferences.webtoonSidePadding(), R.array.webtoon_side_padding_values) | ||||
|  | ||||
|         binding.webtoonPrefsGroup.dualPageSplit.bindToPreference(readerPreferences.dualPageSplitWebtoon()) | ||||
|         // Makes it so that dual page invert gets hidden away when dual page split is turned off | ||||
|         readerPreferences.dualPageSplitWebtoon().changes() | ||||
|             .onEach { binding.webtoonPrefsGroup.dualPageInvert.isVisible = it } | ||||
|             .launchIn(activity.lifecycleScope) | ||||
|         binding.webtoonPrefsGroup.dualPageInvert.bindToPreference(readerPreferences.dualPageInvertWebtoon()) | ||||
|  | ||||
|         binding.webtoonPrefsGroup.longStripSplit.isVisible = !isReleaseBuildType | ||||
|         binding.webtoonPrefsGroup.longStripSplit.bindToPreference(readerPreferences.longStripSplitWebtoon()) | ||||
|  | ||||
|         binding.webtoonPrefsGroup.doubleTapZoom.bindToPreference(readerPreferences.webtoonDoubleTapZoomEnabled()) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,10 +12,10 @@ import androidx.compose.ui.Modifier | ||||
| import cafe.adriel.voyager.navigator.LocalNavigator | ||||
| import cafe.adriel.voyager.navigator.Navigator | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import eu.kanade.presentation.more.settings.screen.AboutScreen | ||||
| import eu.kanade.presentation.more.settings.screen.SettingsAppearanceScreen | ||||
| import eu.kanade.presentation.more.settings.screen.SettingsBackupAndSyncScreen | ||||
| import eu.kanade.presentation.more.settings.screen.SettingsMainScreen | ||||
| import eu.kanade.presentation.more.settings.screen.about.AboutScreen | ||||
| import eu.kanade.presentation.util.DefaultNavigatorScreenTransition | ||||
| import eu.kanade.presentation.util.LocalBackPress | ||||
| import eu.kanade.presentation.util.Screen | ||||
|   | ||||
| @@ -1,55 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.sheet | ||||
|  | ||||
| import android.content.Context | ||||
| import android.os.Build | ||||
| import android.os.Bundle | ||||
| import android.util.DisplayMetrics | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import com.google.android.material.bottomsheet.BottomSheetDialog | ||||
| import com.google.android.material.bottomsheet.getElevation | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.system.displayCompat | ||||
| import eu.kanade.tachiyomi.util.system.isNightMode | ||||
| import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat | ||||
|  | ||||
| abstract class BaseBottomSheetDialog(context: Context) : BottomSheetDialog(context) { | ||||
|  | ||||
|     abstract fun createView(inflater: LayoutInflater): View | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|  | ||||
|         val rootView = createView(layoutInflater) | ||||
|         setContentView(rootView) | ||||
|  | ||||
|         // Enforce max width for tablets | ||||
|         val width = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_width) | ||||
|         if (width > 0) { | ||||
|             behavior.maxWidth = width | ||||
|         } | ||||
|  | ||||
|         // Set peek height to 50% display height | ||||
|         context.displayCompat?.let { | ||||
|             val metrics = DisplayMetrics() | ||||
|             it.getRealMetrics(metrics) | ||||
|             behavior.peekHeight = metrics.heightPixels / 2 | ||||
|         } | ||||
|  | ||||
|         // Set navbar color to transparent for edge-to-edge bottom sheet if we can use light navigation bar | ||||
|         // TODO Replace deprecated systemUiVisibility when material-components uses new API to modify status bar icons | ||||
|         @Suppress("DEPRECATION") | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             window?.setNavigationBarTransparentCompat(context, behavior.getElevation()) | ||||
|             val bottomSheet = rootView.parent as ViewGroup | ||||
|             var flags = bottomSheet.systemUiVisibility | ||||
|             flags = if (context.isNightMode()) { | ||||
|                 flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() | ||||
|             } else { | ||||
|                 flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | ||||
|             } | ||||
|             bottomSheet.systemUiVisibility = flags | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,56 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.sheet | ||||
| import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| import android.view.View | ||||
| import androidx.viewpager.widget.ViewPager | ||||
| import java.lang.reflect.Field | ||||
|  | ||||
| /** | ||||
|  * From https://github.com/kafumi/android-bottomsheet-viewpager | ||||
|  */ | ||||
| class BottomSheetViewPager @JvmOverloads constructor( | ||||
|     context: Context, | ||||
|     attrs: AttributeSet? = null, | ||||
| ) : ViewPager(context, attrs) { | ||||
|  | ||||
|     private val positionField: Field = LayoutParams::class.java.getDeclaredField("position").also { | ||||
|         it.isAccessible = true | ||||
|     } | ||||
|  | ||||
|     override fun getChildAt(index: Int): View { | ||||
|         val currentView = getCurrentView() ?: return super.getChildAt(index) | ||||
|         return if (index == 0) { | ||||
|             currentView | ||||
|         } else { | ||||
|             var view = super.getChildAt(index) | ||||
|             if (view == currentView) { | ||||
|                 view = super.getChildAt(0) | ||||
|             } | ||||
|             return view | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun getCurrentView(): View? { | ||||
|         for (i in 0 until childCount) { | ||||
|             val child = super.getChildAt(i) | ||||
|             val lp = child.layoutParams as? LayoutParams | ||||
|             if (lp != null) { | ||||
|                 val position = positionField.getInt(lp) | ||||
|                 if (!lp.isDecor && currentItem == position) { | ||||
|                     return child | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return null | ||||
|     } | ||||
|  | ||||
|     init { | ||||
|         addOnPageChangeListener( | ||||
|             object : SimpleOnPageChangeListener() { | ||||
|                 override fun onPageSelected(position: Int) { | ||||
|                     requestLayout() | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="24dp" | ||||
|     android:height="24dp" | ||||
|     android:viewportWidth="24" | ||||
|     android:viewportHeight="24"> | ||||
|     <path | ||||
|         android:fillColor="@android:color/black" | ||||
|         android:pathData="M20,15.31L23.31,12 20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6z" /> | ||||
| </vector> | ||||
| @@ -1,50 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:orientation="vertical"> | ||||
|  | ||||
|     <androidx.constraintlayout.widget.ConstraintLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:background="@drawable/transparent_tabs_background"> | ||||
|  | ||||
|         <!-- Remove background color so rounded sheet corners work --> | ||||
|         <com.google.android.material.tabs.TabLayout | ||||
|             android:id="@+id/tabs" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:background="@android:color/transparent" | ||||
|             app:layout_constraintBottom_toBottomOf="parent" | ||||
|             app:layout_constraintEnd_toStartOf="@+id/menu" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toTopOf="parent" | ||||
|             app:tabGravity="fill" | ||||
|             app:tabMode="fixed" /> | ||||
|  | ||||
|         <ImageButton | ||||
|             android:id="@+id/menu" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:background="?attr/selectableItemBackgroundBorderless" | ||||
|             android:contentDescription="@string/action_menu" | ||||
|             android:paddingStart="10dp" | ||||
|             android:paddingEnd="10dp" | ||||
|             android:visibility="gone" | ||||
|             app:layout_constraintBottom_toBottomOf="parent" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintTop_toTopOf="parent" | ||||
|             app:srcCompat="@drawable/ic_overflow_24dp" | ||||
|             app:tint="?attr/colorOnSurface" | ||||
|             tools:visibility="visible" /> | ||||
|  | ||||
|     </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|  | ||||
|     <eu.kanade.tachiyomi.widget.sheet.BottomSheetViewPager | ||||
|         android:id="@+id/pager" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" /> | ||||
|  | ||||
| </LinearLayout> | ||||
| @@ -119,29 +119,29 @@ | ||||
|                     app:tint="?attr/colorOnSurface" /> | ||||
|  | ||||
|                 <ImageButton | ||||
|                     android:id="@+id/action_settings" | ||||
|                     android:id="@+id/action_settings_legacy" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="match_parent" | ||||
|                     android:background="?attr/selectableItemBackgroundBorderless" | ||||
|                     android:contentDescription="@string/action_settings" | ||||
|                     android:padding="@dimen/screen_edge_margin" | ||||
|                     app:layout_constraintEnd_toStartOf="@+id/action_color_settings" | ||||
|                     app:layout_constraintEnd_toStartOf="@+id/action_settings" | ||||
|                     app:layout_constraintStart_toEndOf="@id/action_rotation" | ||||
|                     app:layout_constraintTop_toTopOf="parent" | ||||
|                     app:srcCompat="@drawable/ic_settings_24dp" | ||||
|                     app:tint="?attr/colorOnSurface" /> | ||||
|  | ||||
|                 <ImageButton | ||||
|                     android:id="@+id/action_color_settings" | ||||
|                     android:id="@+id/action_settings" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="match_parent" | ||||
|                     android:background="?attr/selectableItemBackgroundBorderless" | ||||
|                     android:contentDescription="@string/custom_filter" | ||||
|                     android:contentDescription="@string/action_settings" | ||||
|                     android:padding="@dimen/screen_edge_margin" | ||||
|                     app:layout_constraintEnd_toEndOf="parent" | ||||
|                     app:layout_constraintStart_toEndOf="@id/action_settings" | ||||
|                     app:layout_constraintStart_toEndOf="@id/action_settings_legacy" | ||||
|                     app:layout_constraintTop_toTopOf="parent" | ||||
|                     app:srcCompat="@drawable/ic_brightness_5_24dp" | ||||
|                     app:srcCompat="@drawable/ic_settings_24dp" | ||||
|                     app:tint="?attr/colorOnSurface" /> | ||||
|  | ||||
|             </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|   | ||||
| @@ -1,87 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content"> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:orientation="vertical"> | ||||
|  | ||||
|         <eu.kanade.tachiyomi.widget.MaterialSpinnerView | ||||
|             android:id="@+id/background_color" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:entries="@array/reader_themes" | ||||
|             app:title="@string/pref_reader_theme" /> | ||||
|  | ||||
|         <com.google.android.material.materialswitch.MaterialSwitch | ||||
|             android:id="@+id/show_page_number" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:paddingHorizontal="16dp" | ||||
|             android:paddingVertical="16dp" | ||||
|             android:text="@string/pref_show_page_number" | ||||
|             android:textColor="?android:attr/textColorSecondary" /> | ||||
|  | ||||
|         <com.google.android.material.materialswitch.MaterialSwitch | ||||
|             android:id="@+id/fullscreen" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:paddingHorizontal="16dp" | ||||
|             android:paddingVertical="16dp" | ||||
|             android:text="@string/pref_fullscreen" | ||||
|             android:textColor="?android:attr/textColorSecondary" /> | ||||
|  | ||||
|         <com.google.android.material.materialswitch.MaterialSwitch | ||||
|             android:id="@+id/cutout_short" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:paddingHorizontal="16dp" | ||||
|             android:paddingVertical="16dp" | ||||
|             android:text="@string/pref_cutout_short" | ||||
|             android:textColor="?android:attr/textColorSecondary" | ||||
|             android:visibility="gone" | ||||
|             tools:visibility="visible" /> | ||||
|  | ||||
|         <com.google.android.material.materialswitch.MaterialSwitch | ||||
|             android:id="@+id/keepscreen" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:paddingHorizontal="16dp" | ||||
|             android:paddingVertical="16dp" | ||||
|             android:text="@string/pref_keep_screen_on" | ||||
|             android:textColor="?android:attr/textColorSecondary" /> | ||||
|  | ||||
|         <com.google.android.material.materialswitch.MaterialSwitch | ||||
|             android:id="@+id/long_tap" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:paddingHorizontal="16dp" | ||||
|             android:paddingVertical="16dp" | ||||
|             android:text="@string/pref_read_with_long_tap" | ||||
|             android:textColor="?android:attr/textColorSecondary" /> | ||||
|  | ||||
|         <com.google.android.material.materialswitch.MaterialSwitch | ||||
|             android:id="@+id/always_show_chapter_transition" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:paddingHorizontal="16dp" | ||||
|             android:paddingVertical="16dp" | ||||
|             android:text="@string/pref_always_show_chapter_transition" | ||||
|             android:textColor="?android:attr/textColorSecondary" /> | ||||
|  | ||||
|         <com.google.android.material.materialswitch.MaterialSwitch | ||||
|             android:id="@+id/page_transitions" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:paddingHorizontal="16dp" | ||||
|             android:paddingVertical="16dp" | ||||
|             android:text="@string/pref_page_transitions" | ||||
|             android:textColor="?android:attr/textColorSecondary" /> | ||||
|  | ||||
|     </LinearLayout> | ||||
|  | ||||
| </androidx.core.widget.NestedScrollView> | ||||
| @@ -1,5 +1,3 @@ | ||||
| <resources> | ||||
|     <dimen name="bottom_sheet_width">480dp</dimen> | ||||
|  | ||||
|     <dimen name="screen_edge_margin">24dp</dimen> | ||||
| </resources> | ||||
|   | ||||
| @@ -9,20 +9,6 @@ | ||||
|         <item>@string/vertical_plus_viewer</item> | ||||
|     </string-array> | ||||
|  | ||||
|     <string-array name="reader_themes"> | ||||
|         <item>@string/black_background</item> | ||||
|         <item>@string/gray_background</item> | ||||
|         <item>@string/white_background</item> | ||||
|         <item>@string/automatic_background</item> | ||||
|     </string-array> | ||||
|  | ||||
|     <string-array name="reader_themes_values"> | ||||
|         <item>1</item> | ||||
|         <item>2</item> | ||||
|         <item>0</item> | ||||
|         <item>3</item> | ||||
|     </string-array> | ||||
|  | ||||
|     <string-array name="image_scale_type"> | ||||
|         <item>@string/scale_type_fit_screen</item> | ||||
|         <item>@string/scale_type_stretch</item> | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| <resources> | ||||
|     <dimen name="bottom_sheet_width">0dp</dimen> | ||||
|  | ||||
|     <dimen name="dialog_radius">8dp</dimen> | ||||
|  | ||||
|     <dimen name="screen_edge_margin">16dp</dimen> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user