Add swipe actions for chapters (#9304)

* added chapter swipe

* Rework corner animtion

* Update i18n/src/main/res/values/strings.xml

Co-authored-by: arkon <arkon@users.noreply.github.com>

* Replace LTR/RTL with Start/End layout

* Added label to the animation so the warning will go away

* Getting rid of the swipe threshold setting

* adding disabled option, renaming stuff, other stuff?

* Getting rid of the snackbar

* Getting rid of unecessary strings

* changing enum names as requested

* Renaming Raio to Ratio (I need a better keyboard as well -__-)

* Replacing error with download icon and action

* backup

* minor cleanup

* fixing an nasty edge case

* fixing mistakes in the previous conflict

* space

* fixing bug

fixed bug where the user could dismiss already dismissed item leading to item getting stuck

* fixing lint errors

* fixing lints (hopefully)

* Added "swipe disabled" to the list of actions

* Replacing string value and moving value as requested

* replacing rest of the strings with generic ones

---------

Co-authored-by: arkon <arkon@users.noreply.github.com>
This commit is contained in:
d-najd 2023-04-25 23:29:39 +02:00 committed by GitHub
parent ef3d2c14b4
commit a8f17a3fab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 419 additions and 89 deletions

View File

@ -66,6 +66,7 @@ import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.missingChaptersCount import tachiyomi.domain.chapter.service.missingChaptersCount
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
import tachiyomi.presentation.core.components.LazyColumn import tachiyomi.presentation.core.components.LazyColumn
@ -86,6 +87,8 @@ fun MangaScreen(
dateRelativeTime: Int, dateRelativeTime: Int,
dateFormat: DateFormat, dateFormat: DateFormat,
isTabletUi: Boolean, isTabletUi: Boolean,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
@ -117,6 +120,9 @@ fun MangaScreen(
onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For chapter swipe
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection // Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit,
@ -135,6 +141,8 @@ fun MangaScreen(
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime, dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
chapterSwipeEndAction = chapterSwipeEndAction,
chapterSwipeStartAction = chapterSwipeStartAction,
onBackClicked = onBackClicked, onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
@ -157,6 +165,7 @@ fun MangaScreen(
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
onMultiDeleteClicked = onMultiDeleteClicked, onMultiDeleteClicked = onMultiDeleteClicked,
onChapterSwipe = onChapterSwipe,
onChapterSelected = onChapterSelected, onChapterSelected = onChapterSelected,
onAllChapterSelected = onAllChapterSelected, onAllChapterSelected = onAllChapterSelected,
onInvertSelection = onInvertSelection, onInvertSelection = onInvertSelection,
@ -166,6 +175,8 @@ fun MangaScreen(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime, dateRelativeTime = dateRelativeTime,
chapterSwipeEndAction = chapterSwipeEndAction,
chapterSwipeStartAction = chapterSwipeStartAction,
dateFormat = dateFormat, dateFormat = dateFormat,
onBackClicked = onBackClicked, onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
@ -189,6 +200,7 @@ fun MangaScreen(
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
onMultiDeleteClicked = onMultiDeleteClicked, onMultiDeleteClicked = onMultiDeleteClicked,
onChapterSwipe = onChapterSwipe,
onChapterSelected = onChapterSelected, onChapterSelected = onChapterSelected,
onAllChapterSelected = onAllChapterSelected, onAllChapterSelected = onAllChapterSelected,
onInvertSelection = onInvertSelection, onInvertSelection = onInvertSelection,
@ -202,6 +214,8 @@ private fun MangaScreenSmallImpl(
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Int, dateRelativeTime: Int,
dateFormat: DateFormat, dateFormat: DateFormat,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
@ -234,6 +248,9 @@ private fun MangaScreenSmallImpl(
onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For chapter swipe
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection // Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit,
@ -404,9 +421,12 @@ private fun MangaScreenSmallImpl(
chapters = chapters, chapters = chapters,
dateRelativeTime = dateRelativeTime, dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
chapterSwipeEndAction = chapterSwipeEndAction,
chapterSwipeStartAction = chapterSwipeStartAction,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
onChapterSelected = onChapterSelected, onChapterSelected = onChapterSelected,
onChapterSwipe = onChapterSwipe,
) )
} }
} }
@ -420,6 +440,8 @@ fun MangaScreenLargeImpl(
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Int, dateRelativeTime: Int,
dateFormat: DateFormat, dateFormat: DateFormat,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
@ -452,6 +474,9 @@ fun MangaScreenLargeImpl(
onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For swipe actions
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection // Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit,
@ -616,9 +641,12 @@ fun MangaScreenLargeImpl(
chapters = chapters, chapters = chapters,
dateRelativeTime = dateRelativeTime, dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
chapterSwipeEndAction = chapterSwipeEndAction,
chapterSwipeStartAction = chapterSwipeStartAction,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
onChapterSelected = onChapterSelected, onChapterSelected = onChapterSelected,
onChapterSwipe = onChapterSwipe,
) )
} }
} }
@ -675,9 +703,12 @@ private fun LazyListScope.sharedChapterItems(
chapters: List<ChapterItem>, chapters: List<ChapterItem>,
dateRelativeTime: Int, dateRelativeTime: Int,
dateFormat: DateFormat, dateFormat: DateFormat,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
) { ) {
items( items(
items = chapters, items = chapters,
@ -720,6 +751,8 @@ private fun LazyListScope.sharedChapterItems(
downloadIndicatorEnabled = chapters.fastAll { !it.selected }, downloadIndicatorEnabled = chapters.fastAll { !it.selected },
downloadStateProvider = { chapterItem.downloadState }, downloadStateProvider = { chapterItem.downloadState },
downloadProgressProvider = { chapterItem.downloadProgress }, downloadProgressProvider = { chapterItem.downloadProgress },
chapterSwipeEndAction = chapterSwipeEndAction,
chapterSwipeStartAction = chapterSwipeStartAction,
onLongClick = { onLongClick = {
onChapterSelected(chapterItem, !chapterItem.selected, true, true) onChapterSelected(chapterItem, !chapterItem.selected, true, true)
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@ -737,6 +770,9 @@ private fun LazyListScope.sharedChapterItems(
} else { } else {
null null
}, },
onChapterSwipe = {
onChapterSwipe(chapterItem, it)
},
) )
} }
} }

