Migrate library settings sheet to Compose

This commit is contained in:
arkon 2022-12-09 17:34:24 -05:00
parent 94232a4937
commit 727399611d
12 changed files with 443 additions and 830 deletions

View File

@ -4,7 +4,7 @@ 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.widget.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.TriState
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.model.LibrarySort
@ -36,17 +36,17 @@ class LibraryPreferences(
// region Filter
fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", TriState.DISABLED.value)
fun filterUnread() = preferenceStore.getInt("pref_filter_library_unread", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
fun filterUnread() = preferenceStore.getInt("pref_filter_library_unread", TriState.DISABLED.value)
fun filterStarted() = preferenceStore.getInt("pref_filter_library_started", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
fun filterStarted() = preferenceStore.getInt("pref_filter_library_started", TriState.DISABLED.value)
fun filterBookmarked() = preferenceStore.getInt("pref_filter_library_bookmarked", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
fun filterBookmarked() = preferenceStore.getInt("pref_filter_library_bookmarked", TriState.DISABLED.value)
fun filterCompleted() = preferenceStore.getInt("pref_filter_library_completed", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
fun filterCompleted() = preferenceStore.getInt("pref_filter_library_completed", TriState.DISABLED.value)
fun filterTracking(name: Int) = preferenceStore.getInt("pref_filter_library_tracked_$name", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
fun filterTracking(name: Int) = preferenceStore.getInt("pref_filter_library_tracked_$name", TriState.DISABLED.value)
// endregion

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.components
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@ -14,6 +15,7 @@ import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.rounded.CheckBox
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
import androidx.compose.material.icons.rounded.DisabledByDefault
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
@ -21,19 +23,35 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import tachiyomi.domain.manga.model.TriStateFilter
import tachiyomi.presentation.core.theme.header
@Composable
fun HeadingItem(
@StringRes labelRes: Int,
) {
Text(
text = stringResource(labelRes),
style = MaterialTheme.typography.header,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
)
}
@Composable
fun TriStateItem(
label: String,
state: TriStateFilter,
enabled: Boolean = true,
onClick: ((TriStateFilter) -> Unit)?,
) {
Row(
modifier = Modifier
.clickable(
enabled = onClick != null,
enabled = enabled && onClick != null,
onClick = {
when (state) {
TriStateFilter.DISABLED -> onClick?.invoke(TriStateFilter.ENABLED_IS)
@ -47,7 +65,7 @@ fun TriStateItem(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
val stateAlpha = if (onClick != null) 1f else ContentAlpha.disabled
val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled
Icon(
imageVector = when (state) {
@ -56,7 +74,7 @@ fun TriStateItem(
TriStateFilter.ENABLED_NOT -> Icons.Rounded.DisabledByDefault
},
contentDescription = null,
tint = if (state == TriStateFilter.DISABLED) {
tint = if (!enabled || state == TriStateFilter.DISABLED) {
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha)
} else {
when (onClick) {
@ -109,6 +127,31 @@ fun SortItem(
}
}
@Composable
fun CheckboxItem(
label: String,
checked: Boolean,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.clickable(onClick = onClick)
.fillMaxWidth()
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
Checkbox(
checked = checked,
onCheckedChange = null,
)
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
)
}
}
@Composable
fun RadioItem(
label: String,

View File

@ -1,11 +1,11 @@
package eu.kanade.presentation.components
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
@ -20,12 +20,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
@ -85,30 +82,17 @@ fun TabbedDialog(
}
Divider()
val density = LocalDensity.current
var largestHeight by rememberSaveable { mutableStateOf(0f) }
HorizontalPager(
modifier = Modifier.heightIn(min = largestHeight.dp),
modifier = Modifier.animateContentSize(),
count = tabTitles.size,
state = pagerState,
verticalAlignment = Alignment.Top,
) { page ->
Box(
modifier = Modifier.onSizeChanged {
with(density) {
val heightDp = it.height.toDp()
if (heightDp.value > largestHeight) {
largestHeight = heightDp.value
}
}
},
) {
content(contentPadding, page)
}
}
}
}
}
@Composable
private fun MoreMenu(

View File

@ -0,0 +1,249 @@
package eu.kanade.presentation.library
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.presentation.components.CheckboxItem
import eu.kanade.presentation.components.HeadingItem
import eu.kanade.presentation.components.RadioItem
import eu.kanade.presentation.components.SortItem
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.components.TriStateItem
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel
import eu.kanade.tachiyomi.widget.toTriStateFilter
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.model.LibrarySort
import tachiyomi.domain.library.model.display
import tachiyomi.domain.library.model.sort
import tachiyomi.domain.manga.model.TriStateFilter
@Composable
fun LibrarySettingsDialog(
onDismissRequest: () -> Unit,
screenModel: LibrarySettingsScreenModel,
activeCategoryIndex: Int,
) {
val state by screenModel.state.collectAsState()
val category by remember(activeCategoryIndex) {
derivedStateOf { state.categories[activeCategoryIndex] }
}
TabbedDialog(
onDismissRequest = onDismissRequest,
tabTitles = listOf(
stringResource(R.string.action_filter),
stringResource(R.string.action_sort),
stringResource(R.string.action_display),
),
) { contentPadding, page ->
Column(
modifier = Modifier
.padding(contentPadding)
.padding(vertical = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState()),
) {
when (page) {
0 -> FilterPage(
screenModel = screenModel,
)
1 -> SortPage(
category = category,
screenModel = screenModel,
)
2 -> DisplayPage(
category = category,
screenModel = screenModel,
)
}
}
}
}
@Composable
private fun ColumnScope.FilterPage(
screenModel: LibrarySettingsScreenModel,
) {
val filterDownloaded by screenModel.libraryPreferences.filterDownloaded().collectAsState()
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
TriStateItem(
label = stringResource(R.string.label_downloaded),
state = if (downloadedOnly) {
TriStateFilter.ENABLED_IS
} else {
filterDownloaded.toTriStateFilter()
},
enabled = !downloadedOnly,
onClick = { screenModel.toggleFilter(LibraryPreferences::filterDownloaded) },
)
val filterUnread by screenModel.libraryPreferences.filterUnread().collectAsState()
TriStateItem(
label = stringResource(R.string.action_filter_unread),
state = filterUnread.toTriStateFilter(),
onClick = { screenModel.toggleFilter(LibraryPreferences::filterUnread) },
)
val filterStarted by screenModel.libraryPreferences.filterStarted().collectAsState()
TriStateItem(
label = stringResource(R.string.label_started),
state = filterStarted.toTriStateFilter(),
onClick = { screenModel.toggleFilter(LibraryPreferences::filterStarted) },
)
val filterBookmarked by screenModel.libraryPreferences.filterBookmarked().collectAsState()
TriStateItem(
label = stringResource(R.string.action_filter_bookmarked),
state = filterBookmarked.toTriStateFilter(),
onClick = { screenModel.toggleFilter(LibraryPreferences::filterBookmarked) },
)
val filterCompleted by screenModel.libraryPreferences.filterCompleted().collectAsState()
TriStateItem(
label = stringResource(R.string.completed),
state = filterCompleted.toTriStateFilter(),
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
)
when (screenModel.trackServices.size) {
0 -> {
// No trackers
}
1 -> {
val service = screenModel.trackServices[0]
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
TriStateItem(
label = stringResource(R.string.action_filter_tracked),
state = filterTracker.toTriStateFilter(),
onClick = { screenModel.toggleTracker(service.id.toInt()) },
)
}
else -> {
HeadingItem(R.string.action_filter_tracked)
screenModel.trackServices.map { service ->
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
TriStateItem(
label = stringResource(service.nameRes()),
state = filterTracker.toTriStateFilter(),
onClick = { screenModel.toggleTracker(service.id.toInt()) },
)
}
}
}
}
@Composable
private fun ColumnScope.SortPage(
category: Category,
screenModel: LibrarySettingsScreenModel,
) {
val sortingMode = category.sort.type
val sortDescending = !category.sort.isAscending
listOf(
R.string.action_sort_alpha to LibrarySort.Type.Alphabetical,
R.string.action_sort_total to LibrarySort.Type.TotalChapters,
R.string.action_sort_last_read to LibrarySort.Type.LastRead,
R.string.action_sort_last_manga_update to LibrarySort.Type.LastUpdate,
R.string.action_sort_unread_count to LibrarySort.Type.UnreadCount,
R.string.action_sort_latest_chapter to LibrarySort.Type.LatestChapter,
R.string.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
R.string.action_sort_date_added to LibrarySort.Type.DateAdded,
).map { (titleRes, mode) ->
SortItem(
label = stringResource(titleRes),
sortDescending = sortDescending.takeIf { sortingMode == mode },
onClick = {
val isTogglingDirection = sortingMode == mode
val direction = when {
isTogglingDirection -> if (sortDescending) LibrarySort.Direction.Ascending else LibrarySort.Direction.Descending
else -> if (sortDescending) LibrarySort.Direction.Descending else LibrarySort.Direction.Ascending
}
screenModel.setSort(category, mode, direction)
},
)
}
}
@Composable
private fun ColumnScope.DisplayPage(
category: Category,
screenModel: LibrarySettingsScreenModel,
) {
HeadingItem(R.string.action_display_mode)
listOf(
R.string.action_display_grid to LibraryDisplayMode.CompactGrid,
R.string.action_display_comfortable_grid to LibraryDisplayMode.ComfortableGrid,
R.string.action_display_cover_only_grid to LibraryDisplayMode.CoverOnlyGrid,
R.string.action_display_list to LibraryDisplayMode.List,
).map { (titleRes, mode) ->
RadioItem(
label = stringResource(titleRes),
selected = category.display == mode,
onClick = { screenModel.setDisplayMode(category, mode) },
)
}
HeadingItem(R.string.badges_header)
val downloadBadge by screenModel.libraryPreferences.downloadBadge().collectAsState()
CheckboxItem(
label = stringResource(R.string.action_display_download_badge),
checked = downloadBadge,
onClick = {
screenModel.togglePreference(LibraryPreferences::downloadBadge)
},
)
val localBadge by screenModel.libraryPreferences.localBadge().collectAsState()
CheckboxItem(
label = stringResource(R.string.action_display_local_badge),
checked = localBadge,
onClick = {
screenModel.togglePreference(LibraryPreferences::localBadge)
},
)
val languageBadge by screenModel.libraryPreferences.languageBadge().collectAsState()
CheckboxItem(
label = stringResource(R.string.action_display_language_badge),
checked = languageBadge,
onClick = {
screenModel.togglePreference(LibraryPreferences::languageBadge)
},
)
HeadingItem(R.string.tabs_header)
val categoryTabs by screenModel.libraryPreferences.categoryTabs().collectAsState()
CheckboxItem(
label = stringResource(R.string.action_display_show_tabs),
checked = categoryTabs,
onClick = {
screenModel.togglePreference(LibraryPreferences::categoryTabs)
},
)
val categoryNumberOfItems by screenModel.libraryPreferences.categoryNumberOfItems().collectAsState()
CheckboxItem(
label = stringResource(R.string.action_display_show_number_of_items),
checked = categoryNumberOfItems,
onClick = {
screenModel.togglePreference(LibraryPreferences::categoryNumberOfItems)
},
)
HeadingItem(R.string.other_header)
val showContinueReadingButton by screenModel.libraryPreferences.showContinueReadingButton().collectAsState()
CheckboxItem(
label = stringResource(R.string.action_display_show_continue_reading_button),
checked = showContinueReadingButton,
onClick = {
screenModel.togglePreference(LibraryPreferences::showContinueReadingButton)
},
)
}

View File

@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.TriState
import tachiyomi.core.preference.PreferenceStore
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -114,9 +114,9 @@ object Migrations {
fun convertBooleanPrefToTriState(key: String): Int {
val oldPrefValue = prefs.getBoolean(key, false)
return if (oldPrefValue) {
ExtendedNavigationView.Item.TriStateGroup.State.ENABLED_IS.value
TriState.ENABLED_IS.value
} else {
ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value
TriState.DISABLED.value
}
}
prefs.edit {

View File

@ -33,7 +33,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.getNextUnread
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup
import eu.kanade.tachiyomi.widget.TriState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
@ -149,8 +149,8 @@ class LibraryScreenModel(
prefs.filterStarted or
prefs.filterBookmarked or
prefs.filterCompleted
) != TriStateGroup.State.DISABLED.value
val b = trackFilter.values.any { it != TriStateGroup.State.DISABLED.value }
) != TriState.DISABLED.value
val b = trackFilter.values.any { it != TriState.DISABLED.value }
a || b
}
.distinctUntilChanged()
@ -179,17 +179,17 @@ class LibraryScreenModel(
val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty()
val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.ENABLED_NOT.value) it.key else null }
val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.ENABLED_IS.value) it.key else null }
val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_NOT.value) it.key else null }
val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_IS.value) it.key else null }
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{
if (!downloadedOnly && filterDownloaded == TriStateGroup.State.DISABLED.value) return@downloaded true
if (!downloadedOnly && filterDownloaded == TriState.DISABLED.value) return@downloaded true
val isDownloaded = it.libraryManga.manga.isLocal() ||
it.downloadCount > 0 ||
downloadManager.getDownloadCount(it.libraryManga.manga) > 0
return@downloaded if (downloadedOnly || filterDownloaded == TriStateGroup.State.ENABLED_IS.value) {
return@downloaded if (downloadedOnly || filterDownloaded == TriState.ENABLED_IS.value) {
isDownloaded
} else {
!isDownloaded
@ -197,10 +197,10 @@ class LibraryScreenModel(
}
val filterFnUnread: (LibraryItem) -> Boolean = unread@{
if (filterUnread == TriStateGroup.State.DISABLED.value) return@unread true
if (filterUnread == TriState.DISABLED.value) return@unread true
val isUnread = it.libraryManga.unreadCount > 0
return@unread if (filterUnread == TriStateGroup.State.ENABLED_IS.value) {
return@unread if (filterUnread == TriState.ENABLED_IS.value) {
isUnread
} else {
!isUnread
@ -208,10 +208,10 @@ class LibraryScreenModel(
}
val filterFnStarted: (LibraryItem) -> Boolean = started@{
if (filterStarted == TriStateGroup.State.DISABLED.value) return@started true
if (filterStarted == TriState.DISABLED.value) return@started true
val hasStarted = it.libraryManga.hasStarted
return@started if (filterStarted == TriStateGroup.State.ENABLED_IS.value) {
return@started if (filterStarted == TriState.ENABLED_IS.value) {
hasStarted
} else {
!hasStarted
@ -219,10 +219,10 @@ class LibraryScreenModel(
}
val filterFnBookmarked: (LibraryItem) -> Boolean = bookmarked@{
if (filterBookmarked == TriStateGroup.State.DISABLED.value) return@bookmarked true
if (filterBookmarked == TriState.DISABLED.value) return@bookmarked true
val hasBookmarks = it.libraryManga.hasBookmarks
return@bookmarked if (filterBookmarked == TriStateGroup.State.ENABLED_IS.value) {
return@bookmarked if (filterBookmarked == TriState.ENABLED_IS.value) {
hasBookmarks
} else {
!hasBookmarks
@ -230,10 +230,10 @@ class LibraryScreenModel(
}
val filterFnCompleted: (LibraryItem) -> Boolean = completed@{
if (filterCompleted == TriStateGroup.State.DISABLED.value) return@completed true
if (filterCompleted == TriState.DISABLED.value) return@completed true
val isCompleted = it.libraryManga.manga.status.toInt() == SManga.COMPLETED
return@completed if (filterCompleted == TriStateGroup.State.ENABLED_IS.value) {
return@completed if (filterCompleted == TriState.ENABLED_IS.value) {
isCompleted
} else {
!isCompleted
@ -572,6 +572,10 @@ class LibraryScreenModel(
}
}
fun showSettingsDialog() {
mutableState.update { it.copy(dialog = Dialog.SettingsSheet) }
}
fun clearSelection() {
mutableState.update { it.copy(selection = emptyList()) }
}
@ -690,6 +694,7 @@ class LibraryScreenModel(
}
sealed class Dialog {
object SettingsSheet : Dialog()
data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog()
data class DeleteManga(val manga: List<Manga>) : Dialog()
}

View File

@ -0,0 +1,84 @@
package eu.kanade.tachiyomi.ui.library
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
import eu.kanade.domain.category.interactor.SetSortModeForCategory
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.util.preference.toggle
import eu.kanade.tachiyomi.widget.TriState
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.getAndSet
import tachiyomi.core.util.lang.launchIO
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.model.LibrarySort
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class LibrarySettingsScreenModel(
val preferences: BasePreferences = Injekt.get(),
val libraryPreferences: LibraryPreferences = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val setDisplayModeForCategory: SetDisplayModeForCategory = Injekt.get(),
private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
trackManager: TrackManager = Injekt.get(),
) : StateScreenModel<LibrarySettingsScreenModel.State>(State()) {
val trackServices = trackManager.services.filter { service -> service.isLogged }
init {
coroutineScope.launchIO {
getCategories.subscribe()
.collectLatest {
mutableState.update { state ->
state.copy(
categories = it,
)
}
}
}
}
fun togglePreference(preference: (LibraryPreferences) -> Preference<Boolean>) {
preference(libraryPreferences).toggle()
}
fun toggleFilter(preference: (LibraryPreferences) -> Preference<Int>) {
preference(libraryPreferences).getAndSet {
when (it) {
TriState.DISABLED.value -> TriState.ENABLED_IS.value
TriState.ENABLED_IS.value -> TriState.ENABLED_NOT.value
TriState.ENABLED_NOT.value -> TriState.DISABLED.value
else -> throw IllegalStateException("Unknown TriStateGroup state: $this")
}
}
}
fun toggleTracker(id: Int) {
toggleFilter { libraryPreferences.filterTracking(id) }
}
fun setDisplayMode(category: Category, mode: LibraryDisplayMode) {
coroutineScope.launchIO {
setDisplayModeForCategory.await(category, mode)
}
}
fun setSort(category: Category, mode: LibrarySort.Type, direction: LibrarySort.Direction) {
coroutineScope.launchIO {
setSortModeForCategory.await(category, mode, direction)
}
}
@Immutable
data class State(
val categories: List<Category> = emptyList(),
)
}

View File

@ -1,474 +0,0 @@
package eu.kanade.tachiyomi.ui.library
import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import android.view.View
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
import eu.kanade.domain.category.interactor.SetSortModeForCategory
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import tachiyomi.core.util.lang.launchIO
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.model.LibrarySort
import tachiyomi.domain.library.model.display
import tachiyomi.domain.library.model.sort
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class LibrarySettingsSheet(
activity: Activity,
private val trackManager: TrackManager = Injekt.get(),
private val setDisplayModeForCategory: SetDisplayModeForCategory = Injekt.get(),
private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
) : TabbedBottomSheetDialog(activity) {
val filters: Filter
private val sort: Sort
private val display: Display
val sheetScope = CoroutineScope(Job() + Dispatchers.IO)
init {
filters = Filter(activity)
sort = Sort(activity)
display = Display(activity)
}
/**
* adjusts selected button to match real state.
* @param currentCategory ID of currently shown category
*/
fun show(currentCategory: Category) {
filters.adjustFilterSelection()
sort.currentCategory = currentCategory
sort.adjustDisplaySelection()
display.currentCategory = currentCategory
display.adjustDisplaySelection()
super.show()
}
override fun getTabViews(): List<View> = listOf(
filters,
sort,
display,
)
override fun getTabTitles(): List<Int> = listOf(
R.string.action_filter,
R.string.action_sort,
R.string.action_display,
)
/**
* Filters group (unread, downloaded, ...).
*/
inner class Filter @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Settings(context, attrs) {
private val filterGroup = FilterGroup()
init {
setGroups(listOf(filterGroup))
}
// Refreshes Filter Setting selections
fun adjustFilterSelection() {
filterGroup.initModels()
filterGroup.items.forEach { adapter.notifyItemChanged(it) }
}
/**
* Returns true if there's at least one filter from [FilterGroup] active.
*/
fun hasActiveFilters(): Boolean {
return filterGroup.items.filterIsInstance<Item.TriStateGroup>().any { it.state != State.DISABLED.value }
}
inner class FilterGroup : Group {
private val downloaded = Item.TriStateGroup(R.string.label_downloaded, this)
private val unread = Item.TriStateGroup(R.string.action_filter_unread, this)
private val started = Item.TriStateGroup(R.string.label_started, this)
private val bookmarked = Item.TriStateGroup(R.string.action_filter_bookmarked, this)
private val completed = Item.TriStateGroup(R.string.completed, this)
private val trackFilters: Map<Long, Item.TriStateGroup>
override val header = null
override val items: List<Item>
override val footer = null
init {
trackManager.services.filter { service -> service.isLogged }
.also { services ->
val size = services.size
trackFilters = services.associate { service ->
Pair(service.id, Item.TriStateGroup(getServiceResId(service, size), this))
}
val list: MutableList<Item> = mutableListOf(downloaded, unread, started, bookmarked, completed)
if (size > 1) list.add(Item.Header(R.string.action_filter_tracked))
list.addAll(trackFilters.values)
items = list
}
}
private fun getServiceResId(service: TrackService, size: Int): Int {
return if (size > 1) service.nameRes() else R.string.action_filter_tracked
}
override fun initModels() {
if (preferences.downloadedOnly().get()) {
downloaded.state = State.ENABLED_IS.value
downloaded.enabled = false
} else {
downloaded.state = libraryPreferences.filterDownloaded().get()
downloaded.enabled = true
}
unread.state = libraryPreferences.filterUnread().get()
started.state = libraryPreferences.filterStarted().get()
bookmarked.state = libraryPreferences.filterBookmarked().get()
completed.state = libraryPreferences.filterCompleted().get()
trackFilters.forEach { trackFilter ->
trackFilter.value.state = libraryPreferences.filterTracking(trackFilter.key.toInt()).get()
}
}
override fun onItemClicked(item: Item) {
item as Item.TriStateGroup
val newState = when (item.state) {
State.DISABLED.value -> State.ENABLED_IS.value
State.ENABLED_IS.value -> State.ENABLED_NOT.value
State.ENABLED_NOT.value -> State.DISABLED.value
else -> throw Exception("Unknown State")
}
item.state = newState
when (item) {
downloaded -> libraryPreferences.filterDownloaded().set(newState)
unread -> libraryPreferences.filterUnread().set(newState)
started -> libraryPreferences.filterStarted().set(newState)
bookmarked -> libraryPreferences.filterBookmarked().set(newState)
completed -> libraryPreferences.filterCompleted().set(newState)
else -> {
trackFilters.forEach { trackFilter ->
if (trackFilter.value == item) {
libraryPreferences.filterTracking(trackFilter.key.toInt()).set(newState)
}
}
}
}
adapter.notifyItemChanged(item)
}
}
}
/**
* Sorting group (alphabetically, by last read, ...) and ascending or descending.
*/
inner class Sort @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Settings(context, attrs) {
private val sort = SortGroup()
init {
setGroups(listOf(sort))
}
// Refreshes Display Setting selections
fun adjustDisplaySelection() {
sort.initModels()
sort.items.forEach { adapter.notifyItemChanged(it) }
}
inner class SortGroup : Group {
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
private val total = Item.MultiSort(R.string.action_sort_total, this)
private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this)
private val lastChecked = Item.MultiSort(R.string.action_sort_last_manga_update, this)
private val unread = Item.MultiSort(R.string.action_sort_unread_count, this)
private val latestChapter = Item.MultiSort(R.string.action_sort_latest_chapter, this)
private val chapterFetchDate = Item.MultiSort(R.string.action_sort_chapter_fetch_date, this)
private val dateAdded = Item.MultiSort(R.string.action_sort_date_added, this)
override val header = null
override val items =
listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter, chapterFetchDate, dateAdded)
override val footer = null
override fun initModels() {
val sort = currentCategory.sort
val order = if (sort.isAscending) Item.MultiSort.SORT_ASC else Item.MultiSort.SORT_DESC
alphabetically.state =
if (sort.type == LibrarySort.Type.Alphabetical) order else Item.MultiSort.SORT_NONE
lastRead.state =
if (sort.type == LibrarySort.Type.LastRead) order else Item.MultiSort.SORT_NONE
lastChecked.state =
if (sort.type == LibrarySort.Type.LastUpdate) order else Item.MultiSort.SORT_NONE
unread.state =
if (sort.type == LibrarySort.Type.UnreadCount) order else Item.MultiSort.SORT_NONE
total.state =
if (sort.type == LibrarySort.Type.TotalChapters) order else Item.MultiSort.SORT_NONE
latestChapter.state =
if (sort.type == LibrarySort.Type.LatestChapter) order else Item.MultiSort.SORT_NONE
chapterFetchDate.state =
if (sort.type == LibrarySort.Type.ChapterFetchDate) order else Item.MultiSort.SORT_NONE
dateAdded.state =
if (sort.type == LibrarySort.Type.DateAdded) order else Item.MultiSort.SORT_NONE
}
override fun onItemClicked(item: Item) {
item as Item.MultiStateGroup
val prevState = item.state
item.group.items.forEach {
(it as Item.MultiStateGroup).state =
Item.MultiSort.SORT_NONE
}
item.state = when (prevState) {
Item.MultiSort.SORT_NONE -> Item.MultiSort.SORT_ASC
Item.MultiSort.SORT_ASC -> Item.MultiSort.SORT_DESC
Item.MultiSort.SORT_DESC -> Item.MultiSort.SORT_ASC
else -> throw Exception("Unknown state")
}
setSortPreference(item)
item.group.items.forEach { adapter.notifyItemChanged(it) }
}
private fun setSortPreference(item: Item.MultiStateGroup) {
val mode = when (item) {
alphabetically -> LibrarySort.Type.Alphabetical
lastRead -> LibrarySort.Type.LastRead
lastChecked -> LibrarySort.Type.LastUpdate
unread -> LibrarySort.Type.UnreadCount
total -> LibrarySort.Type.TotalChapters
latestChapter -> LibrarySort.Type.LatestChapter
chapterFetchDate -> LibrarySort.Type.ChapterFetchDate
dateAdded -> LibrarySort.Type.DateAdded
else -> throw NotImplementedError("Unknown display mode")
}
val direction = if (item.state == Item.MultiSort.SORT_ASC) {
LibrarySort.Direction.Ascending
} else {
LibrarySort.Direction.Descending
}
sheetScope.launchIO {
setSortModeForCategory.await(currentCategory!!, mode, direction)
}
}
}
}
/**
* Display group, to show the library as a list or a grid.
*/
inner class Display @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Settings(context, attrs) {
private val displayGroup: DisplayGroup
private val badgeGroup: BadgeGroup
private val tabsGroup: TabsGroup
private val otherGroup: OtherGroup
init {
displayGroup = DisplayGroup()
badgeGroup = BadgeGroup()
tabsGroup = TabsGroup()
otherGroup = OtherGroup()
setGroups(listOf(displayGroup, badgeGroup, tabsGroup, otherGroup))
}
// Refreshes Display Setting selections
fun adjustDisplaySelection() {
val mode = getDisplayModePreference()
displayGroup.setGroupSelections(mode)
displayGroup.items.forEach { adapter.notifyItemChanged(it) }
}
// Gets user preference of currently selected display mode at current category
private fun getDisplayModePreference(): LibraryDisplayMode {
return currentCategory.display
}
inner class DisplayGroup : Group {
private val compactGrid = Item.Radio(R.string.action_display_grid, this)
private val comfortableGrid = Item.Radio(R.string.action_display_comfortable_grid, this)
private val coverOnlyGrid = Item.Radio(R.string.action_display_cover_only_grid, this)
private val list = Item.Radio(R.string.action_display_list, this)
override val header = Item.Header(R.string.action_display_mode)
override val items = listOf(compactGrid, comfortableGrid, coverOnlyGrid, list)
override val footer = null
override fun initModels() {
val mode = getDisplayModePreference()
setGroupSelections(mode)
}
override fun onItemClicked(item: Item) {
item as Item.Radio
if (item.checked) return
item.group.items.forEach { (it as Item.Radio).checked = false }
item.checked = true
setDisplayModePreference(item)
item.group.items.forEach { adapter.notifyItemChanged(it) }
}
// Sets display group selections based on given mode
fun setGroupSelections(mode: LibraryDisplayMode) {
compactGrid.checked = mode == LibraryDisplayMode.CompactGrid
comfortableGrid.checked = mode == LibraryDisplayMode.ComfortableGrid
coverOnlyGrid.checked = mode == LibraryDisplayMode.CoverOnlyGrid
list.checked = mode == LibraryDisplayMode.List
}
private fun setDisplayModePreference(item: Item) {
val flag = when (item) {
compactGrid -> LibraryDisplayMode.CompactGrid
comfortableGrid -> LibraryDisplayMode.ComfortableGrid
coverOnlyGrid -> LibraryDisplayMode.CoverOnlyGrid
list -> LibraryDisplayMode.List
else -> throw NotImplementedError("Unknown display mode")
}
sheetScope.launchIO {
setDisplayModeForCategory.await(currentCategory!!, flag)
}
}
}
inner class BadgeGroup : Group {
private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this)
private val localBadge = Item.CheckboxGroup(R.string.action_display_local_badge, this)
private val languageBadge = Item.CheckboxGroup(R.string.action_display_language_badge, this)
override val header = Item.Header(R.string.badges_header)
override val items = listOf(downloadBadge, localBadge, languageBadge)
override val footer = null
override fun initModels() {
downloadBadge.checked = libraryPreferences.downloadBadge().get()
localBadge.checked = libraryPreferences.localBadge().get()
languageBadge.checked = libraryPreferences.languageBadge().get()
}
override fun onItemClicked(item: Item) {
item as Item.CheckboxGroup
item.checked = !item.checked
when (item) {
downloadBadge -> libraryPreferences.downloadBadge().set((item.checked))
localBadge -> libraryPreferences.localBadge().set((item.checked))
languageBadge -> libraryPreferences.languageBadge().set((item.checked))
else -> {}
}
adapter.notifyItemChanged(item)
}
}
inner class TabsGroup : Group {
private val showTabs = Item.CheckboxGroup(R.string.action_display_show_tabs, this)
private val showNumberOfItems = Item.CheckboxGroup(R.string.action_display_show_number_of_items, this)
override val header = Item.Header(R.string.tabs_header)
override val items = listOf(showTabs, showNumberOfItems)
override val footer = null
override fun initModels() {
showTabs.checked = libraryPreferences.categoryTabs().get()
showNumberOfItems.checked = libraryPreferences.categoryNumberOfItems().get()
}
override fun onItemClicked(item: Item) {
item as Item.CheckboxGroup
item.checked = !item.checked
when (item) {
showTabs -> libraryPreferences.categoryTabs().set(item.checked)
showNumberOfItems -> libraryPreferences.categoryNumberOfItems().set(item.checked)
else -> {}
}
adapter.notifyItemChanged(item)
}
}
inner class OtherGroup : Group {
private val showContinueReadingButton = Item.CheckboxGroup(R.string.action_display_show_continue_reading_button, this)
override val header = Item.Header(R.string.other_header)
override val items = listOf(showContinueReadingButton)
override val footer = null
override fun initModels() {
showContinueReadingButton.checked = libraryPreferences.showContinueReadingButton().get()
}
override fun onItemClicked(item: Item) {
item as Item.CheckboxGroup
item.checked = !item.checked
when (item) {
showContinueReadingButton -> libraryPreferences.showContinueReadingButton().set(item.checked)
else -> {}
}
adapter.notifyItemChanged(item)
}
}
}
open inner class Settings(context: Context, attrs: AttributeSet?) :
ExtendedNavigationView(context, attrs) {
val preferences: BasePreferences by injectLazy()
val libraryPreferences: LibraryPreferences by injectLazy()
lateinit var adapter: Adapter
/**
* Click listener to notify the parent fragment when an item from a group is clicked.
*/
var onGroupClicked: (Group) -> Unit = {}
var currentCategory: Category? = null
fun setGroups(groups: List<Group>) {
adapter = Adapter(groups.map { it.createItems() }.flatten())
recycler.adapter = adapter
groups.forEach { it.initModels() }
addView(recycler)
}
/**
* Adapter of the recycler view.
*/
inner class Adapter(items: List<Item>) : ExtendedNavigationView.Adapter(items) {
override fun onItemClicked(item: Item) {
if (item is GroupedItem) {
item.group.onItemClicked(item)
onGroupClicked(item.group)
}
}
}
}
}

View File

@ -33,6 +33,7 @@ import eu.kanade.presentation.category.ChangeCategoryDialog
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.EmptyScreenAction
import eu.kanade.presentation.library.DeleteLibraryMangaDialog
import eu.kanade.presentation.library.LibrarySettingsDialog
import eu.kanade.presentation.library.components.LibraryContent
import eu.kanade.presentation.library.components.LibraryToolbar
import eu.kanade.presentation.manga.components.LibraryBottomActionMenu
@ -83,6 +84,7 @@ object LibraryTab : Tab {
val haptic = LocalHapticFeedback.current
val screenModel = rememberScreenModel { LibraryScreenModel() }
val settingsScreenModel = rememberScreenModel { LibrarySettingsScreenModel() }
val state by screenModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
@ -95,9 +97,6 @@ object LibraryTab : Tab {
}
started
}
val onClickFilter: () -> Unit = {
scope.launch { sendSettingsSheetIntent(state.categories[screenModel.activeCategoryIndex]) }
}
Scaffold(
topBar = { scrollBehavior ->
@ -114,7 +113,7 @@ object LibraryTab : Tab {
onClickUnselectAll = screenModel::clearSelection,
onClickSelectAll = { screenModel.selectAll(screenModel.activeCategoryIndex) },
onClickInvertSelection = { screenModel.invertSelection(screenModel.activeCategoryIndex) },
onClickFilter = onClickFilter,
onClickFilter = { screenModel.showSettingsDialog() },
onClickRefresh = { onClickRefresh(null) },
onClickOpenRandomManga = {
scope.launch {
@ -201,6 +200,11 @@ object LibraryTab : Tab {
val onDismissRequest = screenModel::closeDialog
when (val dialog = state.dialog) {
is LibraryScreenModel.Dialog.SettingsSheet -> LibrarySettingsDialog(
onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel,
activeCategoryIndex = screenModel.activeCategoryIndex,
)
is LibraryScreenModel.Dialog.ChangeCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
@ -235,8 +239,8 @@ object LibraryTab : Tab {
}
}
LaunchedEffect(state.selectionMode) {
HomeScreen.showBottomNav(!state.selectionMode)
LaunchedEffect(state.selectionMode, state.dialog) {
HomeScreen.showBottomNav(!state.selectionMode && state.dialog !is LibraryScreenModel.Dialog.SettingsSheet)
}
LaunchedEffect(state.isLoading) {
@ -247,7 +251,7 @@ object LibraryTab : Tab {
LaunchedEffect(Unit) {
launch { queryEvent.receiveAsFlow().collect(screenModel::search) }
launch { requestSettingsSheetEvent.receiveAsFlow().collectLatest { onClickFilter() } }
launch { requestSettingsSheetEvent.receiveAsFlow().collectLatest { screenModel.showSettingsDialog() } }
}
}
@ -257,8 +261,5 @@ object LibraryTab : Tab {
// For opening settings sheet in LibraryController
private val requestSettingsSheetEvent = Channel<Unit>()
private val openSettingsSheetEvent_ = Channel<Category>()
val openSettingsSheetEvent = openSettingsSheetEvent_.receiveAsFlow()
private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.send(category)
suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.send(Unit)
private suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.send(Unit)
}

View File

@ -78,8 +78,6 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.ui.library.LibrarySettingsSheet
import eu.kanade.tachiyomi.ui.library.LibraryTab
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.util.system.dpToPx
@ -87,7 +85,6 @@ import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setComposeContent
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.callbackFlow
@ -100,7 +97,6 @@ import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.Constants
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.category.model.Category
import tachiyomi.presentation.core.components.material.Scaffold
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -121,11 +117,6 @@ class MainActivity : BaseActivity() {
// To be checked by splash screen. If true then splash screen will be removed.
var ready = false
/**
* Sheet containing filter/sort/display items.
*/
private var settingsSheet: LibrarySettingsSheet? = null
private var navigator: Navigator? = null
override fun onCreate(savedInstanceState: Bundle?) {
@ -160,11 +151,6 @@ class MainActivity : BaseActivity() {
// Draw edge-to-edge
WindowCompat.setDecorFitsSystemWindows(window, false)
settingsSheet = LibrarySettingsSheet(this)
LibraryTab.openSettingsSheetEvent
.onEach(::showSettingsSheet)
.launchIn(lifecycleScope)
setComposeContent {
val incognito by preferences.incognitoMode().collectAsState()
val downloadOnly by preferences.downloadedOnly().collectAsState()
@ -303,14 +289,6 @@ class MainActivity : BaseActivity() {
}
}
private fun showSettingsSheet(category: Category? = null) {
if (category != null) {
settingsSheet?.show(category)
} else {
lifecycleScope.launch { LibraryTab.requestOpenSettingsSheet() }
}
}
@Composable
private fun ConfirmExit() {
val scope = rememberCoroutineScope()
@ -470,12 +448,6 @@ class MainActivity : BaseActivity() {
return true
}
override fun onDestroy() {
settingsSheet?.sheetScope?.cancel()
settingsSheet = null
super.onDestroy()
}
override fun onBackPressed() {
if (navigator?.size == 1 &&
!onBackPressedDispatcher.hasEnabledCallbacks() &&

View File

@ -1,270 +0,0 @@
package eu.kanade.tachiyomi.widget
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View.OnClickListener
import android.view.ViewGroup
import androidx.annotation.AttrRes
import androidx.annotation.CallSuper
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
/**
* An alternative implementation of [com.google.android.material.navigation.NavigationView], without menu
* inflation and allowing customizable items (multiple selections, custom views, etc).
*/
open class ExtendedNavigationView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : SimpleNavigationView(context, attrs, defStyleAttr) {
/**
* Every item of the nav view. Generic items must belong to this list, custom items could be
* implemented by an abstract class. If more customization is needed in the future, this can be
* changed to an interface instead of sealed class.
*/
sealed class Item {
/**
* A view separator.
*/
class Separator(val paddingTop: Int = 0, val paddingBottom: Int = 0) : Item()
/**
* A header with a title.
*/
class Header(val resTitle: Int) : Item()
/**
* A checkbox.
*/
open class Checkbox(val resTitle: Int, var checked: Boolean = false, var enabled: Boolean = true) : Item()
/**
* A checkbox belonging to a group. The group must handle selections and restrictions.
*/
class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false, enabled: Boolean = true) :
Checkbox(resTitle, checked, enabled), GroupedItem
/**
* A radio belonging to a group (a sole radio makes no sense). The group must handle
* selections and restrictions.
*/
class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false, var enabled: Boolean = true) :
Item(), GroupedItem
/**
* An item with which needs more than two states (selected/deselected).
*/
abstract class MultiState(val resTitle: Int, var state: Int = 0, var enabled: Boolean = true, var isVisible: Boolean = true) : Item() {
/**
* Returns the drawable associated to every possible each state.
*/
abstract fun getStateDrawable(context: Context): Drawable?
/**
* Creates a vector tinted with the accent color.
*
* @param context any context.
* @param resId the vector resource to load and tint
*/
fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorPrimary): Drawable {
return AppCompatResources.getDrawable(context, resId)!!.apply {
setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal))
}
}
}
/**
* An item with which needs more than two states (selected/deselected) belonging to a group.
* The group must handle selections and restrictions.
*/
abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0, enabled: Boolean = true) :
MultiState(resTitle, state, enabled), GroupedItem
/**
* A multistate item for sorting lists (unselected, ascending, descending).
*/
class MultiSort(resId: Int, group: Group) : MultiStateGroup(resId, group) {
companion object {
const val SORT_NONE = 0
const val SORT_ASC = 1
const val SORT_DESC = 2
}
override fun getStateDrawable(context: Context): Drawable? {
return when (state) {
SORT_ASC -> tintVector(context, R.drawable.ic_arrow_up_white_32dp)
SORT_DESC -> tintVector(context, R.drawable.ic_arrow_down_white_32dp)
SORT_NONE -> AppCompatResources.getDrawable(context, R.drawable.empty_drawable_32dp)
else -> null
}
}
}
/**
* A checkbox with 3 states (unselected, checked, explicitly unchecked).
*/
class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) {
enum class State(val value: Int) {
DISABLED(0),
ENABLED_IS(1),
ENABLED_NOT(2),
}
override fun getStateDrawable(context: Context): Drawable? {
return when (state) {
State.DISABLED.value -> tintVector(context, R.drawable.ic_check_box_outline_blank_24dp, R.attr.colorControlNormal)
State.ENABLED_IS.value -> tintVector(context, R.drawable.ic_check_box_24dp)
State.ENABLED_NOT.value -> tintVector(context, R.drawable.ic_check_box_x_24dp)
else -> throw Exception("Unknown state")
}
}
}
}
/**
* Interface for an item belonging to a group.
*/
interface GroupedItem {
val group: Group
}
/**
* A group containing a list of items.
*/
interface Group {
/**
* An optional header for the group, typically a [Item.Header].
*/
val header: Item?
/**
* An optional footer for the group, typically a [Item.Separator].
*/
val footer: Item?
/**
* The items of the group, excluding header and footer.
*/
val items: List<Item>
/**
* Creates all the elements of this group. Implementations can override this method for more
* customization.
*/
fun createItems() = (mutableListOf<Item>() + header + items + footer).filterNotNull()
/**
* Called after creating the list of items. Implementations should load the current values
* into the models.
*/
fun initModels()
/**
* Called when an item of this group is clicked. The group is responsible for all the
* selections of its items.
*/
fun onItemClicked(item: Item)
}
/**
* Base adapter for the navigation view. It knows how to create and render every subclass of
* [Item].
*/
abstract inner class Adapter(private val items: List<Item>) : RecyclerView.Adapter<Holder>() {
private val onClick = OnClickListener {
val pos = recycler.getChildAdapterPosition(it)
val item = items[pos]
onItemClicked(item)
}
fun notifyItemChanged(item: Item) {
val pos = items.indexOf(item)
if (pos != -1) notifyItemChanged(pos)
}
override fun getItemCount(): Int {
return items.size
}
@CallSuper
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is Item.Header -> VIEW_TYPE_HEADER
is Item.Separator -> VIEW_TYPE_SEPARATOR
is Item.Radio -> VIEW_TYPE_RADIO
is Item.Checkbox -> VIEW_TYPE_CHECKBOX
is Item.MultiState -> VIEW_TYPE_MULTISTATE
}
}
@CallSuper
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
return when (viewType) {
VIEW_TYPE_HEADER -> HeaderHolder(parent)
VIEW_TYPE_SEPARATOR -> SeparatorHolder(parent)
VIEW_TYPE_RADIO -> RadioHolder(parent, onClick)
VIEW_TYPE_CHECKBOX -> CheckboxHolder(parent, onClick)
VIEW_TYPE_MULTISTATE -> MultiStateHolder(parent, onClick)
else -> throw Exception("Unknown view type")
}
}
@CallSuper
override fun onBindViewHolder(holder: Holder, position: Int) {
when (holder) {
is HeaderHolder -> {
val item = items[position] as Item.Header
holder.title.setText(item.resTitle)
}
is SeparatorHolder -> {
val view = holder.itemView
val item = items[position] as Item.Separator
view.updatePadding(top = item.paddingTop, bottom = item.paddingBottom)
}
is RadioHolder -> {
val item = items[position] as Item.Radio
holder.radio.setText(item.resTitle)
holder.radio.isChecked = item.checked
holder.itemView.isClickable = item.enabled
holder.radio.isEnabled = item.enabled
}
is CheckboxHolder -> {
val item = items[position] as Item.CheckboxGroup
holder.check.setText(item.resTitle)
holder.check.isChecked = item.checked
holder.itemView.isClickable = item.enabled
holder.check.isEnabled = item.enabled
}
is MultiStateHolder -> {
val item = items[position] as Item.MultiStateGroup
val drawable = item.getStateDrawable(context)
holder.text.setText(item.resTitle)
holder.text.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
holder.itemView.isClickable = item.enabled
holder.text.isEnabled = item.enabled
// Mimics checkbox/radio button
holder.text.alpha = if (item.enabled) 1f else 0.4f
holder.itemView.isVisible = item.isVisible
}
}
}
abstract fun onItemClicked(item: Item)
}
}

View File

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.widget
import tachiyomi.domain.manga.model.TriStateFilter
// TODO: replace this with TriStateFilter entirely
enum class TriState(val value: Int) {
DISABLED(0),
ENABLED_IS(1),
ENABLED_NOT(2),
}
fun Int.toTriStateFilter(): TriStateFilter {
return when (this) {
TriState.DISABLED.value -> TriStateFilter.DISABLED
TriState.ENABLED_IS.value -> TriStateFilter.ENABLED_IS
TriState.ENABLED_NOT.value -> TriStateFilter.ENABLED_NOT
else -> throw IllegalStateException("Unknown TriStateGroup state: $this")
}
}