Last Commit Merged: ec46b2281b
This commit is contained in:
LuftVerbot 2023-05-30 19:04:13 +02:00
parent e385b2bf67
commit 5d92a784d3
17 changed files with 163 additions and 173 deletions

View file

@ -1,13 +1,15 @@
package eu.kanade.presentation.browse
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CollectionsBookmark
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.Badge
import eu.kanade.tachiyomi.R
@Composable
fun InLibraryBadge(enabled: Boolean) {
if (enabled) {
Badge(text = stringResource(R.string.in_library))
Badge(
imageVector = Icons.Outlined.CollectionsBookmark,
)
}
}

View file

@ -4,6 +4,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -12,6 +15,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@ -45,3 +52,47 @@ fun Badge(
style = MaterialTheme.typography.bodySmall,
)
}
@Composable
fun Badge(
imageVector: ImageVector,
color: Color = MaterialTheme.colorScheme.secondary,
iconColor: Color = MaterialTheme.colorScheme.onSecondary,
shape: Shape = RectangleShape,
) {
val iconContentPlaceholder = "[icon]"
val text = buildAnnotatedString {
appendInlineContent(iconContentPlaceholder)
}
val inlineContent = mapOf(
Pair(
iconContentPlaceholder,
InlineTextContent(
Placeholder(
width = MaterialTheme.typography.bodySmall.fontSize,
height = MaterialTheme.typography.bodySmall.fontSize,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
),
) {
Icon(
imageVector = imageVector,
tint = iconColor,
contentDescription = null,
)
},
),
)
Text(
text = text,
inlineContent = inlineContent,
modifier = Modifier
.clip(shape)
.background(color)
.padding(horizontal = 3.dp, vertical = 1.dp),
color = iconColor,
fontWeight = FontWeight.Medium,
maxLines = 1,
style = MaterialTheme.typography.bodySmall,
)
}

View file