View File

@ -1,21 +1,41 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissValue
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.BookmarkRemove
import androidx.compose.material.icons.filled.Circle import androidx.compose.material.icons.filled.Circle
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.FileDownloadOff
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.rememberDismissState
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -23,6 +43,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -30,9 +51,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.ReadItemAlpha
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.util.selectedBackground import tachiyomi.presentation.core.util.selectedBackground
import kotlin.math.min
@Composable @Composable
fun MangaChapterListItem( fun MangaChapterListItem(
@ -47,103 +70,271 @@ fun MangaChapterListItem(
downloadIndicatorEnabled: Boolean, downloadIndicatorEnabled: Boolean,
downloadStateProvider: () -> Download.State, downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int, downloadProgressProvider: () -> Int,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
onLongClick: () -> Unit, onLongClick: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
onDownloadClick: ((ChapterDownloadAction) -> Unit)?, onDownloadClick: ((ChapterDownloadAction) -> Unit)?,
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
) { ) {
val textAlpha = if (read) ReadItemAlpha else 1f val textAlpha = if (read) ReadItemAlpha else 1f
val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha
Row( val chapterSwipeStartEnabled = chapterSwipeStartAction != LibraryPreferences.ChapterSwipeAction.Disabled
modifier = modifier val chapterSwipeEndEnabled = chapterSwipeEndAction != LibraryPreferences.ChapterSwipeAction.Disabled
.selectedBackground(selected)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
var textHeight by remember { mutableStateOf(0) }
if (!read) {
Icon(
imageVector = Icons.Filled.Circle,
contentDescription = stringResource(R.string.unread),
modifier = Modifier
.height(8.dp)
.padding(end = 4.dp),
tint = MaterialTheme.colorScheme.primary,
)
}
if (bookmark) {
Icon(
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
tint = MaterialTheme.colorScheme.primary,
)
}
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = textAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onTextLayout = { textHeight = it.size.height },
)
}
Row { val dismissState = rememberDismissState()
ProvideTextStyle( val dismissDirections = remember { mutableSetOf<DismissDirection>() }
value = MaterialTheme.typography.bodyMedium.copy( var lastDismissDirection: DismissDirection? by remember { mutableStateOf(null) }
fontSize = 12.sp, if (lastDismissDirection == null) {
color = LocalContentColor.current.copy(alpha = textSubtitleAlpha), if (chapterSwipeStartEnabled) {
), dismissDirections.add(DismissDirection.EndToStart)
) {
if (date != null) {
Text(
text = date,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (readProgress != null || scanlator != null) DotSeparatorText()
}
if (readProgress != null) {
Text(
text = readProgress,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.alpha(ReadItemAlpha),
)
if (scanlator != null) DotSeparatorText()
}
if (scanlator != null) {
Text(
text = scanlator,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
} }
if (chapterSwipeEndEnabled) {
if (onDownloadClick != null) { dismissDirections.add(DismissDirection.StartToEnd)
ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,
onClick = onDownloadClick,
)
} }
} }
val animateDismissContentAlpha by animateFloatAsState(
label = "animateDismissContentAlpha",
targetValue = if (lastDismissDirection != null) 1f else 0f,
animationSpec = tween(durationMillis = if (lastDismissDirection != null) 500 else 0),
finishedListener = {
lastDismissDirection = null
},
)
LaunchedEffect(dismissState.currentValue) {
when (dismissState.currentValue) {
DismissValue.DismissedToEnd -> {
lastDismissDirection = DismissDirection.StartToEnd
val dismissDirectionsCopy = dismissDirections.toSet()
dismissDirections.clear()
onChapterSwipe(chapterSwipeEndAction)
dismissState.snapTo(DismissValue.Default)
dismissDirections.addAll(dismissDirectionsCopy)
}
DismissValue.DismissedToStart -> {
lastDismissDirection = DismissDirection.EndToStart
val dismissDirectionsCopy = dismissDirections.toSet()
dismissDirections.clear()
onChapterSwipe(chapterSwipeStartAction)
dismissState.snapTo(DismissValue.Default)
dismissDirections.addAll(dismissDirectionsCopy)
}
DismissValue.Default -> { }
}
}
SwipeToDismiss(
state = dismissState,
directions = dismissDirections,
background = {
val backgroundColor = if (chapterSwipeEndEnabled && (dismissState.dismissDirection == DismissDirection.StartToEnd || lastDismissDirection == DismissDirection.StartToEnd)) {
MaterialTheme.colorScheme.primary
} else if (chapterSwipeStartEnabled && (dismissState.dismissDirection == DismissDirection.EndToStart || lastDismissDirection == DismissDirection.EndToStart)) {
MaterialTheme.colorScheme.primary
} else {
Color.Unspecified
}
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor),
) {
if (dismissState.dismissDirection in dismissDirections) {
val downloadState = downloadStateProvider()
SwipeBackgroundIcon(
modifier = Modifier
.padding(start = 16.dp)
.align(Alignment.CenterStart)
.alpha(
if (dismissState.dismissDirection == DismissDirection.StartToEnd) 1f else 0f,
),
tint = contentColorFor(backgroundColor),
swipeAction = chapterSwipeEndAction,
read = read,
bookmark = bookmark,
downloadState = downloadState,
)
SwipeBackgroundIcon(
modifier = Modifier
.padding(end = 16.dp)
.align(Alignment.CenterEnd)
.alpha(
if (dismissState.dismissDirection == DismissDirection.EndToStart) 1f else 0f,
),
tint = contentColorFor(backgroundColor),
swipeAction = chapterSwipeStartAction,
read = read,
bookmark = bookmark,
downloadState = downloadState,
)
}
}
},
dismissContent = {
val animateCornerRatio = if (dismissState.offset.value != 0f) {
min(
dismissState.progress.fraction / .075f,
1f,
)
} else {
0f
}
val animateCornerShape = (8f * animateCornerRatio).dp
val dismissContentAlpha =
if (lastDismissDirection != null) animateDismissContentAlpha else 1f
Card(
modifier = modifier,
colors = CardDefaults.elevatedCardColors(
containerColor = Color.Transparent,
),
shape = RoundedCornerShape(animateCornerShape),
) {
Row(
modifier = Modifier
.background(
MaterialTheme.colorScheme.background.copy(dismissContentAlpha),
)
.selectedBackground(selected)
.alpha(dismissContentAlpha)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
var textHeight by remember { mutableStateOf(0) }
if (!read) {
Icon(
imageVector = Icons.Filled.Circle,
contentDescription = stringResource(R.string.unread),
modifier = Modifier
.height(8.dp)
.padding(end = 4.dp),
tint = MaterialTheme.colorScheme.primary,
)
}
if (bookmark) {
Icon(
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
tint = MaterialTheme.colorScheme.primary,
)
}
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = textAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onTextLayout = { textHeight = it.size.height },
)
}
Row {
ProvideTextStyle(
value = MaterialTheme.typography.bodyMedium.copy(
fontSize = 12.sp,
color = LocalContentColor.current.copy(alpha = textSubtitleAlpha),
),
) {
if (date != null) {
Text(
text = date,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (readProgress != null || scanlator != null) DotSeparatorText()
}
if (readProgress != null) {
Text(
text = readProgress,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.alpha(ReadItemAlpha),
)
if (scanlator != null) DotSeparatorText()
}
if (scanlator != null) {
Text(
text = scanlator,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
if (onDownloadClick != null) {
ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,
onClick = onDownloadClick,
)
}
}
}
},
)
}
@Composable
private fun SwipeBackgroundIcon(
modifier: Modifier = Modifier,
tint: Color,
swipeAction: LibraryPreferences.ChapterSwipeAction,
read: Boolean,
bookmark: Boolean,
downloadState: Download.State,
) {
val imageVector = when (swipeAction) {
LibraryPreferences.ChapterSwipeAction.ToggleRead -> {
if (!read) {
Icons.Default.Visibility
} else {
Icons.Default.VisibilityOff
}
}
LibraryPreferences.ChapterSwipeAction.ToggleBookmark -> {
if (!bookmark) {
Icons.Default.Bookmark
} else {
Icons.Default.BookmarkRemove
}
}
LibraryPreferences.ChapterSwipeAction.Download -> {
when (downloadState) {
Download.State.NOT_DOWNLOADED,
Download.State.ERROR,
-> { Icons.Default.Download }
Download.State.QUEUE,
Download.State.DOWNLOADING,
-> { Icons.Default.FileDownloadOff }
Download.State.DOWNLOADED -> { Icons.Default.Delete }
}
}
LibraryPreferences.ChapterSwipeAction.Disabled -> {
null
}
}
imageVector?.let {
Icon(
modifier = modifier,
imageVector = imageVector,
tint = tint,
contentDescription = null,
)
}
} }

View File

@ -58,6 +58,7 @@ object SettingsLibraryScreen : SearchableSettings {
return mutableListOf( return mutableListOf(
getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences), getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences),
getGlobalUpdateGroup(allCategories, libraryPreferences), getGlobalUpdateGroup(allCategories, libraryPreferences),
getChapterSwipeActionsGroup(libraryPreferences),
) )
} }
@ -216,4 +217,38 @@ object SettingsLibraryScreen : SearchableSettings {
), ),
) )
} }
@Composable
private fun getChapterSwipeActionsGroup(
libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup {
val chapterSwipeEndActionPref = libraryPreferences.swipeEndAction()
val chapterSwipeStartActionPref = libraryPreferences.swipeStartAction()
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_chapter_swipe),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = chapterSwipeEndActionPref,
title = stringResource(R.string.pref_chapter_swipe_end),
entries = mapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.action_disable),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark),
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(R.string.action_mark_as_read),
LibraryPreferences.ChapterSwipeAction.Download to stringResource(R.string.action_download),
),
),
Preference.PreferenceItem.ListPreference(
pref = chapterSwipeStartActionPref,
title = stringResource(R.string.pref_chapter_swipe_start),
entries = mapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.action_disable),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark),
LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(R.string.action_mark_as_read),
LibraryPreferences.ChapterSwipeAction.Download to stringResource(R.string.action_download),
),
),
),
)
}
} }

