Migrate library settings sheet to Compose

(cherry picked from commit 727399611ddff3530d66ad7a758f357f95053b92)

# Conflicts:
#	app/src/main/java/eu/kanade/domain/library/service/LibraryPreferences.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
#	app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt
This commit is contained in:
arkon
2022-12-09 17:34:24 -05:00
committed by Jobobby04
parent e910fa3f70
commit 3257366ec0
12 changed files with 588 additions and 972 deletions
@@ -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,36 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
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 +66,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 +75,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 +128,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,
@@ -133,3 +177,36 @@ fun RadioItem(
)
}
}
// SY -->
@Composable
fun IconItem(
label: String,
icon: Painter,
selected: 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),
) {
Icon(
painter = icon,
contentDescription = label,
tint = if (selected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface
},
)
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
)
}
}
// SY <--
@@ -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,26 +82,13 @@ 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)
}
content(contentPadding, page)
}
}
}
@@ -0,0 +1,343 @@
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.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastForEach
import eu.kanade.domain.library.model.LibraryGroup
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.presentation.components.CheckboxItem
import eu.kanade.presentation.components.HeadingItem
import eu.kanade.presentation.components.IconItem
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 kotlinx.coroutines.flow.map
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),
// SY -->
stringResource(R.string.group),
// SY <--
),
) { 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,
)
// SY -->
3 -> GroupPage(
screenModel = screenModel,
categories = state.categories,
)
// SY <--
}
}
}
}
@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) },
)
// SY -->
val filterLewd by screenModel.libraryPreferences.filterLewd().collectAsState()
TriStateItem(
label = stringResource(R.string.lewd),
state = filterLewd.toTriStateFilter(),
onClick = { screenModel.toggleFilter(LibraryPreferences::filterLewd) },
)
// SY <--
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,
) {
// SY -->
val globalSortMode by screenModel.libraryPreferences.librarySortingMode().collectAsState()
val sortingMode = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) {
category.sort.type
} else {
globalSortMode.type
}
val sortDescending = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) {
category.sort.isAscending
} else {
globalSortMode.isAscending
}.not()
val hasSortTags by remember {
screenModel.libraryPreferences.sortTagsForLibrary().changes()
.map { it.isNotEmpty() }
}.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty())
// SY <--
listOfNotNull(
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,
if (hasSortTags) {
R.string.tag_sorting to LibrarySort.Type.TagList
} else {
null
},
).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,
) {
// SY -->
val globalDisplayMode by screenModel.libraryPreferences.libraryDisplayMode().collectAsState()
// SY <--
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),
// SY -->
selected = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) {
category.display
} else {
globalDisplayMode
} == mode,
// SY <--
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)
},
)
}
data class GroupMode(
val int: Int,
val nameRes: Int,
val drawableRes: Int,
)
@Composable
private fun ColumnScope.GroupPage(
screenModel: LibrarySettingsScreenModel,
categories: List<Category>,
) {
val groups = remember(categories.isNotEmpty(), screenModel.trackServices) {
buildList {
add(LibraryGroup.BY_DEFAULT)
add(LibraryGroup.BY_SOURCE)
add(LibraryGroup.BY_STATUS)
if (screenModel.trackServices.isNotEmpty()) {
add(LibraryGroup.BY_TRACK_STATUS)
}
if (categories.isNotEmpty()) {
add(LibraryGroup.UNGROUPED)
}
}.map {
GroupMode(
it,
LibraryGroup.groupTypeStringRes(it, categories.isNotEmpty()),
LibraryGroup.groupTypeDrawableRes(it),
)
}
}
groups.fastForEach {
IconItem(
label = stringResource(it.nameRes),
icon = painterResource(it.drawableRes),
selected = it.int == screenModel.grouping,
onClick = {
screenModel.setGrouping(it.int)
},
)
}
}