Allow/Skip one by one

This commit is contained in:
Cuong M. Tran 2024-03-06 18:27:54 +07:00 committed by Cuong Tran
parent 83910501b7
commit a617528fda
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
8 changed files with 254 additions and 58 deletions

View file

@ -1,13 +1,16 @@
package eu.kanade.presentation.manga package eu.kanade.presentation.manga
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowColumn
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
@ -62,53 +65,91 @@ fun DuplicateMangaDialog(
@Composable @Composable
fun AllowDuplicateDialog( fun AllowDuplicateDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onAllowDuplicate: () -> Unit, onAllowAllDuplicate: () -> Unit,
onSkipDuplicate: () -> Unit, onSkipAllDuplicate: () -> Unit,
onOpenManga: () -> Unit = { }, onOpenManga: () -> Unit = {},
onAllowDuplicate: () -> Unit = {},
onSkipDuplicate: () -> Unit = {},
duplicatedName: String = "",
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
title = { title = {
Text(text = stringResource(MR.strings.are_you_sure)) Text(text = duplicatedName)
}, },
text = { text = {
Text(text = stringResource(MR.strings.confirm_add_duplicate_manga)) Text(text = stringResource(MR.strings.confirm_add_duplicate_manga))
}, },
confirmButton = { confirmButton = {
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround,
) { ) {
TextButton( FlowColumn {
onClick = { TextButton(
onDismissRequest() onClick = {
onOpenManga() onDismissRequest()
}, onAllowDuplicate()
) { },
Text(text = stringResource(MR.strings.action_show_manga)) Modifier.align(Alignment.CenterHorizontally),
) {
Text(text = stringResource(MR.strings.action_allow_duplicate_manga))
}
TextButton(
onClick = {
onDismissRequest()
onAllowAllDuplicate()
},
Modifier.align(Alignment.CenterHorizontally),
) {
Text(text = stringResource(MR.strings.action_allow_all_duplicate_manga))
}
} }
TextButton( FlowColumn {
onClick = { TextButton(
onDismissRequest() onClick = {
onSkipDuplicate() onDismissRequest()
}, onSkipDuplicate()
) { },
Text(text = stringResource(MR.strings.action_skip_duplicate_manga)) Modifier.align(Alignment.CenterHorizontally),
) {
Text(text = stringResource(MR.strings.action_skip_duplicate_manga))
}
TextButton(
onClick = {
onDismissRequest()
onSkipAllDuplicate()
},
Modifier.align(Alignment.CenterHorizontally),
) {
Text(text = stringResource(MR.strings.action_skip_all_duplicate_manga))
}
} }
TextButton( FlowColumn {
onClick = { TextButton(
onDismissRequest() onClick = {
onAllowDuplicate() onDismissRequest()
}, onOpenManga()
) { },
Text(text = stringResource(MR.strings.action_allow_duplicate_manga)) Modifier.align(Alignment.CenterHorizontally),
) {
Text(text = stringResource(MR.strings.action_show_manga))
}
TextButton(
onClick = {
onDismissRequest()
},
Modifier.align(Alignment.CenterHorizontally),
) {
Text(text = stringResource(MR.strings.action_cancel))
}
} }
} }
}, },
) )

View file