View File

@ -101,6 +101,8 @@ class MangaScreen(
dateRelativeTime = screenModel.relativeTime, dateRelativeTime = screenModel.relativeTime,
dateFormat = screenModel.dateFormat, dateFormat = screenModel.dateFormat,
isTabletUi = isTabletUi(), isTabletUi = isTabletUi(),
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
onBackClicked = navigator::pop, onBackClicked = navigator::pop,
onChapterClicked = { openChapter(context, it) }, onChapterClicked = { openChapter(context, it) },
onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() }, onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() },
@ -125,6 +127,7 @@ class MangaScreen(
onMultiMarkAsReadClicked = screenModel::markChaptersRead, onMultiMarkAsReadClicked = screenModel::markChaptersRead,
onMarkPreviousAsReadClicked = screenModel::markPreviousChapterRead, onMarkPreviousAsReadClicked = screenModel::markPreviousChapterRead,
onMultiDeleteClicked = screenModel::showDeleteChapterDialog, onMultiDeleteClicked = screenModel::showDeleteChapterDialog,
onChapterSwipe = screenModel::chapterSwipe,
onChapterSelected = screenModel::toggleSelection, onChapterSelected = screenModel::toggleSelection,
onAllChapterSelected = screenModel::toggleAllSelection, onAllChapterSelected = screenModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection, onInvertSelection = screenModel::invertSelection,

View File

@ -121,6 +121,9 @@ class MangaInfoScreenModel(
private val filteredChapters: Sequence<ChapterItem>? private val filteredChapters: Sequence<ChapterItem>?
get() = successState?.processedChapters get() = successState?.processedChapters
val chapterSwipeEndAction = libraryPreferences.swipeEndAction().get()
val chapterSwipeStartAction = libraryPreferences.swipeStartAction().get()
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope) val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get())) val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
private val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope) private val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
@ -523,6 +526,49 @@ class MangaInfoScreenModel(
} }
} }
/**
* @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled]
*/
fun chapterSwipe(chapterItem: ChapterItem, swipeAction: LibraryPreferences.ChapterSwipeAction) {
coroutineScope.launch {
executeChapterSwipeAction(chapterItem, swipeAction)
}
}
/**
* @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled]
*/
private fun executeChapterSwipeAction(
chapterItem: ChapterItem,
swipeAction: LibraryPreferences.ChapterSwipeAction,
) {
val chapter = chapterItem.chapter
when (swipeAction) {
LibraryPreferences.ChapterSwipeAction.ToggleRead -> {
markChaptersRead(listOf(chapter), !chapter.read)
}
LibraryPreferences.ChapterSwipeAction.ToggleBookmark -> {
bookmarkChapters(listOf(chapter), !chapter.bookmark)
}
LibraryPreferences.ChapterSwipeAction.Download -> {
val downloadAction: ChapterDownloadAction = when (chapterItem.downloadState) {
Download.State.ERROR,
Download.State.NOT_DOWNLOADED,
-> ChapterDownloadAction.START_NOW
Download.State.QUEUE,
Download.State.DOWNLOADING,
-> ChapterDownloadAction.CANCEL
Download.State.DOWNLOADED -> ChapterDownloadAction.DELETE
}
runChapterDownloadActions(
items = listOf(chapterItem),
action = downloadAction,
)
}
LibraryPreferences.ChapterSwipeAction.Disabled -> throw IllegalStateException()
}
}
/** /**
* Returns the next unread chapter or null if everything is read. * Returns the next unread chapter or null if everything is read.
*/ */

