Remove dead code

Mostly from settings rewrite, but some other things too.
This commit is contained in:
arkon 2022-10-16 12:40:56 -04:00
parent 5c5468f9af
commit 69cdba71eb
56 changed files with 6 additions and 4397 deletions

View file

@ -254,7 +254,6 @@ dependencies {
// UI libraries // UI libraries
implementation(libs.material) implementation(libs.material)
implementation(libs.androidprocessbutton)
implementation(libs.flexible.adapter.core) implementation(libs.flexible.adapter.core)
implementation(libs.flexible.adapter.ui) implementation(libs.flexible.adapter.ui)
implementation(libs.photoview) implementation(libs.photoview)

View file

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

View file

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

View file

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

View file

@ -6,14 +6,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import eu.kanade.domain.source.model.SourceWithCount import eu.kanade.domain.source.model.SourceWithCount
import eu.kanade.tachiyomi.ui.setting.database.ClearDatabasePresenter
@Stable @Stable
interface ClearDatabaseState { interface ClearDatabaseState {
val items: List<SourceWithCount> val items: List<SourceWithCount>
val selection: List<Long> val selection: List<Long>
val isEmpty: Boolean val isEmpty: Boolean
var dialog: ClearDatabasePresenter.Dialog? var dialog: Dialog?
} }
fun ClearDatabaseState(): ClearDatabaseState { fun ClearDatabaseState(): ClearDatabaseState {
@ -24,5 +23,9 @@ class ClearDatabaseStateImpl : ClearDatabaseState {
override var items: List<SourceWithCount> by mutableStateOf(emptyList()) override var items: List<SourceWithCount> by mutableStateOf(emptyList())
override var selection: List<Long> by mutableStateOf(emptyList()) override var selection: List<Long> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } 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()
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4c-1.48,0 -2.85,0.43 -4.01,1.17l1.46,1.46C10.21,6.23 11.08,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3 0,1.13 -0.64,2.11 -1.56,2.62l1.45,1.45C23.16,18.16 24,16.68 24,15c0,-2.64 -2.05,-4.78 -4.65,-4.96zM3,5.27l2.75,2.74C2.56,8.15 0,10.77 0,14c0,3.31 2.69,6 6,6h11.73l2,2L21,20.73 4.27,4 3,5.27zM7.73,10l8,8H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h1.73z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF4CAF50"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M9,7L7,7v2h2L9,7zM9,11L7,11v2h2v-2zM9,3c-1.11,0 -2,0.9 -2,2h2L9,3zM13,15h-2v2h2v-2zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM13,3h-2v2h2L13,3zM9,17v-2L7,15c0,1.1 0.89,2 2,2zM19,13h2v-2h-2v2zM19,9h2L21,7h-2v2zM19,17c1.1,0 2,-0.9 2,-2h-2v2zM5,7L3,7v12c0,1.1 0.89,2 2,2h12v-2L5,19L5,7zM15,5h2L17,3h-2v2zM15,17h2v-2h-2v2z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M7,13H17V11H7ZM5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21Z"/>
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M4,11h5L9,5L4,5v6zM4,18h5v-6L4,12v6zM10,18h5v-6h-5v6zM16,18h5v-6h-5v6zM10,11h5L15,5h-5v6zM16,5v6h5L21,5h-5z" />
</vector>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:endColor="#00ffffff"
android:startColor="#ffffffff" />
<corners android:radius="0dp" />
</shape>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:endColor="#00000000"
android:centerColor="#CC000000"
android:startColor="#E6000000" />
<corners android:radius="0dp" />
</shape>

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:type="radial"
android:gradientRadius="18dp"
android:startColor="#CC000000"
android:centerColor="#CC000000"
android:endColor="#0D000000" />
<corners android:radius="0dp" />
</shape>

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="?attr/materialAlertDialogBodyTextStyle"
android:id="@+id/quad_state_control"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:gravity="start|center_vertical"
android:textAlignment="viewStart"
android:paddingStart="@dimen/abc_select_dialog_padding_start_material"
android:paddingEnd="?attr/dialogPreferredPadding"
android:drawablePadding="20dp"
android:ellipsize="marquee"
app:drawableStartCompat="@drawable/ic_check_box_outline_blank_24dp"
tools:text="Quad-state item" />

View file

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:minHeight="48dp">
<Space
android:layout_width="match_parent"
android:layout_height="@dimen/abc_dialog_title_divider_material" />
<TextView
android:id="@+id/message"
style="?attr/materialAlertDialogBodyTextStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/abc_dialog_title_divider_material"
android:paddingHorizontal="?attr/dialogPreferredPadding"
android:visibility="gone"
tools:text="Dialog Message for quad-state dialog"
tools:visibility="visible" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.divider.MaterialDivider
android:id="@+id/scrollIndicatorUp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollIndicators="none"
tools:listitem="@layout/dialog_quadstatemultichoice_item" />
<com.google.android.material.divider.MaterialDivider
android:id="@+id/scrollIndicatorDown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
</FrameLayout>
</LinearLayout>

View file

@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/username_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/username">
<eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
app:endIconMode="password_toggle">
<eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.dd.processbutton.iml.ActionProcessButton
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/login"
android:textColor="?attr/colorOnPrimary"
app:pb_colorNormal="?attr/colorPrimary"
app:pb_colorPressed="?attr/colorPrimary"
app:pb_textComplete="@string/login_success"
app:pb_textError="@string/invalid_login"
app:pb_textProgress="@string/loading" />
</LinearLayout>

View file

@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/portrait" />
<eu.kanade.tachiyomi.widget.MinMaxNumberPicker
android:id="@+id/portrait_columns"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:max="10"
app:min="0" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/landscape" />
<eu.kanade.tachiyomi.widget.MinMaxNumberPicker
android:id="@+id/landscape_columns"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:max="10"
app:min="0" />
</LinearLayout>
</LinearLayout>

View file

@ -1,148 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="114dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="4dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/theme_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checkable="true"
android:clickable="true"
android:focusable="true"
android:importantForAccessibility="no"
app:cardCornerRadius="17dp"
app:strokeColor="?attr/colorAccent"
app:strokeWidth="4dp"
app:cardElevation="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="170dp"
android:background="?android:attr/colorBackground">
<View
android:id="@+id/top_nav"
android:layout_width="0dp"
android:layout_height="40dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/top_nav_text"
android:layout_width="54dp"
android:layout_height="17dp"
android:layout_marginTop="5dp"
android:layout_marginStart="12dp"
app:layout_constraintBottom_toBottomOf="@+id/top_nav"
app:layout_constraintStart_toStartOf="@+id/top_nav"
app:layout_constraintTop_toTopOf="@+id/top_nav"
app:cardBackgroundColor="?attr/colorOnSurface"
app:cardCornerRadius="4dp"/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/cover_container"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="4dp"
app:layout_constraintTop_toBottomOf="@+id/top_nav"
app:layout_constraintDimensionRatio="2:2.7"
app:layout_constraintStart_toStartOf="@id/top_nav_text"
app:layout_constraintEnd_toStartOf="@id/center_guideline"
app:cardBackgroundColor="?android:attr/divider"
app:cardElevation="0dp" />
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="@+id/cover_container"
app:layout_constraintTop_toTopOf="@+id/cover_container"
app:cardCornerRadius="6dp">
<LinearLayout
android:id="@+id/badges"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/rounded_rectangle">
<View
android:layout_width="12dp"
android:layout_height="16dp"
android:background="?attr/colorTertiary" />
<View
android:layout_width="12dp"
android:layout_height="16dp"
android:background="?attr/colorSecondary" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/bottom_nav"
android:layout_width="0dp"
android:layout_height="40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:cardCornerRadius="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/bottom_nav_selected_item"
android:layout_width="17dp"
android:layout_height="17dp"
android:layout_marginEnd="8dp"
app:cardBackgroundColor="?attr/colorPrimary"
app:cardCornerRadius="100dp"/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/bottom_nav_unselected_item"
android:layout_width="match_parent"
android:layout_height="17dp"
android:alpha="0.6"
app:cardBackgroundColor="?attr/colorOnSurface"
app:cardCornerRadius="4dp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/center_guideline"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="32sp"
android:maxLines="2"
android:layout_marginTop="4dp"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:scrollbars="none"
tools:text="Theme Name" />
</LinearLayout>

View file

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingTop="4dp"
android:textAppearance="@style/TextAppearance.AppCompat.Menu"
tools:text="App theme" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/themes_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp"
android:clipToPadding="false"
tools:listitem="@layout/pref_theme_item" />
</LinearLayout>

View file

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?listPreferredItemHeight"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/logo_container"
android:layout_width="48dp"
android:layout_height="48dp"
app:cardBackgroundColor="#2E51A2"
app:cardElevation="0dp"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.MaterialCardView.Tracker">
<ImageView
android:id="@android:id/icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:padding="4dp"
tools:src="@drawable/ic_tracker_mal" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@android:id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:ellipsize="end"
android:maxLines="1"
android:paddingHorizontal="16dp"
android:textAppearance="?attr/textAppearanceTitleMedium"
tools:text="MyAnimeList" />
<ImageView
android:id="@+id/checked_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:padding="4dp"
android:src="@drawable/ic_done_green_24dp" />
</LinearLayout>

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_select_all"
android:icon="@drawable/ic_select_all_24dp"
android:title="@string/action_select_all"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_select_inverse"
android:icon="@drawable/ic_flip_to_back_24dp"
android:title="@string/action_select_inverse"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
</menu>

View file

@ -1,11 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_backup_help"
android:icon="@drawable/ic_help_24dp"
android:title="@string/label_help"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
</menu>

View file

@ -1,11 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_tracking_help"
android:icon="@drawable/ic_help_24dp"
android:title="@string/tracking_guide"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
</menu>

View file

@ -56,7 +56,6 @@ natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
markwon = "io.noties.markwon:core:4.6.2" markwon = "io.noties.markwon:core:4.6.2"
material = "com.google.android.material:material:1.7.0-rc01" material = "com.google.android.material:material:1.7.0-rc01"
androidprocessbutton = "com.github.dmytrodanylyk.android-process-button:library:1.0.4"
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533" flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533" flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533"
photoview = "com.github.chrisbanes:PhotoView:2.3.0" photoview = "com.github.chrisbanes:PhotoView:2.3.0"