Compare commits

...

23 Commits

Author SHA1 Message Date
5b12c144da Release v0.14.1 2022-10-29 11:51:25 -04:00
f38130d086 Translations update from Hosted Weblate (#8316)
Weblate translations

Co-authored-by: Abay Emes <abayemes@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Gediminas Murauskas <muziejusinfo@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jetspectre <jetspectre1@gmail.com>
Co-authored-by: Karl Stenlund <hikolo.92@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/kk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translation: Tachiyomi/Tachiyomi 0.x

Co-authored-by: Abay Emes <abayemes@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Gediminas Murauskas <muziejusinfo@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jetspectre <jetspectre1@gmail.com>
Co-authored-by: Karl Stenlund <hikolo.92@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: jinu147 <nesqea20@gmail.com>
2022-10-29 11:50:17 -04:00
4b60138d41 Clean up strings and icons (#8326)
* Clean up strings and icons

* fix incorrect usages of label_more

* restore strings and reduce usage of android.R

* removing icon desc of FABs anyway as app's not for visual impaired users
2022-10-29 11:43:51 -04:00
fde7bfa3d1 Show notification while download cache is renewing
Since users seem to be confused now that the library loads before download info is shown...
2022-10-29 11:39:04 -04:00
69635ee66a Make Compose DropdownMenu overlap the trigger
Closes #8329
2022-10-29 10:37:51 -04:00
224f29077d Sort library items alphabetically in secondary pass
Fixes #7461
2022-10-29 10:11:12 -04:00
e1ab1fdb65 Prompt Extension update if ext-lib is updated
Co-authored-by: arkon <arkon@users.noreply.github.com>
2022-10-29 10:05:30 -04:00
3e86cb094b PreferenceModel: Add subtitle provider to ListPreference (#8322)
* PreferenceModel: Add subtitle provider to ListPreference

So that it's possible to avoid value formatting when needed

* cleanups
2022-10-29 09:44:12 -04:00
9fbd3fe33f build: Add param to generate Compose compiler metrics (#8330)
./gradlew assembledevPreview -Ptachiyomi.enableComposeCompilerMetrics=true
2022-10-29 09:37:48 -04:00
073e9f94ff Reorder parameters of JSON parsing method (#8321) 2022-10-28 22:44:31 -04:00
64c0d9506d Update dependency androidx.paging:paging-compose to v1.0.0-alpha17 (#8319)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-28 22:09:13 -04:00
f638092ab9 Update voyager to v1.0.0-rc05 (#8320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-28 22:09:04 -04:00
d0c4463ab3 Avoid concurrency crashes in SourceManager 2022-10-28 21:29:38 -04:00
ad107860b9 Consider downloaded only mode when getting download counts in library
Fixes #8318
2022-10-28 21:29:25 -04:00
5efb31bd71 Fix some crashes 2022-10-28 21:10:03 -04:00
e4a2f35907 Fix library download counts not being loaded if downloaded filter is in exclusion state 2022-10-28 19:05:55 -04:00
e49781de7a Reword "manga" to more generic "entry"/"entries"
Closes #8306
2022-10-28 18:49:46 -04:00
37cb4ec0c2 Don't filter out partially read chapters when marking as unread
Fixes #8313
2022-10-28 18:29:00 -04:00
401134fa8e Use MaterialTheme.shapes in more places 2022-10-28 16:18:05 -04:00
87391832ba Touch up manga grid/list items (#8307)
* Touch up library item touch indicator

Now the touch indicator has the same coverage as the selection indicator.
Experimental Modifier.Node API is used to draw the selection indicator

* Unify library and browse source list item layouts
2022-10-28 11:46:10 -04:00
e36d31bf0f Cleanup Library presenter (#8284)
* yeet observable + minor cleanup

* move [getTracksFlow] to domain

* Lint

* Review changes

Co-Authored-By: Andreas <6576096+ghostbear@users.noreply.github.com>

* Review Changes 2

* Stuff

* Rename + Rebase

* Lint

Co-authored-by: Andreas <6576096+ghostbear@users.noreply.github.com>
2022-10-28 11:44:05 -04:00
37b7efbc87 WebView for chapter link (#8281)
* backup

* doing logic

* cleanup

* applying suggestion

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>

* requested changes

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2022-10-28 11:41:51 -04:00
6e4a30e593 Fix "Download split" not working while using SD card (#8305)
* Fix "Download split" not working while using SD card

* Update app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt

Co-authored-by: arkon <arkon@users.noreply.github.com>
2022-10-28 11:40:43 -04:00
122 changed files with 1313 additions and 1435 deletions

View File

@ -3,7 +3,7 @@
I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v0.14.0)
- To the latest version of the app (stable is v0.14.1)
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions

View File

@ -53,7 +53,7 @@ body:
label: Tachiyomi version
description: You can find your Tachiyomi version in **More → About**.
placeholder: |
Example: "0.14.0"
Example: "0.14.1"
validations:
required: true
@ -98,7 +98,7 @@ body:
required: true
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
required: true
- label: I have updated the app to version **[0.14.0](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
- label: I have updated the app to version **[0.14.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true
- label: I have updated all installed extensions.
required: true

View File

@ -33,7 +33,7 @@ body:
required: true
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
required: true
- label: I have updated the app to version **[0.14.0](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
- label: I have updated the app to version **[0.14.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@ -27,8 +27,8 @@ android {
applicationId = "eu.kanade.tachiyomi"
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
versionCode = 89
versionName = "0.14.0"
versionCode = 90
versionName = "0.14.1"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -329,6 +329,19 @@ tasks {
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_metrics"
)
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_metrics"
)
}
}
preBuild {

View File

@ -63,6 +63,7 @@ import eu.kanade.domain.source.repository.SourceDataRepository
import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.domain.track.interactor.DeleteTrack
import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.GetTracksPerManga
import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.repository.TrackRepository
import eu.kanade.domain.updates.interactor.GetUpdates
@ -104,6 +105,7 @@ class DomainModule : InjektModule {
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { DeleteTrack(get()) }
addFactory { GetTracksPerManga(get()) }
addFactory { GetTracks(get()) }
addFactory { InsertTrack(get()) }

View File

@ -27,7 +27,12 @@ class SetReadStatus(
}
suspend fun await(read: Boolean, vararg chapters: Chapter): Result = withNonCancellableContext {
val chaptersToUpdate = chapters.filterNot { it.read == read }
val chaptersToUpdate = chapters.filter {
when (read) {
true -> !it.read
false -> it.read || it.lastPageRead > 0
}
}
if (chaptersToUpdate.isEmpty()) {
return@withNonCancellableContext Result.NoChapters
}

View File

@ -21,8 +21,8 @@ class GetNextChapter(
}
suspend fun await(mangaId: Long, chapterId: Long): Chapter? {
val chapter = getChapter.await(chapterId)!!
val manga = getManga.await(mangaId)!!
val chapter = getChapter.await(chapterId) ?: return null
val manga = getManga.await(mangaId) ?: return null
if (!chapter.read) return chapter

View File

@ -19,10 +19,6 @@ class GetTracks(
}
}
fun subscribe(): Flow<List<Track>> {
return trackRepository.getTracksAsFlow()
}
fun subscribe(mangaId: Long): Flow<List<Track>> {
return trackRepository.getTracksByMangaIdAsFlow(mangaId)
}

View File

@ -0,0 +1,20 @@
package eu.kanade.domain.track.interactor
import eu.kanade.domain.track.repository.TrackRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class GetTracksPerManga(
private val trackRepository: TrackRepository,
) {
fun subscribe(): Flow<Map<Long, List<Long>>> {
return trackRepository.getTracksAsFlow().map { tracks ->
tracks
.groupBy { it.mangaId }
.mapValues { entry ->
entry.value.map { it.syncId }
}
}
}
}

View File

@ -13,12 +13,12 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
@ -89,7 +89,7 @@ fun BrowseSourceScreen(
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
BrowseSourceToolbar(
state = presenter,
source = presenter.source!!,
source = presenter.source,
displayMode = presenter.displayMode,
onDisplayModeChange = { presenter.displayMode = it },
navigateUp = navigateUp,
@ -253,7 +253,7 @@ fun BrowseSourceContent(
listOf(
EmptyScreenAction(
stringResId = R.string.local_source_help_guide,
icon = Icons.Default.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = onLocalSourceHelpClick,
),
)
@ -261,17 +261,17 @@ fun BrowseSourceContent(
listOf(
EmptyScreenAction(
stringResId = R.string.action_retry,
icon = Icons.Default.Refresh,
icon = Icons.Outlined.Refresh,
onClick = mangaList::refresh,
),
EmptyScreenAction(
stringResId = R.string.action_open_in_web_view,
icon = Icons.Default.Public,
icon = Icons.Outlined.Public,
onClick = onWebViewClick,
),
EmptyScreenAction(
stringResId = R.string.label_help,
icon = Icons.Default.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = onHelpClick,
),
)

View File

@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
@ -368,7 +368,7 @@ private fun ExtensionItemActions(
} else {
IconButton(onClick = { onClickItemCancel(extension) }) {
Icon(
imageVector = Icons.Default.Close,
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_cancel),
)
}

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Dangerous
import androidx.compose.material.icons.filled.Warning
@ -48,7 +47,7 @@ fun SourceIcon(
when {
source.isStub && icon == null -> {
Image(
imageVector = Icons.Default.Warning,
imageVector = Icons.Filled.Warning,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
modifier = modifier.then(defaultModifier),
@ -85,7 +84,7 @@ fun ExtensionIcon(
placeholder = ColorPainter(Color(0x1F888888)),
error = rememberResourceBitmapPainter(id = R.drawable.cover_error),
modifier = modifier
.clip(RoundedCornerShape(4.dp)),
.clip(MaterialTheme.shapes.extraSmall),
)
}
is Extension.Installed -> {
@ -105,7 +104,7 @@ fun ExtensionIcon(
}
}
is Extension.Untrusted -> Image(
imageVector = Icons.Default.Dangerous,
imageVector = Icons.Filled.Dangerous,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
modifier = modifier.then(defaultModifier),

View File

@ -1,28 +1,22 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.Badge
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.library.components.MangaGridComfortableText
import eu.kanade.presentation.library.components.MangaGridCover
import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.MangaComfortableGridItem
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
@ -37,9 +31,9 @@ fun BrowseSourceComfortableGrid(
) {
LazyVerticalGrid(
columns = columns,
contentPadding = PaddingValues(8.dp, 4.dp) + contentPadding,
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = contentPadding + PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
) {
if (mangaList.loadState.prepend is LoadState.Loading) {
item(span = { GridItemSpan(maxLineSpan) }) {
@ -71,36 +65,22 @@ fun BrowseSourceComfortableGridItem(
onClick: () -> Unit = {},
onLongClick: () -> Unit = onClick,
) {
val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
Column(
modifier = Modifier
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
),
) {
MangaGridCover(
cover = {
MangaCover.Book(
modifier = Modifier
.fillMaxWidth()
.drawWithContent {
drawContent()
if (manga.favorite) {
drawRect(overlayColor)
}
},
data = manga.thumbnailUrl,
)
},
badgesStart = {
if (manga.favorite) {
Badge(text = stringResource(R.string.in_library))
}
},
)
MangaGridComfortableText(
text = manga.title,
)
}
MangaComfortableGridItem(
title = manga.title,
coverData = MangaCover(
mangaId = manga.id,
sourceId = manga.source,
isMangaFavorite = manga.favorite,
url = manga.thumbnailUrl,
lastModified = manga.coverLastModified,
),
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
coverBadgeStart = {
if (manga.favorite) {
Badge(text = stringResource(R.string.in_library))
}
},
onLongClick = onLongClick,
onClick = onClick,
)
}

View File

@ -1,35 +1,22 @@
package eu.kanade.presentation.browse.components
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.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.Badge
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.library.components.MangaGridCompactText
import eu.kanade.presentation.library.components.MangaGridCover
import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.MangaCompactGridItem
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
@ -44,12 +31,12 @@ fun BrowseSourceCompactGrid(
) {
LazyVerticalGrid(
columns = columns,
contentPadding = PaddingValues(8.dp, 4.dp) + contentPadding,
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = contentPadding + PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
) {
item(span = { GridItemSpan(maxLineSpan) }) {
if (mangaList.loadState.prepend is LoadState.Loading) {
if (mangaList.loadState.prepend is LoadState.Loading) {
item(span = { GridItemSpan(maxLineSpan) }) {
BrowseSourceLoadingItem()
}
}
@ -64,8 +51,8 @@ fun BrowseSourceCompactGrid(
)
}
item(span = { GridItemSpan(maxLineSpan) }) {
if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
item(span = { GridItemSpan(maxLineSpan) }) {
BrowseSourceLoadingItem()
}
}
@ -73,57 +60,27 @@ fun BrowseSourceCompactGrid(
}
@Composable
fun BrowseSourceCompactGridItem(
private fun BrowseSourceCompactGridItem(
manga: Manga,
onClick: () -> Unit = {},
onLongClick: () -> Unit = onClick,
) {
val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
MangaGridCover(
modifier = Modifier
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
),
cover = {
MangaCover.Book(
modifier = Modifier
.fillMaxHeight()
.drawWithContent {
drawContent()
if (manga.favorite) {
drawRect(overlayColor)
}
},
data = eu.kanade.domain.manga.model.MangaCover(
manga.id,
manga.source,
manga.favorite,
manga.thumbnailUrl,
manga.coverLastModified,
),
)
},
badgesStart = {
MangaCompactGridItem(
title = manga.title,
coverData = MangaCover(
mangaId = manga.id,
sourceId = manga.source,
isMangaFavorite = manga.favorite,
url = manga.thumbnailUrl,
lastModified = manga.coverLastModified,
),
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
coverBadgeStart = {
if (manga.favorite) {
Badge(text = stringResource(R.string.in_library))
}
},
content = {
Box(
modifier = Modifier
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
.background(
Brush.verticalGradient(
0f to Color.Transparent,
1f to Color(0xAA000000),
),
)
.fillMaxHeight(0.33f)
.fillMaxWidth()
.align(Alignment.BottomCenter),
)
MangaGridCompactText(manga.title)
},
onLongClick = onLongClick,
onClick = onClick,
)
}

View File

@ -20,7 +20,7 @@ fun RemoveMangaDialog(
onDismissRequest = onDismissRequest,
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {

View File

@ -1,26 +1,21 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.items
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.Badge
import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.library.components.MangaListItem
import eu.kanade.presentation.library.components.MangaListItemContent
import eu.kanade.presentation.components.MangaListItem
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.verticalPadding
import eu.kanade.tachiyomi.R
@Composable
@ -32,7 +27,7 @@ fun BrowseSourceList(
onMangaLongClick: (Manga) -> Unit,
) {
LazyColumn(
contentPadding = contentPadding,
contentPadding = contentPadding + PaddingValues(vertical = 8.dp),
) {
item {
if (mangaList.loadState.prepend is LoadState.Loading) {
@ -64,31 +59,22 @@ fun BrowseSourceListItem(
onClick: () -> Unit = {},
onLongClick: () -> Unit = onClick,
) {
val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
MangaListItem(
coverContent = {
MangaCover.Square(
modifier = Modifier
.padding(vertical = verticalPadding)
.fillMaxHeight()
.drawWithContent {
drawContent()
if (manga.favorite) {
drawRect(overlayColor)
}
},
data = manga.thumbnailUrl,
)
},
onClick = onClick,
onLongClick = onLongClick,
badges = {
title = manga.title,
coverData = MangaCover(
mangaId = manga.id,
sourceId = manga.source,
isMangaFavorite = manga.favorite,
url = manga.thumbnailUrl,
lastModified = manga.coverLastModified,
),
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
badge = {
if (manga.favorite) {
Badge(text = stringResource(R.string.in_library))
}
},
content = {
MangaListItemContent(text = manga.title)
},
onLongClick = onLongClick,
onClick = onClick,
)
}

View File

@ -33,7 +33,7 @@ import eu.kanade.tachiyomi.source.LocalSource
@Composable
fun BrowseSourceToolbar(
state: BrowseSourceState,
source: CatalogueSource,
source: CatalogueSource?,
displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit,
@ -44,7 +44,7 @@ fun BrowseSourceToolbar(
) {
if (state.searchQuery == null) {
BrowseSourceRegularToolbar(
title = if (state.isUserQuery) state.currentFilter.query else source.name,
title = if (state.isUserQuery) state.currentFilter.query else source?.name.orEmpty(),
isLocalSource = source is LocalSource,
displayMode = displayMode,
onDisplayModeChange = onDisplayModeChange,

View File

@ -130,7 +130,7 @@ fun CategoryDeleteDialog(
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
title = {

View File

@ -18,8 +18,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.R
@Composable
fun CategoryListItem(
@ -64,10 +66,10 @@ fun CategoryListItem(
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) {
Icon(imageVector = Icons.Outlined.Edit, contentDescription = "")
Icon(imageVector = Icons.Outlined.Edit, contentDescription = stringResource(R.string.action_rename_category))
}
IconButton(onClick = onDelete) {
Icon(imageVector = Icons.Outlined.Delete, contentDescription = "")
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(R.string.action_delete))
}
}
}

View File

@ -12,11 +12,9 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -55,7 +53,7 @@ fun AppBar(
subtitle: String? = null,
// Up button
navigateUp: (() -> Unit)? = null,
navigationIcon: ImageVector = Icons.Default.ArrowBack,
navigationIcon: ImageVector = Icons.Outlined.ArrowBack,
// Menu
actions: @Composable RowScope.() -> Unit = {},
// Action mode
@ -105,7 +103,7 @@ fun AppBar(
titleContent: @Composable () -> Unit,
// Up button
navigateUp: (() -> Unit)? = null,
navigationIcon: ImageVector = Icons.Default.ArrowBack,
navigationIcon: ImageVector = Icons.Outlined.ArrowBack,
// Menu
actions: @Composable RowScope.() -> Unit = {},
// Action mode
@ -125,7 +123,7 @@ fun AppBar(
if (isActionMode) {
IconButton(onClick = onCancelActionMode) {
Icon(
imageVector = Icons.Default.Close,
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_cancel),
)
}
@ -200,7 +198,7 @@ fun AppBarActions(
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
if (overflowActions.isNotEmpty()) {
IconButton(onClick = { showMenu = !showMenu }) {
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.label_more))
Icon(Icons.Outlined.MoreVert, contentDescription = stringResource(R.string.abc_action_menu_overflow_description))
}
DropdownMenu(

View File

@ -4,7 +4,6 @@ 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.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -19,7 +18,7 @@ import androidx.compose.ui.unit.dp
@Composable
fun BadgeGroup(
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(4.dp),
shape: Shape = MaterialTheme.shapes.extraSmall,
content: @Composable RowScope.() -> Unit,
) {
Row(modifier = modifier.clip(shape)) {

View File

@ -68,7 +68,7 @@ fun ChangeCategoryDialog(
}
Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
TextButton(
onClick = {

View File

@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ErrorOutline
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
@ -78,7 +78,7 @@ private fun NotDownloadedIndicator(
) {
Icon(
painter = painterResource(id = R.drawable.ic_download_chapter_24dp),
contentDescription = null,
contentDescription = stringResource(R.string.manga_download),
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -148,7 +148,7 @@ private fun DownloadingIndicator(
)
}
Icon(
imageVector = Icons.Default.ArrowDownward,
imageVector = Icons.Outlined.ArrowDownward,
contentDescription = null,
modifier = ArrowModifier,
tint = arrowColor,
@ -172,7 +172,7 @@ private fun DownloadedIndicator(
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.CheckCircle,
imageVector = Icons.Filled.CheckCircle,
contentDescription = null,
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
@ -204,8 +204,8 @@ private fun ErrorIndicator(
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.ErrorOutline,
contentDescription = null,
imageVector = Icons.Outlined.ErrorOutline,
contentDescription = stringResource(R.string.chapter_error),
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.error,
)

View File

@ -0,0 +1,317 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.modifierElementOf
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.util.selectedBackground
object CommonMangaItemDefaults {
val GridHorizontalSpacer = 4.dp
val GridVerticalSpacer = 4.dp
const val BrowseFavoriteCoverAlpha = 0.34f
}
private const val GridSelectedCoverAlpha = 0.76f
/**
* Layout of grid list item with title overlaying the cover.
* Accepts null [title] for a cover-only view.
*/
@Composable
fun MangaCompactGridItem(
isSelected: Boolean = false,
title: String? = null,
coverData: eu.kanade.domain.manga.model.MangaCover,
coverAlpha: Float = 1f,
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
onLongClick: () -> Unit,
onClick: () -> Unit,
) {
GridItemSelectable(
isSelected = isSelected,
onClick = onClick,
onLongClick = onLongClick,
) {
MangaGridCover(
cover = {
MangaCover.Book(
modifier = Modifier
.fillMaxWidth()
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
data = coverData,
)
},
badgesStart = coverBadgeStart,
badgesEnd = coverBadgeEnd,
content = {
if (title != null) {
CoverTextOverlay(title = title)
}
},
)
}
}
/**
* Title overlay for [MangaCompactGridItem]
*/
@Composable
private fun BoxScope.CoverTextOverlay(title: String) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
.background(
Brush.verticalGradient(
0f to Color.Transparent,
1f to Color(0xAA000000),
),
)
.fillMaxHeight(0.33f)
.fillMaxWidth()
.align(Alignment.BottomCenter),
)
GridItemTitle(
modifier = Modifier
.padding(8.dp)
.align(Alignment.BottomStart),
title = title,
style = MaterialTheme.typography.titleSmall.copy(
color = Color.White,
shadow = Shadow(
color = Color.Black,
blurRadius = 4f,
),
),
)
}
/**
* Layout of grid list item with title below the cover.
*/
@Composable
fun MangaComfortableGridItem(
isSelected: Boolean = false,
title: String,
coverData: eu.kanade.domain.manga.model.MangaCover,
coverAlpha: Float = 1f,
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
onLongClick: () -> Unit,
onClick: () -> Unit,
) {
GridItemSelectable(
isSelected = isSelected,
onClick = onClick,
onLongClick = onLongClick,
) {
Column {
MangaGridCover(
cover = {
MangaCover.Book(
modifier = Modifier
.fillMaxWidth()
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
data = coverData,
)
},
badgesStart = coverBadgeStart,
badgesEnd = coverBadgeEnd,
)
GridItemTitle(
modifier = Modifier.padding(4.dp),
title = title,
style = MaterialTheme.typography.titleSmall,
)
}
}
}
/**
* Common cover layout to add contents to be drawn on top of the cover.
*/
@Composable
private fun MangaGridCover(
modifier: Modifier = Modifier,
cover: @Composable BoxScope.() -> Unit = {},
badgesStart: (@Composable RowScope.() -> Unit)? = null,
badgesEnd: (@Composable RowScope.() -> Unit)? = null,
content: @Composable (BoxScope.() -> Unit)? = null,
) {
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(MangaCover.Book.ratio),
) {
cover()
content?.invoke(this)
if (badgesStart != null) {
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
content = badgesStart,
)
}
if (badgesEnd != null) {
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopEnd),
content = badgesEnd,
)
}
}
}
@Composable
private fun GridItemTitle(
modifier: Modifier,
title: String,
style: TextStyle,
) {
Text(
modifier = modifier,
text = title,
fontSize = 12.sp,
lineHeight = 18.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = style,
)
}
/**
* Wrapper for grid items to handle selection state, click and long click.
*/
@Composable
private fun GridItemSelectable(
modifier: Modifier = Modifier,
isSelected: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
content: @Composable () -> Unit,
) {
Box(
modifier = modifier
.clip(MaterialTheme.shapes.small)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.selectedOutline(isSelected = isSelected, color = MaterialTheme.colorScheme.secondary)
.padding(4.dp),
) {
val contentColor = if (isSelected) {
MaterialTheme.colorScheme.onSecondary
} else {
LocalContentColor.current
}
CompositionLocalProvider(LocalContentColor provides contentColor) {
content()
}
}
}
/**
* @see GridItemSelectable
*/
private fun Modifier.selectedOutline(
isSelected: Boolean,
color: Color,
): Modifier {
class SelectedOutlineNode(var selected: Boolean, var color: Color) : DrawModifierNode, Modifier.Node() {
override fun ContentDrawScope.draw() {
if (selected) drawRect(color)
drawContent()
}
}
return this then modifierElementOf(
params = isSelected.hashCode() + color.hashCode(),
create = { SelectedOutlineNode(isSelected, color) },
update = {
it.selected = isSelected
it.color = color
},
definitions = {
name = "selectionOutline"
properties["isSelected"] = isSelected
properties["color"] = color
},
)
}
/**
* Layout of list item.
*/
@Composable
fun MangaListItem(
isSelected: Boolean = false,
title: String,
coverData: eu.kanade.domain.manga.model.MangaCover,
coverAlpha: Float = 1f,
badge: @Composable RowScope.() -> Unit,
onLongClick: () -> Unit,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.selectedBackground(isSelected)
.height(56.dp)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MangaCover.Square(
modifier = Modifier
.fillMaxHeight()
.alpha(coverAlpha),
data = coverData,
)
Text(
text = title,
modifier = Modifier
.padding(horizontal = 16.dp)
.weight(1f),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
BadgeGroup(content = badge)
}
}

View File

@ -39,7 +39,7 @@ fun DeleteLibraryMangaDialog(
onDismissRequest = onDismissRequest,
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {

View File

@ -10,9 +10,11 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import eu.kanade.tachiyomi.R
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
@Composable
@ -27,7 +29,7 @@ fun DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
offset = DpOffset(8.dp, (-8).dp),
offset = DpOffset(8.dp, (-56).dp),
properties = properties,
content = content,
)
@ -46,13 +48,13 @@ fun RadioMenuItem(
if (isChecked) {
Icon(
imageVector = Icons.Outlined.RadioButtonChecked,
contentDescription = "",
contentDescription = stringResource(R.string.selected),
tint = MaterialTheme.colorScheme.primary,
)
} else {
Icon(
imageVector = Icons.Outlined.RadioButtonUnchecked,
contentDescription = "",
contentDescription = stringResource(R.string.not_selected),
)
}
},

View File

@ -30,7 +30,7 @@ fun DuplicateMangaDialog(
}
Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
TextButton(
onClick = {

View File

@ -11,8 +11,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@ -187,12 +187,12 @@ private fun WithActionPreview() {
actions = listOf(
EmptyScreenAction(
stringResId = R.string.action_retry,
icon = Icons.Default.Refresh,
icon = Icons.Outlined.Refresh,
onClick = {},
),
EmptyScreenAction(
stringResId = R.string.getting_started_guide,
icon = Icons.Default.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = {},
),
),

View File

@ -22,13 +22,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BookmarkAdd
import androidx.compose.material.icons.filled.BookmarkRemove
import androidx.compose.material.icons.filled.DoneAll
import androidx.compose.material.icons.filled.RemoveDone
import androidx.compose.material.icons.outlined.BookmarkAdd
import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -98,7 +98,7 @@ fun MangaBottomActionMenu(
if (onBookmarkClicked != null) {
Button(
title = stringResource(R.string.action_bookmark),
icon = Icons.Default.BookmarkAdd,
icon = Icons.Outlined.BookmarkAdd,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
onClick = onBookmarkClicked,
@ -107,7 +107,7 @@ fun MangaBottomActionMenu(
if (onRemoveBookmarkClicked != null) {
Button(
title = stringResource(R.string.action_remove_bookmark),
icon = Icons.Default.BookmarkRemove,
icon = Icons.Outlined.BookmarkRemove,
toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) },
onClick = onRemoveBookmarkClicked,
@ -116,7 +116,7 @@ fun MangaBottomActionMenu(
if (onMarkAsReadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_read),
icon = Icons.Default.DoneAll,
icon = Icons.Outlined.DoneAll,
toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) },
onClick = onMarkAsReadClicked,
@ -125,7 +125,7 @@ fun MangaBottomActionMenu(
if (onMarkAsUnreadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_unread),
icon = Icons.Default.RemoveDone,
icon = Icons.Outlined.RemoveDone,
toConfirm = confirm[3],
onLongClick = { onLongClickItem(3) },
onClick = onMarkAsUnreadClicked,
@ -254,7 +254,7 @@ fun LibraryBottomActionMenu(
if (onMarkAsReadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_read),
icon = Icons.Default.DoneAll,
icon = Icons.Outlined.DoneAll,
toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) },
onClick = onMarkAsReadClicked,
@ -263,7 +263,7 @@ fun LibraryBottomActionMenu(
if (onMarkAsUnreadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_unread),
icon = Icons.Default.RemoveDone,
icon = Icons.Outlined.RemoveDone,
toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) },
onClick = onMarkAsUnreadClicked,

View File

@ -2,7 +2,7 @@ package eu.kanade.presentation.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -11,7 +11,6 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R
@ -26,7 +25,7 @@ enum class MangaCover(val ratio: Float) {
modifier: Modifier = Modifier,
data: Any?,
contentDescription: String = "",
shape: Shape = RoundedCornerShape(4.dp),
shape: Shape = MaterialTheme.shapes.extraSmall,
onClick: (() -> Unit)? = null,
) {
AsyncImage(

View File

@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -28,7 +27,7 @@ fun Pill(
androidx.compose.material3.Surface(
modifier = modifier
.padding(start = 4.dp),
shape = RoundedCornerShape(100),
shape = MaterialTheme.shapes.extraLarge,
color = color,
contentColor = contentColor,
tonalElevation = elevation,

View File

@ -67,7 +67,7 @@ fun HistoryDeleteDialog(
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
)
@ -96,7 +96,7 @@ fun HistoryDeleteAllDialog(
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
)

View File

@ -2,7 +2,7 @@ package eu.kanade.presentation.library
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@ -79,7 +79,7 @@ fun LibraryScreen(
actions = listOf(
EmptyScreenAction(
stringResId = R.string.getting_started_guide,
icon = Icons.Default.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
),
),

View File

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.FastScrollLazyVerticalGrid
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
@ -26,9 +27,9 @@ fun LazyLibraryGrid(
FastScrollLazyVerticalGrid(
columns = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns),
modifier = modifier,
contentPadding = contentPadding + PaddingValues(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = contentPadding + PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
content = content,
)
}

View File

@ -1,21 +1,14 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastAny
import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.MangaComfortableGridItem
import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable
@ -44,76 +37,37 @@ fun LibraryComfortableGrid(
items = items,
contentType = { "library_comfortable_grid_item" },
) { libraryItem ->
LibraryComfortableGridItem(
item = libraryItem,
showDownloadBadge = showDownloadBadges,
showUnreadBadge = showUnreadBadges,
showLocalBadge = showLocalBadges,
showLanguageBadge = showLanguageBadges,
val manga = libraryItem.libraryManga.manga
MangaComfortableGridItem(
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
onClick = onClick,
onLongClick = onLongClick,
title = manga.title,
coverData = MangaCover(
mangaId = manga.id,
sourceId = manga.source,
isMangaFavorite = manga.favorite,
url = manga.thumbnailUrl,
lastModified = manga.coverLastModified,
),
coverBadgeStart = {
DownloadsBadge(
enabled = showDownloadBadges,
item = libraryItem,
)
UnreadBadge(
enabled = showUnreadBadges,
item = libraryItem,
)
},
coverBadgeEnd = {
LanguageBadge(
showLanguage = showLanguageBadges,
showLocal = showLocalBadges,
item = libraryItem,
)
},
onLongClick = { onLongClick(libraryItem.libraryManga) },
onClick = { onClick(libraryItem.libraryManga) },
)
}
}
}
@Composable
fun LibraryComfortableGridItem(
item: LibraryItem,
showDownloadBadge: Boolean,
showUnreadBadge: Boolean,
showLocalBadge: Boolean,
showLanguageBadge: Boolean,
isSelected: Boolean,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
val libraryManga = item.libraryManga
val manga = libraryManga.manga
LibraryGridItemSelectable(isSelected = isSelected) {
Column(
modifier = Modifier
.combinedClickable(
onClick = {
onClick(libraryManga)
},
onLongClick = {
onLongClick(libraryManga)
},
),
) {
LibraryGridCover(
mangaCover = MangaCover(
manga.id,
manga.source,
manga.favorite,
manga.thumbnailUrl,
manga.coverLastModified,
),
item = item,
showDownloadBadge = showDownloadBadge,
showUnreadBadge = showUnreadBadge,
showLocalBadge = showLocalBadge,
showLanguageBadge = showLanguageBadge,
)
MangaGridComfortableText(
text = manga.title,
)
}
}
}
@Composable
fun MangaGridComfortableText(
text: String,
) {
Text(
modifier = Modifier.padding(4.dp),
text = text,
fontSize = 12.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleSmall,
)
}

View File

@ -1,35 +1,20 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastAny
import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.MangaCompactGridItem
import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable
fun LibraryCompactGrid(
items: List<LibraryItem>,
showTitle: Boolean,
showDownloadBadges: Boolean,
showUnreadBadges: Boolean,
showLocalBadges: Boolean,
@ -53,92 +38,37 @@ fun LibraryCompactGrid(
items = items,
contentType = { "library_compact_grid_item" },
) { libraryItem ->
LibraryCompactGridItem(
item = libraryItem,
showDownloadBadge = showDownloadBadges,
showUnreadBadge = showUnreadBadges,
showLocalBadge = showLocalBadges,
showLanguageBadge = showLanguageBadges,
val manga = libraryItem.libraryManga.manga
MangaCompactGridItem(
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
onClick = onClick,
onLongClick = onLongClick,
title = manga.title.takeIf { showTitle },
coverData = MangaCover(
mangaId = manga.id,
sourceId = manga.source,
isMangaFavorite = manga.favorite,
url = manga.thumbnailUrl,
lastModified = manga.coverLastModified,
),
coverBadgeStart = {
DownloadsBadge(
enabled = showDownloadBadges,
item = libraryItem,
)
UnreadBadge(
enabled = showUnreadBadges,
item = libraryItem,
)
},
coverBadgeEnd = {
LanguageBadge(
showLanguage = showLanguageBadges,
showLocal = showLocalBadges,
item = libraryItem,
)
},
onLongClick = { onLongClick(libraryItem.libraryManga) },
onClick = { onClick(libraryItem.libraryManga) },
)
}
}
}
@Composable
fun LibraryCompactGridItem(
item: LibraryItem,
showDownloadBadge: Boolean,
showUnreadBadge: Boolean,
showLocalBadge: Boolean,
showLanguageBadge: Boolean,
isSelected: Boolean,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
val libraryManga = item.libraryManga
val manga = libraryManga.manga
LibraryGridCover(
modifier = Modifier
.selectedOutline(isSelected)
.combinedClickable(
onClick = {
onClick(libraryManga)
},
onLongClick = {
onLongClick(libraryManga)
},
),
mangaCover = eu.kanade.domain.manga.model.MangaCover(
manga.id,
manga.source,
manga.favorite,
manga.thumbnailUrl,
manga.coverLastModified,
),
item = item,
showDownloadBadge = showDownloadBadge,
showUnreadBadge = showUnreadBadge,
showLocalBadge = showLocalBadge,
showLanguageBadge = showLanguageBadge,
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
.background(
Brush.verticalGradient(
0f to Color.Transparent,
1f to Color(0xAA000000),
),
)
.fillMaxHeight(0.33f)
.fillMaxWidth()
.align(Alignment.BottomCenter),
)
MangaGridCompactText(manga.title)
}
}
@Composable
fun BoxScope.MangaGridCompactText(
text: String,
) {
Text(
text = text,
modifier = Modifier
.padding(8.dp)
.align(Alignment.BottomStart),
color = Color.White,
fontSize = 12.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleSmall.copy(
shadow = Shadow(
color = Color.Black,
blurRadius = 4f,
),
),
)
}

View File

@ -1,90 +0,0 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.util.fastAny
import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable
fun LibraryCoverOnlyGrid(
items: List<LibraryItem>,
showDownloadBadges: Boolean,
showUnreadBadges: Boolean,
showLocalBadges: Boolean,
showLanguageBadges: Boolean,
columns: Int,
contentPadding: PaddingValues,
selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
searchQuery: String?,
onGlobalSearchClicked: () -> Unit,
) {
LazyLibraryGrid(
modifier = Modifier.fillMaxSize(),
columns = columns,
contentPadding = contentPadding,
) {
globalSearchItem(searchQuery, onGlobalSearchClicked)
items(
items = items,
contentType = { "library_only_cover_grid_item" },
) { libraryItem ->
LibraryCoverOnlyGridItem(
item = libraryItem,
showDownloadBadge = showDownloadBadges,
showUnreadBadge = showUnreadBadges,
showLocalBadge = showLocalBadges,
showLanguageBadge = showLanguageBadges,
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
onClick = onClick,
onLongClick = onLongClick,
)
}
}
}
@Composable
fun LibraryCoverOnlyGridItem(
item: LibraryItem,
showDownloadBadge: Boolean,
showUnreadBadge: Boolean,
showLocalBadge: Boolean,
showLanguageBadge: Boolean,
isSelected: Boolean,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
val libraryManga = item.libraryManga
val manga = libraryManga.manga
LibraryGridCover(
modifier = Modifier
.selectedOutline(isSelected)
.combinedClickable(
onClick = {
onClick(libraryManga)
},
onLongClick = {
onLongClick(libraryManga)
},
),
mangaCover = eu.kanade.domain.manga.model.MangaCover(
manga.id,
manga.source,
manga.favorite,
manga.thumbnailUrl,
manga.coverLastModified,
),
item = item,
showDownloadBadge = showDownloadBadge,
showUnreadBadge = showUnreadBadge,
showLocalBadge = showLocalBadge,
showLanguageBadge = showLanguageBadge,
)
}

View File

@ -1,80 +0,0 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.BadgeGroup
import eu.kanade.presentation.components.MangaCover
import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable
fun MangaGridCover(
modifier: Modifier = Modifier,
cover: @Composable BoxScope.() -> Unit = {},
badgesStart: (@Composable RowScope.() -> Unit)? = null,
badgesEnd: (@Composable RowScope.() -> Unit)? = null,
content: @Composable BoxScope.() -> Unit = {},
) {
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(MangaCover.Book.ratio),
) {
cover()
content()
if (badgesStart != null) {
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
content = badgesStart,
)
}
if (badgesEnd != null) {
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopEnd),
content = badgesEnd,
)
}
}
}
@Composable
fun LibraryGridCover(
modifier: Modifier = Modifier,
mangaCover: eu.kanade.domain.manga.model.MangaCover,
item: LibraryItem,
showDownloadBadge: Boolean,
showUnreadBadge: Boolean,
showLocalBadge: Boolean,
showLanguageBadge: Boolean,
content: @Composable BoxScope.() -> Unit = {},
) {
MangaGridCover(
modifier = modifier,
cover = {
MangaCover.Book(
modifier = Modifier.fillMaxWidth(),
data = mangaCover,
)
},
badgesStart = {
DownloadsBadge(enabled = showDownloadBadge, item = item)
UnreadBadge(enabled = showUnreadBadge, item = item)
},
badgesEnd = {
LanguageBadge(showLanguage = showLanguageBadge, showLocal = showLocalBadge, item = item)
},
content = content,
)
}

View File

@ -1,46 +0,0 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.dp
fun Modifier.selectedOutline(isSelected: Boolean) = composed {
val secondary = MaterialTheme.colorScheme.secondary
if (isSelected) {
drawBehind {
val additional = 24.dp.value
val offset = additional / 2
val height = size.height + additional
val width = size.width + additional
drawRoundRect(
color = secondary,
topLeft = Offset(-offset, -offset),
size = Size(width, height),
cornerRadius = CornerRadius(offset),
)
}
} else {
this
}
}
@Composable
fun LibraryGridItemSelectable(
isSelected: Boolean,
content: @Composable () -> Unit,
) {
Box(Modifier.selectedOutline(isSelected)) {
CompositionLocalProvider(LocalContentColor provides if (isSelected) MaterialTheme.colorScheme.onSecondary else MaterialTheme.colorScheme.onBackground) {
content()
}
}
}

View File

@ -1,33 +1,21 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.zIndex
import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.BadgeGroup
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.MangaCover.Square
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.selectedBackground
import eu.kanade.presentation.util.verticalPadding
import eu.kanade.presentation.components.MangaListItem
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibraryItem
@ -47,7 +35,7 @@ fun LibraryList(
) {
FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = contentPadding,
contentPadding = contentPadding + PaddingValues(vertical = 8.dp),
) {
item {
if (searchQuery.isNullOrEmpty().not()) {
@ -64,116 +52,25 @@ fun LibraryList(
items = items,
contentType = { "library_list_item" },
) { libraryItem ->
LibraryListItem(
item = libraryItem,
showDownloadBadge = showDownloadBadges,
showUnreadBadge = showUnreadBadges,
showLocalBadge = showLocalBadges,
showLanguageBadge = showLanguageBadges,
val manga = libraryItem.libraryManga.manga
MangaListItem(
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
onClick = onClick,
onLongClick = onLongClick,
title = manga.title,
coverData = MangaCover(
mangaId = manga.id,
sourceId = manga.source,
isMangaFavorite = manga.favorite,
url = manga.thumbnailUrl,
lastModified = manga.coverLastModified,
),
badge = {
DownloadsBadge(enabled = showDownloadBadges, item = libraryItem)
UnreadBadge(enabled = showUnreadBadges, item = libraryItem)
LanguageBadge(showLanguage = showLanguageBadges, showLocal = showLocalBadges, item = libraryItem)
},
onLongClick = { onLongClick(libraryItem.libraryManga) },
onClick = { onClick(libraryItem.libraryManga) },
)
}
}
}
@Composable
fun LibraryListItem(
item: LibraryItem,
showDownloadBadge: Boolean,
showUnreadBadge: Boolean,
showLocalBadge: Boolean,
showLanguageBadge: Boolean,
isSelected: Boolean,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
) {
val libraryManga = item.libraryManga
val manga = libraryManga.manga
MangaListItem(
modifier = Modifier.selectedBackground(isSelected),
title = manga.title,
cover = MangaCover(
manga.id,
manga.source,
manga.favorite,
manga.thumbnailUrl,
manga.coverLastModified,
),
onClick = { onClick(libraryManga) },
onLongClick = { onLongClick(libraryManga) },
) {
DownloadsBadge(enabled = showDownloadBadge, item = item)
UnreadBadge(enabled = showUnreadBadge, item = item)
LanguageBadge(showLanguage = showLanguageBadge, showLocal = showLocalBadge, item = item)
}
}
@Composable
fun MangaListItem(
modifier: Modifier = Modifier,
title: String,
cover: MangaCover,
onClick: () -> Unit,
onLongClick: () -> Unit = onClick,
badges: @Composable RowScope.() -> Unit,
) {
MangaListItem(
modifier = modifier,
coverContent = {
Square(
modifier = Modifier
.padding(vertical = verticalPadding)
.fillMaxHeight(),
data = cover,
)
},
badges = badges,
onClick = onClick,
onLongClick = onLongClick,
content = {
MangaListItemContent(title)
},
)
}
@Composable
fun MangaListItem(
modifier: Modifier = Modifier,
coverContent: @Composable RowScope.() -> Unit,
badges: @Composable RowScope.() -> Unit,
onClick: () -> Unit,
onLongClick: () -> Unit,
content: @Composable RowScope.() -> Unit,
) {
Row(
modifier = modifier
.height(56.dp)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(horizontal = horizontalPadding),
verticalAlignment = Alignment.CenterVertically,
) {
coverContent()
content()
BadgeGroup(content = badges)
}
}
@Composable
fun RowScope.MangaListItemContent(
text: String,
) {
Text(
text = text,
modifier = Modifier
.padding(horizontal = horizontalPadding)
.weight(1f),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
}

View File

@ -72,9 +72,10 @@ fun LibraryPager(
onGlobalSearchClicked = onGlobalSearchClicked,
)
}
LibraryDisplayMode.CompactGrid -> {
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
LibraryCompactGrid(
items = library,
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
showDownloadBadges = showDownloadBadges,
showUnreadBadges = showUnreadBadges,
showLocalBadges = showLocalBadges,
@ -104,22 +105,6 @@ fun LibraryPager(
onGlobalSearchClicked = onGlobalSearchClicked,
)
}
LibraryDisplayMode.CoverOnlyGrid -> {
LibraryCoverOnlyGrid(
items = library,
showDownloadBadges = showDownloadBadges,
showUnreadBadges = showUnreadBadges,
showLocalBadges = showLocalBadges,
showLanguageBadges = showLanguageBadges,
columns = columns,
contentPadding = contentPadding,
selection = selectedManga,
onClick = onClickManga,
onLongClick = onLongClickManga,
searchQuery = searchQuery,
onGlobalSearchClicked = onGlobalSearchClicked,
)
}
}
}
}

View File

@ -150,10 +150,10 @@ fun LibrarySelectionToolbar(
titleContent = { Text(text = "${state.selection.size}") },
actions = {
IconButton(onClick = onClickSelectAll) {
Icon(Icons.Outlined.SelectAll, contentDescription = "search")
Icon(Icons.Outlined.SelectAll, contentDescription = stringResource(R.string.action_select_all))
}
IconButton(onClick = onClickInvertSelection) {
Icon(Icons.Outlined.FlipToBack, contentDescription = "invert")
Icon(Icons.Outlined.FlipToBack, contentDescription = stringResource(R.string.action_select_inverse))
}
},
isActionMode = true,

View File

@ -273,7 +273,7 @@ private fun MangaScreenSmallImpl(
}
Text(text = stringResource(id))
},
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
modifier = Modifier
@ -486,7 +486,7 @@ fun MangaScreenLargeImpl(
}
Text(text = stringResource(id))
},
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
modifier = Modifier

View File

@ -37,7 +37,7 @@ fun DownloadCustomAmountDialog(
onDismissRequest = onDismissRequest,
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
@ -62,13 +62,13 @@ fun DownloadCustomAmountDialog(
onClick = { setAmount(amount - 10) },
enabled = amount > 0,
) {
Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowLeft, contentDescription = "")
Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowLeft, contentDescription = "-10")
}
IconButton(
onClick = { setAmount(amount - 1) },
enabled = amount > 0,
) {
Icon(imageVector = Icons.Outlined.ChevronLeft, contentDescription = "")
Icon(imageVector = Icons.Outlined.ChevronLeft, contentDescription = "-1")
}
OutlinedTextField(
modifier = Modifier.weight(1f),
@ -81,13 +81,13 @@ fun DownloadCustomAmountDialog(
onClick = { setAmount(amount + 1) },
enabled = amount < maxAmount,
) {
Icon(imageVector = Icons.Outlined.ChevronRight, contentDescription = "")
Icon(imageVector = Icons.Outlined.ChevronRight, contentDescription = "+1")
}
IconButton(
onClick = { setAmount(amount + 10) },
enabled = amount < maxAmount,
) {
Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowRight, contentDescription = "")
Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowRight, contentDescription = "+10")
}
}
},

View File

@ -72,7 +72,7 @@ fun MangaChapterListItem(
var textHeight by remember { mutableStateOf(0) }
if (bookmark) {
Icon(
imageVector = Icons.Default.Bookmark,
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),

View File

@ -14,7 +14,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Share
@ -63,7 +63,7 @@ fun MangaCoverDialog(
) {
IconButton(onClick = onDismissRequest) {
Icon(
imageVector = Icons.Default.Close,
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_close),
)
}

View File

@ -16,7 +16,7 @@ fun DeleteChaptersDialog(
onDismissRequest = onDismissRequest,
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {

View File

@ -23,18 +23,18 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AttachMoney
import androidx.compose.material.icons.filled.Block
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.DoneAll
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material.icons.filled.Sync
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.AttachMoney
import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.icons.outlined.Pause
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
@ -173,7 +173,7 @@ fun MangaActionRow(
} else {
stringResource(R.string.add_to_library)
},
icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
icon = if (favorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = onAddToLibraryClicked,
onLongClick = onEditCategory,
@ -185,7 +185,7 @@ fun MangaActionRow(
} else {
pluralStringResource(id = R.plurals.num_trackers, count = trackingCount, trackingCount)
},
icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
icon = if (trackingCount == 0) Icons.Outlined.Sync else Icons.Outlined.Done,
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
onClick = onTrackingClicked,
)
@ -193,7 +193,7 @@ fun MangaActionRow(
if (onWebViewClicked != null) {
MangaActionButton(
title = stringResource(R.string.action_web_view),
icon = Icons.Default.Public,
icon = Icons.Outlined.Public,
color = defaultActionButtonColor,
onClick = onWebViewClicked,
)
@ -345,13 +345,13 @@ private fun MangaAndSourceTitlesLarge(
) {
Icon(
imageVector = when (status) {
SManga.ONGOING.toLong() -> Icons.Default.Schedule
SManga.COMPLETED.toLong() -> Icons.Default.DoneAll
SManga.LICENSED.toLong() -> Icons.Default.AttachMoney
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
SManga.CANCELLED.toLong() -> Icons.Default.Close
SManga.ON_HIATUS.toLong() -> Icons.Default.Pause
else -> Icons.Default.Block
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
else -> Icons.Outlined.Block
},
contentDescription = null,
modifier = Modifier
@ -375,7 +375,7 @@ private fun MangaAndSourceTitlesLarge(
DotSeparatorText()
if (isStubSource) {
Icon(
imageVector = Icons.Default.Warning,
imageVector = Icons.Filled.Warning,
contentDescription = null,
modifier = Modifier
.padding(end = 4.dp)
@ -478,13 +478,13 @@ private fun MangaAndSourceTitlesSmall(
) {
Icon(
imageVector = when (status) {
SManga.ONGOING.toLong() -> Icons.Default.Schedule
SManga.COMPLETED.toLong() -> Icons.Default.DoneAll
SManga.LICENSED.toLong() -> Icons.Default.AttachMoney
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
SManga.CANCELLED.toLong() -> Icons.Default.Close
SManga.ON_HIATUS.toLong() -> Icons.Default.Pause
else -> Icons.Default.Block
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
else -> Icons.Outlined.Block
},
contentDescription = null,
modifier = Modifier
@ -508,7 +508,7 @@ private fun MangaAndSourceTitlesSmall(
DotSeparatorText()
if (isStubSource) {
Icon(
imageVector = Icons.Default.Warning,
imageVector = Icons.Filled.Warning,
contentDescription = null,
modifier = Modifier
.padding(end = 4.dp)

View File

@ -5,13 +5,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.statusBars
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.FlipToBack
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -71,7 +71,7 @@ fun MangaToolbar(
navigationIcon = {
IconButton(onClick = onBackClicked) {
Icon(
imageVector = if (isActionMode) Icons.Default.Close else Icons.Default.ArrowBack,
imageVector = if (isActionMode) Icons.Outlined.Close else Icons.Outlined.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
}
@ -80,13 +80,13 @@ fun MangaToolbar(
if (isActionMode) {
IconButton(onClick = onSelectAll) {
Icon(
imageVector = Icons.Default.SelectAll,
imageVector = Icons.Outlined.SelectAll,
contentDescription = stringResource(R.string.action_select_all),
)
}
IconButton(onClick = onInvertSelection) {
Icon(
imageVector = Icons.Default.FlipToBack,
imageVector = Icons.Outlined.FlipToBack,
contentDescription = stringResource(R.string.action_select_inverse),
)
}
@ -161,7 +161,7 @@ fun MangaToolbar(
Box {
IconButton(onClick = { onMoreExpanded(!moreExpanded) }) {
Icon(
imageVector = Icons.Default.MoreVert,
imageVector = Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
)
}

View File

@ -82,7 +82,7 @@ internal fun PreferenceItem(
ListPreferenceWidget(
value = value,
title = item.title,
subtitle = item.subtitle,
subtitle = item.internalSubtitleProvider(value, item.entries),
icon = item.icon,
entries = item.entries,
onValueChange = { newValue ->
@ -98,7 +98,7 @@ internal fun PreferenceItem(
ListPreferenceWidget(
value = item.value,
title = item.title,
subtitle = item.subtitle,
subtitle = item.subtitleProvider(item.value, item.entries),
icon = item.icon,
entries = item.entries,
onValueChange = { scope.launch { item.onValueChanged(it) } },

View File

@ -1,7 +1,11 @@
package eu.kanade.presentation.more.settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.core.preference.Preference as PreferenceData
@ -47,6 +51,8 @@ sealed class Preference {
val pref: PreferenceData<T>,
override val title: String,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: T, entries: Map<T, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
@ -55,6 +61,10 @@ sealed class Preference {
) : PreferenceItem<T>() {
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
@Composable
internal fun internalSubtitleProvider(value: Any?, entries: Map<out Any?, String>) =
subtitleProvider(value as T, entries as Map<T, String>)
}
/**
@ -64,6 +74,8 @@ sealed class Preference {
val value: String,
override val title: String,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: String, entries: Map<String, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
@ -78,7 +90,15 @@ sealed class Preference {
data class MultiSelectListPreference(
val pref: PreferenceData<Set<String>>,
override val title: String,
override val subtitle: String? = null,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: Set<String>, entries: Map<String, String>) -> String? = { v, e ->
val combined = remember(v) {
v.map { e[it] }
.takeIf { it.isNotEmpty() }
?.joinToString()
} ?: stringResource(id = R.string.none)
subtitle?.format(combined)
},
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },

View File

@ -3,7 +3,7 @@ package eu.kanade.presentation.more.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
@ -28,7 +28,7 @@ fun PreferenceScaffold(
if (onBackPressed != null) {
IconButton(onClick = onBackPressed) {
Icon(
imageVector = Icons.Default.ArrowBack,
imageVector = Icons.Outlined.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
}

View File

@ -81,7 +81,7 @@ class ClearDatabaseScreen : Screen {
},
dismissButton = {
TextButton(onClick = model::hideConfirmation) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
text = {

View File

@ -345,7 +345,7 @@ class SettingsAdvancedScreen : SearchableSettings {
text = { Text(text = stringResource(R.string.ext_installer_shizuku_unavailable_dialog)) },
dismissButton = {
TextButton(onClick = dismiss) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {

View File

@ -72,7 +72,6 @@ class SettingsAppearanceScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = themeModePref,
title = stringResource(R.string.pref_theme_mode),
subtitle = "%s",
entries = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mapOf(
ThemeMode.SYSTEM to stringResource(R.string.theme_system),
@ -129,7 +128,6 @@ class SettingsAppearanceScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.relativeTime(),
title = stringResource(R.string.pref_relative_format),
subtitle = "%s",
entries = mapOf(
0 to stringResource(R.string.off),
2 to stringResource(R.string.pref_relative_time_short),
@ -139,7 +137,6 @@ class SettingsAppearanceScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.dateFormat(),
title = stringResource(R.string.pref_date_format),
subtitle = "%s",
entries = DateFormats
.associateWith {
val formattedDate = UiPreferences.dateFormat(it).format(now)

View File

@ -192,7 +192,7 @@ class SettingsBackupScreen : SearchableSettings {
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
@ -252,7 +252,7 @@ class SettingsBackupScreen : SearchableSettings {
onDismissRequest()
},
) {
Text(text = stringResource(R.string.copy))
Text(text = stringResource(android.R.string.copy))
}
},
confirmButton = {

View File

@ -10,7 +10,6 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@ -27,7 +26,6 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -101,9 +99,11 @@ class SettingsDownloadScreen : SearchableSettings {
return Preference.PreferenceItem.ListPreference(
pref = currentDirPref,
title = stringResource(R.string.pref_download_directory),
subtitle = remember(currentDir) {
UniFile.fromUri(context, currentDir.toUri())?.filePath
} ?: stringResource(R.string.invalid_location, currentDir),
subtitleProvider = { value, _ ->
remember(value) {
UniFile.fromUri(context, value.toUri())?.filePath
} ?: stringResource(R.string.invalid_location, value)
},
entries = mapOf(
defaultDirPair,
customDirEntryKey to stringResource(R.string.custom_dir),
@ -173,25 +173,10 @@ class SettingsDownloadScreen : SearchableSettings {
downloadPreferences: DownloadPreferences,
categories: () -> List<Category>,
): Preference.PreferenceItem.MultiSelectListPreference {
val none = stringResource(R.string.none)
val pref = downloadPreferences.removeExcludeCategories()
val entries = categories().associate { it.id.toString() to it.visualName }
val subtitle by produceState(initialValue = "") {
pref.changes()
.stateIn(this)
.collect { mutable ->
value = mutable
.mapNotNull { id -> entries[id] }
.sortedBy { entries.values.indexOf(it) }
.joinToString()
.ifEmpty { none }
}
}
return Preference.PreferenceItem.MultiSelectListPreference(
pref = pref,
pref = downloadPreferences.removeExcludeCategories(),
title = stringResource(R.string.pref_remove_exclude_categories),
subtitle = subtitle,
entries = entries,
entries = categories().associate { it.id.toString() to it.visualName },
)
}

View File

@ -72,7 +72,6 @@ class SettingsGeneralScreen : SearchableSettings {
Preference.PreferenceItem.BasicListPreference(
value = currentLanguage,
title = stringResource(R.string.pref_app_language),
subtitle = "%s",
entries = langs,
onValueChanged = { newValue ->
currentLanguage = newValue

View File

@ -177,28 +177,6 @@ class SettingsLibraryScreen : SearchableSettings {
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
val deviceRestrictionEntries = mapOf(
DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered),
DEVICE_CHARGING to stringResource(R.string.charging),
DEVICE_BATTERY_NOT_LOW to stringResource(R.string.battery_not_low),
)
val deviceRestrictions = libraryUpdateDeviceRestrictionPref.collectAsState()
.value
.sorted()
.map { deviceRestrictionEntries.getOrElse(it) { it } }
.let { if (it.isEmpty()) stringResource(R.string.none) else it.joinToString() }
val mangaRestrictionEntries = mapOf(
MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
MANGA_NON_READ to stringResource(R.string.pref_update_only_started),
MANGA_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
)
val mangaRestrictions = libraryUpdateMangaRestrictionPref.collectAsState()
.value
.map { mangaRestrictionEntries.getOrElse(it) { it } }
.let { if (it.isEmpty()) stringResource(R.string.none) else it.joinToString() }
val included by libraryUpdateCategoriesPref.collectAsState()
val excluded by libraryUpdateCategoriesExcludePref.collectAsState()
var showDialog by rememberSaveable { mutableStateOf(false) }
@ -224,7 +202,6 @@ class SettingsLibraryScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = libraryUpdateIntervalPref,
title = stringResource(R.string.pref_library_update_interval),
subtitle = "%s",
entries = mapOf(
0 to stringResource(R.string.update_never),
12 to stringResource(R.string.update_12hour),
@ -242,8 +219,13 @@ class SettingsLibraryScreen : SearchableSettings {
pref = libraryUpdateDeviceRestrictionPref,
enabled = libraryUpdateInterval > 0,
title = stringResource(R.string.pref_library_update_restriction),
subtitle = stringResource(R.string.restrictions, deviceRestrictions),
entries = deviceRestrictionEntries,
subtitle = stringResource(R.string.restrictions),
entries = mapOf(
DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered),
DEVICE_CHARGING to stringResource(R.string.charging),
DEVICE_BATTERY_NOT_LOW to stringResource(R.string.battery_not_low),
),
onValueChanged = {
// Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
@ -253,8 +235,11 @@ class SettingsLibraryScreen : SearchableSettings {
Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryUpdateMangaRestrictionPref,
title = stringResource(R.string.pref_library_update_manga_restriction),
subtitle = mangaRestrictions,
entries = mangaRestrictionEntries,
entries = mapOf(
MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
MANGA_NON_READ to stringResource(R.string.pref_update_only_started),
MANGA_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
),
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.categories),
@ -341,7 +326,7 @@ class SettingsLibraryScreen : SearchableSettings {
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {

View File

@ -8,7 +8,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ChromeReaderMode
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.CollectionsBookmark
@ -98,7 +98,7 @@ object SettingsMainScreen : Screen {
navigationIcon = {
IconButton(onClick = backPress::invoke) {
Icon(
imageVector = Icons.Default.ArrowBack,
imageVector = Icons.Outlined.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
}

View File

@ -17,8 +17,8 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -95,8 +95,8 @@ class SettingsSearchScreen : Screen {
if (canPop) {
IconButton(onClick = navigator::pop) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
imageVector = Icons.Outlined.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
@ -131,7 +131,7 @@ class SettingsSearchScreen : Screen {
if (textFieldValue.text.isNotEmpty()) {
IconButton(onClick = { textFieldValue = TextFieldValue() }) {
Icon(
imageVector = Icons.Default.Close,
imageVector = Icons.Outlined.Close,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)

View File

@ -49,7 +49,6 @@ class SettingsSecurityScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.lockAppAfter(),
title = stringResource(R.string.lock_when_idle),
subtitle = "%s",
enabled = authSupported && useAuth,
entries = LockAfterValues
.associateWith {
@ -72,7 +71,6 @@ class SettingsSecurityScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.secureScreen(),
title = stringResource(R.string.secure_screen),
subtitle = "%s",
entries = SecurityPreferences.SecureScreenMode.values()
.associateWith { stringResource(it.titleResId) },
),

View File

@ -10,10 +10,10 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@ -71,7 +71,7 @@ class SettingsTrackingScreen : SearchableSettings {
val uriHandler = LocalUriHandler.current
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/help/guides/tracking/") }) {
Icon(
imageVector = Icons.Default.HelpOutline,
imageVector = Icons.Outlined.HelpOutline,
contentDescription = stringResource(R.string.tracking_guide),
)
}
@ -199,7 +199,7 @@ class SettingsTrackingScreen : SearchableSettings {
)
IconButton(onClick = onDismissRequest) {
Icon(
imageVector = Icons.Default.Close,
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_close),
)
}
@ -227,9 +227,9 @@ class SettingsTrackingScreen : SearchableSettings {
IconButton(onClick = { hidePassword = !hidePassword }) {
Icon(
imageVector = if (hidePassword) {
Icons.Default.Visibility
Icons.Filled.Visibility
} else {
Icons.Default.VisibilityOff
Icons.Filled.VisibilityOff
},
contentDescription = null,
)
@ -317,7 +317,7 @@ class SettingsTrackingScreen : SearchableSettings {
modifier = Modifier.weight(1f),
onClick = onDismissRequest,
) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
Button(
modifier = Modifier.weight(1f),

View File

@ -45,6 +45,7 @@ import eu.kanade.presentation.components.DIVIDER_ALPHA
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
@ -158,7 +159,7 @@ fun AppThemePreviewItem(
.padding(end = 4.dp)
.background(
color = MaterialTheme.colorScheme.onSurface,
shape = RoundedCornerShape(9.dp),
shape = MaterialTheme.shapes.small,
),
)
@ -168,8 +169,8 @@ fun AppThemePreviewItem(
) {
if (selected) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
imageVector = Icons.Filled.CheckCircle,
contentDescription = stringResource(R.string.selected),
tint = MaterialTheme.colorScheme.primary,
)
}
@ -182,7 +183,7 @@ fun AppThemePreviewItem(
.padding(start = 8.dp, top = 2.dp)
.background(
color = dividerColor,
shape = RoundedCornerShape(9.dp),
shape = MaterialTheme.shapes.small,
)
.fillMaxWidth(0.5f)
.aspectRatio(MangaCover.Book.ratio),
@ -242,7 +243,7 @@ fun AppThemePreviewItem(
.weight(1f)
.background(
color = MaterialTheme.colorScheme.onSurface,
shape = RoundedCornerShape(9.dp),
shape = MaterialTheme.shapes.small,
),
)
}

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.window.DialogProperties
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.launch
@Composable
@ -71,7 +72,7 @@ fun EditTextPreferenceWidget(
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
)

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
@ -26,6 +25,7 @@ import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.isScrolledToEnd
import eu.kanade.presentation.util.isScrolledToStart
import eu.kanade.presentation.util.minimumTouchTargetSize
import eu.kanade.tachiyomi.R
@Composable
fun <T> ListPreferenceWidget(
@ -40,7 +40,7 @@ fun <T> ListPreferenceWidget(
TextPreferenceWidget(
title = title,
subtitle = subtitle?.format(entries[value]),
subtitle = subtitle,
icon = icon,
onPreferenceClick = { showDialog(true) },
)
@ -73,7 +73,7 @@ fun <T> ListPreferenceWidget(
},
confirmButton = {
TextButton(onClick = { showDialog(false) }) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
)
@ -89,7 +89,7 @@ private fun DialogRow(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.clip(MaterialTheme.shapes.small)
.selectable(
selected = isSelected,
onClick = { if (!isSelected) onSelected() },

View File

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
@ -23,6 +22,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.minimumTouchTargetSize
import eu.kanade.tachiyomi.R
@Composable
fun MultiSelectListPreferenceWidget(
@ -34,7 +34,7 @@ fun MultiSelectListPreferenceWidget(
TextPreferenceWidget(
title = preference.title,
subtitle = preference.subtitle,
subtitle = preference.subtitleProvider(values, preference.entries),
icon = preference.icon,
onPreferenceClick = { showDialog(true) },
)
@ -62,7 +62,7 @@ fun MultiSelectListPreferenceWidget(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.clip(MaterialTheme.shapes.small)
.selectable(
selected = isSelected,
onClick = { onSelectionChanged() },
@ -99,7 +99,7 @@ fun MultiSelectListPreferenceWidget(
},
dismissButton = {
TextButton(onClick = { showDialog(false) }) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
)

View File

@ -44,7 +44,7 @@ private fun SwitchPreferenceWidgetPreview() {
SwitchPreferenceWidget(
title = "Text preference with icon",
subtitle = "Text preference summary",
icon = Icons.Default.Preview,
icon = Icons.Filled.Preview,
checked = true,
onCheckedChanged = {},
)

View File

@ -67,7 +67,7 @@ private fun TextPreferenceWidgetPreview() {
TextPreferenceWidget(
title = "Text preference with icon",
subtitle = "Text preference summary",
icon = Icons.Default.Preview,
icon = Icons.Filled.Preview,
onPreferenceClick = {},
)
TextPreferenceWidget(

View File

@ -10,9 +10,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -21,8 +20,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
import eu.kanade.tachiyomi.R
@Composable
fun TrackingPreferenceWidget(
@ -45,7 +46,7 @@ fun TrackingPreferenceWidget(
Box(
modifier = Modifier
.size(48.dp)
.background(color = Color(logoColor), shape = RoundedCornerShape(8.dp))
.background(color = Color(logoColor), shape = MaterialTheme.shapes.small)
.padding(4.dp),
contentAlignment = Alignment.Center,
) {
@ -65,12 +66,12 @@ fun TrackingPreferenceWidget(
)
if (checked) {
Icon(
imageVector = Icons.Default.Check,
imageVector = Icons.Outlined.Done,
modifier = Modifier
.padding(4.dp)
.size(32.dp),
tint = Color(0xFF4CAF50),
contentDescription = null,
contentDescription = stringResource(R.string.login_success),
)
}
}

View File

@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CheckBox
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
@ -79,7 +78,7 @@ fun <T> TriStateListDialog(
val state = selected[index]
Row(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.clip(MaterialTheme.shapes.small)
.clickable {
selected[index] = when (state) {
State.UNCHECKED -> State.CHECKED
@ -103,7 +102,13 @@ fun <T> TriStateListDialog(
} else {
MaterialTheme.colorScheme.primary
},
contentDescription = null,
contentDescription = stringResource(
when (state) {
State.UNCHECKED -> R.string.not_selected
State.CHECKED -> R.string.selected
State.INVERSED -> R.string.disabled
},
),
)
Text(text = itemLabel(item))
}
@ -117,7 +122,7 @@ fun <T> TriStateListDialog(
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {

View File

@ -27,7 +27,7 @@ fun UpdatesDeleteConfirmationDialog(
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
)

View File

@ -8,9 +8,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FlipToBack
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBarScrollBehavior
@ -215,7 +215,7 @@ private fun UpdatesAppBar(
actions = {
IconButton(onClick = onUpdateLibrary) {
Icon(
imageVector = Icons.Default.Refresh,
imageVector = Icons.Outlined.Refresh,
contentDescription = stringResource(R.string.action_update_library),
)
}
@ -225,13 +225,13 @@ private fun UpdatesAppBar(
actionModeActions = {
IconButton(onClick = onSelectAll) {
Icon(
imageVector = Icons.Default.SelectAll,
imageVector = Icons.Outlined.SelectAll,
contentDescription = stringResource(R.string.action_select_all),
)
}
IconButton(onClick = onInvertSelection) {
Icon(
imageVector = Icons.Default.FlipToBack,
imageVector = Icons.Outlined.FlipToBack,
contentDescription = stringResource(R.string.action_select_inverse),
)
}

View File

@ -205,7 +205,7 @@ fun UpdatesUiItem(
var textHeight by remember { mutableStateOf(0) }
if (bookmark) {
Icon(
imageVector = Icons.Default.Bookmark,
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),

View File

@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -48,13 +48,13 @@ fun WebViewScreen(
title = state.pageTitle ?: initialTitle,
subtitle = state.content.getCurrentUrl(),
navigateUp = onNavigateUp,
navigationIcon = Icons.Default.Close,
navigationIcon = Icons.Outlined.Close,
actions = {
AppBarActions(
listOf(
AppBar.Action(
title = stringResource(R.string.action_webview_back),
icon = Icons.Default.ArrowBack,
icon = Icons.Outlined.ArrowBack,
onClick = {
if (navigator.canGoBack) {
navigator.navigateBack()
@ -64,7 +64,7 @@ fun WebViewScreen(
),
AppBar.Action(
title = stringResource(R.string.action_webview_forward),
icon = Icons.Default.ArrowForward,
icon = Icons.Outlined.ArrowForward,
onClick = {
if (navigator.canGoForward) {
navigator.navigateForward()

View File

@ -48,6 +48,8 @@ class DownloadCache(
private val scope = CoroutineScope(Dispatchers.IO)
private val notifier by lazy { DownloadNotifier(context) }
/**
* The interval after which this cache should be invalidated. 1 hour shouldn't cause major
* issues, as the cache is only used for UI feedback.
@ -241,56 +243,62 @@ class DownloadCache(
}
renewalJob = scope.launchIO {
var sources = getSources()
try {
notifier.onCacheProgress()
// Try to wait until extensions and sources have loaded
withTimeout(30.seconds) {
while (!extensionManager.isInitialized) {
delay(2.seconds)
}
var sources = getSources()
while (sources.isEmpty()) {
delay(2.seconds)
sources = getSources()
}
}
// Try to wait until extensions and sources have loaded
withTimeout(30.seconds) {
while (!extensionManager.isInitialized) {
delay(2.seconds)
}
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
.associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry ->
sources.find {
provider.getSourceDirName(it).equals(entry.key, ignoreCase = true)
}?.id
}
rootDownloadsDir.sourceDirs = sourceDirs
sourceDirs.values
.map { sourceDir ->
async {
val mangaDirs = sourceDir.dir.listFiles().orEmpty()
.filterNot { it.name.isNullOrBlank() }
.associate { it.name!! to MangaDirectory(it) }
sourceDir.mangaDirs = ConcurrentHashMap(mangaDirs)
mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles().orEmpty()
.mapNotNull { chapterDir ->
chapterDir.name
?.replace(".cbz", "")
?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) }
}
.toMutableSet()
mangaDir.chapterDirs = chapterDirs
}
while (sources.isEmpty()) {
delay(2.seconds)
sources = getSources()
}
}
.awaitAll()
lastRenew = System.currentTimeMillis()
notifyChanges()
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
.associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry ->
sources.find {
provider.getSourceDirName(it).equals(entry.key, ignoreCase = true)
}?.id
}
rootDownloadsDir.sourceDirs = sourceDirs
sourceDirs.values
.map { sourceDir ->
async {
val mangaDirs = sourceDir.dir.listFiles().orEmpty()
.filterNot { it.name.isNullOrBlank() }
.associate { it.name!! to MangaDirectory(it) }
sourceDir.mangaDirs = ConcurrentHashMap(mangaDirs)
mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles().orEmpty()
.mapNotNull { chapterDir ->
chapterDir.name
?.replace(".cbz", "")
?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) }
}
.toMutableSet()
mangaDir.chapterDirs = chapterDirs
}
}
}
.awaitAll()
lastRenew = System.currentTimeMillis()
notifyChanges()
} finally {
notifier.dismissCacheProgress()
}
}
}

View File

@ -45,6 +45,17 @@ internal class DownloadNotifier(private val context: Context) {
}
}
private val cacheNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_CACHE) {
setSmallIcon(R.drawable.ic_tachi)
setContentTitle(context.getString(R.string.download_notifier_cache_renewal))
setProgress(100, 100, true)
setOngoing(true)
setAutoCancel(false)
setOnlyAlertOnce(true)
}
}
/**
* Status of download. Used for correct notification icon.
*/
@ -233,4 +244,14 @@ internal class DownloadNotifier(private val context: Context) {
errorThrown = true
isDownloading = false
}
fun onCacheProgress() {
with(cacheNotificationBuilder) {
show(Notifications.ID_DOWNLOAD_CACHE)
}
}
fun dismissCacheProgress() {
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CACHE)
}
}

View File

@ -485,17 +485,15 @@ class Downloader(
private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile): Boolean {
if (!downloadPreferences.splitTallImages().get()) return true
val filename = String.format("%03d", page.number)
val imageFile = tmpDir.listFiles()?.find { it.name!!.startsWith(filename) }
val filenamePrefix = String.format("%03d", page.number)
val imageFile = tmpDir.listFiles()?.firstOrNull { it.name.orEmpty().startsWith(filenamePrefix) }
?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number))
val imageFilePath = imageFile.filePath
?: throw Error(context.getString(R.string.download_notifier_split_page_path_not_found, page.number))
// check if the original page was previously splitted before then skip.
if (imageFile.name!!.contains("__")) return true
return try {
ImageUtil.splitTallImage(imageFile, imageFilePath)
ImageUtil.splitTallImage(tmpDir, imageFile, filenamePrefix)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false

View File

@ -59,7 +59,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setLargeIcon(notificationBitmap)
setOngoing(true)
setOnlyAlertOnce(true)
addAction(R.drawable.ic_close_24dp, context.getString(android.R.string.cancel), cancelIntent)
addAction(R.drawable.ic_close_24dp, context.getString(R.string.action_cancel), cancelIntent)
}
}

View File

@ -42,6 +42,8 @@ object Notifications {
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
const val CHANNEL_DOWNLOADER_ERROR = "downloader_error_channel"
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
const val CHANNEL_DOWNLOADER_CACHE = "downloader_cache_renewal"
const val ID_DOWNLOAD_CACHE = -204
/**
* Notification channel and ids used by the library updater.
@ -159,6 +161,11 @@ object Notifications {
setGroup(GROUP_DOWNLOADER)
setShowBadge(false)
},
buildNotificationChannel(CHANNEL_DOWNLOADER_CACHE, IMPORTANCE_LOW) {
setName(context.getString(R.string.channel_downloader_cache))
setGroup(GROUP_DOWNLOADER)
setShowBadge(false)
},
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) {
setName(context.getString(R.string.channel_progress))
setGroup(GROUP_BACKUP_RESTORE)

View File

@ -180,8 +180,7 @@ class ExtensionManager(
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
changed = true
} else if (availableExt != null) {
val hasUpdate = !installedExt.isUnofficial &&
availableExt.versionCode > installedExt.versionCode
val hasUpdate = installedExt.updateExists(availableExt)
if (installedExt.hasUpdate != hasUpdate) {
mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
@ -347,11 +346,18 @@ class ExtensionManager(
* Extension method to set the update field of an installed extension.
*/
private fun Extension.Installed.withUpdateCheck(): Extension.Installed {
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == pkgName }
if (!isUnofficial && availableExt != null && availableExt.versionCode > versionCode) {
return copy(hasUpdate = true)
return if (updateExists()) {
copy(hasUpdate = true)
} else {
this
}
return this
}
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
if (isUnofficial || availableExt == null) return false
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
}
private fun updatePendingUpdatesCount() {

View File

@ -100,7 +100,7 @@ internal class ExtensionGithubApi {
private fun List<ExtensionJsonObject>.toExtensions(): List<Extension.Available> {
return this
.filter {
val libVersion = it.version.substringBeforeLast('.').toDouble()
val libVersion = it.extractLibVersion()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
}
.map {
@ -109,6 +109,7 @@ internal class ExtensionGithubApi {
pkgName = it.pkg,
versionName = it.version,
versionCode = it.code,
libVersion = it.extractLibVersion(),
lang = it.lang,
isNsfw = it.nsfw == 1,
hasReadme = it.hasReadme == 1,
@ -142,6 +143,10 @@ internal class ExtensionGithubApi {
REPO_URL_PREFIX
}
}
private fun ExtensionJsonObject.extractLibVersion(): Double {
return version.substringBeforeLast('.').toDouble()
}
}
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"

View File

@ -10,6 +10,7 @@ sealed class Extension {
abstract val pkgName: String
abstract val versionName: String
abstract val versionCode: Long
abstract val libVersion: Double
abstract val lang: String?
abstract val isNsfw: Boolean
abstract val hasReadme: Boolean
@ -20,6 +21,7 @@ sealed class Extension {
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Double,
override val lang: String,
override val isNsfw: Boolean,
override val hasReadme: Boolean,
@ -37,6 +39,7 @@ sealed class Extension {
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Double,
override val lang: String,
override val isNsfw: Boolean,
override val hasReadme: Boolean,
@ -51,6 +54,7 @@ sealed class Extension {
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Double,
val signatureHash: String,
override val lang: String? = null,
override val isNsfw: Boolean = false,

View File

@ -141,7 +141,7 @@ internal object ExtensionLoader {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return LoadResult.Error
} else if (signatureHash !in trustedSignatures) {
val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
return LoadResult.Untrusted(extension)
}
@ -190,14 +190,15 @@ internal object ExtensionLoader {
}
val extension = Extension.Installed(
extName,
pkgName,
versionName,
versionCode,
lang,
isNsfw,
hasReadme,
hasChangelog,
name = extName,
pkgName = pkgName,
versionName = versionName,
versionCode = versionCode,
libVersion = libVersion,
lang = lang,
isNsfw = isNsfw,
hasReadme = hasReadme,
hasChangelog = hasChangelog,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = signatureHash != officialSignature,

View File

@ -21,6 +21,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.ConcurrentHashMap
class SourceManager(
private val context: Context,
@ -31,7 +32,7 @@ class SourceManager(
private val scope = CoroutineScope(Job() + Dispatchers.IO)
private var sourcesMap = emptyMap<Long, Source>()
private var sourcesMap = ConcurrentHashMap<Long, Source>()
set(value) {
field = value
sourcesMapFlow.value = field
@ -39,7 +40,7 @@ class SourceManager(
private val sourcesMapFlow = MutableStateFlow(sourcesMap)
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
private val stubSourcesMap = ConcurrentHashMap<Long, StubSource>()
val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() }
val onlineSources: Flow<List<HttpSource>> = catalogueSources.map { sources -> sources.filterIsInstance<HttpSource>() }
@ -48,7 +49,7 @@ class SourceManager(
scope.launch {
extensionManager.installedExtensionsFlow
.collectLatest { extensions ->
val mutableMap = mutableMapOf<Long, Source>(LocalSource.ID to LocalSource(context))
val mutableMap = ConcurrentHashMap<Long, Source>(mapOf(LocalSource.ID to LocalSource(context)))
extensions.forEach { extension ->
extension.sources.forEach {
mutableMap[it.id] = it

View File

@ -13,6 +13,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -35,6 +36,7 @@ class ExtensionFilterPresenter(
logcat(LogPriority.ERROR, exception)
_events.send(Event.FailedFetchingLanguages)
}
.stateIn(presenterScope)
.collectLatest(::collectLatestSourceLangMap)
}
}

View File

@ -29,10 +29,6 @@ class SourceSearchController(
@Composable
override fun ComposeContent() {
// LocalContext is not a first available to us when we try access it
// Decoupling from BrowseSourceController is needed
val context = applicationContext!!
SourceSearchScreen(
presenter = presenter,
navigateUp = { router.popCurrentController() },
@ -46,8 +42,10 @@ class SourceSearchController(
},
onWebViewClick = f@{
val source = presenter.source as? HttpSource ?: return@f
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
activity?.let { context ->
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
}
},
)

View File

@ -15,6 +15,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -39,6 +40,7 @@ class SourcesFilterPresenter(
logcat(LogPriority.ERROR, exception)
_events.send(Event.FailedFetchingLanguages)
}
.stateIn(presenterScope)
.collectLatest(::collectLatestSourceLangMap)
}
}

View File

@ -13,9 +13,9 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Pause
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -152,7 +152,7 @@ class DownloadController :
IconButton(onClick = { onExpanded(!expanded) }) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.label_more),
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
)
}
CascadeDropdownMenu(
@ -234,9 +234,9 @@ class DownloadController :
},
icon = {
val icon = if (isRunning) {
Icons.Default.Pause
Icons.Outlined.Pause
} else {
Icons.Default.PlayArrow
Icons.Filled.PlayArrow
}
Icon(imageVector = icon, contentDescription = null)
},

View File

@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.cancel
@ -126,7 +127,6 @@ class LibraryController(
settingsSheet = LibrarySettingsSheet(router) { group ->
when (group) {
is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged()
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
else -> {} // Handled via different mechanisms
}
}
@ -152,12 +152,10 @@ class LibraryController(
}
private fun onFilterChanged() {
presenter.requestFilterUpdate()
activity?.invalidateOptionsMenu()
}
private fun onSortChanged() {
presenter.requestSortUpdate()
viewScope.launchUI {
presenter.requestFilterUpdate()
activity?.invalidateOptionsMenu()
}
}
fun search(query: String) {
@ -180,7 +178,7 @@ class LibraryController(
* Clear all of the manga currently selected, and
* invalidate the action mode to revert the top toolbar
*/
fun clearSelection() {
private fun clearSelection() {
presenter.clearSelection()
}

View File

@ -11,11 +11,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.core.prefs.CheckboxState
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.core.util.asFlow
import eu.kanade.core.util.asObservable
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.SetMangaCategories
@ -32,7 +29,7 @@ import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.MangaUpdate
import eu.kanade.domain.manga.model.isLocal
import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.GetTracksPerManga
import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.library.LibraryState
import eu.kanade.presentation.library.LibraryStateImpl
@ -49,16 +46,16 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.Collator
@ -79,7 +76,7 @@ typealias LibraryMap = Map<Long, List<LibraryItem>>
class LibraryPresenter(
private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl,
private val getLibraryManga: GetLibraryManga = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val setReadStatus: SetReadStatus = Injekt.get(),
@ -111,15 +108,8 @@ class LibraryPresenter(
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
/**
* Relay used to apply the UI filters to the last emission of the library.
*/
private val filterTriggerRelay = BehaviorRelay.create(Unit)
/**
* Relay used to apply the selected sorting method to the last emission of the library.
*/
private val sortTriggerRelay = BehaviorRelay.create(Unit)
private val _filterChanges: Channel<Unit> = Channel(Int.MAX_VALUE)
private val filterChanges = _filterChanges.receiveAsFlow().onStart { emit(Unit) }
private var librarySubscription: Job? = null
@ -129,30 +119,23 @@ class LibraryPresenter(
subscribeLibrary()
}
/**
* Subscribes to library if needed.
*/
fun subscribeLibrary() {
/**
* TODO: Move this to a coroutine world
* TODO:
* - Move filter and sort to getMangaForCategory and only filter and sort the current display category instead of whole library as some has 5000+ items in the library
* - Create new db view and new query to just fetch the current category save as needed to instance variable
* - Fetch badges to maps and retrieve as needed instead of fetching all of them at once
*/
if (librarySubscription == null || librarySubscription!!.isCancelled) {
librarySubscription = presenterScope.launchIO {
getLibraryFlow().asObservable()
.combineLatest(getFilterObservable()) { lib, tracks ->
lib.copy(mangaMap = applyFilters(lib.mangaMap, tracks))
}
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
lib.copy(mangaMap = applySort(lib.categories, lib.mangaMap))
}
.observeOn(AndroidSchedulers.mainThread())
.asFlow()
combine(getLibraryFlow(), getTracksPerManga.subscribe(), filterChanges) { library, tracks, _ ->
library.mangaMap
.applyFilters(tracks)
.applySort(library.categories)
}
.collectLatest {
state.isLoading = false
loadedManga = it.mangaMap
loadedManga = it
}
}
}
@ -160,21 +143,24 @@ class LibraryPresenter(
/**
* Applies library filters to the given map of manga.
*
* @param map the map to filter.
*/
private fun applyFilters(map: LibraryMap, trackMap: Map<Long, Map<Long, Boolean>>): LibraryMap {
private fun LibraryMap.applyFilters(trackMap: Map<Long, List<Long>>): LibraryMap {
val downloadedOnly = preferences.downloadedOnly().get()
val filterDownloaded = libraryPreferences.filterDownloaded().get()
val filterUnread = libraryPreferences.filterUnread().get()
val filterStarted = libraryPreferences.filterStarted().get()
val filterBookmarked = libraryPreferences.filterBookmarked().get()
val filterCompleted = libraryPreferences.filterCompleted().get()
val loggedInServices = trackManager.services.filter { trackService -> trackService.isLogged }
val loggedInTrackServices = trackManager.services.filter { trackService -> trackService.isLogged }
.associate { trackService ->
Pair(trackService.id, libraryPreferences.filterTracking(trackService.id.toInt()).get())
trackService.id to libraryPreferences.filterTracking(trackService.id.toInt()).get()
}
val isNotAnyLoggedIn = !loggedInServices.values.any()
val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty()
val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.EXCLUDE.value) it.key else null }
val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.INCLUDE.value) it.key else null }
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{ item ->
if (!downloadedOnly && filterDownloaded == State.IGNORE.value) return@downloaded true
@ -237,25 +223,21 @@ class LibraryPresenter(
}
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
if (isNotAnyLoggedIn) return@tracking true
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
val trackedManga = trackMap[item.libraryManga.manga.id]
val mangaTracks = trackMap[item.libraryManga.id].orEmpty()
val containsExclude = loggedInServices.filterValues { it == State.EXCLUDE.value }
val containsInclude = loggedInServices.filterValues { it == State.INCLUDE.value }
val exclude = mangaTracks.filter { it in excludedTracks }
val include = mangaTracks.filter { it in includedTracks }
if (!containsExclude.any() && !containsInclude.any()) return@tracking true
val exclude = trackedManga?.filterKeys { containsExclude.containsKey(it) }?.values ?: emptyList()
val include = trackedManga?.filterKeys { containsInclude.containsKey(it) }?.values ?: emptyList()
if (containsInclude.any() && containsExclude.any()) {
return@tracking if (exclude.isNotEmpty()) !exclude.any() else include.any()
// TODO: Simplify the filter logic
if (includedTracks.isNotEmpty() && excludedTracks.isNotEmpty()) {
return@tracking if (exclude.isNotEmpty()) false else include.isNotEmpty()
}
if (containsExclude.any()) return@tracking !exclude.any()
if (excludedTracks.isNotEmpty()) return@tracking exclude.isEmpty()
if (containsInclude.any()) return@tracking include.any()
if (includedTracks.isNotEmpty()) return@tracking include.isNotEmpty()
return@tracking false
}
@ -271,26 +253,28 @@ class LibraryPresenter(
)
}
return map.mapValues { entry -> entry.value.filter(filterFn) }
return this.mapValues { entry -> entry.value.filter(filterFn) }
}
/**
* Applies library sorting to the given map of manga.
*
* @param map the map to sort.
*/
private fun applySort(categories: List<Category>, map: LibraryMap): LibraryMap {
private fun LibraryMap.applySort(categories: List<Category>): LibraryMap {
val sortModes = categories.associate { it.id to it.sort }
val locale = Locale.getDefault()
val collator = Collator.getInstance(locale).apply {
strength = Collator.PRIMARY
}
val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
collator.compare(i1.libraryManga.manga.title.lowercase(locale), i2.libraryManga.manga.title.lowercase(locale))
}
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
val sort = sortModes[i1.libraryManga.category]!!
when (sort.type) {
LibrarySort.Type.Alphabetical -> {
collator.compare(i1.libraryManga.manga.title.lowercase(locale), i2.libraryManga.manga.title.lowercase(locale))
sortAlphabetically(i1, i2)
}
LibrarySort.Type.LastRead -> {
i1.libraryManga.lastRead.compareTo(i2.libraryManga.lastRead)
@ -321,14 +305,14 @@ class LibraryPresenter(
}
}
return map.mapValues { entry ->
return this.mapValues { entry ->
val comparator = if (sortModes[entry.key]!!.isAscending) {
Comparator(sortFn)
} else {
Collections.reverseOrder(sortFn)
}
entry.value.sortedWith(comparator)
entry.value.sortedWith(comparator.thenComparator(sortAlphabetically))
}
}
@ -342,13 +326,18 @@ class LibraryPresenter(
getLibraryManga.subscribe(),
libraryPreferences.downloadBadge().changes(),
libraryPreferences.filterDownloaded().changes(),
preferences.downloadedOnly().changes(),
downloadCache.changes,
) { libraryMangaList, downloadBadgePref, filterDownloadedPref, _ ->
) { libraryMangaList, downloadBadgePref, filterDownloadedPref, downloadedOnly, _ ->
libraryMangaList
.map { libraryManga ->
val needsDownloadCounts = downloadBadgePref ||
filterDownloadedPref != State.IGNORE.value ||
downloadedOnly
// Display mode based on user preference: take it from global library setting or category
LibraryItem(libraryManga).apply {
downloadCount = if (downloadBadgePref || filterDownloadedPref == State.INCLUDE.value) {
downloadCount = if (needsDownloadCounts) {
downloadManager.getDownloadCount(libraryManga.manga).toLong()
} else {
0
@ -373,48 +362,11 @@ class LibraryPresenter(
}
}
/**
* Get the tracked manga from the database and checks if the filter gets changed
*
* @return an observable of tracked manga.
*/
private fun getFilterObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
return filterTriggerRelay.observeOn(Schedulers.io())
.combineLatest(getTracksFlow().asObservable().observeOn(Schedulers.io())) { _, tracks -> tracks }
}
/**
* Get the tracked manga from the database
*
* @return an observable of tracked manga.
*/
private fun getTracksFlow(): Flow<Map<Long, Map<Long, Boolean>>> {
// TODO: Move this to domain/data layer
return getTracks.subscribe()
.map { tracks ->
tracks
.groupBy { it.mangaId }
.mapValues { tracksForMangaId ->
// Check if any of the trackers is logged in for the current manga id
tracksForMangaId.value.associate {
Pair(it.syncId, trackManager.getService(it.syncId)?.isLogged ?: false)
}
}
}
}
/**
* Requests the library to be filtered.
*/
fun requestFilterUpdate() {
filterTriggerRelay.call(Unit)
}
/**
* Requests the library to be sorted.
*/
fun requestSortUpdate() {
sortTriggerRelay.call(Unit)
suspend fun requestFilterUpdate() = withIOContext {
_filterChanges.send(Unit)
}
/**
@ -432,9 +384,9 @@ class LibraryPresenter(
*/
suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
return mangas.toSet()
.map { getCategories.await(it.id) }
.reduce { set1, set2 -> set1.intersect(set2).toMutableList() }
return mangas
.map { getCategories.await(it.id).toSet() }
.reduce { set1, set2 -> set1.intersect(set2) }
}
/**
@ -444,9 +396,9 @@ class LibraryPresenter(
*/
suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
val mangaCategories = mangas.toSet().map { getCategories.await(it.id) }
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2).toMutableList() }
return mangaCategories.flatten().distinct().subtract(common).toMutableList()
val mangaCategories = mangas.map { getCategories.await(it.id).toSet() }
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
return mangaCategories.flatten().distinct().subtract(common)
}
/**
@ -524,10 +476,10 @@ class LibraryPresenter(
*/
fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
presenterScope.launchNonCancellable {
mangaList.map { manga ->
mangaList.forEach { manga ->
val categoryIds = getCategories.await(manga.id)
.map { it.id }
.subtract(removeCategories)
.subtract(removeCategories.toSet())
.plus(addCategories)
.toList()
@ -649,10 +601,6 @@ class LibraryPresenter(
}
}
private fun <T, U, R> Observable<T>.combineLatest(o2: Observable<U>, combineFn: (T, U) -> R): Observable<R> {
return Observable.combineLatest(this, o2, combineFn)
}
sealed class Dialog {
data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog()
data class DeleteManga(val manga: List<Manga>) : Dialog()

View File

@ -48,7 +48,7 @@ class SetChapterSettingsDialog(bundle: Bundle? = null) : DialogController(bundle
activity?.toast(R.string.chapter_settings_updated)
}
.setNegativeButton(android.R.string.cancel, null)
.setNegativeButton(R.string.action_cancel, null)
.create()
}

View File

@ -59,7 +59,7 @@ class SetTrackChaptersDialog<T> : DialogController
np.clearFocus()
listener.setChaptersRead(item, np.value)
}
.setNegativeButton(android.R.string.cancel, null)
.setNegativeButton(R.string.action_cancel, null)
.create()
}

View File

@ -59,7 +59,7 @@ class SetTrackScoreDialog<T> : DialogController
np.clearFocus()
listener.setScore(item, np.value)
}
.setNegativeButton(android.R.string.cancel, null)
.setNegativeButton(R.string.action_cancel, null)
.create()
}

View File

@ -48,7 +48,7 @@ class SetTrackStatusDialog<T> : DialogController
.setPositiveButton(android.R.string.ok) { _, _ ->
listener.setStatus(item, selectedIndex)
}
.setNegativeButton(android.R.string.cancel, null)
.setNegativeButton(R.string.action_cancel, null)
.create()
}

View File

@ -66,6 +66,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.preference.toggle
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
@ -275,6 +276,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_open_in_web_view -> {
openChapterInWebview()
}
R.id.action_bookmark -> {
presenter.bookmarkCurrentChapter(true)
invalidateOptionsMenu()
@ -665,6 +669,15 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
startPostponedEnterTransition()
}
private fun openChapterInWebview() {
val manga = presenter.manga ?: return
val source = presenter.getSource() ?: return
val url = presenter.getChapterUrl() ?: return
val intent = WebViewActivity.newIntent(this, url, source.id, manga.title)
startActivity(intent)
}
private fun showReadingModeToast(mode: Int) {
try {
val strings = resources.getStringArray(R.array.viewers_selector)

View File

@ -40,7 +40,7 @@ class ReaderPageSheet(
.setPositiveButton(android.R.string.ok) { _, _ ->
activity.setAsCover(page)
}
.setNegativeButton(android.R.string.cancel, null)
.setNegativeButton(R.string.action_cancel, null)
.show()
}

View File

@ -35,6 +35,7 @@ import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingUpdateJob
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
@ -606,6 +607,15 @@ class ReaderPresenter(
return viewerChaptersRelay.value?.currChapter
}
fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource
fun getChapterUrl(): String? {
val sChapter = getCurrentChapter()?.chapter ?: return null
val source = getSource() ?: return null
return source.getChapterUrl(sChapter)
}
/**
* Bookmarks the currently active chapter.
*/

View File

@ -27,8 +27,6 @@ import tachiyomi.decoder.ImageDecoder
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.net.URLConnection
import kotlin.math.abs
@ -202,7 +200,7 @@ object ImageUtil {
/**
* Splits tall images to improve performance of reader
*/
fun splitTallImage(imageFile: UniFile, imageFilePath: String): Boolean {
fun splitTallImage(tmpDir: UniFile, imageFile: UniFile, filenamePrefix: String): Boolean {
if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(imageFile.openInputStream())) {
return true
}
@ -221,11 +219,15 @@ object ImageUtil {
return try {
splitDataList.forEach { splitData ->
val splitPath = splitImagePath(imageFilePath, splitData.index)
val splitImageName = splitImageName(filenamePrefix, splitData.index)
// Remove pre-existing split if exists (this split shouldn't exist under normal circumstances)
tmpDir.findFile(splitImageName)?.delete()
val splitFile = tmpDir.createFile(splitImageName)
val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset)
FileOutputStream(splitPath).use { outputStream ->
splitFile.openOutputStream().use { outputStream ->
val splitBitmap = bitmapRegionDecoder.decodeRegion(region, options)
splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
splitBitmap.recycle()
@ -240,8 +242,8 @@ object ImageUtil {
} catch (e: Exception) {
// Image splits were not successfully saved so delete them and keep the original image
splitDataList
.map { splitImagePath(imageFilePath, it.index) }
.forEach { File(it).delete() }
.map { splitImageName(filenamePrefix, it.index) }
.forEach { tmpDir.findFile(it)?.delete() }
logcat(LogPriority.ERROR, e)
false
} finally {
@ -249,8 +251,7 @@ object ImageUtil {
}
}
private fun splitImagePath(imageFilePath: String, index: Int) =
imageFilePath.substringBeforeLast(".") + "__${"%03d".format(index + 1)}.jpg"
private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format(index + 1)}.jpg"
/**
* Check whether the image is a long Strip that needs splitting

View File

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More