View File

@ -118,6 +118,21 @@ class LibraryPreferences(
// endregion // endregion
// region Swipe Actions
fun swipeEndAction() = preferenceStore.getEnum("pref_chapter_swipe_end_action", ChapterSwipeAction.ToggleBookmark)
fun swipeStartAction() = preferenceStore.getEnum("pref_chapter_swipe_start_action", ChapterSwipeAction.ToggleRead)
// endregion
enum class ChapterSwipeAction {
ToggleRead,
ToggleBookmark,
Download,
Disabled,
}
companion object { companion object {
const val DEVICE_ONLY_ON_WIFI = "wifi" const val DEVICE_ONLY_ON_WIFI = "wifi"
const val DEVICE_NETWORK_NOT_METERED = "network_not_metered" const val DEVICE_NETWORK_NOT_METERED = "network_not_metered"

View File

@ -167,7 +167,7 @@
<string name="pref_general_summary">App language, notifications</string> <string name="pref_general_summary">App language, notifications</string>
<string name="pref_appearance_summary">Theme, date &amp; time format</string> <string name="pref_appearance_summary">Theme, date &amp; time format</string>
<string name="pref_library_summary">Categories, global update</string> <string name="pref_library_summary">Categories, global update, chapter swipe</string>
<string name="pref_reader_summary">Reading mode, display, navigation</string> <string name="pref_reader_summary">Reading mode, display, navigation</string>
<string name="pref_downloads_summary">Automatic download, download ahead</string> <string name="pref_downloads_summary">Automatic download, download ahead</string>
<string name="pref_tracking_summary">One-way progress sync, enhanced sync</string> <string name="pref_tracking_summary">One-way progress sync, enhanced sync</string>
@ -273,6 +273,10 @@
<string name="include">Include: %s</string> <string name="include">Include: %s</string>
<string name="exclude">Exclude: %s</string> <string name="exclude">Exclude: %s</string>
<string name="pref_chapter_swipe">Chapter swipe</string>
<string name="pref_chapter_swipe_end">Swipe to end action</string>
<string name="pref_chapter_swipe_start">Swipe to start action</string>
<!-- Extension section --> <!-- Extension section -->
<string name="multi_lang">Multi</string> <string name="multi_lang">Multi</string>
<string name="ext_updates_pending">Updates pending</string> <string name="ext_updates_pending">Updates pending</string>