@ -5,6 +5,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
@ -42,6 +43,7 @@ import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.source.interactor.CountFeedSavedSearchGlobal import tachiyomi.domain.source.interactor.CountFeedSavedSearchGlobal
@ -78,6 +80,7 @@ open class FeedScreenModel(
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
// KMK <-- // KMK <--
) : StateScreenModel<FeedScreenState>(FeedScreenState()) { ) : StateScreenModel<FeedScreenState>(FeedScreenState()) {
@ -337,9 +340,19 @@ open class FeedScreenModel(
} }
} }
fun addFavorite() { fun addFavorite(startIdx: Int = 0) {
screenModelScope.launch { screenModelScope.launch {
val mangaList = state.value.selection val mangaWithDup = getDuplicateLibraryManga(startIdx)
if (mangaWithDup != null)
setDialog(Dialog.AllowDuplicate(mangaWithDup))
else
addFavoriteDuplicate()
}
}
fun addFavoriteDuplicate(skipAllDuplicates: Boolean = false) {
screenModelScope.launch {
val mangaList = if (skipAllDuplicates) getNotDuplicateLibraryMangas() else state.value.selection
val categories = getCategories() val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get() val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
@ -376,6 +389,32 @@ open class FeedScreenModel(
} }
} }
private suspend fun getNotDuplicateLibraryMangas(): List<DomainManga> {
return state.value.selection.filterNot { manga ->
getDuplicateLibraryManga.await(manga).isNotEmpty()
}
}
private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair<Int, DomainManga>? {
val mangas = state.value.selection
mangas.fastForEachIndexed { index, manga ->
if (index < startIdx) return@fastForEachIndexed
val dup = getDuplicateLibraryManga.await(manga)
if (dup.isEmpty()) return@fastForEachIndexed
return Pair(index, dup.first())
}
return null
}
fun removeDuplicateSelectedManga(index: Int) {
mutableState.update { state ->
val newSelection = state.selection.mutate { list ->
list.removeAt(index)
}
state.copy(selection = newSelection)
}
}
/** /**
* Bulk update categories of manga using old and new common categories. * Bulk update categories of manga using old and new common categories.
* *
@ -463,6 +502,7 @@ open class FeedScreenModel(
val mangas: List<DomainManga>, val mangas: List<DomainManga>,
val initialSelection: ImmutableList<CheckboxState<Category>>, val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog() ) : Dialog()
data class AllowDuplicate(val duplicatedManga: Pair<Int, DomainManga>) : Dialog()
// KMK <-- // KMK <--
} }

View file

@ -23,6 +23,7 @@ import eu.kanade.presentation.browse.FeedScreen
import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.TabContent import eu.kanade.presentation.components.TabContent
import eu.kanade.presentation.manga.AllowDuplicateDialog
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.home.HomeScreen import eu.kanade.tachiyomi.ui.home.HomeScreen
@ -204,6 +205,28 @@ fun Screen.feedTab(
}, },
) )
} }
is FeedScreenModel.Dialog.AllowDuplicate -> {
AllowDuplicateDialog(
onDismissRequest = onDismissRequest,
onAllowAllDuplicate = {
screenModel.addFavoriteDuplicate()
},
onSkipAllDuplicate = {
screenModel.addFavoriteDuplicate(skipAllDuplicates = true)
},
onOpenManga = {
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
},
onAllowDuplicate = {
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
},
onSkipDuplicate = {
screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
},
duplicatedName = dialog.duplicatedManga.second.title,
)
}
// KMK <-- // KMK <--
} }
} }

View file

@ -399,12 +399,23 @@ data class BrowseSourceScreen(
is BrowseSourceScreenModel.Dialog.AllowDuplicate -> { is BrowseSourceScreenModel.Dialog.AllowDuplicate -> {
AllowDuplicateDialog( AllowDuplicateDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onAllowDuplicate = { onAllowAllDuplicate = {
screenModel.addFavoriteDuplicate() screenModel.addFavoriteDuplicate()
}, },
onSkipDuplicate = { onSkipAllDuplicate = {
screenModel.addFavoriteDuplicate(skipDuplicate = true) screenModel.addFavoriteDuplicate(skipAllDuplicates = true)
}, },
onOpenManga = {
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
},
onAllowDuplicate = {
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
},
onSkipDuplicate = {
screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
},
duplicatedName = dialog.duplicatedManga.second.title,
) )
} }
// KMK <-- // KMK <--

View file

@ -8,6 +8,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.cachedIn import androidx.paging.cachedIn
@ -475,18 +476,19 @@ open class BrowseSourceScreenModel(
} }
} }
fun addFavorite() { fun addFavorite(startIdx: Int = 0) {
screenModelScope.launch { screenModelScope.launch {
if (hasDuplicateLibraryMangas(state.value.selection)) val mangaWithDup = getDuplicateLibraryManga(startIdx)
setDialog(Dialog.AllowDuplicate) if (mangaWithDup != null)
setDialog(Dialog.AllowDuplicate(mangaWithDup))
else else
addFavoriteDuplicate() addFavoriteDuplicate()
} }
} }
fun addFavoriteDuplicate(skipDuplicate: Boolean = false) { fun addFavoriteDuplicate(skipAllDuplicates: Boolean = false) {
screenModelScope.launch { screenModelScope.launch {
val mangaList = if (skipDuplicate) getNotDuplicateLibraryMangas() else state.value.selection val mangaList = if (skipAllDuplicates) getNotDuplicateLibraryMangas() else state.value.selection
val categories = getCategories() val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get() val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
@ -523,6 +525,32 @@ open class BrowseSourceScreenModel(
} }
} }
private suspend fun getNotDuplicateLibraryMangas(): List<Manga> {
return state.value.selection.filterNot { manga ->
getDuplicateLibraryManga.await(manga).isNotEmpty()
}
}
private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair<Int, Manga>? {
val mangas = state.value.selection
mangas.fastForEachIndexed { index, manga ->
if (index < startIdx) return@fastForEachIndexed
val dup = getDuplicateLibraryManga.await(manga)
if (dup.isEmpty()) return@fastForEachIndexed
return Pair(index, dup.first())
}
return null
}
fun removeDuplicateSelectedManga(index: Int) {
mutableState.update { state ->
val newSelection = state.selection.mutate { list ->
list.removeAt(index)
}
state.copy(selection = newSelection)
}
}
/** /**
* Bulk update categories of manga using old and new common categories. * Bulk update categories of manga using old and new common categories.
* *
@ -583,18 +611,6 @@ open class BrowseSourceScreenModel(
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
return mangaCategories.flatten().distinct().subtract(common) return mangaCategories.flatten().distinct().subtract(common)
} }
private suspend fun getNotDuplicateLibraryMangas(): List<Manga> {
return state.value.selection.filterNot { manga ->
getDuplicateLibraryManga.await(manga).isNotEmpty()
}
}
private suspend fun hasDuplicateLibraryMangas(mangas: List<Manga>): Boolean {
return mangas.fastAny { manga ->
getDuplicateLibraryManga.await(manga).isNotEmpty()
}
}
// KMK <-- // KMK <--
sealed interface Dialog { sealed interface Dialog {
@ -616,7 +632,7 @@ open class BrowseSourceScreenModel(
val mangas: List<Manga>, val mangas: List<Manga>,
val initialSelection: ImmutableList<CheckboxState<Category>>, val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog ) : Dialog
data object AllowDuplicate : Dialog data class AllowDuplicate(val duplicatedManga: Pair<Int, Manga>) : Dialog
// KMK <-- // KMK <--
} }

View file

@ -15,6 +15,7 @@ import eu.kanade.presentation.browse.SourceFeedScreen
import eu.kanade.presentation.browse.components.SourceFeedAddDialog import eu.kanade.presentation.browse.components.SourceFeedAddDialog
import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog
import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.manga.AllowDuplicateDialog
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
@ -109,6 +110,28 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
}, },
) )
} }
is SourceFeedScreenModel.Dialog.AllowDuplicate -> {
AllowDuplicateDialog(
onDismissRequest = onDismissRequest,
onAllowAllDuplicate = {
screenModel.addFavoriteDuplicate()
},
onSkipAllDuplicate = {
screenModel.addFavoriteDuplicate(skipAllDuplicates = true)
},
onOpenManga = {
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
},
onAllowDuplicate = {
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
},
onSkipDuplicate = {
screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
},
duplicatedName = dialog.duplicatedManga.second.title,
)
}
// KMK <-- // KMK <--
SourceFeedScreenModel.Dialog.Filter -> { SourceFeedScreenModel.Dialog.Filter -> {
SourceFilterDialog( SourceFilterDialog(

View file

@ -7,6 +7,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
@ -49,6 +50,7 @@ import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.source.interactor.CountFeedSavedSearchBySourceId import tachiyomi.domain.source.interactor.CountFeedSavedSearchBySourceId
@ -84,6 +86,7 @@ open class SourceFeedScreenModel(
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
// KMK <-- // KMK <--
) : StateScreenModel<SourceFeedState>(SourceFeedState()) { ) : StateScreenModel<SourceFeedState>(SourceFeedState()) {
@ -341,9 +344,19 @@ open class SourceFeedScreenModel(
} }
} }
fun addFavorite() { fun addFavorite(startIdx: Int = 0) {
screenModelScope.launch { screenModelScope.launch {
val mangaList = state.value.selection val mangaWithDup = getDuplicateLibraryManga(startIdx)
if (mangaWithDup != null)
setDialog(Dialog.AllowDuplicate(mangaWithDup))
else
addFavoriteDuplicate()
}
}
fun addFavoriteDuplicate(skipAllDuplicates: Boolean = false) {
screenModelScope.launch {
val mangaList = if (skipAllDuplicates) getNotDuplicateLibraryMangas() else state.value.selection
val categories = getCategories() val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get() val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
@ -380,6 +393,32 @@ open class SourceFeedScreenModel(
} }
} }
private suspend fun getNotDuplicateLibraryMangas(): List<DomainManga> {
return state.value.selection.filterNot { manga ->
getDuplicateLibraryManga.await(manga).isNotEmpty()
}
}
private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair<Int, DomainManga>? {
val mangas = state.value.selection
mangas.fastForEachIndexed { index, manga ->
if (index < startIdx) return@fastForEachIndexed
val dup = getDuplicateLibraryManga.await(manga)
if (dup.isEmpty()) return@fastForEachIndexed
return Pair(index, dup.first())
}
return null
}
fun removeDuplicateSelectedManga(index: Int) {
mutableState.update { state ->
val newSelection = state.selection.mutate { list ->
list.removeAt(index)
}
state.copy(selection = newSelection)
}
}
/** /**
* Bulk update categories of manga using old and new common categories. * Bulk update categories of manga using old and new common categories.
* *
@ -467,6 +506,7 @@ open class SourceFeedScreenModel(
val mangas: List<DomainManga>, val mangas: List<DomainManga>,
val initialSelection: ImmutableList<CheckboxState<Category>>, val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog() ) : Dialog()
data class AllowDuplicate(val duplicatedManga: Pair<Int, DomainManga>) : Dialog()
// KMK <-- // KMK <--
} }

View file

@ -678,6 +678,10 @@
<string name="remove_from_library">Remove from library</string> <string name="remove_from_library">Remove from library</string>
<string name="unknown_title">Unknown title</string> <string name="unknown_title">Unknown title</string>
<string name="confirm_add_duplicate_manga">You have an entry in your library with the same name.\n\nDo you still wish to continue?</string> <string name="confirm_add_duplicate_manga">You have an entry in your library with the same name.\n\nDo you still wish to continue?</string>
<string name="action_allow_duplicate_manga">Allow it</string>
<string name="action_skip_duplicate_manga">Skip it</string>
<string name="action_allow_all_duplicate_manga">Allow all</string>
<string name="action_skip_all_duplicate_manga">Skip all</string>
<string name="manga_added_library">Added to library</string> <string name="manga_added_library">Added to library</string>
<string name="manga_removed_library">Removed from library</string> <string name="manga_removed_library">Removed from library</string>
<string name="manga_info_expand">More</string> <string name="manga_info_expand">More</string>
@ -689,8 +693,6 @@
<string name="source_not_installed">Source not installed: %1$s</string> <string name="source_not_installed">Source not installed: %1$s</string>
<string name="snack_add_to_library">Add to library?</string> <string name="snack_add_to_library">Add to library?</string>
<string name="description_placeholder">No description</string> <string name="description_placeholder">No description</string>
<string name="action_allow_duplicate_manga">Allow duplicate</string>
<string name="action_skip_duplicate_manga">Skip duplicate</string>
<!-- Manga chapters --> <!-- Manga chapters -->
<string name="display_mode_chapter">Chapter %1$s</string> <string name="display_mode_chapter">Chapter %1$s</string>