@ -1,13 +1,16 @@
package eu.kanade.presentation.animelib.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.Badge
import eu.kanade.tachiyomi.R
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.ThemePreviews
@Composable
fun DownloadsBadge(count: Int) {
fun DownloadsBadge(count: Long) {
if (count > 0) {
Badge(
text = "$count",
@ -18,7 +21,7 @@ fun DownloadsBadge(count: Int) {
}
@Composable
fun UnviewedBadge(count: Int) {
fun UnviewedBadge(count: Long) {
if (count > 0) {
Badge(text = "$count")
}
@ -31,9 +34,9 @@ fun LanguageBadge(
) {
if (isLocal) {
Badge(
text = stringResource(R.string.label_local),
imageVector = Icons.Outlined.Folder,
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
iconColor = MaterialTheme.colorScheme.onTertiary,
)
} else if (sourceLanguage.isNotEmpty()) {
Badge(
@ -43,3 +46,16 @@ fun LanguageBadge(
)
}
}
@ThemePreviews
@Composable
private fun BadgePreview() {
TachiyomiTheme {
Column {
DownloadsBadge(count = 10)
UnviewedBadge(count = 10)
LanguageBadge(isLocal = true, sourceLanguage = "EN")
LanguageBadge(isLocal = false, sourceLanguage = "EN")
}
}
}

View file

@ -51,8 +51,8 @@ fun AnimeLibraryComfortableGrid(
lastModified = anime.coverLastModified,
),
coverBadgeStart = {
DownloadsBadge(count = libraryItem.downloadCount.toInt())
UnviewedBadge(count = libraryItem.unseenCount.toInt())
DownloadsBadge(count = libraryItem.downloadCount)
UnviewedBadge(count = libraryItem.unseenCount)
},
coverBadgeEnd = {
LanguageBadge(

View file

@ -52,8 +52,8 @@ fun AnimeLibraryCompactGrid(
lastModified = anime.coverLastModified,
),
coverBadgeStart = {
DownloadsBadge(count = libraryItem.downloadCount.toInt())
UnviewedBadge(count = libraryItem.unseenCount.toInt())
DownloadsBadge(count = libraryItem.downloadCount)
UnviewedBadge(count = libraryItem.unseenCount)
},
coverBadgeEnd = {
LanguageBadge(

View file

@ -60,8 +60,8 @@ fun AnimeLibraryList(
lastModified = anime.coverLastModified,
),
badge = {
DownloadsBadge(count = libraryItem.downloadCount.toInt())
UnviewedBadge(count = libraryItem.unseenCount.toInt())
DownloadsBadge(count = libraryItem.downloadCount)
UnviewedBadge(count = libraryItem.unseenCount)
LanguageBadge(
isLocal = libraryItem.isLocal,
sourceLanguage = libraryItem.sourceLanguage,

View file

@ -51,8 +51,8 @@ fun MangaLibraryComfortableGrid(
lastModified = manga.coverLastModified,
),
coverBadgeStart = {
DownloadsBadge(count = libraryItem.downloadCount.toInt())
UnviewedBadge(count = libraryItem.unreadCount.toInt())
DownloadsBadge(count = libraryItem.downloadCount)
UnviewedBadge(count = libraryItem.unreadCount)
},
coverBadgeEnd = {
LanguageBadge(

View file

@ -52,8 +52,8 @@ fun MangaLibraryCompactGrid(
lastModified = manga.coverLastModified,
),
coverBadgeStart = {
DownloadsBadge(count = libraryItem.downloadCount.toInt())
UnviewedBadge(count = libraryItem.unreadCount.toInt())
DownloadsBadge(count = libraryItem.downloadCount)
UnviewedBadge(count = libraryItem.unreadCount)
},
coverBadgeEnd = {
LanguageBadge(

View file

@ -60,8 +60,8 @@ fun MangaLibraryList(
lastModified = manga.coverLastModified,
),
badge = {
DownloadsBadge(count = libraryItem.downloadCount.toInt())
UnviewedBadge(count = libraryItem.unreadCount.toInt())
DownloadsBadge(count = libraryItem.downloadCount)
UnviewedBadge(count = libraryItem.unreadCount)
LanguageBadge(
isLocal = libraryItem.isLocal,
sourceLanguage = libraryItem.sourceLanguage,

View file

@ -6,17 +6,14 @@ import tachiyomi.domain.library.anime.LibraryAnime
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class AnimeLibraryItem(
data class AnimeLibraryItem(
val libraryAnime: LibraryAnime,
var downloadCount: Long = -1,
var unseenCount: Long = -1,
var isLocal: Boolean = false,
var sourceLanguage: String = "",
private val sourceManager: AnimeSourceManager = Injekt.get(),
) {
var displayMode: Long = -1
var downloadCount: Long = -1
var unseenCount: Long = -1
var isLocal = false
var sourceLanguage = ""
/**
* Checks if a query matches the anime
*
@ -25,73 +22,34 @@ class AnimeLibraryItem(
*/
fun matches(constraint: String): Boolean {
val sourceName by lazy { sourceManager.getOrStub(libraryAnime.anime.source).getNameForAnimeInfo() }
val genres by lazy { libraryAnime.anime.genre }
return libraryAnime.anime.title.contains(constraint, true) ||
(libraryAnime.anime.author?.contains(constraint, true) ?: false) ||
(libraryAnime.anime.artist?.contains(constraint, true) ?: false) ||
(libraryAnime.anime.description?.contains(constraint, true) ?: false) ||
if (constraint.contains(",")) {
constraint.split(",").all { containsSourceOrGenre(it.trim(), sourceName, genres) }
} else {
containsSourceOrGenre(constraint, sourceName, genres)
constraint.split(",").map { it.trim() }.all { subconstraint ->
checkNegatableConstraint(subconstraint) {
sourceName.contains(it, true) ||
(libraryAnime.anime.genre?.any { genre -> genre.equals(it, true) } ?: false)
}
}
}
/**
* Filters an anime by checking whether the query is the anime's source OR part of
* the genres of the anime
* Checking for genre is done only if the query isn't part of the source name.
* Checks a predicate on a negatable constraint. If the constraint starts with a minus character,
* the minus is stripped and the result of the predicate is inverted.
*
* @param query the query to check
* @param sourceName name of the anime's source
* @param genres list containing anime's genres
* @param constraint the argument to the predicate. Inverts the predicate if it starts with '-'.
* @param predicate the check to be run against the constraint.
* @return !predicate(x) if constraint = "-x", otherwise predicate(constraint)
*/
private fun containsSourceOrGenre(query: String, sourceName: String, genres: List<String>?): Boolean {
val minus = query.startsWith("-")
val tag = if (minus) { query.substringAfter("-") } else query
return when (sourceName.contains(tag, true)) {
false -> containsGenre(query, genres)
else -> !minus
}
}
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
return if (tag.startsWith("-")) {
genres?.find {
it.trim().equals(tag.substringAfter("-"), ignoreCase = true)
} == null
private fun checkNegatableConstraint(
constraint: String,
predicate: (String) -> Boolean,
): Boolean {
return if (constraint.startsWith("-")) {
!predicate(constraint.substringAfter("-").trimStart())
} else {
genres?.find {
it.trim().equals(tag, ignoreCase = true)
} != null
predicate(constraint)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AnimeLibraryItem
if (libraryAnime != other.libraryAnime) return false
if (sourceManager != other.sourceManager) return false
if (displayMode != other.displayMode) return false
if (downloadCount != other.downloadCount) return false
if (unseenCount != other.unseenCount) return false
if (isLocal != other.isLocal) return false
if (sourceLanguage != other.sourceLanguage) return false
return true
}
override fun hashCode(): Int {
var result = libraryAnime.hashCode()
result = 31 * result + sourceManager.hashCode()
result = 31 * result + displayMode.hashCode()
result = 31 * result + downloadCount.toInt()
result = 31 * result + unseenCount.toInt()
result = 31 * result + isLocal.hashCode()
result = 31 * result + sourceLanguage.hashCode()
return result
}
}

View file

@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.update
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.data.entries.anime.libraryAnime
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.entries.anime.model.Anime
@ -372,20 +373,21 @@ class AnimeLibraryScreenModel(
animelibAnimeList
.map { animelibAnime ->
// Display mode based on user preference: take it from global library setting or category
AnimeLibraryItem(animelibAnime).apply {
AnimeLibraryItem(
animelibAnime,
downloadCount = if (prefs.downloadBadge) {
downloadManager.getDownloadCount(animelibAnime.anime).toLong()
} else {
0
}
unseenCount = animelibAnime.unseenCount
isLocal = if (prefs.localBadge) animelibAnime.anime.isLocal() else false
},
unseenCount = animelibAnime.unseenCount,
isLocal = if (prefs.localBadge) animelibAnime.anime.isLocal() else false,
sourceLanguage = if (prefs.languageBadge) {
sourceManager.getOrStub(animelibAnime.anime.source).lang
} else {
""
}
}
},
)
}
.groupBy { it.libraryAnime.category }
}

View file

@ -8,15 +8,12 @@ import uy.kohesive.injekt.api.get
class MangaLibraryItem(
val libraryManga: LibraryManga,
var downloadCount: Long = -1,
var unreadCount: Long = -1,
var isLocal: Boolean = false,
var sourceLanguage: String = "",
private val sourceManager: MangaSourceManager = Injekt.get(),
) {
var displayMode: Long = -1
var downloadCount: Long = -1
var unreadCount: Long = -1
var isLocal = false
var sourceLanguage = ""
/**
* Checks if a query matches the manga
*
@ -25,73 +22,34 @@ class MangaLibraryItem(
*/
fun matches(constraint: String): Boolean {
val sourceName by lazy { sourceManager.getOrStub(libraryManga.manga.source).getNameForMangaInfo() }
val genres by lazy { libraryManga.manga.genre }
return libraryManga.manga.title.contains(constraint, true) ||
(libraryManga.manga.author?.contains(constraint, true) ?: false) ||
(libraryManga.manga.artist?.contains(constraint, true) ?: false) ||
(libraryManga.manga.description?.contains(constraint, true) ?: false) ||
if (constraint.contains(",")) {
constraint.split(",").all { containsSourceOrGenre(it.trim(), sourceName, genres) }
} else {
containsSourceOrGenre(constraint, sourceName, genres)
constraint.split(",").map { it.trim() }.all { subconstraint ->
checkNegatableConstraint(subconstraint) {
sourceName.contains(it, true) ||
(libraryManga.manga.genre?.any { genre -> genre.equals(it, true) } ?: false)
}
}
}
/**
* Filters a manga by checking whether the query is the manga's source OR part of
* the genres of the manga
* Checking for genre is done only if the query isn't part of the source name.
* Checks a predicate on a negatable constraint. If the constraint starts with a minus character,
* the minus is stripped and the result of the predicate is inverted.
*
* @param query the query to check
* @param sourceName name of the manga's source
* @param genres list containing manga's genres
* @param constraint the argument to the predicate. Inverts the predicate if it starts with '-'.
* @param predicate the check to be run against the constraint.
* @return !predicate(x) if constraint = "-x", otherwise predicate(constraint)
*/
private fun containsSourceOrGenre(query: String, sourceName: String, genres: List<String>?): Boolean {
val minus = query.startsWith("-")
val tag = if (minus) { query.substringAfter("-") } else query
return when (sourceName.contains(tag, true)) {
false -> containsGenre(query, genres)
else -> !minus
}
}
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
return if (tag.startsWith("-")) {
genres?.find {
it.trim().equals(tag.substringAfter("-"), ignoreCase = true)
} == null
private fun checkNegatableConstraint(
constraint: String,
predicate: (String) -> Boolean,
): Boolean {
return if (constraint.startsWith("-")) {
!predicate(constraint.substringAfter("-").trimStart())
} else {
genres?.find {
it.trim().equals(tag, ignoreCase = true)
} != null
predicate(constraint)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaLibraryItem
if (libraryManga != other.libraryManga) return false
if (sourceManager != other.sourceManager) return false
if (displayMode != other.displayMode) return false
if (downloadCount != other.downloadCount) return false
if (unreadCount != other.unreadCount) return false
if (isLocal != other.isLocal) return false
if (sourceLanguage != other.sourceLanguage) return false
return true
}
override fun hashCode(): Int {
var result = libraryManga.hashCode()
result = 31 * result + sourceManager.hashCode()
result = 31 * result + displayMode.hashCode()
result = 31 * result + downloadCount.toInt()
result = 31 * result + unreadCount.toInt()
result = 31 * result + isLocal.hashCode()
result = 31 * result + sourceLanguage.hashCode()
return result
}
}

View file

@ -372,20 +372,21 @@ class MangaLibraryScreenModel(
libraryMangaList
.map { libraryManga ->
// Display mode based on user preference: take it from global library setting or category
MangaLibraryItem(libraryManga).apply {
MangaLibraryItem(
libraryManga,
downloadCount = if (prefs.downloadBadge) {
downloadManager.getDownloadCount(libraryManga.manga).toLong()
} else {
0
}
unreadCount = libraryManga.unreadCount
isLocal = if (prefs.localBadge) libraryManga.manga.isLocal() else false
},
unreadCount = libraryManga.unreadCount,
isLocal = if (prefs.localBadge) libraryManga.manga.isLocal() else false,
sourceLanguage = if (prefs.languageBadge) {
sourceManager.getOrStub(libraryManga.manga.source).lang
} else {
""
}
}
},
)
}
.groupBy { it.libraryManga.category }
}

View file

@ -872,7 +872,7 @@ class ReaderActivity : BaseActivity() {
* the viewer is reaching the beginning or end of a chapter or the transition page is active.
*/
fun requestPreloadChapter(chapter: ReaderChapter) {
lifecycleScope.launch { viewModel.preloadChapter(chapter) }
lifecycleScope.launchIO { viewModel.preloadChapter(chapter) }
}
/**

View file

@ -365,6 +365,10 @@ class ReaderViewModel(
* that the user doesn't have to wait too long to continue reading.
*/
private suspend fun preload(chapter: ReaderChapter) {
if (chapter.state is ReaderChapter.State.Loaded || chapter.state == ReaderChapter.State.Loading) {
return
}
if (chapter.pageLoader is HttpPageLoader) {
val manga = manga ?: return
val dbChapter = chapter.chapter
@ -384,20 +388,17 @@ class ReaderViewModel(
return
}
logcat { "Preloading ${chapter.chapter.url}" }
val loader = loader ?: return
withIOContext {
try {
loader.loadChapter(chapter)
} catch (e: Throwable) {
if (e is CancellationException) {
throw e
}
return@withIOContext
try {
logcat { "Preloading ${chapter.chapter.url}" }
loader.loadChapter(chapter)
} catch (e: Throwable) {
if (e is CancellationException) {
throw e
}
eventChannel.trySend(Event.ReloadViewerChapters)
return
}
eventChannel.trySend(Event.ReloadViewerChapters)
}
/**

View file

@ -173,7 +173,7 @@
<string name="sort_by_episode_number">By episode number</string>
<plurals name="download_amount_anime">
<item quantity="one">Next episode</item>
<item quantity="other">Next %d episode</item>
<item quantity="other">Next %d episodes</item>
</plurals>
<string name="download_unseen">Unseen</string>
<string name="confirm_delete_episodes">Are you sure you want to delete the selected episodes?</string>

View file

@ -757,6 +757,7 @@
<string name="migration_dialog_what_to_include">Select data to include</string>
<string name="migration_selection_prompt">Select a source to migrate from</string>
<string name="migrate">Migrate</string>
<!-- Make a copy (noun) when migrating. Don't use for copying to clipboard. -->
<string name="copy">Copy</string>
<string name="empty_screen">Well, this is awkward</string>
<string name="not_installed">Not installed</string>