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

View File

@ -1,21 +1,41 @@
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.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.filled.Bookmark
import androidx.compose.material.icons.filled.BookmarkRemove
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.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -23,6 +43,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@ -30,9 +51,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.tachiyomi.R
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.SecondaryItemAlpha
import tachiyomi.presentation.core.util.selectedBackground
import kotlin.math.min
@Composable
fun MangaChapterListItem(
@ -47,16 +70,132 @@ fun MangaChapterListItem(
downloadIndicatorEnabled: Boolean,
downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
onLongClick: () -> Unit,
onClick: () -> Unit,
onDownloadClick: ((ChapterDownloadAction) -> Unit)?,
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
) {
val textAlpha = if (read) ReadItemAlpha else 1f
val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha
val chapterSwipeStartEnabled = chapterSwipeStartAction != LibraryPreferences.ChapterSwipeAction.Disabled
val chapterSwipeEndEnabled = chapterSwipeEndAction != LibraryPreferences.ChapterSwipeAction.Disabled
val dismissState = rememberDismissState()
val dismissDirections = remember { mutableSetOf<DismissDirection>() }
var lastDismissDirection: DismissDirection? by remember { mutableStateOf(null) }
if (lastDismissDirection == null) {
if (chapterSwipeStartEnabled) {
dismissDirections.add(DismissDirection.EndToStart)
}
if (chapterSwipeEndEnabled) {
dismissDirections.add(DismissDirection.StartToEnd)
}
}
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
modifier = Modifier
.background(
MaterialTheme.colorScheme.background.copy(dismissContentAlpha),
)
.selectedBackground(selected)
.alpha(dismissContentAlpha)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
@ -147,3 +286,55 @@ fun MangaChapterListItem(
}
}
}
},
)
}
@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(
getCategoriesGroup(LocalNavigator.currentOrThrow, 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,
dateFormat = screenModel.dateFormat,
isTabletUi = isTabletUi(),
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
onBackClicked = navigator::pop,
onChapterClicked = { openChapter(context, it) },
onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() },
@ -125,6 +127,7 @@ class MangaScreen(
onMultiMarkAsReadClicked = screenModel::markChaptersRead,
onMarkPreviousAsReadClicked = screenModel::markPreviousChapterRead,
onMultiDeleteClicked = screenModel::showDeleteChapterDialog,
onChapterSwipe = screenModel::chapterSwipe,
onChapterSelected = screenModel::toggleSelection,
onAllChapterSelected = screenModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection,

View File

@ -121,6 +121,9 @@ class MangaInfoScreenModel(
private val filteredChapters: Sequence<ChapterItem>?
get() = successState?.processedChapters
val chapterSwipeEndAction = libraryPreferences.swipeEndAction().get()
val chapterSwipeStartAction = libraryPreferences.swipeStartAction().get()
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
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.
*/

View File

@ -118,6 +118,21 @@ class LibraryPreferences(
// 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 {
const val DEVICE_ONLY_ON_WIFI = "wifi"
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_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_downloads_summary">Automatic download, download ahead</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="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 -->
<string name="multi_lang">Multi</string>
<string name="ext_updates_pending">Updates pending</string>