mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Remove dead code
Mostly from settings rewrite, but some other things too.
This commit is contained in:
		| @@ -1,62 +0,0 @@ | ||||
| package eu.kanade.presentation.more.settings | ||||
|  | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.Search | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.graphics.painter.Painter | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.AppBarActions | ||||
| import eu.kanade.presentation.components.PreferenceRow | ||||
| import eu.kanade.presentation.components.Scaffold | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
| import eu.kanade.tachiyomi.R | ||||
|  | ||||
| @Composable | ||||
| fun SettingsMainScreen( | ||||
|     navigateUp: () -> Unit, | ||||
|     sections: List<SettingsSection>, | ||||
|     onClickSearch: () -> Unit, | ||||
| ) { | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             AppBar( | ||||
|                 title = stringResource(R.string.label_settings), | ||||
|                 navigateUp = navigateUp, | ||||
|                 actions = { | ||||
|                     AppBarActions( | ||||
|                         listOf( | ||||
|                             AppBar.Action( | ||||
|                                 title = stringResource(R.string.action_search), | ||||
|                                 icon = Icons.Outlined.Search, | ||||
|                                 onClick = onClickSearch, | ||||
|                             ), | ||||
|                         ), | ||||
|                     ) | ||||
|                 }, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { contentPadding -> | ||||
|         ScrollbarLazyColumn( | ||||
|             contentPadding = contentPadding, | ||||
|         ) { | ||||
|             sections.map { | ||||
|                 item { | ||||
|                     PreferenceRow( | ||||
|                         title = stringResource(it.titleRes), | ||||
|                         painter = it.painter, | ||||
|                         onClick = it.onClick, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| data class SettingsSection( | ||||
|     @StringRes val titleRes: Int, | ||||
|     val painter: Painter, | ||||
|     val onClick: () -> Unit, | ||||
| ) | ||||
| @@ -1,116 +0,0 @@ | ||||
| package eu.kanade.presentation.more.settings | ||||
|  | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.foundation.text.KeyboardActions | ||||
| import androidx.compose.foundation.text.KeyboardOptions | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalFocusManager | ||||
| import androidx.compose.ui.platform.LocalSoftwareKeyboardController | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.text.input.ImeAction | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.presentation.components.Scaffold | ||||
| import eu.kanade.presentation.components.ScrollbarLazyColumn | ||||
| import eu.kanade.presentation.components.SearchToolbar | ||||
| import eu.kanade.presentation.util.horizontalPadding | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsController | ||||
| import eu.kanade.tachiyomi.ui.setting.search.SettingsSearchHelper | ||||
| import eu.kanade.tachiyomi.ui.setting.search.SettingsSearchPresenter | ||||
| import kotlin.reflect.full.createInstance | ||||
|  | ||||
| @Composable | ||||
| fun SettingsSearchScreen( | ||||
|     navigateUp: () -> Unit, | ||||
|     presenter: SettingsSearchPresenter, | ||||
|     onClickResult: (SettingsController) -> Unit, | ||||
| ) { | ||||
|     val results by presenter.state.collectAsState() | ||||
|     var query by remember { mutableStateOf("") } | ||||
|  | ||||
|     val keyboardController = LocalSoftwareKeyboardController.current | ||||
|     val focusManager = LocalFocusManager.current | ||||
|  | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             SearchToolbar( | ||||
|                 searchQuery = query, | ||||
|                 onChangeSearchQuery = { | ||||
|                     query = it | ||||
|                     presenter.searchSettings(it) | ||||
|                 }, | ||||
|                 placeholderText = stringResource(R.string.action_search_settings), | ||||
|                 onClickCloseSearch = navigateUp, | ||||
|                 onClickResetSearch = { query = "" }, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|                 keyboardOptions = KeyboardOptions.Default.copy( | ||||
|                     imeAction = ImeAction.Search, | ||||
|                 ), | ||||
|                 keyboardActions = KeyboardActions( | ||||
|                     onSearch = { | ||||
|                         focusManager.clearFocus() | ||||
|                         keyboardController?.hide() | ||||
|                     }, | ||||
|                 ), | ||||
|             ) | ||||
|         }, | ||||
|     ) { contentPadding -> | ||||
|         ScrollbarLazyColumn( | ||||
|             contentPadding = contentPadding, | ||||
|         ) { | ||||
|             items( | ||||
|                 items = results, | ||||
|                 key = { it.key.toString() }, | ||||
|             ) { result -> | ||||
|                 SearchResult(result, onClickResult) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun SearchResult( | ||||
|     result: SettingsSearchHelper.SettingsSearchResult, | ||||
|     onClickResult: (SettingsController) -> Unit, | ||||
| ) { | ||||
|     Column( | ||||
|         modifier = Modifier | ||||
|             .fillMaxWidth() | ||||
|             .clickable { | ||||
|                 // Must pass a new Controller instance to avoid this error | ||||
|                 // https://github.com/bluelinelabs/Conductor/issues/446 | ||||
|                 val controller = result.searchController::class.createInstance() | ||||
|                 controller.preferenceKey = result.key | ||||
|                 onClickResult(controller) | ||||
|             } | ||||
|             .padding(horizontal = horizontalPadding, vertical = 8.dp), | ||||
|     ) { | ||||
|         Text( | ||||
|             text = result.title, | ||||
|         ) | ||||
|  | ||||
|         Text( | ||||
|             text = result.summary, | ||||
|             style = MaterialTheme.typography.bodySmall.copy( | ||||
|                 color = MaterialTheme.colorScheme.outline, | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|         Text( | ||||
|             text = result.breadcrumb, | ||||
|             style = MaterialTheme.typography.bodySmall, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,53 +0,0 @@ | ||||
| package eu.kanade.presentation.more.settings.database | ||||
|  | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import eu.kanade.presentation.components.Scaffold | ||||
| import eu.kanade.presentation.more.settings.database.components.ClearDatabaseContent | ||||
| import eu.kanade.presentation.more.settings.database.components.ClearDatabaseDeleteDialog | ||||
| import eu.kanade.presentation.more.settings.database.components.ClearDatabaseToolbar | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.setting.database.ClearDatabasePresenter | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
|  | ||||
| @Composable | ||||
| fun ClearDatabaseScreen( | ||||
|     presenter: ClearDatabasePresenter, | ||||
|     navigateUp: () -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             ClearDatabaseToolbar( | ||||
|                 state = presenter, | ||||
|                 navigateUp = navigateUp, | ||||
|                 onClickSelectAll = { presenter.selectAll() }, | ||||
|                 onClickInvertSelection = { presenter.invertSelection() }, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { paddingValues -> | ||||
|         ClearDatabaseContent( | ||||
|             state = presenter, | ||||
|             contentPadding = paddingValues, | ||||
|             onClickSelection = { source -> | ||||
|                 presenter.toggleSelection(source) | ||||
|             }, | ||||
|             onClickDelete = { | ||||
|                 presenter.dialog = ClearDatabasePresenter.Dialog.Delete(presenter.selection) | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
|     val dialog = presenter.dialog | ||||
|     if (dialog is ClearDatabasePresenter.Dialog.Delete) { | ||||
|         ClearDatabaseDeleteDialog( | ||||
|             onDismissRequest = { presenter.dialog = null }, | ||||
|             onDelete = { | ||||
|                 presenter.removeMangaBySourceId(dialog.sourceIds) | ||||
|                 presenter.clearSelection() | ||||
|                 presenter.dialog = null | ||||
|                 context.toast(R.string.clear_database_completed) | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -6,14 +6,13 @@ import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.setValue | ||||
| import eu.kanade.domain.source.model.SourceWithCount | ||||
| import eu.kanade.tachiyomi.ui.setting.database.ClearDatabasePresenter | ||||
|  | ||||
| @Stable | ||||
| interface ClearDatabaseState { | ||||
|     val items: List<SourceWithCount> | ||||
|     val selection: List<Long> | ||||
|     val isEmpty: Boolean | ||||
|     var dialog: ClearDatabasePresenter.Dialog? | ||||
|     var dialog: Dialog? | ||||
| } | ||||
|  | ||||
| fun ClearDatabaseState(): ClearDatabaseState { | ||||
| @@ -24,5 +23,9 @@ class ClearDatabaseStateImpl : ClearDatabaseState { | ||||
|     override var items: List<SourceWithCount> by mutableStateOf(emptyList()) | ||||
|     override var selection: List<Long> by mutableStateOf(emptyList()) | ||||
|     override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } | ||||
|     override var dialog: ClearDatabasePresenter.Dialog? by mutableStateOf(null) | ||||
|     override var dialog: Dialog? by mutableStateOf(null) | ||||
| } | ||||
|  | ||||
| sealed class Dialog { | ||||
|     data class Delete(val sourceIds: List<Long>) : Dialog() | ||||
| } | ||||
|   | ||||
| @@ -1,401 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.ActivityNotFoundException | ||||
| import android.content.Intent | ||||
| import android.provider.Settings | ||||
| import android.webkit.WebStorage | ||||
| import android.webkit.WebView | ||||
| import androidx.core.net.toUri | ||||
| import androidx.preference.PreferenceScreen | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import eu.kanade.domain.library.service.LibraryPreferences | ||||
| import eu.kanade.domain.manga.repository.MangaRepository | ||||
| import eu.kanade.domain.ui.UiPreferences | ||||
| import eu.kanade.domain.ui.model.TabletUiMode | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.cache.ChapterCache | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateService | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceValues | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import eu.kanade.tachiyomi.network.NetworkPreferences | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_360 | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_ALIDNS | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_CONTROLD | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_DNSPOD | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_MULLVAD | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_NJALLA | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101 | ||||
| import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9 | ||||
| import eu.kanade.tachiyomi.ui.base.controller.openInBrowser | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.setting.database.ClearDatabaseController | ||||
| import eu.kanade.tachiyomi.util.CrashLogUtil | ||||
| import eu.kanade.tachiyomi.util.lang.launchNonCancellable | ||||
| import eu.kanade.tachiyomi.util.lang.withUIContext | ||||
| import eu.kanade.tachiyomi.util.preference.bindTo | ||||
| import eu.kanade.tachiyomi.util.preference.defaultValue | ||||
| import eu.kanade.tachiyomi.util.preference.editTextPreference | ||||
| import eu.kanade.tachiyomi.util.preference.entriesRes | ||||
| import eu.kanade.tachiyomi.util.preference.intListPreference | ||||
| import eu.kanade.tachiyomi.util.preference.listPreference | ||||
| import eu.kanade.tachiyomi.util.preference.onChange | ||||
| import eu.kanade.tachiyomi.util.preference.onClick | ||||
| import eu.kanade.tachiyomi.util.preference.preference | ||||
| import eu.kanade.tachiyomi.util.preference.preferenceCategory | ||||
| import eu.kanade.tachiyomi.util.preference.summaryRes | ||||
| import eu.kanade.tachiyomi.util.preference.switchPreference | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.util.system.DeviceUtil | ||||
| import eu.kanade.tachiyomi.util.system.isDevFlavor | ||||
| import eu.kanade.tachiyomi.util.system.isPackageInstalled | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import eu.kanade.tachiyomi.util.system.powerManager | ||||
| import eu.kanade.tachiyomi.util.system.setDefaultSettings | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import logcat.LogPriority | ||||
| import rikka.sui.Sui | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
|  | ||||
| class SettingsAdvancedController( | ||||
|     private val mangaRepository: MangaRepository = Injekt.get(), | ||||
| ) : SettingsController() { | ||||
|  | ||||
|     private val network: NetworkHelper by injectLazy() | ||||
|     private val chapterCache: ChapterCache by injectLazy() | ||||
|     private val trackManager: TrackManager by injectLazy() | ||||
|     private val networkPreferences: NetworkPreferences by injectLazy() | ||||
|     private val libraryPreferences: LibraryPreferences by injectLazy() | ||||
|     private val uiPreferences: UiPreferences by injectLazy() | ||||
|  | ||||
|     @SuppressLint("BatteryLife") | ||||
|     override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { | ||||
|         titleRes = R.string.pref_category_advanced | ||||
|  | ||||
|         if (isDevFlavor.not()) { | ||||
|             switchPreference { | ||||
|                 key = "acra.enable" | ||||
|                 titleRes = R.string.pref_enable_acra | ||||
|                 summaryRes = R.string.pref_acra_summary | ||||
|                 defaultValue = true | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preference { | ||||
|             key = "dump_crash_logs" | ||||
|             titleRes = R.string.pref_dump_crash_logs | ||||
|             summaryRes = R.string.pref_dump_crash_logs_summary | ||||
|  | ||||
|             onClick { | ||||
|                 viewScope.launchNonCancellable { | ||||
|                     CrashLogUtil(context).dumpLogs() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         switchPreference { | ||||
|             key = networkPreferences.verboseLogging().key() | ||||
|             titleRes = R.string.pref_verbose_logging | ||||
|             summaryRes = R.string.pref_verbose_logging_summary | ||||
|             defaultValue = isDevFlavor | ||||
|  | ||||
|             onChange { | ||||
|                 activity?.toast(R.string.requires_app_restart) | ||||
|                 true | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.label_background_activity | ||||
|  | ||||
|             preference { | ||||
|                 key = "pref_disable_battery_optimization" | ||||
|                 titleRes = R.string.pref_disable_battery_optimization | ||||
|                 summaryRes = R.string.pref_disable_battery_optimization_summary | ||||
|  | ||||
|                 onClick { | ||||
|                     val packageName: String = context.packageName | ||||
|                     if (!context.powerManager.isIgnoringBatteryOptimizations(packageName)) { | ||||
|                         try { | ||||
|                             val intent = Intent().apply { | ||||
|                                 action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS | ||||
|                                 data = "package:$packageName".toUri() | ||||
|                             } | ||||
|                             startActivity(intent) | ||||
|                         } catch (e: ActivityNotFoundException) { | ||||
|                             context.toast(R.string.battery_optimization_setting_activity_not_found) | ||||
|                         } | ||||
|                     } else { | ||||
|                         context.toast(R.string.battery_optimization_disabled) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             preference { | ||||
|                 key = "pref_dont_kill_my_app" | ||||
|                 title = "Don't kill my app!" | ||||
|                 summaryRes = R.string.about_dont_kill_my_app | ||||
|  | ||||
|                 onClick { | ||||
|                     openInBrowser("https://dontkillmyapp.com/") | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.label_data | ||||
|  | ||||
|             preference { | ||||
|                 key = CLEAR_CACHE_KEY | ||||
|                 titleRes = R.string.pref_clear_chapter_cache | ||||
|                 summary = context.getString(R.string.used_cache, chapterCache.readableSize) | ||||
|  | ||||
|                 onClick { clearChapterCache() } | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(libraryPreferences.autoClearChapterCache()) | ||||
|                 titleRes = R.string.pref_auto_clear_chapter_cache | ||||
|             } | ||||
|             preference { | ||||
|                 key = "pref_clear_database" | ||||
|                 titleRes = R.string.pref_clear_database | ||||
|                 summaryRes = R.string.pref_clear_database_summary | ||||
|  | ||||
|                 onClick { | ||||
|                     router.pushController(ClearDatabaseController()) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.label_network | ||||
|  | ||||
|             preference { | ||||
|                 key = "pref_clear_cookies" | ||||
|                 titleRes = R.string.pref_clear_cookies | ||||
|  | ||||
|                 onClick { | ||||
|                     network.cookieManager.removeAll() | ||||
|                     activity?.toast(R.string.cookies_cleared) | ||||
|                 } | ||||
|             } | ||||
|             preference { | ||||
|                 key = "pref_clear_webview_data" | ||||
|                 titleRes = R.string.pref_clear_webview_data | ||||
|  | ||||
|                 onClick { clearWebViewData() } | ||||
|             } | ||||
|             intListPreference { | ||||
|                 key = networkPreferences.dohProvider().key() | ||||
|                 titleRes = R.string.pref_dns_over_https | ||||
|                 entries = arrayOf( | ||||
|                     context.getString(R.string.disabled), | ||||
|                     "Cloudflare", | ||||
|                     "Google", | ||||
|                     "AdGuard", | ||||
|                     "Quad9", | ||||
|                     "AliDNS", | ||||
|                     "DNSPod", | ||||
|                     "360", | ||||
|                     "Quad 101", | ||||
|                     "Mullvad", | ||||
|                     "Control D", | ||||
|                     "Njalla", | ||||
|                 ) | ||||
|                 entryValues = arrayOf( | ||||
|                     "-1", | ||||
|                     PREF_DOH_CLOUDFLARE.toString(), | ||||
|                     PREF_DOH_GOOGLE.toString(), | ||||
|                     PREF_DOH_ADGUARD.toString(), | ||||
|                     PREF_DOH_QUAD9.toString(), | ||||
|                     PREF_DOH_ALIDNS.toString(), | ||||
|                     PREF_DOH_DNSPOD.toString(), | ||||
|                     PREF_DOH_360.toString(), | ||||
|                     PREF_DOH_QUAD101.toString(), | ||||
|                     PREF_DOH_MULLVAD.toString(), | ||||
|                     PREF_DOH_CONTROLD.toString(), | ||||
|                     PREF_DOH_NJALLA.toString(), | ||||
|                 ) | ||||
|                 defaultValue = "-1" | ||||
|                 summary = "%s" | ||||
|  | ||||
|                 onChange { | ||||
|                     activity?.toast(R.string.requires_app_restart) | ||||
|                     true | ||||
|                 } | ||||
|             } | ||||
|             val defaultUserAgent = networkPreferences.defaultUserAgent() | ||||
|             editTextPreference { | ||||
|                 key = defaultUserAgent.key() | ||||
|                 titleRes = R.string.pref_user_agent_string | ||||
|                 text = defaultUserAgent.get() | ||||
|                 summary = network.defaultUserAgent | ||||
|  | ||||
|                 onChange { | ||||
|                     if (it.toString().isBlank()) { | ||||
|                         activity?.toast(R.string.error_user_agent_string_blank) | ||||
|                     } else { | ||||
|                         text = it.toString().trim() | ||||
|                         activity?.toast(R.string.requires_app_restart) | ||||
|                     } | ||||
|                     false | ||||
|                 } | ||||
|             } | ||||
|             preference { | ||||
|                 key = "pref_reset_user_agent" | ||||
|                 titleRes = R.string.pref_reset_user_agent_string | ||||
|  | ||||
|                 visibleIf(defaultUserAgent) { it != defaultUserAgent.defaultValue() } | ||||
|  | ||||
|                 onClick { | ||||
|                     defaultUserAgent.delete() | ||||
|                     activity?.toast(R.string.requires_app_restart) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.label_library | ||||
|  | ||||
|             preference { | ||||
|                 key = "pref_refresh_library_covers" | ||||
|                 titleRes = R.string.pref_refresh_library_covers | ||||
|  | ||||
|                 onClick { LibraryUpdateService.start(context, target = Target.COVERS) } | ||||
|             } | ||||
|             if (trackManager.hasLoggedServices()) { | ||||
|                 preference { | ||||
|                     key = "pref_refresh_library_tracking" | ||||
|                     titleRes = R.string.pref_refresh_library_tracking | ||||
|                     summaryRes = R.string.pref_refresh_library_tracking_summary | ||||
|  | ||||
|                     onClick { LibraryUpdateService.start(context, target = Target.TRACKING) } | ||||
|                 } | ||||
|             } | ||||
|             preference { | ||||
|                 key = "pref_reset_viewer_flags" | ||||
|                 titleRes = R.string.pref_reset_viewer_flags | ||||
|                 summaryRes = R.string.pref_reset_viewer_flags_summary | ||||
|  | ||||
|                 onClick { resetViewerFlags() } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.label_extensions | ||||
|  | ||||
|             listPreference { | ||||
|                 bindTo(preferences.extensionInstaller()) | ||||
|                 titleRes = R.string.ext_installer_pref | ||||
|                 summary = "%s" | ||||
|  | ||||
|                 // PackageInstaller doesn't work on MIUI properly for non-allowlisted apps | ||||
|                 val values = if (DeviceUtil.isMiui) { | ||||
|                     PreferenceValues.ExtensionInstaller.values() | ||||
|                         .filter { it != PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER } | ||||
|                 } else { | ||||
|                     PreferenceValues.ExtensionInstaller.values().toList() | ||||
|                 } | ||||
|  | ||||
|                 entriesRes = values.map { it.titleResId }.toTypedArray() | ||||
|                 entryValues = values.map { it.name }.toTypedArray() | ||||
|  | ||||
|                 onChange { | ||||
|                     if (it == PreferenceValues.ExtensionInstaller.SHIZUKU.name && | ||||
|                         !(context.isPackageInstalled("moe.shizuku.privileged.api") || Sui.isSui()) | ||||
|                     ) { | ||||
|                         MaterialAlertDialogBuilder(context) | ||||
|                             .setTitle(R.string.ext_installer_shizuku) | ||||
|                             .setMessage(R.string.ext_installer_shizuku_unavailable_dialog) | ||||
|                             .setPositiveButton(android.R.string.ok) { _, _ -> | ||||
|                                 openInBrowser("https://shizuku.rikka.app/download") | ||||
|                             } | ||||
|                             .setNegativeButton(android.R.string.cancel, null) | ||||
|                             .show() | ||||
|                         false | ||||
|                     } else { | ||||
|                         true | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_category_display | ||||
|  | ||||
|             listPreference { | ||||
|                 bindTo(uiPreferences.tabletUiMode()) | ||||
|                 titleRes = R.string.pref_tablet_ui_mode | ||||
|                 summary = "%s" | ||||
|                 entriesRes = TabletUiMode.values().map { it.titleResId }.toTypedArray() | ||||
|                 entryValues = TabletUiMode.values().map { it.name }.toTypedArray() | ||||
|  | ||||
|                 onChange { | ||||
|                     activity?.toast(R.string.requires_app_restart) | ||||
|                     true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun clearChapterCache() { | ||||
|         val activity = activity ?: return | ||||
|         viewScope.launchNonCancellable { | ||||
|             try { | ||||
|                 val deletedFiles = chapterCache.clear() | ||||
|                 withUIContext { | ||||
|                     activity.toast(resources?.getString(R.string.cache_deleted, deletedFiles)) | ||||
|                     findPreference(CLEAR_CACHE_KEY)?.summary = | ||||
|                         resources?.getString(R.string.used_cache, chapterCache.readableSize) | ||||
|                 } | ||||
|             } catch (e: Throwable) { | ||||
|                 logcat(LogPriority.ERROR, e) | ||||
|                 withUIContext { activity.toast(R.string.cache_delete_error) } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun clearWebViewData() { | ||||
|         val activity = activity ?: return | ||||
|         try { | ||||
|             WebView(activity).run { | ||||
|                 setDefaultSettings() | ||||
|                 clearCache(true) | ||||
|                 clearFormData() | ||||
|                 clearHistory() | ||||
|                 clearSslPreferences() | ||||
|             } | ||||
|             WebStorage.getInstance().deleteAllData() | ||||
|             activity.applicationInfo?.dataDir?.let { File("$it/app_webview/").deleteRecursively() } | ||||
|             activity.toast(R.string.webview_data_deleted) | ||||
|         } catch (e: Throwable) { | ||||
|             logcat(LogPriority.ERROR, e) | ||||
|             activity.toast(R.string.cache_delete_error) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun resetViewerFlags() { | ||||
|         val activity = activity ?: return | ||||
|         viewScope.launchNonCancellable { | ||||
|             val success = mangaRepository.resetViewerFlags() | ||||
|             withUIContext { | ||||
|                 val message = if (success) { | ||||
|                     R.string.pref_reset_viewer_flags_success | ||||
|                 } else { | ||||
|                     R.string.pref_reset_viewer_flags_error | ||||
|                 } | ||||
|                 activity.toast(message) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private const val CLEAR_CACHE_KEY = "pref_clear_cache_key" | ||||
| @@ -1,173 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting | ||||
|  | ||||
| import android.os.Build | ||||
| import android.os.Bundle | ||||
| import android.view.View | ||||
| import androidx.core.app.ActivityCompat | ||||
| import androidx.preference.PreferenceScreen | ||||
| import eu.kanade.domain.ui.UiPreferences | ||||
| import eu.kanade.domain.ui.model.AppTheme | ||||
| import eu.kanade.domain.ui.model.ThemeMode | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.preference.bindTo | ||||
| import eu.kanade.tachiyomi.util.preference.entriesRes | ||||
| import eu.kanade.tachiyomi.util.preference.initThenAdd | ||||
| import eu.kanade.tachiyomi.util.preference.intListPreference | ||||
| import eu.kanade.tachiyomi.util.preference.listPreference | ||||
| import eu.kanade.tachiyomi.util.preference.onChange | ||||
| import eu.kanade.tachiyomi.util.preference.preferenceCategory | ||||
| import eu.kanade.tachiyomi.util.preference.switchPreference | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.util.system.DeviceUtil | ||||
| import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable | ||||
| import eu.kanade.tachiyomi.util.system.isTablet | ||||
| import eu.kanade.tachiyomi.widget.preference.ThemesPreference | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.util.Date | ||||
|  | ||||
| class SettingsAppearanceController : SettingsController() { | ||||
|  | ||||
|     private var themesPreference: ThemesPreference? = null | ||||
|     private val uiPreferences: UiPreferences by injectLazy() | ||||
|  | ||||
|     override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { | ||||
|         titleRes = R.string.pref_category_appearance | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_category_theme | ||||
|  | ||||
|             listPreference { | ||||
|                 bindTo(uiPreferences.themeMode()) | ||||
|                 titleRes = R.string.pref_theme_mode | ||||
|  | ||||
|                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|                     entriesRes = arrayOf( | ||||
|                         R.string.theme_system, | ||||
|                         R.string.theme_light, | ||||
|                         R.string.theme_dark, | ||||
|                     ) | ||||
|                     entryValues = arrayOf( | ||||
|                         ThemeMode.SYSTEM.name, | ||||
|                         ThemeMode.LIGHT.name, | ||||
|                         ThemeMode.DARK.name, | ||||
|                     ) | ||||
|                 } else { | ||||
|                     entriesRes = arrayOf( | ||||
|                         R.string.theme_light, | ||||
|                         R.string.theme_dark, | ||||
|                     ) | ||||
|                     entryValues = arrayOf( | ||||
|                         ThemeMode.LIGHT.name, | ||||
|                         ThemeMode.DARK.name, | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|                 summary = "%s" | ||||
|             } | ||||
|             themesPreference = initThenAdd(ThemesPreference(context)) { | ||||
|                 bindTo(uiPreferences.appTheme()) | ||||
|                 titleRes = R.string.pref_app_theme | ||||
|  | ||||
|                 val appThemes = AppTheme.values().filter { | ||||
|                     val monetFilter = if (it == AppTheme.MONET) { | ||||
|                         DeviceUtil.isDynamicColorAvailable | ||||
|                     } else { | ||||
|                         true | ||||
|                     } | ||||
|                     it.titleResId != null && monetFilter | ||||
|                 } | ||||
|                 entries = appThemes | ||||
|  | ||||
|                 onChange { | ||||
|                     activity?.let { ActivityCompat.recreate(it) } | ||||
|                     true | ||||
|                 } | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(uiPreferences.themeDarkAmoled()) | ||||
|                 titleRes = R.string.pref_dark_theme_pure_black | ||||
|  | ||||
|                 visibleIf(uiPreferences.themeMode()) { it != ThemeMode.LIGHT } | ||||
|  | ||||
|                 onChange { | ||||
|                     activity?.let { ActivityCompat.recreate(it) } | ||||
|                     true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (context.isTablet()) { | ||||
|             preferenceCategory { | ||||
|                 titleRes = R.string.pref_category_navigation | ||||
|  | ||||
|                 intListPreference { | ||||
|                     bindTo(uiPreferences.sideNavIconAlignment()) | ||||
|                     titleRes = R.string.pref_side_nav_icon_alignment | ||||
|                     entriesRes = arrayOf( | ||||
|                         R.string.alignment_top, | ||||
|                         R.string.alignment_center, | ||||
|                         R.string.alignment_bottom, | ||||
|                     ) | ||||
|                     entryValues = arrayOf("0", "1", "2") | ||||
|                     summary = "%s" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_category_timestamps | ||||
|  | ||||
|             intListPreference { | ||||
|                 bindTo(uiPreferences.relativeTime()) | ||||
|                 titleRes = R.string.pref_relative_format | ||||
|                 val values = arrayOf("0", "2", "7") | ||||
|                 entryValues = values | ||||
|                 entries = values.map { | ||||
|                     when (it) { | ||||
|                         "0" -> context.getString(R.string.off) | ||||
|                         "2" -> context.getString(R.string.pref_relative_time_short) | ||||
|                         else -> context.getString(R.string.pref_relative_time_long) | ||||
|                     } | ||||
|                 }.toTypedArray() | ||||
|                 summary = "%s" | ||||
|             } | ||||
|  | ||||
|             listPreference { | ||||
|                 bindTo(uiPreferences.dateFormat()) | ||||
|                 titleRes = R.string.pref_date_format | ||||
|                 entryValues = arrayOf("", "MM/dd/yy", "dd/MM/yy", "yyyy-MM-dd", "dd MMM yyyy", "MMM dd, yyyy") | ||||
|  | ||||
|                 val now = Date().time | ||||
|                 entries = entryValues.map { value -> | ||||
|                     val formattedDate = UiPreferences.dateFormat(value.toString()).format(now) | ||||
|                     if (value == "") { | ||||
|                         "${context.getString(R.string.label_default)} ($formattedDate)" | ||||
|                     } else { | ||||
|                         "$value ($formattedDate)" | ||||
|                     } | ||||
|                 }.toTypedArray() | ||||
|  | ||||
|                 summary = "%s" | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onSaveViewState(view: View, outState: Bundle) { | ||||
|         themesPreference?.let { | ||||
|             outState.putInt(THEMES_SCROLL_POSITION, it.lastScrollPosition ?: 0) | ||||
|         } | ||||
|         super.onSaveInstanceState(outState) | ||||
|     } | ||||
|  | ||||
|     override fun onRestoreViewState(view: View, savedViewState: Bundle) { | ||||
|         super.onRestoreViewState(view, savedViewState) | ||||
|         themesPreference?.lastScrollPosition = savedViewState.getInt(THEMES_SCROLL_POSITION, 0) | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         super.onDestroyView(view) | ||||
|         themesPreference = null | ||||
|     } | ||||
| } | ||||
|  | ||||
| private const val THEMES_SCROLL_POSITION = "themesScrollPosition" | ||||
| @@ -1,306 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting | ||||
|  | ||||
| import android.Manifest.permission.WRITE_EXTERNAL_STORAGE | ||||
| import android.app.Activity | ||||
| import android.app.Dialog | ||||
| import android.content.ActivityNotFoundException | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.view.Menu | ||||
| import android.view.MenuInflater | ||||
| import android.view.MenuItem | ||||
| import android.view.View | ||||
| import android.widget.Toast | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.core.net.toUri | ||||
| import androidx.core.os.bundleOf | ||||
| import androidx.preference.PreferenceScreen | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import com.hippo.unifile.UniFile | ||||
| import eu.kanade.domain.backup.service.BackupPreferences | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.backup.BackupConst | ||||
| import eu.kanade.tachiyomi.data.backup.BackupCreatorJob | ||||
| import eu.kanade.tachiyomi.data.backup.BackupFileValidator | ||||
| import eu.kanade.tachiyomi.data.backup.BackupRestoreService | ||||
| import eu.kanade.tachiyomi.data.backup.models.Backup | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe | ||||
| import eu.kanade.tachiyomi.util.preference.bindTo | ||||
| import eu.kanade.tachiyomi.util.preference.entriesRes | ||||
| import eu.kanade.tachiyomi.util.preference.infoPreference | ||||
| import eu.kanade.tachiyomi.util.preference.intListPreference | ||||
| import eu.kanade.tachiyomi.util.preference.onChange | ||||
| import eu.kanade.tachiyomi.util.preference.onClick | ||||
| import eu.kanade.tachiyomi.util.preference.preference | ||||
| import eu.kanade.tachiyomi.util.preference.preferenceCategory | ||||
| import eu.kanade.tachiyomi.util.preference.summaryRes | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.util.system.DeviceUtil | ||||
| import eu.kanade.tachiyomi.util.system.getParcelableCompat | ||||
| import eu.kanade.tachiyomi.util.system.openInBrowser | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class SettingsBackupController : SettingsController() { | ||||
|  | ||||
|     /** | ||||
|      * Flags containing information of what to backup. | ||||
|      */ | ||||
|     private var backupFlags = 0 | ||||
|  | ||||
|     private val backupPreferences: BackupPreferences by injectLazy() | ||||
|  | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 500) | ||||
|     } | ||||
|  | ||||
|     override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { | ||||
|         titleRes = R.string.label_backup | ||||
|  | ||||
|         preference { | ||||
|             key = "pref_create_backup" | ||||
|             titleRes = R.string.pref_create_backup | ||||
|             summaryRes = R.string.pref_create_backup_summ | ||||
|  | ||||
|             onClick { | ||||
|                 if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { | ||||
|                     context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) | ||||
|                 } | ||||
|  | ||||
|                 if (!BackupCreatorJob.isManualJobRunning(context)) { | ||||
|                     val ctrl = CreateBackupDialog() | ||||
|                     ctrl.targetController = this@SettingsBackupController | ||||
|                     ctrl.showDialog(router) | ||||
|                 } else { | ||||
|                     context.toast(R.string.backup_in_progress) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         preference { | ||||
|             key = "pref_restore_backup" | ||||
|             titleRes = R.string.pref_restore_backup | ||||
|             summaryRes = R.string.pref_restore_backup_summ | ||||
|  | ||||
|             onClick { | ||||
|                 if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { | ||||
|                     context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) | ||||
|                 } | ||||
|  | ||||
|                 if (!BackupRestoreService.isRunning(context)) { | ||||
|                     val intent = Intent(Intent.ACTION_GET_CONTENT).apply { | ||||
|                         addCategory(Intent.CATEGORY_OPENABLE) | ||||
|                         type = "*/*" | ||||
|                     } | ||||
|                     val title = resources?.getString(R.string.file_select_backup) | ||||
|                     val chooser = Intent.createChooser(intent, title) | ||||
|                     startActivityForResult(chooser, CODE_BACKUP_RESTORE) | ||||
|                 } else { | ||||
|                     context.toast(R.string.restore_in_progress) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_backup_service_category | ||||
|  | ||||
|             intListPreference { | ||||
|                 bindTo(backupPreferences.backupInterval()) | ||||
|                 titleRes = R.string.pref_backup_interval | ||||
|                 entriesRes = arrayOf( | ||||
|                     R.string.update_6hour, | ||||
|                     R.string.update_12hour, | ||||
|                     R.string.update_24hour, | ||||
|                     R.string.update_48hour, | ||||
|                     R.string.update_weekly, | ||||
|                 ) | ||||
|                 entryValues = arrayOf("6", "12", "24", "48", "168") | ||||
|                 summary = "%s" | ||||
|  | ||||
|                 onChange { newValue -> | ||||
|                     val interval = (newValue as String).toInt() | ||||
|                     BackupCreatorJob.setupTask(context, interval) | ||||
|                     true | ||||
|                 } | ||||
|             } | ||||
|             preference { | ||||
|                 bindTo(backupPreferences.backupsDirectory()) | ||||
|                 titleRes = R.string.pref_backup_directory | ||||
|  | ||||
|                 onClick { | ||||
|                     try { | ||||
|                         val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) | ||||
|                         startActivityForResult(intent, CODE_BACKUP_DIR) | ||||
|                     } catch (e: ActivityNotFoundException) { | ||||
|                         activity?.toast(R.string.file_picker_error) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 backupPreferences.backupsDirectory().changes() | ||||
|                     .onEach { path -> | ||||
|                         val dir = UniFile.fromUri(context, path.toUri()) | ||||
|                         summary = dir.filePath + "/automatic" | ||||
|                     } | ||||
|                     .launchIn(viewScope) | ||||
|             } | ||||
|             intListPreference { | ||||
|                 bindTo(backupPreferences.numberOfBackups()) | ||||
|                 titleRes = R.string.pref_backup_slots | ||||
|                 entries = arrayOf("2", "3", "4", "5") | ||||
|                 entryValues = entries | ||||
|                 summary = "%s" | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         infoPreference(R.string.backup_info) | ||||
|     } | ||||
|  | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||
|         inflater.inflate(R.menu.settings_backup, menu) | ||||
|     } | ||||
|  | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.action_backup_help -> activity?.openInBrowser(HELP_URL) | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item) | ||||
|     } | ||||
|  | ||||
|     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | ||||
|         if (data != null && resultCode == Activity.RESULT_OK) { | ||||
|             val activity = activity ?: return | ||||
|             val uri = data.data | ||||
|  | ||||
|             if (uri == null) { | ||||
|                 activity.toast(R.string.backup_restore_invalid_uri) | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             when (requestCode) { | ||||
|                 CODE_BACKUP_DIR -> { | ||||
|                     // Get UriPermission so it's possible to write files | ||||
|                     val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or | ||||
|                         Intent.FLAG_GRANT_WRITE_URI_PERMISSION | ||||
|  | ||||
|                     activity.contentResolver.takePersistableUriPermission(uri, flags) | ||||
|                     backupPreferences.backupsDirectory().set(uri.toString()) | ||||
|                 } | ||||
|                 CODE_BACKUP_CREATE -> { | ||||
|                     val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or | ||||
|                         Intent.FLAG_GRANT_WRITE_URI_PERMISSION | ||||
|  | ||||
|                     activity.contentResolver.takePersistableUriPermission(uri, flags) | ||||
|                     BackupCreatorJob.startNow(activity, uri, backupFlags) | ||||
|                 } | ||||
|                 CODE_BACKUP_RESTORE -> { | ||||
|                     RestoreBackupDialog(uri).showDialog(router) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun createBackup(flags: Int) { | ||||
|         backupFlags = flags | ||||
|         try { | ||||
|             val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) | ||||
|                 .addCategory(Intent.CATEGORY_OPENABLE) | ||||
|                 .setType("application/*") | ||||
|                 .putExtra(Intent.EXTRA_TITLE, Backup.getBackupFilename()) | ||||
|  | ||||
|             startActivityForResult(intent, CODE_BACKUP_CREATE) | ||||
|         } catch (e: ActivityNotFoundException) { | ||||
|             activity?.toast(R.string.file_picker_error) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class CreateBackupDialog(bundle: Bundle? = null) : DialogController(bundle) { | ||||
|         override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|             val activity = activity!! | ||||
|             val options = arrayOf( | ||||
|                 R.string.manga, | ||||
|                 R.string.categories, | ||||
|                 R.string.chapters, | ||||
|                 R.string.track, | ||||
|                 R.string.history, | ||||
|             ) | ||||
|                 .map { activity.getString(it) } | ||||
|             val selected = options.map { true }.toBooleanArray() | ||||
|  | ||||
|             return MaterialAlertDialogBuilder(activity) | ||||
|                 .setTitle(R.string.backup_choice) | ||||
|                 .setMultiChoiceItems(options.toTypedArray(), selected) { dialog, which, checked -> | ||||
|                     if (which == 0) { | ||||
|                         (dialog as AlertDialog).listView.setItemChecked(which, true) | ||||
|                     } else { | ||||
|                         selected[which] = checked | ||||
|                     } | ||||
|                 } | ||||
|                 .setPositiveButton(R.string.action_create) { _, _ -> | ||||
|                     var flags = 0 | ||||
|                     selected.forEachIndexed { i, checked -> | ||||
|                         if (checked) { | ||||
|                             when (i) { | ||||
|                                 1 -> flags = flags or BackupConst.BACKUP_CATEGORY | ||||
|                                 2 -> flags = flags or BackupConst.BACKUP_CHAPTER | ||||
|                                 3 -> flags = flags or BackupConst.BACKUP_TRACK | ||||
|                                 4 -> flags = flags or BackupConst.BACKUP_HISTORY | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     (targetController as? SettingsBackupController)?.createBackup(flags) | ||||
|                 } | ||||
|                 .setNegativeButton(android.R.string.cancel, null) | ||||
|                 .create() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class RestoreBackupDialog(bundle: Bundle? = null) : DialogController(bundle) { | ||||
|         constructor(uri: Uri) : this( | ||||
|             bundleOf(KEY_URI to uri), | ||||
|         ) | ||||
|  | ||||
|         override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|             val activity = activity!! | ||||
|             val uri = args.getParcelableCompat<Uri>(KEY_URI)!! | ||||
|  | ||||
|             return try { | ||||
|                 val results = BackupFileValidator().validate(activity, uri) | ||||
|  | ||||
|                 var message = activity.getString(R.string.backup_restore_content_full) | ||||
|                 if (results.missingSources.isNotEmpty()) { | ||||
|                     message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}" | ||||
|                 } | ||||
|                 if (results.missingTrackers.isNotEmpty()) { | ||||
|                     message += "\n\n${activity.getString(R.string.backup_restore_missing_trackers)}\n${results.missingTrackers.joinToString("\n") { "- $it" }}" | ||||
|                 } | ||||
|  | ||||
|                 MaterialAlertDialogBuilder(activity) | ||||
|                     .setTitle(R.string.pref_restore_backup) | ||||
|                     .setMessage(message) | ||||
|                     .setPositiveButton(R.string.action_restore) { _, _ -> | ||||
|                         BackupRestoreService.start(activity, uri) | ||||
|                     } | ||||
|                     .create() | ||||
|             } catch (e: Exception) { | ||||
|                 MaterialAlertDialogBuilder(activity) | ||||
|                     .setTitle(R.string.invalid_backup_file) | ||||
|                     .setMessage(e.message) | ||||
|                     .setPositiveButton(android.R.string.cancel, null) | ||||
|                     .create() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private const val KEY_URI = "RestoreBackupDialog.uri" | ||||
|  | ||||
| private const val CODE_BACKUP_DIR = 503 | ||||
| private const val CODE_BACKUP_CREATE = 504 | ||||
| private const val CODE_BACKUP_RESTORE = 505 | ||||
|  | ||||
| private const val HELP_URL = "https://tachiyomi.org/help/guides/backups/" | ||||
| @@ -1,80 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting | ||||
|  | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.preference.PreferenceScreen | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.extension.ExtensionUpdateJob | ||||
| import eu.kanade.tachiyomi.util.preference.bindTo | ||||
| import eu.kanade.tachiyomi.util.preference.infoPreference | ||||
| import eu.kanade.tachiyomi.util.preference.onChange | ||||
| import eu.kanade.tachiyomi.util.preference.preferenceCategory | ||||
| import eu.kanade.tachiyomi.util.preference.requireAuthentication | ||||
| import eu.kanade.tachiyomi.util.preference.summaryRes | ||||
| import eu.kanade.tachiyomi.util.preference.switchPreference | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class SettingsBrowseController : SettingsController() { | ||||
|  | ||||
|     private val sourcePreferences: SourcePreferences by injectLazy() | ||||
|  | ||||
|     override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { | ||||
|         titleRes = R.string.browse | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.label_sources | ||||
|  | ||||
|             switchPreference { | ||||
|                 bindTo(sourcePreferences.duplicatePinnedSources()) | ||||
|                 titleRes = R.string.pref_duplicate_pinned_sources | ||||
|                 summaryRes = R.string.pref_duplicate_pinned_sources_summary | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.label_extensions | ||||
|  | ||||
|             switchPreference { | ||||
|                 bindTo(preferences.automaticExtUpdates()) | ||||
|                 titleRes = R.string.pref_enable_automatic_extension_updates | ||||
|  | ||||
|                 onChange { newValue -> | ||||
|                     val checked = newValue as Boolean | ||||
|                     ExtensionUpdateJob.setupTask(activity!!, checked) | ||||
|                     true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.action_global_search | ||||
|  | ||||
|             switchPreference { | ||||
|                 bindTo(sourcePreferences.searchPinnedSourcesOnly()) | ||||
|                 titleRes = R.string.pref_search_pinned_sources_only | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_category_nsfw_content | ||||
|  | ||||
|             switchPreference { | ||||
|                 bindTo(sourcePreferences.showNsfwSource()) | ||||
|                 titleRes = R.string.pref_show_nsfw_source | ||||
|                 summaryRes = R.string.requires_app_restart | ||||
|  | ||||
|                 if (context.isAuthenticationSupported() && activity != null) { | ||||
|                     requireAuthentication( | ||||
|                         activity as? FragmentActivity, | ||||
|                         context.getString(R.string.pref_category_nsfw_content), | ||||
|                         context.getString(R.string.confirm_lock_change), | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             infoPreference(R.string.parental_controls_info) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,124 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting | ||||
|  | ||||
| import android.animation.ArgbEvaluator | ||||
| import android.animation.ValueAnimator | ||||
| import android.content.Context | ||||
| import android.graphics.Color | ||||
| import android.os.Bundle | ||||
| import android.util.TypedValue | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.appcompat.view.ContextThemeWrapper | ||||
| import androidx.core.animation.doOnEnd | ||||
| import androidx.preference.Preference | ||||
| import androidx.preference.PreferenceController | ||||
| import androidx.preference.PreferenceGroup | ||||
| import androidx.preference.PreferenceScreen | ||||
| import com.bluelinelabs.conductor.ControllerChangeHandler | ||||
| import com.bluelinelabs.conductor.ControllerChangeType | ||||
| import dev.chrisbanes.insetter.applyInsetter | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.preference.asHotFlow | ||||
| import eu.kanade.tachiyomi.util.system.getResourceColor | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.MainScope | ||||
| import kotlinx.coroutines.cancel | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| abstract class SettingsController : PreferenceController() { | ||||
|  | ||||
|     var preferenceKey: String? = null | ||||
|     val preferences: BasePreferences = Injekt.get() | ||||
|     val viewScope: CoroutineScope = MainScope() | ||||
|     private var themedContext: Context? = null | ||||
|  | ||||
|     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View { | ||||
|         val view = super.onCreateView(inflater, container, savedInstanceState) | ||||
|  | ||||
|         listView.applyInsetter { | ||||
|             type(navigationBars = true) { | ||||
|                 padding() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return view | ||||
|     } | ||||
|  | ||||
|     override fun onAttach(view: View) { | ||||
|         super.onAttach(view) | ||||
|  | ||||
|         preferenceKey?.let { prefKey -> | ||||
|             val adapter = listView.adapter | ||||
|             scrollToPreference(prefKey) | ||||
|  | ||||
|             listView.post { | ||||
|                 if (adapter is PreferenceGroup.PreferencePositionCallback) { | ||||
|                     val pos = adapter.getPreferenceAdapterPosition(prefKey) | ||||
|                     listView.findViewHolderForAdapterPosition(pos)?.let { | ||||
|                         animatePreferenceHighlight(it.itemView) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Explicitly clear it to avoid re-scrolling/animating on activity recreations | ||||
|                 preferenceKey = null | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { | ||||
|         if (type.isEnter) { | ||||
|             setTitle() | ||||
|         } | ||||
|         setHasOptionsMenu(type.isEnter) | ||||
|         super.onChangeStarted(handler, type) | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         super.onDestroyView(view) | ||||
|         viewScope.cancel() | ||||
|         themedContext = null | ||||
|     } | ||||
|  | ||||
|     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { | ||||
|         val tv = TypedValue() | ||||
|         activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true) | ||||
|         themedContext = ContextThemeWrapper(activity, tv.resourceId) | ||||
|  | ||||
|         val screen = preferenceManager.createPreferenceScreen(themedContext!!) | ||||
|         preferenceScreen = screen | ||||
|         setupPreferenceScreen(screen) | ||||
|     } | ||||
|  | ||||
|     abstract fun setupPreferenceScreen(screen: PreferenceScreen): PreferenceScreen | ||||
|  | ||||
|     private fun animatePreferenceHighlight(view: View) { | ||||
|         val origBackground = view.background | ||||
|         ValueAnimator | ||||
|             .ofObject(ArgbEvaluator(), Color.TRANSPARENT, view.context.getResourceColor(R.attr.colorControlHighlight)) | ||||
|             .apply { | ||||
|                 duration = 200L | ||||
|                 repeatCount = 5 | ||||
|                 repeatMode = ValueAnimator.REVERSE | ||||
|                 addUpdateListener { animator -> view.setBackgroundColor(animator.animatedValue as Int) } | ||||
|                 start() | ||||
|             } | ||||
|             .doOnEnd { | ||||
|                 // Restore original ripple | ||||
|                 view.background = origBackground | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun setTitle() { | ||||
|         (activity as? AppCompatActivity)?.supportActionBar?.title = preferenceScreen?.title?.toString() | ||||
|     } | ||||
|  | ||||
|     inline fun <T> Preference.visibleIf(preference: eu.kanade.tachiyomi.core.preference.Preference<T>, crossinline block: (T) -> Boolean) { | ||||
|         preference.asHotFlow { isVisible = block(it) } | ||||
|             .launchIn(viewScope) | ||||
|     } | ||||
| } | ||||
| @@ -1,315 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.app.Dialog | ||||
| import android.content.ActivityNotFoundException | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| import android.os.Environment | ||||
| import androidx.core.net.toUri | ||||
| import androidx.core.text.buildSpannedString | ||||
| import androidx.preference.PreferenceScreen | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import com.hippo.unifile.UniFile | ||||
| import eu.kanade.domain.category.interactor.GetCategories | ||||
| import eu.kanade.domain.download.service.DownloadPreferences | ||||
| import eu.kanade.presentation.category.visualName | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import eu.kanade.tachiyomi.util.preference.bindTo | ||||
| import eu.kanade.tachiyomi.util.preference.entriesRes | ||||
| import eu.kanade.tachiyomi.util.preference.infoPreference | ||||
| import eu.kanade.tachiyomi.util.preference.intListPreference | ||||
| import eu.kanade.tachiyomi.util.preference.multiSelectListPreference | ||||
| import eu.kanade.tachiyomi.util.preference.onClick | ||||
| import eu.kanade.tachiyomi.util.preference.preference | ||||
| import eu.kanade.tachiyomi.util.preference.preferenceCategory | ||||
| import eu.kanade.tachiyomi.util.preference.summaryRes | ||||
| import eu.kanade.tachiyomi.util.preference.switchPreference | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView | ||||
| import eu.kanade.tachiyomi.widget.materialdialogs.setQuadStateMultiChoiceItems | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
|  | ||||
| class SettingsDownloadController : SettingsController() { | ||||
|  | ||||
|     private val getCategories: GetCategories by injectLazy() | ||||
|     private val downloadPreferences: DownloadPreferences by injectLazy() | ||||
|  | ||||
|     override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { | ||||
|         titleRes = R.string.pref_category_downloads | ||||
|  | ||||
|         val categories = runBlocking { getCategories.await() } | ||||
|  | ||||
|         preference { | ||||
|             bindTo(downloadPreferences.downloadsDirectory()) | ||||
|             titleRes = R.string.pref_download_directory | ||||
|             onClick { | ||||
|                 val ctrl = DownloadDirectoriesDialog() | ||||
|                 ctrl.targetController = this@SettingsDownloadController | ||||
|                 ctrl.showDialog(router) | ||||
|             } | ||||
|  | ||||
|             downloadPreferences.downloadsDirectory().changes() | ||||
|                 .onEach { path -> | ||||
|                     val dir = UniFile.fromUri(context, path.toUri()) | ||||
|                     summary = dir.filePath ?: path | ||||
|                 } | ||||
|                 .launchIn(viewScope) | ||||
|         } | ||||
|         switchPreference { | ||||
|             bindTo(downloadPreferences.downloadOnlyOverWifi()) | ||||
|             titleRes = R.string.connected_to_wifi | ||||
|         } | ||||
|         switchPreference { | ||||
|             bindTo(downloadPreferences.saveChaptersAsCBZ()) | ||||
|             titleRes = R.string.save_chapter_as_cbz | ||||
|         } | ||||
|         switchPreference { | ||||
|             bindTo(downloadPreferences.splitTallImages()) | ||||
|             titleRes = R.string.split_tall_images | ||||
|             summaryRes = R.string.split_tall_images_summary | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_category_delete_chapters | ||||
|  | ||||
|             switchPreference { | ||||
|                 bindTo(downloadPreferences.removeAfterMarkedAsRead()) | ||||
|                 titleRes = R.string.pref_remove_after_marked_as_read | ||||
|             } | ||||
|             intListPreference { | ||||
|                 bindTo(downloadPreferences.removeAfterReadSlots()) | ||||
|                 titleRes = R.string.pref_remove_after_read | ||||
|                 entriesRes = arrayOf( | ||||
|                     R.string.disabled, | ||||
|                     R.string.last_read_chapter, | ||||
|                     R.string.second_to_last, | ||||
|                     R.string.third_to_last, | ||||
|                     R.string.fourth_to_last, | ||||
|                     R.string.fifth_to_last, | ||||
|                 ) | ||||
|                 entryValues = arrayOf("-1", "0", "1", "2", "3", "4") | ||||
|                 summary = "%s" | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(downloadPreferences.removeBookmarkedChapters()) | ||||
|                 titleRes = R.string.pref_remove_bookmarked_chapters | ||||
|             } | ||||
|             multiSelectListPreference { | ||||
|                 bindTo(downloadPreferences.removeExcludeCategories()) | ||||
|                 titleRes = R.string.pref_remove_exclude_categories | ||||
|                 entries = categories.map { it.visualName(context) }.toTypedArray() | ||||
|                 entryValues = categories.map { it.id.toString() }.toTypedArray() | ||||
|  | ||||
|                 downloadPreferences.removeExcludeCategories().changes() | ||||
|                     .onEach { mutable -> | ||||
|                         val selected = mutable | ||||
|                             .mapNotNull { id -> categories.find { it.id == id.toLong() } } | ||||
|                             .sortedBy { it.order } | ||||
|  | ||||
|                         summary = if (selected.isEmpty()) { | ||||
|                             resources?.getString(R.string.none) | ||||
|                         } else { | ||||
|                             selected.joinToString { it.visualName(context) } | ||||
|                         } | ||||
|                     }.launchIn(viewScope) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_download_new | ||||
|  | ||||
|             switchPreference { | ||||
|                 bindTo(downloadPreferences.downloadNewChapters()) | ||||
|                 titleRes = R.string.pref_download_new | ||||
|             } | ||||
|             preference { | ||||
|                 bindTo(downloadPreferences.downloadNewChapterCategories()) | ||||
|                 titleRes = R.string.categories | ||||
|                 onClick { | ||||
|                     DownloadCategoriesDialog().showDialog(router) | ||||
|                 } | ||||
|  | ||||
|                 visibleIf(downloadPreferences.downloadNewChapters()) { it } | ||||
|  | ||||
|                 fun updateSummary() { | ||||
|                     val selectedCategories = downloadPreferences.downloadNewChapterCategories().get() | ||||
|                         .mapNotNull { id -> categories.find { it.id == id.toLong() } } | ||||
|                         .sortedBy { it.order } | ||||
|                     val includedItemsText = if (selectedCategories.isEmpty()) { | ||||
|                         context.getString(R.string.all) | ||||
|                     } else { | ||||
|                         selectedCategories.joinToString { it.visualName(context) } | ||||
|                     } | ||||
|  | ||||
|                     val excludedCategories = downloadPreferences.downloadNewChapterCategoriesExclude().get() | ||||
|                         .mapNotNull { id -> categories.find { it.id == id.toLong() } } | ||||
|                         .sortedBy { it.order } | ||||
|                     val excludedItemsText = if (excludedCategories.isEmpty()) { | ||||
|                         context.getString(R.string.none) | ||||
|                     } else { | ||||
|                         excludedCategories.joinToString { it.visualName(context) } | ||||
|                     } | ||||
|  | ||||
|                     summary = buildSpannedString { | ||||
|                         append(context.getString(R.string.include, includedItemsText)) | ||||
|                         appendLine() | ||||
|                         append(context.getString(R.string.exclude, excludedItemsText)) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 downloadPreferences.downloadNewChapterCategories().changes() | ||||
|                     .onEach { updateSummary() } | ||||
|                     .launchIn(viewScope) | ||||
|                 downloadPreferences.downloadNewChapterCategoriesExclude().changes() | ||||
|                     .onEach { updateSummary() } | ||||
|                     .launchIn(viewScope) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.download_ahead | ||||
|  | ||||
|             intListPreference { | ||||
|                 bindTo(downloadPreferences.autoDownloadWhileReading()) | ||||
|                 titleRes = R.string.auto_download_while_reading | ||||
|                 entries = arrayOf( | ||||
|                     context.getString(R.string.disabled), | ||||
|                     context.resources.getQuantityString(R.plurals.next_unread_chapters, 2, 2), | ||||
|                     context.resources.getQuantityString(R.plurals.next_unread_chapters, 3, 3), | ||||
|                     context.resources.getQuantityString(R.plurals.next_unread_chapters, 5, 5), | ||||
|                     context.resources.getQuantityString(R.plurals.next_unread_chapters, 10, 10), | ||||
|                 ) | ||||
|                 entryValues = arrayOf("0", "2", "3", "5", "10") | ||||
|                 summary = "%s" | ||||
|             } | ||||
|             infoPreference(R.string.download_ahead_info) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | ||||
|         when (requestCode) { | ||||
|             DOWNLOAD_DIR -> if (data != null && resultCode == Activity.RESULT_OK) { | ||||
|                 val context = applicationContext ?: return | ||||
|                 val uri = data.data | ||||
|                 val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or | ||||
|                     Intent.FLAG_GRANT_WRITE_URI_PERMISSION | ||||
|  | ||||
|                 if (uri != null) { | ||||
|                     @Suppress("NewApi") | ||||
|                     context.contentResolver.takePersistableUriPermission(uri, flags) | ||||
|                 } | ||||
|  | ||||
|                 val file = UniFile.fromUri(context, uri) | ||||
|                 downloadPreferences.downloadsDirectory().set(file.uri.toString()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun predefinedDirectorySelected(selectedDir: String) { | ||||
|         val path = File(selectedDir).toUri() | ||||
|         downloadPreferences.downloadsDirectory().set(path.toString()) | ||||
|     } | ||||
|  | ||||
|     fun customDirectorySelected() { | ||||
|         val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) | ||||
|         try { | ||||
|             startActivityForResult(intent, DOWNLOAD_DIR) | ||||
|         } catch (e: ActivityNotFoundException) { | ||||
|             activity?.toast(R.string.file_picker_error) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class DownloadDirectoriesDialog : DialogController() { | ||||
|  | ||||
|         private val downloadPreferences: DownloadPreferences = Injekt.get() | ||||
|  | ||||
|         override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|             val activity = activity!! | ||||
|             val currentDir = downloadPreferences.downloadsDirectory().get() | ||||
|             val externalDirs = listOf(getDefaultDownloadDir(), File(activity.getString(R.string.custom_dir))).map(File::toString) | ||||
|             var selectedIndex = externalDirs.indexOfFirst { it in currentDir } | ||||
|  | ||||
|             return MaterialAlertDialogBuilder(activity) | ||||
|                 .setTitle(R.string.pref_download_directory) | ||||
|                 .setSingleChoiceItems(externalDirs.toTypedArray(), selectedIndex) { _, which -> | ||||
|                     selectedIndex = which | ||||
|                 } | ||||
|                 .setPositiveButton(android.R.string.ok) { _, _ -> | ||||
|                     val target = targetController as? SettingsDownloadController | ||||
|                     if (selectedIndex == externalDirs.lastIndex) { | ||||
|                         target?.customDirectorySelected() | ||||
|                     } else { | ||||
|                         target?.predefinedDirectorySelected(externalDirs[selectedIndex]) | ||||
|                     } | ||||
|                 } | ||||
|                 .create() | ||||
|         } | ||||
|  | ||||
|         private fun getDefaultDownloadDir(): File { | ||||
|             val defaultDir = Environment.getExternalStorageDirectory().absolutePath + | ||||
|                 File.separator + resources?.getString(R.string.app_name) + | ||||
|                 File.separator + "downloads" | ||||
|  | ||||
|             return File(defaultDir) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class DownloadCategoriesDialog : DialogController() { | ||||
|  | ||||
|         private val downloadPreferences: DownloadPreferences = Injekt.get() | ||||
|         private val getCategories: GetCategories = Injekt.get() | ||||
|  | ||||
|         override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|             val categories = runBlocking { getCategories.await() } | ||||
|  | ||||
|             val items = categories.map { it.visualName(activity!!) } | ||||
|             var selected = categories | ||||
|                 .map { | ||||
|                     when (it.id.toString()) { | ||||
|                         in downloadPreferences.downloadNewChapterCategories().get() -> QuadStateTextView.State.CHECKED.ordinal | ||||
|                         in downloadPreferences.downloadNewChapterCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal | ||||
|                         else -> QuadStateTextView.State.UNCHECKED.ordinal | ||||
|                     } | ||||
|                 } | ||||
|                 .toIntArray() | ||||
|  | ||||
|             return MaterialAlertDialogBuilder(activity!!) | ||||
|                 .setTitle(R.string.categories) | ||||
|                 .setQuadStateMultiChoiceItems( | ||||
|                     message = R.string.pref_download_new_categories_details, | ||||
|                     items = items, | ||||
|                     initialSelected = selected, | ||||
|                 ) { selections -> | ||||
|                     selected = selections | ||||
|                 } | ||||
|                 .setPositiveButton(android.R.string.ok) { _, _ -> | ||||
|                     val included = selected | ||||
|                         .mapIndexed { index, value -> if (value == QuadStateTextView.State.CHECKED.ordinal) index else null } | ||||
|                         .filterNotNull() | ||||
|                         .map { categories[it].id.toString() } | ||||
|                         .toSet() | ||||
|                     val excluded = selected | ||||
|                         .mapIndexed { index, value -> if (value == QuadStateTextView.State.INVERSED.ordinal) index else null } | ||||
|                         .filterNotNull() | ||||
|                         .map { categories[it].id.toString() } | ||||
|                         .toSet() | ||||
|  | ||||
|                     downloadPreferences.downloadNewChapterCategories().set(included) | ||||
|                     downloadPreferences.downloadNewChapterCategoriesExclude().set(excluded) | ||||
|                 } | ||||
|                 .setNegativeButton(android.R.string.cancel, null) | ||||
|                 .create() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private const val DOWNLOAD_DIR = 104 | ||||
| @@ -1,92 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting | ||||
|  | ||||
| import android.content.Intent | ||||
| import android.os.Build | ||||
| import android.provider.Settings | ||||
| import androidx.appcompat.app.AppCompatDelegate | ||||
| import androidx.core.os.LocaleListCompat | ||||
| import androidx.preference.PreferenceScreen | ||||
| import eu.kanade.domain.library.service.LibraryPreferences | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.preference.bindTo | ||||
| import eu.kanade.tachiyomi.util.preference.listPreference | ||||
| import eu.kanade.tachiyomi.util.preference.onChange | ||||
| import eu.kanade.tachiyomi.util.preference.onClick | ||||
| import eu.kanade.tachiyomi.util.preference.preference | ||||
| import eu.kanade.tachiyomi.util.preference.switchPreference | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import org.xmlpull.v1.XmlPullParser | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class SettingsGeneralController : SettingsController() { | ||||
|  | ||||
|     private val libraryPreferences: LibraryPreferences by injectLazy() | ||||
|  | ||||
|     override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { | ||||
|         titleRes = R.string.pref_category_general | ||||
|  | ||||
|         switchPreference { | ||||
|             bindTo(libraryPreferences.showUpdatesNavBadge()) | ||||
|             titleRes = R.string.pref_library_update_show_tab_badge | ||||
|         } | ||||
|         switchPreference { | ||||
|             bindTo(preferences.confirmExit()) | ||||
|             titleRes = R.string.pref_confirm_exit | ||||
|         } | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             preference { | ||||
|                 key = "pref_manage_notifications" | ||||
|                 titleRes = R.string.pref_manage_notifications | ||||
|                 onClick { | ||||
|                     val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { | ||||
|                         putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) | ||||
|                     } | ||||
|                     startActivity(intent) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         listPreference { | ||||
|             key = "app_lang" | ||||
|             isPersistent = false | ||||
|             titleRes = R.string.pref_app_language | ||||
|  | ||||
|             val langs = mutableListOf<Pair<String, String>>() | ||||
|  | ||||
|             val parser = context.resources.getXml(R.xml.locales_config) | ||||
|             var eventType = parser.eventType | ||||
|             while (eventType != XmlPullParser.END_DOCUMENT) { | ||||
|                 if (eventType == XmlPullParser.START_TAG && parser.name == "locale") { | ||||
|                     for (i in 0 until parser.attributeCount) { | ||||
|                         if (parser.getAttributeName(i) == "name") { | ||||
|                             val langTag = parser.getAttributeValue(i) | ||||
|                             val displayName = LocaleHelper.getDisplayName(langTag) | ||||
|                             if (displayName.isNotEmpty()) { | ||||
|                                 langs.add(Pair(langTag, displayName)) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 eventType = parser.next() | ||||
|             } | ||||
|  | ||||
|             langs.sortBy { it.second } | ||||
|             langs.add(0, Pair("", context.getString(R.string.label_default))) | ||||
|  | ||||
|             entryValues = langs.map { it.first }.toTypedArray() | ||||
|             entries = langs.map { it.second }.toTypedArray() | ||||
|             summary = "%s" | ||||
|             value = AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "" | ||||
|  | ||||
|             onChange { newValue -> | ||||
|                 val locale = if ((newValue as String).isEmpty()) { | ||||
|                     LocaleListCompat.getEmptyLocaleList() | ||||
|                 } else { | ||||
|                     LocaleListCompat.forLanguageTags(newValue) | ||||
|                 } | ||||
|                 AppCompatDelegate.setApplicationLocales(locale) | ||||
|                 true | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,388 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting | ||||
|  | ||||
| import android.app.Dialog | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.core.text.buildSpannedString | ||||
| import androidx.preference.PreferenceScreen | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import eu.kanade.domain.category.interactor.GetCategories | ||||
| import eu.kanade.domain.category.interactor.ResetCategoryFlags | ||||
| import eu.kanade.domain.category.model.Category | ||||
| import eu.kanade.domain.library.service.LibraryPreferences | ||||
| import eu.kanade.presentation.category.visualName | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateJob | ||||
| import eu.kanade.tachiyomi.data.preference.DEVICE_BATTERY_NOT_LOW | ||||
| import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING | ||||
| import eu.kanade.tachiyomi.data.preference.DEVICE_NETWORK_NOT_METERED | ||||
| import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI | ||||
| import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD | ||||
| import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED | ||||
| import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.databinding.PrefLibraryColumnsBinding | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.category.CategoryController | ||||
| import eu.kanade.tachiyomi.util.preference.bindTo | ||||
| import eu.kanade.tachiyomi.util.preference.defaultValue | ||||
| import eu.kanade.tachiyomi.util.preference.entriesRes | ||||
| import eu.kanade.tachiyomi.util.preference.intListPreference | ||||
| import eu.kanade.tachiyomi.util.preference.multiSelectListPreference | ||||
| import eu.kanade.tachiyomi.util.preference.onChange | ||||
| import eu.kanade.tachiyomi.util.preference.onClick | ||||
| import eu.kanade.tachiyomi.util.preference.preference | ||||
| import eu.kanade.tachiyomi.util.preference.preferenceCategory | ||||
| import eu.kanade.tachiyomi.util.preference.summaryRes | ||||
| import eu.kanade.tachiyomi.util.preference.switchPreference | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView | ||||
| import eu.kanade.tachiyomi.widget.materialdialogs.setQuadStateMultiChoiceItems | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class SettingsLibraryController : SettingsController() { | ||||
|  | ||||
|     private val getCategories: GetCategories by injectLazy() | ||||
|     private val trackManager: TrackManager by injectLazy() | ||||
|     private val resetCategoryFlags: ResetCategoryFlags by injectLazy() | ||||
|     private val libraryPreferences: LibraryPreferences by injectLazy() | ||||
|  | ||||
|     override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { | ||||
|         titleRes = R.string.pref_category_library | ||||
|  | ||||
|         val allCategories = runBlocking { getCategories.await() } | ||||
|         val userCategories = allCategories.filterNot(Category::isSystemCategory) | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_category_display | ||||
|  | ||||
|             preference { | ||||
|                 key = "pref_library_columns" | ||||
|                 titleRes = R.string.pref_library_columns | ||||
|                 onClick { | ||||
|                     LibraryColumnsDialog().showDialog(router) | ||||
|                 } | ||||
|  | ||||
|                 fun getColumnValue(value: Int): String { | ||||
|                     return if (value == 0) { | ||||
|                         context.getString(R.string.label_default) | ||||
|                     } else { | ||||
|                         value.toString() | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 combine(libraryPreferences.portraitColumns().changes(), libraryPreferences.landscapeColumns().changes()) { portraitCols, landscapeCols -> Pair(portraitCols, landscapeCols) } | ||||
|                     .onEach { (portraitCols, landscapeCols) -> | ||||
|                         val portrait = getColumnValue(portraitCols) | ||||
|                         val landscape = getColumnValue(landscapeCols) | ||||
|                         summary = "${context.getString(R.string.portrait)}: $portrait, " + | ||||
|                             "${context.getString(R.string.landscape)}: $landscape" | ||||
|                     } | ||||
|                     .launchIn(viewScope) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.categories | ||||
|  | ||||
|             preference { | ||||
|                 key = "pref_action_edit_categories" | ||||
|                 titleRes = R.string.action_edit_categories | ||||
|  | ||||
|                 val catCount = userCategories.size | ||||
|                 summary = context.resources.getQuantityString(R.plurals.num_categories, catCount, catCount) | ||||
|  | ||||
|                 onClick { | ||||
|                     router.pushController(CategoryController()) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             intListPreference { | ||||
|                 val defaultCategory = libraryPreferences.defaultCategory() | ||||
|                 bindTo(defaultCategory) | ||||
|                 titleRes = R.string.default_category | ||||
|  | ||||
|                 entries = arrayOf(context.getString(R.string.default_category_summary)) + | ||||
|                     allCategories.map { it.visualName(context) }.toTypedArray() | ||||
|                 entryValues = arrayOf(defaultCategory.defaultValue().toString()) + allCategories.map { it.id.toString() }.toTypedArray() | ||||
|  | ||||
|                 val selectedCategory = allCategories.find { it.id == defaultCategory.get().toLong() } | ||||
|                 summary = selectedCategory?.visualName(context) | ||||
|                     ?: context.getString(R.string.default_category_summary) | ||||
|                 onChange { newValue -> | ||||
|                     summary = allCategories.find { | ||||
|                         it.id == (newValue as String).toLong() | ||||
|                     }?.visualName(context) ?: context.getString(R.string.default_category_summary) | ||||
|                     true | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             switchPreference { | ||||
|                 bindTo(libraryPreferences.categorizedDisplaySettings()) | ||||
|                 titleRes = R.string.categorized_display_settings | ||||
|  | ||||
|                 libraryPreferences.categorizedDisplaySettings().changes() | ||||
|                     .onEach { | ||||
|                         if (it.not()) { | ||||
|                             resetCategoryFlags.await() | ||||
|                         } | ||||
|                     } | ||||
|                     .launchIn(viewScope) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_category_library_update | ||||
|  | ||||
|             intListPreference { | ||||
|                 bindTo(libraryPreferences.libraryUpdateInterval()) | ||||
|                 titleRes = R.string.pref_library_update_interval | ||||
|                 entriesRes = arrayOf( | ||||
|                     R.string.update_never, | ||||
|                     R.string.update_12hour, | ||||
|                     R.string.update_24hour, | ||||
|                     R.string.update_48hour, | ||||
|                     R.string.update_72hour, | ||||
|                     R.string.update_weekly, | ||||
|                 ) | ||||
|                 entryValues = arrayOf("0", "12", "24", "48", "72", "168") | ||||
|                 summary = "%s" | ||||
|  | ||||
|                 onChange { newValue -> | ||||
|                     val interval = (newValue as String).toInt() | ||||
|                     LibraryUpdateJob.setupTask(context, interval) | ||||
|                     true | ||||
|                 } | ||||
|             } | ||||
|             multiSelectListPreference { | ||||
|                 bindTo(libraryPreferences.libraryUpdateDeviceRestriction()) | ||||
|                 titleRes = R.string.pref_library_update_restriction | ||||
|                 entriesRes = arrayOf(R.string.connected_to_wifi, R.string.network_not_metered, R.string.charging, R.string.battery_not_low) | ||||
|                 entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_NETWORK_NOT_METERED, DEVICE_CHARGING, DEVICE_BATTERY_NOT_LOW) | ||||
|  | ||||
|                 visibleIf(libraryPreferences.libraryUpdateInterval()) { it > 0 } | ||||
|  | ||||
|                 onChange { | ||||
|                     // Post to event looper to allow the preference to be updated. | ||||
|                     ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) } | ||||
|                     true | ||||
|                 } | ||||
|  | ||||
|                 fun updateSummary() { | ||||
|                     val restrictions = libraryPreferences.libraryUpdateDeviceRestriction().get() | ||||
|                         .sorted() | ||||
|                         .map { | ||||
|                             when (it) { | ||||
|                                 DEVICE_ONLY_ON_WIFI -> context.getString(R.string.connected_to_wifi) | ||||
|                                 DEVICE_NETWORK_NOT_METERED -> context.getString(R.string.network_not_metered) | ||||
|                                 DEVICE_CHARGING -> context.getString(R.string.charging) | ||||
|                                 DEVICE_BATTERY_NOT_LOW -> context.getString(R.string.battery_not_low) | ||||
|                                 else -> it | ||||
|                             } | ||||
|                         } | ||||
|                     val restrictionsText = if (restrictions.isEmpty()) { | ||||
|                         context.getString(R.string.none) | ||||
|                     } else { | ||||
|                         restrictions.joinToString() | ||||
|                     } | ||||
|  | ||||
|                     summary = context.getString(R.string.restrictions, restrictionsText) | ||||
|                 } | ||||
|  | ||||
|                 libraryPreferences.libraryUpdateDeviceRestriction().changes() | ||||
|                     .onEach { updateSummary() } | ||||
|                     .launchIn(viewScope) | ||||
|             } | ||||
|             multiSelectListPreference { | ||||
|                 bindTo(libraryPreferences.libraryUpdateMangaRestriction()) | ||||
|                 titleRes = R.string.pref_library_update_manga_restriction | ||||
|                 entriesRes = arrayOf(R.string.pref_update_only_completely_read, R.string.pref_update_only_started, R.string.pref_update_only_non_completed) | ||||
|                 entryValues = arrayOf(MANGA_HAS_UNREAD, MANGA_NON_READ, MANGA_NON_COMPLETED) | ||||
|  | ||||
|                 fun updateSummary() { | ||||
|                     val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get().sorted() | ||||
|                         .map { | ||||
|                             when (it) { | ||||
|                                 MANGA_NON_READ -> context.getString(R.string.pref_update_only_started) | ||||
|                                 MANGA_HAS_UNREAD -> context.getString(R.string.pref_update_only_completely_read) | ||||
|                                 MANGA_NON_COMPLETED -> context.getString(R.string.pref_update_only_non_completed) | ||||
|                                 else -> it | ||||
|                             } | ||||
|                         } | ||||
|                     val restrictionsText = if (restrictions.isEmpty()) { | ||||
|                         context.getString(R.string.none) | ||||
|                     } else { | ||||
|                         restrictions.joinToString() | ||||
|                     } | ||||
|  | ||||
|                     summary = restrictionsText | ||||
|                 } | ||||
|  | ||||
|                 libraryPreferences.libraryUpdateMangaRestriction().changes() | ||||
|                     .onEach { updateSummary() } | ||||
|                     .launchIn(viewScope) | ||||
|             } | ||||
|             preference { | ||||
|                 bindTo(libraryPreferences.libraryUpdateCategories()) | ||||
|                 titleRes = R.string.categories | ||||
|  | ||||
|                 onClick { | ||||
|                     LibraryGlobalUpdateCategoriesDialog().showDialog(router) | ||||
|                 } | ||||
|  | ||||
|                 fun updateSummary() { | ||||
|                     val includedCategories = libraryPreferences.libraryUpdateCategories().get() | ||||
|                         .mapNotNull { id -> allCategories.find { it.id == id.toLong() } } | ||||
|                         .sortedBy { it.order } | ||||
|                     val excludedCategories = libraryPreferences.libraryUpdateCategoriesExclude().get() | ||||
|                         .mapNotNull { id -> allCategories.find { it.id == id.toLong() } } | ||||
|                         .sortedBy { it.order } | ||||
|  | ||||
|                     val allExcluded = excludedCategories.size == allCategories.size | ||||
|  | ||||
|                     val includedItemsText = when { | ||||
|                         // Some selected, but not all | ||||
|                         includedCategories.isNotEmpty() && includedCategories.size != allCategories.size -> includedCategories.joinToString { it.visualName(context) } | ||||
|                         // All explicitly selected | ||||
|                         includedCategories.size == allCategories.size -> context.getString(R.string.all) | ||||
|                         allExcluded -> context.getString(R.string.none) | ||||
|                         else -> context.getString(R.string.all) | ||||
|                     } | ||||
|                     val excludedItemsText = when { | ||||
|                         excludedCategories.isEmpty() -> context.getString(R.string.none) | ||||
|                         allExcluded -> context.getString(R.string.all) | ||||
|                         else -> excludedCategories.joinToString { it.visualName(context) } | ||||
|                     } | ||||
|  | ||||
|                     summary = buildSpannedString { | ||||
|                         append(context.getString(R.string.include, includedItemsText)) | ||||
|                         appendLine() | ||||
|                         append(context.getString(R.string.exclude, excludedItemsText)) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 libraryPreferences.libraryUpdateCategories().changes() | ||||
|                     .onEach { updateSummary() } | ||||
|                     .launchIn(viewScope) | ||||
|                 libraryPreferences.libraryUpdateCategoriesExclude().changes() | ||||
|                     .onEach { updateSummary() } | ||||
|                     .launchIn(viewScope) | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(libraryPreferences.autoUpdateMetadata()) | ||||
|                 titleRes = R.string.pref_library_update_refresh_metadata | ||||
|                 summaryRes = R.string.pref_library_update_refresh_metadata_summary | ||||
|             } | ||||
|             if (trackManager.hasLoggedServices()) { | ||||
|                 switchPreference { | ||||
|                     bindTo(libraryPreferences.autoUpdateTrackers()) | ||||
|                     titleRes = R.string.pref_library_update_refresh_trackers | ||||
|                     summaryRes = R.string.pref_library_update_refresh_trackers_summary | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class LibraryColumnsDialog : DialogController() { | ||||
|  | ||||
|         private val preferences: LibraryPreferences = Injekt.get() | ||||
|  | ||||
|         private var portrait = preferences.portraitColumns().get() | ||||
|         private var landscape = preferences.landscapeColumns().get() | ||||
|  | ||||
|         override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|             val binding = PrefLibraryColumnsBinding.inflate(LayoutInflater.from(activity!!)) | ||||
|             onViewCreated(binding) | ||||
|             return MaterialAlertDialogBuilder(activity!!) | ||||
|                 .setTitle(R.string.pref_library_columns) | ||||
|                 .setView(binding.root) | ||||
|                 .setPositiveButton(android.R.string.ok) { _, _ -> | ||||
|                     preferences.portraitColumns().set(portrait) | ||||
|                     preferences.landscapeColumns().set(landscape) | ||||
|                 } | ||||
|                 .setNegativeButton(android.R.string.cancel, null) | ||||
|                 .create() | ||||
|         } | ||||
|  | ||||
|         fun onViewCreated(binding: PrefLibraryColumnsBinding) { | ||||
|             with(binding.portraitColumns) { | ||||
|                 displayedValues = arrayOf(context.getString(R.string.label_default)) + | ||||
|                     IntRange(1, 10).map(Int::toString) | ||||
|                 value = portrait | ||||
|  | ||||
|                 setOnValueChangedListener { _, _, newValue -> | ||||
|                     portrait = newValue | ||||
|                 } | ||||
|             } | ||||
|             with(binding.landscapeColumns) { | ||||
|                 displayedValues = arrayOf(context.getString(R.string.label_default)) + | ||||
|                     IntRange(1, 10).map(Int::toString) | ||||
|                 value = landscape | ||||
|  | ||||
|                 setOnValueChangedListener { _, _, newValue -> | ||||
|                     landscape = newValue | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class LibraryGlobalUpdateCategoriesDialog : DialogController() { | ||||
|  | ||||
|         private val preferences: LibraryPreferences = Injekt.get() | ||||
|         private val getCategories: GetCategories = Injekt.get() | ||||
|  | ||||
|         override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|             val categories = runBlocking { getCategories.await() } | ||||
|  | ||||
|             val items = categories.map { it.visualName(activity!!) } | ||||
|             var selected = categories | ||||
|                 .map { | ||||
|                     when (it.id.toString()) { | ||||
|                         in preferences.libraryUpdateCategories() | ||||
|                             .get(), | ||||
|                         -> QuadStateTextView.State.CHECKED.ordinal | ||||
|                         in preferences.libraryUpdateCategoriesExclude() | ||||
|                             .get(), | ||||
|                         -> QuadStateTextView.State.INVERSED.ordinal | ||||
|                         else -> QuadStateTextView.State.UNCHECKED.ordinal | ||||
|                     } | ||||
|                 } | ||||
|                 .toIntArray() | ||||
|  | ||||
|             return MaterialAlertDialogBuilder(activity!!) | ||||
|                 .setTitle(R.string.categories) | ||||
|                 .setQuadStateMultiChoiceItems( | ||||
|                     message = R.string.pref_library_update_categories_details, | ||||
|                     items = items, | ||||
|                     initialSelected = selected, | ||||
|                 ) { selections -> | ||||
|                     selected = selections | ||||
|                 } | ||||
|                 .setPositiveButton(android.R.string.ok) { _, _ -> | ||||
|                     val included = selected | ||||
|                         .mapIndexed { index, value -> if (value == QuadStateTextView.State.CHECKED.ordinal) index else null } | ||||
|                         .filterNotNull() | ||||
|                         .map { categories[it].id.toString() } | ||||
|                         .toSet() | ||||
|                     val excluded = selected | ||||
|                         .mapIndexed { index, value -> if (value == QuadStateTextView.State.INVERSED.ordinal) index else null } | ||||
|                         .filterNotNull() | ||||
|                         .map { categories[it].id.toString() } | ||||
|                         .toSet() | ||||
|  | ||||
|                     preferences.libraryUpdateCategories().set(included) | ||||
|                     preferences.libraryUpdateCategoriesExclude().set(excluded) | ||||
|                 } | ||||
|                 .setNegativeButton(android.R.string.cancel, null) | ||||
|                 .create() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,325 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting | ||||
|  | ||||
| import android.os.Build | ||||
| import androidx.preference.PreferenceScreen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceValues | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceValues.TappingInvertMode | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.OrientationType | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType | ||||
| import eu.kanade.tachiyomi.util.preference.bindTo | ||||
| import eu.kanade.tachiyomi.util.preference.entriesRes | ||||
| import eu.kanade.tachiyomi.util.preference.intListPreference | ||||
| import eu.kanade.tachiyomi.util.preference.listPreference | ||||
| import eu.kanade.tachiyomi.util.preference.preferenceCategory | ||||
| import eu.kanade.tachiyomi.util.preference.summaryRes | ||||
| import eu.kanade.tachiyomi.util.preference.switchPreference | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.util.system.hasDisplayCutout | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class SettingsReaderController : SettingsController() { | ||||
|  | ||||
|     private val readerPreferences: ReaderPreferences by injectLazy() | ||||
|  | ||||
|     override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { | ||||
|         titleRes = R.string.pref_category_reader | ||||
|  | ||||
|         intListPreference { | ||||
|             bindTo(readerPreferences.defaultReadingMode()) | ||||
|             titleRes = R.string.pref_viewer_type | ||||
|             entriesRes = arrayOf( | ||||
|                 R.string.left_to_right_viewer, | ||||
|                 R.string.right_to_left_viewer, | ||||
|                 R.string.vertical_viewer, | ||||
|                 R.string.webtoon_viewer, | ||||
|                 R.string.vertical_plus_viewer, | ||||
|             ) | ||||
|             entryValues = ReadingModeType.values().drop(1) | ||||
|                 .map { value -> "${value.flagValue}" }.toTypedArray() | ||||
|             summary = "%s" | ||||
|         } | ||||
|         intListPreference { | ||||
|             bindTo(readerPreferences.doubleTapAnimSpeed()) | ||||
|             titleRes = R.string.pref_double_tap_anim_speed | ||||
|             entries = arrayOf(context.getString(R.string.double_tap_anim_speed_0), context.getString(R.string.double_tap_anim_speed_normal), context.getString(R.string.double_tap_anim_speed_fast)) | ||||
|             entryValues = arrayOf("1", "500", "250") // using a value of 0 breaks the image viewer, so min is 1 | ||||
|             summary = "%s" | ||||
|         } | ||||
|         switchPreference { | ||||
|             bindTo(readerPreferences.showReadingMode()) | ||||
|             titleRes = R.string.pref_show_reading_mode | ||||
|             summaryRes = R.string.pref_show_reading_mode_summary | ||||
|         } | ||||
|         switchPreference { | ||||
|             bindTo(readerPreferences.showNavigationOverlayOnStart()) | ||||
|             titleRes = R.string.pref_show_navigation_mode | ||||
|             summaryRes = R.string.pref_show_navigation_mode_summary | ||||
|         } | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.trueColor()) | ||||
|                 titleRes = R.string.pref_true_color | ||||
|                 summaryRes = R.string.pref_true_color_summary | ||||
|             } | ||||
|         } | ||||
|         switchPreference { | ||||
|             bindTo(readerPreferences.pageTransitions()) | ||||
|             titleRes = R.string.pref_page_transitions | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_category_display | ||||
|  | ||||
|             intListPreference { | ||||
|                 bindTo(readerPreferences.defaultOrientationType()) | ||||
|                 titleRes = R.string.pref_rotation_type | ||||
|                 entriesRes = arrayOf( | ||||
|                     R.string.rotation_free, | ||||
|                     R.string.rotation_portrait, | ||||
|                     R.string.rotation_reverse_portrait, | ||||
|                     R.string.rotation_landscape, | ||||
|                     R.string.rotation_force_portrait, | ||||
|                     R.string.rotation_force_landscape, | ||||
|                 ) | ||||
|                 entryValues = OrientationType.values().drop(1) | ||||
|                     .map { value -> "${value.flagValue}" }.toTypedArray() | ||||
|                 summary = "%s" | ||||
|             } | ||||
|             intListPreference { | ||||
|                 bindTo(readerPreferences.readerTheme()) | ||||
|                 titleRes = R.string.pref_reader_theme | ||||
|                 entriesRes = arrayOf(R.string.black_background, R.string.gray_background, R.string.white_background, R.string.automatic_background) | ||||
|                 entryValues = arrayOf("1", "2", "0", "3") | ||||
|                 summary = "%s" | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.fullscreen()) | ||||
|                 titleRes = R.string.pref_fullscreen | ||||
|             } | ||||
|  | ||||
|             if (activity?.hasDisplayCutout() == true) { | ||||
|                 switchPreference { | ||||
|                     bindTo(readerPreferences.cutoutShort()) | ||||
|                     titleRes = R.string.pref_cutout_short | ||||
|  | ||||
|                     visibleIf(readerPreferences.fullscreen()) { it } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.keepScreenOn()) | ||||
|                 titleRes = R.string.pref_keep_screen_on | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.showPageNumber()) | ||||
|                 titleRes = R.string.pref_show_page_number | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_category_reading | ||||
|  | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.skipRead()) | ||||
|                 titleRes = R.string.pref_skip_read_chapters | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.skipFiltered()) | ||||
|                 titleRes = R.string.pref_skip_filtered_chapters | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.alwaysShowChapterTransition()) | ||||
|                 titleRes = R.string.pref_always_show_chapter_transition | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pager_viewer | ||||
|  | ||||
|             intListPreference { | ||||
|                 bindTo(readerPreferences.navigationModePager()) | ||||
|                 titleRes = R.string.pref_viewer_nav | ||||
|                 entries = context.resources.getStringArray(R.array.pager_nav).also { values -> | ||||
|                     entryValues = values.indices.map { index -> "$index" }.toTypedArray() | ||||
|                 } | ||||
|                 summary = "%s" | ||||
|             } | ||||
|             listPreference { | ||||
|                 bindTo(readerPreferences.pagerNavInverted()) | ||||
|                 titleRes = R.string.pref_read_with_tapping_inverted | ||||
|                 entriesRes = arrayOf( | ||||
|                     R.string.tapping_inverted_none, | ||||
|                     R.string.tapping_inverted_horizontal, | ||||
|                     R.string.tapping_inverted_vertical, | ||||
|                     R.string.tapping_inverted_both, | ||||
|                 ) | ||||
|                 entryValues = arrayOf( | ||||
|                     TappingInvertMode.NONE.name, | ||||
|                     TappingInvertMode.HORIZONTAL.name, | ||||
|                     TappingInvertMode.VERTICAL.name, | ||||
|                     TappingInvertMode.BOTH.name, | ||||
|                 ) | ||||
|                 summary = "%s" | ||||
|                 visibleIf(readerPreferences.navigationModePager()) { it != 5 } | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.navigateToPan()) | ||||
|                 titleRes = R.string.pref_navigate_pan | ||||
|                 visibleIf(readerPreferences.navigationModePager()) { it != 5 } | ||||
|             } | ||||
|             intListPreference { | ||||
|                 bindTo(readerPreferences.imageScaleType()) | ||||
|                 titleRes = R.string.pref_image_scale_type | ||||
|                 entriesRes = arrayOf( | ||||
|                     R.string.scale_type_fit_screen, | ||||
|                     R.string.scale_type_stretch, | ||||
|                     R.string.scale_type_fit_width, | ||||
|                     R.string.scale_type_fit_height, | ||||
|                     R.string.scale_type_original_size, | ||||
|                     R.string.scale_type_smart_fit, | ||||
|                 ) | ||||
|                 entryValues = arrayOf("1", "2", "3", "4", "5", "6") | ||||
|                 summary = "%s" | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.landscapeZoom()) | ||||
|                 titleRes = R.string.pref_landscape_zoom | ||||
|                 visibleIf(readerPreferences.imageScaleType()) { it == 1 } | ||||
|             } | ||||
|             intListPreference { | ||||
|                 bindTo(readerPreferences.zoomStart()) | ||||
|                 titleRes = R.string.pref_zoom_start | ||||
|                 entriesRes = arrayOf( | ||||
|                     R.string.zoom_start_automatic, | ||||
|                     R.string.zoom_start_left, | ||||
|                     R.string.zoom_start_right, | ||||
|                     R.string.zoom_start_center, | ||||
|                 ) | ||||
|                 entryValues = arrayOf("1", "2", "3", "4") | ||||
|                 summary = "%s" | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.cropBorders()) | ||||
|                 titleRes = R.string.pref_crop_borders | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.dualPageSplitPaged()) | ||||
|                 titleRes = R.string.pref_dual_page_split | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.dualPageInvertPaged()) | ||||
|                 titleRes = R.string.pref_dual_page_invert | ||||
|                 summaryRes = R.string.pref_dual_page_invert_summary | ||||
|                 visibleIf(readerPreferences.dualPageSplitPaged()) { it } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.webtoon_viewer | ||||
|  | ||||
|             intListPreference { | ||||
|                 bindTo(readerPreferences.navigationModeWebtoon()) | ||||
|                 titleRes = R.string.pref_viewer_nav | ||||
|                 entries = context.resources.getStringArray(R.array.webtoon_nav).also { values -> | ||||
|                     entryValues = values.indices.map { index -> "$index" }.toTypedArray() | ||||
|                 } | ||||
|                 summary = "%s" | ||||
|             } | ||||
|             listPreference { | ||||
|                 bindTo(readerPreferences.webtoonNavInverted()) | ||||
|                 titleRes = R.string.pref_read_with_tapping_inverted | ||||
|                 entriesRes = arrayOf( | ||||
|                     R.string.tapping_inverted_none, | ||||
|                     R.string.tapping_inverted_horizontal, | ||||
|                     R.string.tapping_inverted_vertical, | ||||
|                     R.string.tapping_inverted_both, | ||||
|                 ) | ||||
|                 entryValues = arrayOf( | ||||
|                     TappingInvertMode.NONE.name, | ||||
|                     TappingInvertMode.HORIZONTAL.name, | ||||
|                     TappingInvertMode.VERTICAL.name, | ||||
|                     TappingInvertMode.BOTH.name, | ||||
|                 ) | ||||
|                 summary = "%s" | ||||
|                 visibleIf(readerPreferences.navigationModeWebtoon()) { it != 5 } | ||||
|             } | ||||
|             intListPreference { | ||||
|                 bindTo(readerPreferences.webtoonSidePadding()) | ||||
|                 titleRes = R.string.pref_webtoon_side_padding | ||||
|                 entriesRes = arrayOf( | ||||
|                     R.string.webtoon_side_padding_0, | ||||
|                     R.string.webtoon_side_padding_5, | ||||
|                     R.string.webtoon_side_padding_10, | ||||
|                     R.string.webtoon_side_padding_15, | ||||
|                     R.string.webtoon_side_padding_20, | ||||
|                     R.string.webtoon_side_padding_25, | ||||
|                 ) | ||||
|                 entryValues = arrayOf("0", "5", "10", "15", "20", "25") | ||||
|                 summary = "%s" | ||||
|             } | ||||
|             listPreference { | ||||
|                 bindTo(readerPreferences.readerHideThreshold()) | ||||
|                 titleRes = R.string.pref_hide_threshold | ||||
|                 entriesRes = arrayOf( | ||||
|                     R.string.pref_highest, | ||||
|                     R.string.pref_high, | ||||
|                     R.string.pref_low, | ||||
|                     R.string.pref_lowest, | ||||
|                 ) | ||||
|                 entryValues = PreferenceValues.ReaderHideThreshold.values() | ||||
|                     .map { it.name } | ||||
|                     .toTypedArray() | ||||
|                 summary = "%s" | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.cropBordersWebtoon()) | ||||
|                 titleRes = R.string.pref_crop_borders | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.dualPageSplitWebtoon()) | ||||
|                 titleRes = R.string.pref_dual_page_split | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.dualPageInvertWebtoon()) | ||||
|                 titleRes = R.string.pref_dual_page_invert | ||||
|                 summaryRes = R.string.pref_dual_page_invert_summary | ||||
|                 visibleIf(readerPreferences.dualPageSplitWebtoon()) { it } | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.longStripSplitWebtoon()) | ||||
|                 titleRes = R.string.pref_long_strip_split | ||||
|                 summaryRes = R.string.split_tall_images_summary | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_reader_navigation | ||||
|  | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.readWithVolumeKeys()) | ||||
|                 titleRes = R.string.pref_read_with_volume_keys | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.readWithVolumeKeysInverted()) | ||||
|                 titleRes = R.string.pref_read_with_volume_keys_inverted | ||||
|                 visibleIf(readerPreferences.readWithVolumeKeys()) { it } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.pref_reader_actions | ||||
|  | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.readWithLongTap()) | ||||
|                 titleRes = R.string.pref_read_with_long_tap | ||||
|             } | ||||
|             switchPreference { | ||||
|                 bindTo(readerPreferences.folderPerManga()) | ||||
|                 titleRes = R.string.pref_create_folder_per_manga | ||||
|                 summaryRes = R.string.pref_create_folder_per_manga_summary | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,102 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting | ||||
|  | ||||
| import androidx.biometric.BiometricPrompt | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.preference.Preference | ||||
| import androidx.preference.PreferenceScreen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.core.security.SecurityPreferences | ||||
| import eu.kanade.tachiyomi.util.preference.bindTo | ||||
| import eu.kanade.tachiyomi.util.preference.entriesRes | ||||
| import eu.kanade.tachiyomi.util.preference.infoPreference | ||||
| import eu.kanade.tachiyomi.util.preference.intListPreference | ||||
| import eu.kanade.tachiyomi.util.preference.listPreference | ||||
| import eu.kanade.tachiyomi.util.preference.requireAuthentication | ||||
| import eu.kanade.tachiyomi.util.preference.switchPreference | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.util.system.AuthenticatorUtil | ||||
| import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported | ||||
| import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.startAuthentication | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class SettingsSecurityController : SettingsController() { | ||||
|  | ||||
|     private val securityPreferences: SecurityPreferences by injectLazy() | ||||
|  | ||||
|     override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { | ||||
|         titleRes = R.string.pref_category_security | ||||
|  | ||||
|         if (context.isAuthenticationSupported()) { | ||||
|             switchPreference { | ||||
|                 bindTo(securityPreferences.useAuthenticator()) | ||||
|                 titleRes = R.string.lock_with_biometrics | ||||
|  | ||||
|                 requireAuthentication( | ||||
|                     activity as? FragmentActivity, | ||||
|                     context.getString(R.string.lock_with_biometrics), | ||||
|                     context.getString(R.string.confirm_lock_change), | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             intListPreference { | ||||
|                 bindTo(securityPreferences.lockAppAfter()) | ||||
|                 titleRes = R.string.lock_when_idle | ||||
|                 val values = arrayOf("0", "1", "2", "5", "10", "-1") | ||||
|                 entries = values.mapNotNull { | ||||
|                     when (it) { | ||||
|                         "-1" -> context.getString(R.string.lock_never) | ||||
|                         "0" -> context.getString(R.string.lock_always) | ||||
|                         else -> resources?.getQuantityString(R.plurals.lock_after_mins, it.toInt(), it) | ||||
|                     } | ||||
|                 }.toTypedArray() | ||||
|                 entryValues = values | ||||
|                 summary = "%s" | ||||
|                 onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> | ||||
|                     if (value == newValue) return@OnPreferenceChangeListener false | ||||
|  | ||||
|                     (activity as? FragmentActivity)?.startAuthentication( | ||||
|                         activity!!.getString(R.string.lock_when_idle), | ||||
|                         activity!!.getString(R.string.confirm_lock_change), | ||||
|                         callback = object : AuthenticatorUtil.AuthenticationCallback() { | ||||
|                             override fun onAuthenticationSucceeded( | ||||
|                                 activity: FragmentActivity?, | ||||
|                                 result: BiometricPrompt.AuthenticationResult, | ||||
|                             ) { | ||||
|                                 super.onAuthenticationSucceeded(activity, result) | ||||
|                                 value = newValue as String | ||||
|                             } | ||||
|  | ||||
|                             override fun onAuthenticationError( | ||||
|                                 activity: FragmentActivity?, | ||||
|                                 errorCode: Int, | ||||
|                                 errString: CharSequence, | ||||
|                             ) { | ||||
|                                 super.onAuthenticationError(activity, errorCode, errString) | ||||
|                                 activity?.toast(errString.toString()) | ||||
|                             } | ||||
|                         }, | ||||
|                     ) | ||||
|                     false | ||||
|                 } | ||||
|  | ||||
|                 visibleIf(securityPreferences.useAuthenticator()) { it } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         switchPreference { | ||||
|             bindTo(securityPreferences.hideNotificationContent()) | ||||
|             titleRes = R.string.hide_notification_content | ||||
|         } | ||||
|  | ||||
|         listPreference { | ||||
|             bindTo(securityPreferences.secureScreen()) | ||||
|             titleRes = R.string.secure_screen | ||||
|             summary = "%s" | ||||
|             entriesRes = SecurityPreferences.SecureScreenMode.values().map { it.titleResId }.toTypedArray() | ||||
|             entryValues = SecurityPreferences.SecureScreenMode.values().map { it.name }.toTypedArray() | ||||
|         } | ||||
|  | ||||
|         infoPreference(R.string.secure_screen_summary) | ||||
|     } | ||||
| } | ||||
| @@ -1,163 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.view.Menu | ||||
| import android.view.MenuInflater | ||||
| import android.view.MenuItem | ||||
| import android.widget.Toast | ||||
| import androidx.preference.PreferenceGroup | ||||
| import androidx.preference.PreferenceScreen | ||||
| import eu.kanade.domain.track.service.TrackPreferences | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.track.NoLoginTrackService | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
| import eu.kanade.tachiyomi.data.track.anilist.AnilistApi | ||||
| import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi | ||||
| import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi | ||||
| import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.ui.setting.track.TrackLoginDialog | ||||
| import eu.kanade.tachiyomi.ui.setting.track.TrackLogoutDialog | ||||
| import eu.kanade.tachiyomi.util.preference.add | ||||
| import eu.kanade.tachiyomi.util.preference.bindTo | ||||
| import eu.kanade.tachiyomi.util.preference.iconRes | ||||
| import eu.kanade.tachiyomi.util.preference.infoPreference | ||||
| import eu.kanade.tachiyomi.util.preference.onClick | ||||
| import eu.kanade.tachiyomi.util.preference.preferenceCategory | ||||
| import eu.kanade.tachiyomi.util.preference.switchPreference | ||||
| import eu.kanade.tachiyomi.util.preference.titleRes | ||||
| import eu.kanade.tachiyomi.util.system.openInBrowser | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.widget.preference.TrackerPreference | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class SettingsTrackingController : | ||||
|     SettingsController(), | ||||
|     TrackLoginDialog.Listener, | ||||
|     TrackLogoutDialog.Listener { | ||||
|  | ||||
|     private val trackManager: TrackManager by injectLazy() | ||||
|     private val trackPreferences: TrackPreferences by injectLazy() | ||||
|     private val sourceManager: SourceManager by injectLazy() | ||||
|  | ||||
|     override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { | ||||
|         titleRes = R.string.pref_category_tracking | ||||
|  | ||||
|         switchPreference { | ||||
|             bindTo(trackPreferences.autoUpdateTrack()) | ||||
|             titleRes = R.string.pref_auto_update_manga_sync | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.services | ||||
|  | ||||
|             trackPreference(trackManager.myAnimeList) { | ||||
|                 activity?.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) | ||||
|             } | ||||
|             trackPreference(trackManager.aniList) { | ||||
|                 activity?.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) | ||||
|             } | ||||
|             trackPreference(trackManager.kitsu) { | ||||
|                 val dialog = TrackLoginDialog(trackManager.kitsu, R.string.email) | ||||
|                 dialog.targetController = this@SettingsTrackingController | ||||
|                 dialog.showDialog(router) | ||||
|             } | ||||
|             trackPreference(trackManager.mangaUpdates) { | ||||
|                 val dialog = TrackLoginDialog(trackManager.mangaUpdates, R.string.username) | ||||
|                 dialog.targetController = this@SettingsTrackingController | ||||
|                 dialog.showDialog(router) | ||||
|             } | ||||
|             trackPreference(trackManager.shikimori) { | ||||
|                 activity?.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) | ||||
|             } | ||||
|             trackPreference(trackManager.bangumi) { | ||||
|                 activity?.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) | ||||
|             } | ||||
|             infoPreference(R.string.tracking_info) | ||||
|         } | ||||
|  | ||||
|         preferenceCategory { | ||||
|             titleRes = R.string.enhanced_services | ||||
|  | ||||
|             trackPreference(trackManager.komga) { | ||||
|                 val acceptedSources = trackManager.komga.getAcceptedSources() | ||||
|                 val hasValidSourceInstalled = sourceManager.getCatalogueSources() | ||||
|                     .any { it::class.qualifiedName in acceptedSources } | ||||
|  | ||||
|                 if (hasValidSourceInstalled) { | ||||
|                     trackManager.komga.loginNoop() | ||||
|                     updatePreference(trackManager.komga.id) | ||||
|                 } else { | ||||
|                     context.toast(R.string.tracker_komga_warning, Toast.LENGTH_LONG) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             infoPreference(R.string.enhanced_tracking_info) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private inline fun PreferenceGroup.trackPreference( | ||||
|         service: TrackService, | ||||
|         crossinline login: () -> Unit, | ||||
|     ): TrackerPreference { | ||||
|         return add( | ||||
|             TrackerPreference(context).apply { | ||||
|                 key = TrackPreferences.trackUsername(service.id) | ||||
|                 titleRes = service.nameRes() | ||||
|                 iconRes = service.getLogo() | ||||
|                 iconColor = service.getLogoColor() | ||||
|                 onClick { | ||||
|                     if (service.isLogged) { | ||||
|                         if (service is NoLoginTrackService) { | ||||
|                             service.logout() | ||||
|                             updatePreference(service.id) | ||||
|                         } else { | ||||
|                             val dialog = TrackLogoutDialog(service) | ||||
|                             dialog.targetController = this@SettingsTrackingController | ||||
|                             dialog.showDialog(router) | ||||
|                         } | ||||
|                     } else { | ||||
|                         login() | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun onActivityResumed(activity: Activity) { | ||||
|         super.onActivityResumed(activity) | ||||
|  | ||||
|         // Manually refresh OAuth trackers' holders | ||||
|         updatePreference(trackManager.myAnimeList.id) | ||||
|         updatePreference(trackManager.aniList.id) | ||||
|         updatePreference(trackManager.shikimori.id) | ||||
|         updatePreference(trackManager.bangumi.id) | ||||
|     } | ||||
|  | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||
|         inflater.inflate(R.menu.settings_tracking, menu) | ||||
|     } | ||||
|  | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.action_tracking_help -> activity?.openInBrowser(HELP_URL) | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item) | ||||
|     } | ||||
|  | ||||
|     private fun updatePreference(id: Long) { | ||||
|         val pref = findPreference(TrackPreferences.trackUsername(id)) as? TrackerPreference | ||||
|         pref?.notifyChanged() | ||||
|     } | ||||
|  | ||||
|     override fun trackLoginDialogClosed(service: TrackService) { | ||||
|         updatePreference(service.id) | ||||
|     } | ||||
|  | ||||
|     override fun trackLogoutDialogClosed(service: TrackService) { | ||||
|         updatePreference(service.id) | ||||
|     } | ||||
| } | ||||
|  | ||||
| private const val HELP_URL = "https://tachiyomi.org/help/guides/tracking/" | ||||
| @@ -1,20 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting.database | ||||
|  | ||||
| import androidx.compose.runtime.Composable | ||||
| import eu.kanade.presentation.more.settings.database.ClearDatabaseScreen | ||||
| import eu.kanade.tachiyomi.ui.base.controller.FullComposeController | ||||
|  | ||||
| class ClearDatabaseController : FullComposeController<ClearDatabasePresenter>() { | ||||
|  | ||||
|     override fun createPresenter(): ClearDatabasePresenter { | ||||
|         return ClearDatabasePresenter() | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     override fun ComposeContent() { | ||||
|         ClearDatabaseScreen( | ||||
|             presenter = presenter, | ||||
|             navigateUp = { router.popCurrentController() }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,62 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting.database | ||||
|  | ||||
| import android.os.Bundle | ||||
| import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga | ||||
| import eu.kanade.domain.source.model.Source | ||||
| import eu.kanade.presentation.more.settings.database.ClearDatabaseState | ||||
| import eu.kanade.presentation.more.settings.database.ClearDatabaseStateImpl | ||||
| import eu.kanade.tachiyomi.Database | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class ClearDatabasePresenter( | ||||
|     private val state: ClearDatabaseStateImpl = ClearDatabaseState() as ClearDatabaseStateImpl, | ||||
|     private val database: Database = Injekt.get(), | ||||
|     private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get(), | ||||
| ) : BasePresenter<ClearDatabaseController>(), ClearDatabaseState by state { | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|         presenterScope.launchIO { | ||||
|             getSourcesWithNonLibraryManga.subscribe() | ||||
|                 .collectLatest { list -> | ||||
|                     state.items = list.sortedBy { it.name } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun removeMangaBySourceId(sourceIds: List<Long>) { | ||||
|         database.mangasQueries.deleteMangasNotInLibraryBySourceIds(sourceIds) | ||||
|         database.historyQueries.removeResettedHistory() | ||||
|     } | ||||
|  | ||||
|     fun toggleSelection(source: Source) { | ||||
|         val mutableList = state.selection.toMutableList() | ||||
|         if (mutableList.contains(source.id)) { | ||||
|             mutableList.remove(source.id) | ||||
|         } else { | ||||
|             mutableList.add(source.id) | ||||
|         } | ||||
|         state.selection = mutableList | ||||
|     } | ||||
|  | ||||
|     fun clearSelection() { | ||||
|         state.selection = emptyList() | ||||
|     } | ||||
|  | ||||
|     fun selectAll() { | ||||
|         state.selection = state.items.map { it.id } | ||||
|     } | ||||
|  | ||||
|     fun invertSelection() { | ||||
|         state.selection = state.items.map { it.id }.filterNot { it in state.selection } | ||||
|     } | ||||
|  | ||||
|     sealed class Dialog { | ||||
|         data class Delete(val sourceIds: List<Long>) : Dialog() | ||||
|     } | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting.search | ||||
|  | ||||
| import androidx.compose.runtime.Composable | ||||
| import eu.kanade.presentation.more.settings.SettingsSearchScreen | ||||
| import eu.kanade.tachiyomi.ui.base.controller.FullComposeController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
|  | ||||
| class SettingsSearchController : FullComposeController<SettingsSearchPresenter>() { | ||||
|  | ||||
|     override fun createPresenter() = SettingsSearchPresenter() | ||||
|  | ||||
|     @Composable | ||||
|     override fun ComposeContent() { | ||||
|         SettingsSearchScreen( | ||||
|             navigateUp = router::popCurrentController, | ||||
|             presenter = presenter, | ||||
|             onClickResult = { router.pushController(it) }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,138 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting.search | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.content.res.Resources | ||||
| import androidx.preference.Preference | ||||
| import androidx.preference.PreferenceCategory | ||||
| import androidx.preference.PreferenceGroup | ||||
| import androidx.preference.PreferenceManager | ||||
| import androidx.preference.forEach | ||||
| import androidx.preference.get | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsAdvancedController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsAppearanceController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsBackupController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsBrowseController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsDownloadController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsGeneralController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsLibraryController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsReaderController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsSecurityController | ||||
| import eu.kanade.tachiyomi.ui.setting.SettingsTrackingController | ||||
| import eu.kanade.tachiyomi.util.lang.launchNow | ||||
| import eu.kanade.tachiyomi.util.system.isLTR | ||||
| import kotlin.reflect.KClass | ||||
| import kotlin.reflect.full.createInstance | ||||
|  | ||||
| object SettingsSearchHelper { | ||||
|     private var prefSearchResultList: MutableList<SettingsSearchResult> = mutableListOf() | ||||
|  | ||||
|     /** | ||||
|      * All subclasses of `SettingsController` should be listed here, in order to have their preferences searchable. | ||||
|      */ | ||||
|     private val settingControllersList: List<KClass<out SettingsController>> = listOf( | ||||
|         SettingsAdvancedController::class, | ||||
|         SettingsAppearanceController::class, | ||||
|         SettingsBackupController::class, | ||||
|         SettingsBrowseController::class, | ||||
|         SettingsDownloadController::class, | ||||
|         SettingsGeneralController::class, | ||||
|         SettingsLibraryController::class, | ||||
|         SettingsReaderController::class, | ||||
|         SettingsSecurityController::class, | ||||
|         SettingsTrackingController::class, | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Must be called to populate `prefSearchResultList` | ||||
|      */ | ||||
|     @SuppressLint("RestrictedApi") | ||||
|     fun initPreferenceSearchResults(context: Context) { | ||||
|         val preferenceManager = PreferenceManager(context) | ||||
|         prefSearchResultList.clear() | ||||
|  | ||||
|         launchNow { | ||||
|             settingControllersList.forEach { kClass -> | ||||
|                 val ctrl = kClass.createInstance() | ||||
|                 val settingsPrefScreen = ctrl.setupPreferenceScreen(preferenceManager.createPreferenceScreen(context)) | ||||
|                 val prefCount = settingsPrefScreen.preferenceCount | ||||
|                 for (i in 0 until prefCount) { | ||||
|                     val rootPref = settingsPrefScreen[i] | ||||
|                     if (rootPref.title == null) continue // no title, not a preference. (note: only info notes appear to not have titles) | ||||
|                     getSettingSearchResult(ctrl, rootPref, "${settingsPrefScreen.title}") | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun getFilteredResults(query: String): List<SettingsSearchResult> { | ||||
|         return prefSearchResultList.filter { | ||||
|             val inTitle = it.title.contains(query, true) | ||||
|             val inSummary = it.summary.contains(query, true) | ||||
|             val inBreadcrumb = it.breadcrumb.contains(query, true) | ||||
|  | ||||
|             return@filter inTitle || inSummary || inBreadcrumb | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Extracts the data needed from a `Preference` to create a `SettingsSearchResult`, and then adds it to `prefSearchResultList` | ||||
|      * Future enhancement: make bold the text matched by the search query. | ||||
|      */ | ||||
|     private fun getSettingSearchResult( | ||||
|         ctrl: SettingsController, | ||||
|         pref: Preference, | ||||
|         breadcrumbs: String = "", | ||||
|     ) { | ||||
|         when { | ||||
|             pref is PreferenceGroup -> { | ||||
|                 val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}") | ||||
|                 pref.forEach { | ||||
|                     getSettingSearchResult(ctrl, it, breadcrumbsStr) // recursion | ||||
|                 } | ||||
|             } | ||||
|             pref is PreferenceCategory -> { | ||||
|                 val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}") | ||||
|                 pref.forEach { | ||||
|                     getSettingSearchResult(ctrl, it, breadcrumbsStr) // recursion | ||||
|                 } | ||||
|             } | ||||
|             (pref.title != null && pref.isVisible) -> { | ||||
|                 // Is an actual preference | ||||
|                 val title = pref.title.toString() | ||||
|                 // ListPreferences occasionally run into ArrayIndexOutOfBoundsException issues | ||||
|                 val summary = try { pref.summary?.toString() ?: "" } catch (e: Throwable) { "" } | ||||
|                 val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}") | ||||
|  | ||||
|                 prefSearchResultList.add( | ||||
|                     SettingsSearchResult( | ||||
|                         key = pref.key, | ||||
|                         title = title, | ||||
|                         summary = summary, | ||||
|                         breadcrumb = breadcrumbsStr, | ||||
|                         searchController = ctrl, | ||||
|                     ), | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun addLocalizedBreadcrumb(path: String, node: String): String { | ||||
|         return if (Resources.getSystem().isLTR) { | ||||
|             // This locale reads left to right. | ||||
|             "$path > $node" | ||||
|         } else { | ||||
|             // This locale reads right to left. | ||||
|             "$node < $path" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     data class SettingsSearchResult( | ||||
|         val key: String?, | ||||
|         val title: String, | ||||
|         val summary: String, | ||||
|         val breadcrumb: String, | ||||
|         val searchController: SettingsController, | ||||
|     ) | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting.search | ||||
|  | ||||
| import android.os.Bundle | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import kotlinx.coroutines.flow.asStateFlow | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class SettingsSearchPresenter( | ||||
|     private val preferences: BasePreferences = Injekt.get(), | ||||
| ) : BasePresenter<SettingsSearchController>() { | ||||
|  | ||||
|     private val _state: MutableStateFlow<List<SettingsSearchHelper.SettingsSearchResult>> = | ||||
|         MutableStateFlow(emptyList()) | ||||
|     val state: StateFlow<List<SettingsSearchHelper.SettingsSearchResult>> = _state.asStateFlow() | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|         SettingsSearchHelper.initPreferenceSearchResults(preferences.context) | ||||
|     } | ||||
|  | ||||
|     fun searchSettings(query: String?) { | ||||
|         _state.value = if (!query.isNullOrBlank()) { | ||||
|             SettingsSearchHelper.getFilteredResults(query) | ||||
|         } else { | ||||
|             emptyList() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,66 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting.track | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.view.View | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.core.os.bundleOf | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.lang.withUIContext | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.widget.preference.LoginDialogPreference | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class TrackLoginDialog( | ||||
|     @StringRes usernameLabelRes: Int? = null, | ||||
|     bundle: Bundle? = null, | ||||
| ) : LoginDialogPreference(usernameLabelRes, bundle) { | ||||
|  | ||||
|     private val service = Injekt.get<TrackManager>().getService(args.getLong("serviceId"))!! | ||||
|  | ||||
|     constructor(service: TrackService, @StringRes usernameLabelRes: Int?) : | ||||
|         this(usernameLabelRes, bundleOf("serviceId" to service.id)) | ||||
|  | ||||
|     @StringRes | ||||
|     override fun getTitleName(): Int = service.nameRes() | ||||
|  | ||||
|     override fun setCredentialsOnView(view: View) { | ||||
|         binding?.username?.setText(service.getUsername()) | ||||
|         binding?.password?.setText(service.getPassword()) | ||||
|     } | ||||
|  | ||||
|     override fun checkLogin() { | ||||
|         if (binding!!.username.text.isNullOrEmpty() || binding!!.password.text.isNullOrEmpty()) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         binding!!.login.progress = 1 | ||||
|         val user = binding!!.username.text.toString() | ||||
|         val pass = binding!!.password.text.toString() | ||||
|  | ||||
|         launchIO { | ||||
|             try { | ||||
|                 service.login(user, pass) | ||||
|                 dialog?.dismiss() | ||||
|                 withUIContext { view?.context?.toast(R.string.login_success) } | ||||
|             } catch (e: Throwable) { | ||||
|                 service.logout() | ||||
|                 binding?.login?.progress = -1 | ||||
|                 binding?.login?.setText(R.string.unknown_error) | ||||
|                 withUIContext { e.message?.let { view?.context?.toast(it) } } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onDialogClosed() { | ||||
|         super.onDialogClosed() | ||||
|         (targetController as? Listener)?.trackLoginDialogClosed(service) | ||||
|     } | ||||
|  | ||||
|     interface Listener { | ||||
|         fun trackLoginDialogClosed(service: TrackService) | ||||
|     } | ||||
| } | ||||
| @@ -1,37 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.setting.track | ||||
|  | ||||
| import android.app.Dialog | ||||
| import android.os.Bundle | ||||
| import androidx.core.os.bundleOf | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class TrackLogoutDialog(bundle: Bundle? = null) : DialogController(bundle) { | ||||
|  | ||||
|     private val service = Injekt.get<TrackManager>().getService(args.getLong("serviceId"))!! | ||||
|  | ||||
|     constructor(service: TrackService) : this(bundleOf("serviceId" to service.id)) | ||||
|  | ||||
|     override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|         val serviceName = activity!!.getString(service.nameRes()) | ||||
|         return MaterialAlertDialogBuilder(activity!!) | ||||
|             .setTitle(activity!!.getString(R.string.logout_title, serviceName)) | ||||
|             .setPositiveButton(R.string.logout) { _, _ -> | ||||
|                 service.logout() | ||||
|                 (targetController as? Listener)?.trackLogoutDialogClosed(service) | ||||
|                 activity?.toast(R.string.logout_success) | ||||
|             } | ||||
|             .setNegativeButton(android.R.string.cancel, null) | ||||
|             .create() | ||||
|     } | ||||
|  | ||||
|     interface Listener { | ||||
|         fun trackLogoutDialogClosed(service: TrackService) | ||||
|     } | ||||
| } | ||||
| @@ -1,198 +0,0 @@ | ||||
| @file:Suppress("NOTHING_TO_INLINE") | ||||
|  | ||||
| package eu.kanade.tachiyomi.util.preference | ||||
|  | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.appcompat.content.res.AppCompatResources | ||||
| import androidx.biometric.BiometricPrompt | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.preference.CheckBoxPreference | ||||
| import androidx.preference.EditTextPreference | ||||
| import androidx.preference.ListPreference | ||||
| import androidx.preference.MultiSelectListPreference | ||||
| import androidx.preference.Preference | ||||
| import androidx.preference.PreferenceCategory | ||||
| import androidx.preference.PreferenceGroup | ||||
| import androidx.preference.PreferenceManager | ||||
| import androidx.preference.PreferenceScreen | ||||
| import androidx.preference.SwitchPreferenceCompat | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.system.AuthenticatorUtil | ||||
| import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported | ||||
| import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.startAuthentication | ||||
| import eu.kanade.tachiyomi.util.system.getResourceColor | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.widget.preference.AdaptiveTitlePreferenceCategory | ||||
| import eu.kanade.tachiyomi.widget.preference.IntListPreference | ||||
|  | ||||
| @DslMarker | ||||
| @Target(AnnotationTarget.TYPE) | ||||
| annotation class DSL | ||||
|  | ||||
| inline fun PreferenceManager.newScreen(block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen { | ||||
|     return createPreferenceScreen(context).also { it.block() } | ||||
| } | ||||
|  | ||||
| inline fun PreferenceGroup.preference(block: (@DSL Preference).() -> Unit): Preference { | ||||
|     return initThenAdd(Preference(context), block) | ||||
| } | ||||
|  | ||||
| inline fun PreferenceGroup.infoPreference(@StringRes infoRes: Int): Preference { | ||||
|     return add( | ||||
|         Preference(context).apply { | ||||
|             iconRes = R.drawable.ic_info_24dp | ||||
|             iconTint = context.getResourceColor(android.R.attr.textColorHint) | ||||
|             summaryRes = infoRes | ||||
|             isSelectable = false | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| inline fun PreferenceGroup.switchPreference(block: (@DSL SwitchPreferenceCompat).() -> Unit): SwitchPreferenceCompat { | ||||
|     return initThenAdd(SwitchPreferenceCompat(context), block) | ||||
| } | ||||
|  | ||||
| inline fun PreferenceGroup.checkBoxPreference(block: (@DSL CheckBoxPreference).() -> Unit): CheckBoxPreference { | ||||
|     return initThenAdd(CheckBoxPreference(context), block) | ||||
| } | ||||
|  | ||||
| inline fun PreferenceGroup.editTextPreference(block: (@DSL EditTextPreference).() -> Unit): EditTextPreference { | ||||
|     return initThenAdd(EditTextPreference(context), block) | ||||
| } | ||||
|  | ||||
| inline fun PreferenceGroup.listPreference(block: (@DSL ListPreference).() -> Unit): ListPreference { | ||||
|     return initThenAdd(ListPreference(context), block) | ||||
| } | ||||
|  | ||||
| inline fun PreferenceGroup.intListPreference(block: (@DSL IntListPreference).() -> Unit): IntListPreference { | ||||
|     return initThenAdd(IntListPreference(context), block) | ||||
| } | ||||
|  | ||||
| inline fun PreferenceGroup.multiSelectListPreference(block: (@DSL MultiSelectListPreference).() -> Unit): MultiSelectListPreference { | ||||
|     return initThenAdd(MultiSelectListPreference(context), block) | ||||
| } | ||||
|  | ||||
| inline fun PreferenceScreen.preferenceCategory(block: (@DSL PreferenceCategory).() -> Unit): PreferenceCategory { | ||||
|     return addThenInit(AdaptiveTitlePreferenceCategory(context), block) | ||||
| } | ||||
|  | ||||
| inline fun PreferenceScreen.preferenceScreen(block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen { | ||||
|     return addThenInit(preferenceManager.createPreferenceScreen(context), block) | ||||
| } | ||||
|  | ||||
| inline fun <P : Preference> PreferenceGroup.add(p: P): P { | ||||
|     return p.apply { | ||||
|         this.isIconSpaceReserved = false | ||||
|         this.isSingleLineTitle = false | ||||
|         addPreference(this) | ||||
|     } | ||||
| } | ||||
|  | ||||
| inline fun <P : Preference> PreferenceGroup.initThenAdd(p: P, block: P.() -> Unit): P { | ||||
|     return p.apply { | ||||
|         block() | ||||
|         this.isIconSpaceReserved = false | ||||
|         this.isSingleLineTitle = false | ||||
|         addPreference(this) | ||||
|     } | ||||
| } | ||||
|  | ||||
| inline fun <P : Preference> PreferenceGroup.addThenInit(p: P, block: P.() -> Unit): P { | ||||
|     return p.apply { | ||||
|         this.isIconSpaceReserved = false | ||||
|         this.isSingleLineTitle = false | ||||
|         addPreference(this) | ||||
|         block() | ||||
|     } | ||||
| } | ||||
|  | ||||
| inline fun <T> Preference.bindTo(preference: eu.kanade.tachiyomi.core.preference.Preference<T>) { | ||||
|     key = preference.key() | ||||
|     defaultValue = preference.defaultValue() | ||||
| } | ||||
|  | ||||
| inline fun <T> ListPreference.bindTo(preference: eu.kanade.tachiyomi.core.preference.Preference<T>) { | ||||
|     key = preference.key() | ||||
|     defaultValue = preference.defaultValue().toString() | ||||
| } | ||||
|  | ||||
| inline fun Preference.onClick(crossinline block: () -> Unit) { | ||||
|     setOnPreferenceClickListener { block(); true } | ||||
| } | ||||
|  | ||||
| inline fun Preference.onChange(crossinline block: (Any?) -> Boolean) { | ||||
|     setOnPreferenceChangeListener { _, newValue -> block(newValue) } | ||||
| } | ||||
|  | ||||
| inline fun SwitchPreferenceCompat.requireAuthentication(activity: FragmentActivity?, title: String, subtitle: String?) { | ||||
|     onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> | ||||
|         if (context.isAuthenticationSupported()) { | ||||
|             activity?.startAuthentication( | ||||
|                 title, | ||||
|                 subtitle, | ||||
|                 callback = object : AuthenticatorUtil.AuthenticationCallback() { | ||||
|                     override fun onAuthenticationSucceeded( | ||||
|                         activity: FragmentActivity?, | ||||
|                         result: BiometricPrompt.AuthenticationResult, | ||||
|                     ) { | ||||
|                         super.onAuthenticationSucceeded(activity, result) | ||||
|                         isChecked = newValue as Boolean | ||||
|                     } | ||||
|  | ||||
|                     override fun onAuthenticationError( | ||||
|                         activity: FragmentActivity?, | ||||
|                         errorCode: Int, | ||||
|                         errString: CharSequence, | ||||
|                     ) { | ||||
|                         super.onAuthenticationError(activity, errorCode, errString) | ||||
|                         activity?.toast(errString.toString()) | ||||
|                     } | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         false | ||||
|     } | ||||
| } | ||||
|  | ||||
| var Preference.defaultValue: Any? | ||||
|     get() = null // set only | ||||
|     set(value) { | ||||
|         setDefaultValue(value) | ||||
|     } | ||||
|  | ||||
| var Preference.titleRes: Int | ||||
|     get() = 0 // set only | ||||
|     set(value) { | ||||
|         setTitle(value) | ||||
|     } | ||||
|  | ||||
| var Preference.iconRes: Int | ||||
|     get() = 0 // set only | ||||
|     set(value) { | ||||
|         icon = AppCompatResources.getDrawable(context, value) | ||||
|     } | ||||
|  | ||||
| var Preference.summaryRes: Int | ||||
|     get() = 0 // set only | ||||
|     set(value) { | ||||
|         setSummary(value) | ||||
|     } | ||||
|  | ||||
| var Preference.iconTint: Int | ||||
|     get() = 0 // set only | ||||
|     set(value) { | ||||
|         icon?.setTint(value) | ||||
|     } | ||||
|  | ||||
| var ListPreference.entriesRes: Array<Int> | ||||
|     get() = emptyArray() // set only | ||||
|     set(value) { | ||||
|         entries = value.map { context.getString(it) }.toTypedArray() | ||||
|     } | ||||
|  | ||||
| var MultiSelectListPreference.entriesRes: Array<Int> | ||||
|     get() = emptyArray() // set only | ||||
|     set(value) { | ||||
|         entries = value.map { context.getString(it) }.toTypedArray() | ||||
|     } | ||||
| @@ -1,67 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.materialdialogs | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import eu.kanade.tachiyomi.databinding.DialogStubQuadstatemultichoiceBinding | ||||
| import kotlinx.coroutines.suspendCancellableCoroutine | ||||
| import kotlin.coroutines.resume | ||||
|  | ||||
| /** | ||||
|  * Sets a list of items with checkboxes that supports 4 states. | ||||
|  * | ||||
|  * @see eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView | ||||
|  */ | ||||
| fun MaterialAlertDialogBuilder.setQuadStateMultiChoiceItems( | ||||
|     @StringRes message: Int? = null, | ||||
|     isActionList: Boolean = true, | ||||
|     items: List<CharSequence>, | ||||
|     initialSelected: IntArray, | ||||
|     disabledIndices: IntArray? = null, | ||||
|     selection: QuadStateMultiChoiceListener, | ||||
| ): MaterialAlertDialogBuilder { | ||||
|     val binding = DialogStubQuadstatemultichoiceBinding.inflate(LayoutInflater.from(context)) | ||||
|     binding.list.layoutManager = LinearLayoutManager(context) | ||||
|     binding.list.adapter = QuadStateMultiChoiceDialogAdapter( | ||||
|         items = items, | ||||
|         disabledItems = disabledIndices, | ||||
|         initialSelected = initialSelected, | ||||
|         isActionList = isActionList, | ||||
|         listener = selection, | ||||
|     ) | ||||
|     val updateScrollIndicators = { | ||||
|         binding.scrollIndicatorUp.isVisible = binding.list.canScrollVertically(-1) | ||||
|         binding.scrollIndicatorDown.isVisible = binding.list.canScrollVertically(1) | ||||
|     } | ||||
|     binding.list.setOnScrollChangeListener { _, _, _, _, _ -> | ||||
|         updateScrollIndicators() | ||||
|     } | ||||
|     binding.list.post { | ||||
|         updateScrollIndicators() | ||||
|     } | ||||
|  | ||||
|     if (message != null) { | ||||
|         binding.message.setText(message) | ||||
|         binding.message.isVisible = true | ||||
|     } | ||||
|     return setView(binding.root) | ||||
| } | ||||
|  | ||||
| suspend fun MaterialAlertDialogBuilder.await( | ||||
|     @StringRes positiveLabelId: Int, | ||||
|     @StringRes negativeLabelId: Int, | ||||
|     @StringRes neutralLabelId: Int? = null, | ||||
| ) = suspendCancellableCoroutine { cont -> | ||||
|     setPositiveButton(positiveLabelId) { _, _ -> cont.resume(AlertDialog.BUTTON_POSITIVE) } | ||||
|     setNegativeButton(negativeLabelId) { _, _ -> cont.resume(AlertDialog.BUTTON_NEGATIVE) } | ||||
|     if (neutralLabelId != null) { | ||||
|         setNeutralButton(neutralLabelId) { _, _ -> cont.resume(AlertDialog.BUTTON_NEUTRAL) } | ||||
|     } | ||||
|     setOnDismissListener { cont.cancel() } | ||||
|  | ||||
|     val dialog = show() | ||||
|     cont.invokeOnCancellation { dialog.dismiss() } | ||||
| } | ||||
| @@ -1,128 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.materialdialogs | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import eu.kanade.tachiyomi.databinding.DialogQuadstatemultichoiceItemBinding | ||||
|  | ||||
| private object CheckPayload | ||||
| private object InverseCheckPayload | ||||
| private object UncheckPayload | ||||
| private object IndeterminatePayload | ||||
|  | ||||
| typealias QuadStateMultiChoiceListener = (indices: IntArray) -> Unit | ||||
|  | ||||
| // isAction state: Uncheck-> Check-> Invert else Uncheck-> Indeterminate (only if initial so)-> Check | ||||
| // isAction for list of action to operate on like filter include, exclude | ||||
| internal class QuadStateMultiChoiceDialogAdapter( | ||||
|     internal var items: List<CharSequence>, | ||||
|     disabledItems: IntArray?, | ||||
|     private var initialSelected: IntArray, | ||||
|     internal var listener: QuadStateMultiChoiceListener, | ||||
|     val isActionList: Boolean = true, | ||||
| ) : RecyclerView.Adapter<QuadStateMultiChoiceViewHolder>() { | ||||
|  | ||||
|     private val states = QuadStateTextView.State.values() | ||||
|  | ||||
|     private var currentSelection: IntArray = initialSelected | ||||
|         set(value) { | ||||
|             val previousSelection = field | ||||
|             field = value | ||||
|             previousSelection.forEachIndexed { index, previous -> | ||||
|                 val current = value[index] | ||||
|                 when { | ||||
|                     current == QuadStateTextView.State.CHECKED.ordinal && previous != QuadStateTextView.State.CHECKED.ordinal -> { | ||||
|                         // This value was selected | ||||
|                         notifyItemChanged(index, CheckPayload) | ||||
|                     } | ||||
|                     current == QuadStateTextView.State.INVERSED.ordinal && previous != QuadStateTextView.State.INVERSED.ordinal -> { | ||||
|                         // This value was inverse selected | ||||
|                         notifyItemChanged(index, InverseCheckPayload) | ||||
|                     } | ||||
|                     current == QuadStateTextView.State.UNCHECKED.ordinal && previous != QuadStateTextView.State.UNCHECKED.ordinal -> { | ||||
|                         // This value was unselected | ||||
|                         notifyItemChanged(index, UncheckPayload) | ||||
|                     } | ||||
|                     current == QuadStateTextView.State.INDETERMINATE.ordinal && previous != QuadStateTextView.State.INDETERMINATE.ordinal -> { | ||||
|                         // This value was set back to Indeterminate | ||||
|                         notifyItemChanged(index, IndeterminatePayload) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     private var disabledIndices: IntArray = disabledItems ?: IntArray(0) | ||||
|     internal fun itemActionClicked(index: Int) { | ||||
|         val newSelection = this.currentSelection.toMutableList() | ||||
|         newSelection[index] = when (currentSelection[index]) { | ||||
|             QuadStateTextView.State.CHECKED.ordinal -> QuadStateTextView.State.INVERSED.ordinal | ||||
|             QuadStateTextView.State.INVERSED.ordinal -> QuadStateTextView.State.UNCHECKED.ordinal | ||||
|             // INDETERMINATE or UNCHECKED | ||||
|             else -> QuadStateTextView.State.CHECKED.ordinal | ||||
|         } | ||||
|         this.currentSelection = newSelection.toIntArray() | ||||
|         listener(currentSelection) | ||||
|     } | ||||
|  | ||||
|     internal fun itemDisplayClicked(index: Int) { | ||||
|         val newSelection = this.currentSelection.toMutableList() | ||||
|         newSelection[index] = when (currentSelection[index]) { | ||||
|             QuadStateTextView.State.UNCHECKED.ordinal -> QuadStateTextView.State.CHECKED.ordinal | ||||
|             QuadStateTextView.State.CHECKED.ordinal -> when (initialSelected[index]) { | ||||
|                 QuadStateTextView.State.INDETERMINATE.ordinal -> QuadStateTextView.State.INDETERMINATE.ordinal | ||||
|                 else -> QuadStateTextView.State.UNCHECKED.ordinal | ||||
|             } | ||||
|             // INDETERMINATE or UNCHECKED | ||||
|             else -> QuadStateTextView.State.UNCHECKED.ordinal | ||||
|         } | ||||
|         this.currentSelection = newSelection.toIntArray() | ||||
|         listener(currentSelection) | ||||
|     } | ||||
|  | ||||
|     override fun onCreateViewHolder( | ||||
|         parent: ViewGroup, | ||||
|         viewType: Int, | ||||
|     ): QuadStateMultiChoiceViewHolder { | ||||
|         return QuadStateMultiChoiceViewHolder( | ||||
|             itemBinding = DialogQuadstatemultichoiceItemBinding | ||||
|                 .inflate(LayoutInflater.from(parent.context), parent, false), | ||||
|             adapter = this, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun getItemCount() = items.size | ||||
|  | ||||
|     override fun onBindViewHolder( | ||||
|         holder: QuadStateMultiChoiceViewHolder, | ||||
|         position: Int, | ||||
|     ) { | ||||
|         holder.isEnabled = !disabledIndices.contains(position) | ||||
|         holder.controlView.state = states[currentSelection[position]] | ||||
|         holder.controlView.text = items[position] | ||||
|     } | ||||
|  | ||||
|     override fun onBindViewHolder( | ||||
|         holder: QuadStateMultiChoiceViewHolder, | ||||
|         position: Int, | ||||
|         payloads: MutableList<Any>, | ||||
|     ) { | ||||
|         when (payloads.firstOrNull()) { | ||||
|             CheckPayload -> { | ||||
|                 holder.controlView.state = QuadStateTextView.State.CHECKED | ||||
|                 return | ||||
|             } | ||||
|             InverseCheckPayload -> { | ||||
|                 holder.controlView.state = QuadStateTextView.State.INVERSED | ||||
|                 return | ||||
|             } | ||||
|             UncheckPayload -> { | ||||
|                 holder.controlView.state = QuadStateTextView.State.UNCHECKED | ||||
|                 return | ||||
|             } | ||||
|             IndeterminatePayload -> { | ||||
|                 holder.controlView.state = QuadStateTextView.State.INDETERMINATE | ||||
|                 return | ||||
|             } | ||||
|         } | ||||
|         super.onBindViewHolder(holder, position, payloads) | ||||
|     } | ||||
| } | ||||
| @@ -1,28 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.materialdialogs | ||||
|  | ||||
| import android.view.View | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import eu.kanade.tachiyomi.databinding.DialogQuadstatemultichoiceItemBinding | ||||
|  | ||||
| internal class QuadStateMultiChoiceViewHolder( | ||||
|     itemBinding: DialogQuadstatemultichoiceItemBinding, | ||||
|     private val adapter: QuadStateMultiChoiceDialogAdapter, | ||||
| ) : RecyclerView.ViewHolder(itemBinding.root), View.OnClickListener { | ||||
|     init { | ||||
|         itemView.setOnClickListener(this) | ||||
|     } | ||||
|  | ||||
|     val controlView = itemBinding.quadStateControl | ||||
|  | ||||
|     var isEnabled: Boolean | ||||
|         get() = itemView.isEnabled | ||||
|         set(value) { | ||||
|             itemView.isEnabled = value | ||||
|             controlView.isEnabled = value | ||||
|         } | ||||
|  | ||||
|     override fun onClick(view: View) = when (adapter.isActionList) { | ||||
|         true -> adapter.itemActionClicked(bindingAdapterPosition) | ||||
|         false -> adapter.itemDisplayClicked(bindingAdapterPosition) | ||||
|     } | ||||
| } | ||||
| @@ -1,46 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.materialdialogs | ||||
|  | ||||
| import android.content.Context | ||||
| import android.content.res.ColorStateList | ||||
| import android.util.AttributeSet | ||||
| import androidx.appcompat.widget.AppCompatTextView | ||||
| import androidx.core.widget.TextViewCompat | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.system.getThemeColor | ||||
|  | ||||
| class QuadStateTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : | ||||
|     AppCompatTextView(context, attrs) { | ||||
|  | ||||
|     var state: State = State.UNCHECKED | ||||
|         set(value) { | ||||
|             field = value | ||||
|             updateDrawable() | ||||
|         } | ||||
|  | ||||
|     private fun updateDrawable() { | ||||
|         val drawableStartId = when (state) { | ||||
|             State.UNCHECKED -> R.drawable.ic_check_box_outline_blank_24dp | ||||
|             State.INDETERMINATE -> R.drawable.ic_indeterminate_check_box_24dp | ||||
|             State.CHECKED -> R.drawable.ic_check_box_24dp | ||||
|             State.INVERSED -> R.drawable.ic_check_box_x_24dp | ||||
|         } | ||||
|         setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStartId, 0, 0, 0) | ||||
|  | ||||
|         val tint = if (state == State.UNCHECKED) { | ||||
|             context.getThemeColor(R.attr.colorControlNormal) | ||||
|         } else { | ||||
|             context.getThemeColor(R.attr.colorPrimary) | ||||
|         } | ||||
|         if (tint != 0) { | ||||
|             TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(tint)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     enum class State { | ||||
|         UNCHECKED, | ||||
|         INDETERMINATE, | ||||
|         CHECKED, | ||||
|         INVERSED, | ||||
|         ; | ||||
|     } | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.preference | ||||
|  | ||||
| import android.content.Context | ||||
| import androidx.core.view.updateLayoutParams | ||||
| import androidx.preference.PreferenceCategory | ||||
| import androidx.preference.PreferenceViewHolder | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
|  | ||||
| /** | ||||
|  * PreferenceCategory that hides the title placeholder layout if the title is unset | ||||
|  */ | ||||
| class AdaptiveTitlePreferenceCategory(context: Context) : PreferenceCategory(context) { | ||||
|     override fun onBindViewHolder(holder: PreferenceViewHolder) { | ||||
|         super.onBindViewHolder(holder) | ||||
|         if (title.isNullOrBlank()) { | ||||
|             holder.itemView.updateLayoutParams<RecyclerView.LayoutParams> { | ||||
|                 height = 0 | ||||
|                 topMargin = 0 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.preference | ||||
|  | ||||
| import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| import androidx.preference.ListPreference | ||||
|  | ||||
| class IntListPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : | ||||
|     ListPreference(context, attrs) { | ||||
|  | ||||
|     override fun persistString(value: String?): Boolean { | ||||
|         return value != null && persistInt(value.toInt()) | ||||
|     } | ||||
|  | ||||
|     override fun getPersistedString(defaultReturnValue: String?): String? { | ||||
|         // When the underlying preference is using a PreferenceDataStore, there's no way (for now) | ||||
|         // to check if a value is in the store, so we use a most likely unused value as workaround | ||||
|         val defaultIntValue = Int.MIN_VALUE + 1 | ||||
|  | ||||
|         val value = getPersistedInt(defaultIntValue) | ||||
|         return if (value != defaultIntValue) { | ||||
|             value.toString() | ||||
|         } else { | ||||
|             defaultReturnValue | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,67 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.preference | ||||
|  | ||||
| import android.app.Dialog | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import androidx.annotation.StringRes | ||||
| import com.bluelinelabs.conductor.ControllerChangeHandler | ||||
| import com.bluelinelabs.conductor.ControllerChangeType | ||||
| import com.dd.processbutton.iml.ActionProcessButton | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.databinding.PrefAccountLoginBinding | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| abstract class LoginDialogPreference( | ||||
|     @StringRes private val usernameLabelRes: Int? = null, | ||||
|     bundle: Bundle? = null, | ||||
| ) : DialogController(bundle) { | ||||
|  | ||||
|     var binding: PrefAccountLoginBinding? = null | ||||
|         private set | ||||
|  | ||||
|     val preferences: BasePreferences by injectLazy() | ||||
|  | ||||
|     override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|         binding = PrefAccountLoginBinding.inflate(LayoutInflater.from(activity!!)) | ||||
|         onViewCreated(binding!!.root) | ||||
|         val titleName = activity!!.getString(getTitleName()) | ||||
|         return MaterialAlertDialogBuilder(activity!!) | ||||
|             .setTitle(activity!!.getString(R.string.login_title, titleName)) | ||||
|             .setView(binding!!.root) | ||||
|             .setNegativeButton(android.R.string.cancel, null) | ||||
|             .create() | ||||
|     } | ||||
|  | ||||
|     fun onViewCreated(view: View) { | ||||
|         if (usernameLabelRes != null) { | ||||
|             binding!!.usernameLabel.hint = view.context.getString(usernameLabelRes) | ||||
|         } | ||||
|  | ||||
|         binding!!.login.setMode(ActionProcessButton.Mode.ENDLESS) | ||||
|         binding!!.login.setOnClickListener { checkLogin() } | ||||
|  | ||||
|         setCredentialsOnView(view) | ||||
|     } | ||||
|  | ||||
|     override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { | ||||
|         super.onChangeStarted(handler, type) | ||||
|         if (!type.isEnter) { | ||||
|             onDialogClosed() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     open fun onDialogClosed() { | ||||
|         binding = null | ||||
|     } | ||||
|  | ||||
|     @StringRes | ||||
|     protected abstract fun getTitleName(): Int | ||||
|  | ||||
|     protected abstract fun checkLogin() | ||||
|  | ||||
|     protected abstract fun setCredentialsOnView(view: View) | ||||
| } | ||||
| @@ -1,76 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.preference | ||||
|  | ||||
| import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| import androidx.preference.ListPreference | ||||
| import androidx.preference.PreferenceViewHolder | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import eu.kanade.domain.ui.model.AppTheme | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.system.dpToPx | ||||
|  | ||||
| class ThemesPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : | ||||
|     ListPreference(context, attrs), | ||||
|     ThemesPreferenceAdapter.OnItemClickListener { | ||||
|  | ||||
|     private var recycler: RecyclerView? = null | ||||
|     private val adapter = ThemesPreferenceAdapter(this) | ||||
|  | ||||
|     var lastScrollPosition: Int? = null | ||||
|  | ||||
|     var entries: List<AppTheme> = emptyList() | ||||
|         set(value) { | ||||
|             field = value | ||||
|             adapter.setItems(value) | ||||
|         } | ||||
|  | ||||
|     init { | ||||
|         layoutResource = R.layout.pref_themes_list | ||||
|     } | ||||
|  | ||||
|     override fun onBindViewHolder(holder: PreferenceViewHolder) { | ||||
|         super.onBindViewHolder(holder) | ||||
|  | ||||
|         recycler = holder.findViewById(R.id.themes_list) as RecyclerView | ||||
|         recycler?.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) | ||||
|         recycler?.adapter = adapter | ||||
|  | ||||
|         // Retain scroll position on activity recreate after changing theme | ||||
|         recycler?.addOnScrollListener( | ||||
|             object : RecyclerView.OnScrollListener() { | ||||
|                 override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||||
|                     super.onScrolled(recyclerView, dx, dy) | ||||
|                     lastScrollPosition = recyclerView.computeHorizontalScrollOffset() | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|         lastScrollPosition?.let { scrollToOffset(it) } | ||||
|     } | ||||
|  | ||||
|     override fun onItemClick(position: Int) { | ||||
|         if (position !in 0..entries.size) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         callChangeListener(value) | ||||
|         value = entries[position].name | ||||
|     } | ||||
|  | ||||
|     override fun onClick() { | ||||
|         // no-op; not actually a DialogPreference | ||||
|     } | ||||
|  | ||||
|     private fun scrollToOffset(lX: Int) { | ||||
|         recycler?.let { | ||||
|             (it.layoutManager as LinearLayoutManager).apply { | ||||
|                 scrollToPositionWithOffset( | ||||
|                     // 114dp is the width of the pref_theme_item layout | ||||
|                     lX / 114.dpToPx, | ||||
|                     -lX % 114.dpToPx, | ||||
|                 ) | ||||
|             } | ||||
|             lastScrollPosition = it.computeHorizontalScrollOffset() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,75 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.preference | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.appcompat.view.ContextThemeWrapper | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import eu.kanade.domain.ui.UiPreferences | ||||
| import eu.kanade.domain.ui.model.AppTheme | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.databinding.PrefThemeItemBinding | ||||
| import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate | ||||
| import eu.kanade.tachiyomi.util.system.getResourceColor | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class ThemesPreferenceAdapter(private val clickListener: OnItemClickListener) : | ||||
|     RecyclerView.Adapter<ThemesPreferenceAdapter.ThemeViewHolder>() { | ||||
|  | ||||
|     private val preferences: UiPreferences by injectLazy() | ||||
|  | ||||
|     private var themes = emptyList<AppTheme>() | ||||
|  | ||||
|     private lateinit var binding: PrefThemeItemBinding | ||||
|  | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThemeViewHolder { | ||||
|         val themeResIds = ThemingDelegate.getThemeResIds(themes[viewType], preferences.themeDarkAmoled().get()) | ||||
|         val themedContext = themeResIds.fold(parent.context) { | ||||
|                 context, themeResId -> | ||||
|             ContextThemeWrapper(context, themeResId) | ||||
|         } | ||||
|  | ||||
|         binding = PrefThemeItemBinding.inflate(LayoutInflater.from(themedContext), parent, false) | ||||
|         return ThemeViewHolder(binding.root) | ||||
|     } | ||||
|  | ||||
|     override fun getItemViewType(position: Int): Int = position | ||||
|  | ||||
|     override fun getItemCount(): Int = themes.size | ||||
|  | ||||
|     override fun onBindViewHolder(holder: ThemesPreferenceAdapter.ThemeViewHolder, position: Int) { | ||||
|         holder.bind(themes[position]) | ||||
|     } | ||||
|  | ||||
|     fun setItems(themes: List<AppTheme>) { | ||||
|         this.themes = themes | ||||
|         notifyDataSetChanged() | ||||
|     } | ||||
|  | ||||
|     inner class ThemeViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { | ||||
|  | ||||
|         private val selectedColor = view.context.getResourceColor(R.attr.colorAccent) | ||||
|         private val unselectedColor = view.context.getResourceColor(android.R.attr.divider) | ||||
|  | ||||
|         fun bind(appTheme: AppTheme) { | ||||
|             binding.name.text = view.context.getString(appTheme.titleResId!!) | ||||
|  | ||||
|             // For rounded corners | ||||
|             binding.badges.clipToOutline = true | ||||
|  | ||||
|             val isSelected = preferences.appTheme().get() == appTheme | ||||
|             binding.themeCard.isChecked = isSelected | ||||
|             binding.themeCard.strokeColor = if (isSelected) selectedColor else unselectedColor | ||||
|  | ||||
|             listOf(binding.root, binding.themeCard).forEach { | ||||
|                 it.setOnClickListener { | ||||
|                     clickListener.onItemClick(bindingAdapterPosition) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     interface OnItemClickListener { | ||||
|         fun onItemClick(position: Int) | ||||
|     } | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.widget.preference | ||||
|  | ||||
| import android.content.Context | ||||
| import android.graphics.Color | ||||
| import android.util.AttributeSet | ||||
| import android.widget.ImageView | ||||
| import androidx.annotation.ColorInt | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.preference.Preference | ||||
| import androidx.preference.PreferenceViewHolder | ||||
| import com.google.android.material.card.MaterialCardView | ||||
| import eu.kanade.tachiyomi.R | ||||
|  | ||||
| class TrackerPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : | ||||
|     Preference(context, attrs) { | ||||
|  | ||||
|     init { | ||||
|         layoutResource = R.layout.pref_tracker_item | ||||
|     } | ||||
|  | ||||
|     override fun onBindViewHolder(holder: PreferenceViewHolder) { | ||||
|         super.onBindViewHolder(holder) | ||||
|  | ||||
|         val logoContainer = holder.findViewById(R.id.logo_container) as MaterialCardView | ||||
|         val checkedIcon = holder.findViewById(R.id.checked_icon) as ImageView | ||||
|  | ||||
|         logoContainer.setCardBackgroundColor(iconColor) | ||||
|         checkedIcon.isVisible = !getPersistedString("").isNullOrEmpty() | ||||
|     } | ||||
|  | ||||
|     @ColorInt | ||||
|     var iconColor: Int = Color.TRANSPARENT | ||||
|         set(value) { | ||||
|             field = value | ||||
|             notifyChanged() | ||||
|         } | ||||
|  | ||||
|     public override fun notifyChanged() { | ||||
|         super.notifyChanged() | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user