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 acknowledge that:
- I have updated: - 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 - All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/ - 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 - 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 label: Tachiyomi version
description: You can find your Tachiyomi version in **More → About**. description: You can find your Tachiyomi version in **More → About**.
placeholder: | placeholder: |
Example: "0.14.0" Example: "0.14.1"
validations: validations:
required: true required: true
@ -98,7 +98,7 @@ body:
required: true required: true
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/). - label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
required: true 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 required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true

View File

@ -33,7 +33,7 @@ body:
required: true 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). - 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 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 required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View File

@ -27,8 +27,8 @@ android {
applicationId = "eu.kanade.tachiyomi" applicationId = "eu.kanade.tachiyomi"
minSdk = AndroidConfig.minSdk minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk targetSdk = AndroidConfig.targetSdk
versionCode = 89 versionCode = 90
versionName = "0.14.0" versionName = "0.14.1"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -329,6 +329,19 @@ tasks {
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi", "-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi", "-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 { 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.source.repository.SourceRepository
import eu.kanade.domain.track.interactor.DeleteTrack import eu.kanade.domain.track.interactor.DeleteTrack
import eu.kanade.domain.track.interactor.GetTracks 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.interactor.InsertTrack
import eu.kanade.domain.track.repository.TrackRepository import eu.kanade.domain.track.repository.TrackRepository
import eu.kanade.domain.updates.interactor.GetUpdates import eu.kanade.domain.updates.interactor.GetUpdates
@ -104,6 +105,7 @@ class DomainModule : InjektModule {
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) } addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { DeleteTrack(get()) } addFactory { DeleteTrack(get()) }
addFactory { GetTracksPerManga(get()) }
addFactory { GetTracks(get()) } addFactory { GetTracks(get()) }
addFactory { InsertTrack(get()) } addFactory { InsertTrack(get()) }

View File

@ -27,7 +27,12 @@ class SetReadStatus(
} }
suspend fun await(read: Boolean, vararg chapters: Chapter): Result = withNonCancellableContext { 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()) { if (chaptersToUpdate.isEmpty()) {
return@withNonCancellableContext Result.NoChapters return@withNonCancellableContext Result.NoChapters
} }

View File

@ -21,8 +21,8 @@ class GetNextChapter(
} }
suspend fun await(mangaId: Long, chapterId: Long): Chapter? { suspend fun await(mangaId: Long, chapterId: Long): Chapter? {
val chapter = getChapter.await(chapterId)!! val chapter = getChapter.await(chapterId) ?: return null
val manga = getManga.await(mangaId)!! val manga = getManga.await(mangaId) ?: return null
if (!chapter.read) return chapter 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>> { fun subscribe(mangaId: Long): Flow<List<Track>> {
return trackRepository.getTracksByMangaIdAsFlow(mangaId) 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.lazy.grid.GridCells
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons 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.Favorite
import androidx.compose.material.icons.outlined.FilterList 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.NewReleases
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -89,7 +89,7 @@ fun BrowseSourceScreen(
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
BrowseSourceToolbar( BrowseSourceToolbar(
state = presenter, state = presenter,
source = presenter.source!!, source = presenter.source,
displayMode = presenter.displayMode, displayMode = presenter.displayMode,
onDisplayModeChange = { presenter.displayMode = it }, onDisplayModeChange = { presenter.displayMode = it },
navigateUp = navigateUp, navigateUp = navigateUp,
@ -253,7 +253,7 @@ fun BrowseSourceContent(
listOf( listOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.local_source_help_guide, stringResId = R.string.local_source_help_guide,
icon = Icons.Default.HelpOutline, icon = Icons.Outlined.HelpOutline,
onClick = onLocalSourceHelpClick, onClick = onLocalSourceHelpClick,
), ),
) )
@ -261,17 +261,17 @@ fun BrowseSourceContent(
listOf( listOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.action_retry, stringResId = R.string.action_retry,
icon = Icons.Default.Refresh, icon = Icons.Outlined.Refresh,
onClick = mangaList::refresh, onClick = mangaList::refresh,
), ),
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.action_open_in_web_view, stringResId = R.string.action_open_in_web_view,
icon = Icons.Default.Public, icon = Icons.Outlined.Public,
onClick = onWebViewClick, onClick = onWebViewClick,
), ),
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.label_help, stringResId = R.string.label_help,
icon = Icons.Default.HelpOutline, icon = Icons.Outlined.HelpOutline,
onClick = onHelpClick, onClick = onHelpClick,
), ),
) )

View File

@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons 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.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@ -368,7 +368,7 @@ private fun ExtensionItemActions(
} else { } else {
IconButton(onClick = { onClickItemCancel(extension) }) { IconButton(onClick = { onClickItemCancel(extension) }) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_cancel), 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.Box
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Dangerous import androidx.compose.material.icons.filled.Dangerous
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
@ -48,7 +47,7 @@ fun SourceIcon(
when { when {
source.isStub && icon == null -> { source.isStub && icon == null -> {
Image( Image(
imageVector = Icons.Default.Warning, imageVector = Icons.Filled.Warning,
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error), colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
modifier = modifier.then(defaultModifier), modifier = modifier.then(defaultModifier),
@ -85,7 +84,7 @@ fun ExtensionIcon(
placeholder = ColorPainter(Color(0x1F888888)), placeholder = ColorPainter(Color(0x1F888888)),
error = rememberResourceBitmapPainter(id = R.drawable.cover_error), error = rememberResourceBitmapPainter(id = R.drawable.cover_error),
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(4.dp)), .clip(MaterialTheme.shapes.extraSmall),
) )
} }
is Extension.Installed -> { is Extension.Installed -> {
@ -105,7 +104,7 @@ fun ExtensionIcon(
} }
} }
is Extension.Untrusted -> Image( is Extension.Untrusted -> Image(
imageVector = Icons.Default.Dangerous, imageVector = Icons.Filled.Dangerous,
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error), colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
modifier = modifier.then(defaultModifier), modifier = modifier.then(defaultModifier),

View File

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

View File

@ -1,35 +1,22 @@
package eu.kanade.presentation.browse.components 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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues 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.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 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.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import eu.kanade.domain.manga.model.Manga 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.Badge
import eu.kanade.presentation.components.MangaCover import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.library.components.MangaGridCompactText import eu.kanade.presentation.components.MangaCompactGridItem
import eu.kanade.presentation.library.components.MangaGridCover
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -44,12 +31,12 @@ fun BrowseSourceCompactGrid(
) { ) {
LazyVerticalGrid( LazyVerticalGrid(
columns = columns, columns = columns,
contentPadding = PaddingValues(8.dp, 4.dp) + contentPadding, contentPadding = contentPadding + PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
verticalArrangement = Arrangement.spacedBy(8.dp), 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() 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() BrowseSourceLoadingItem()
} }
} }
@ -73,57 +60,27 @@ fun BrowseSourceCompactGrid(
} }
@Composable @Composable
fun BrowseSourceCompactGridItem( private fun BrowseSourceCompactGridItem(
manga: Manga, manga: Manga,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
onLongClick: () -> Unit = onClick, onLongClick: () -> Unit = onClick,
) { ) {
val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f) MangaCompactGridItem(
MangaGridCover( title = manga.title,
modifier = Modifier coverData = MangaCover(
.combinedClickable( mangaId = manga.id,
onClick = onClick, sourceId = manga.source,
onLongClick = onLongClick, isMangaFavorite = manga.favorite,
), url = manga.thumbnailUrl,
cover = { lastModified = manga.coverLastModified,
MangaCover.Book( ),
modifier = Modifier coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
.fillMaxHeight() coverBadgeStart = {
.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 = {
if (manga.favorite) { if (manga.favorite) {
Badge(text = stringResource(R.string.in_library)) Badge(text = stringResource(R.string.in_library))
} }
}, },
content = { onLongClick = onLongClick,
Box( onClick = onClick,
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)
},
) )
} }

View File

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

View File

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

View File

@ -33,7 +33,7 @@ import eu.kanade.tachiyomi.source.LocalSource
@Composable @Composable
fun BrowseSourceToolbar( fun BrowseSourceToolbar(
state: BrowseSourceState, state: BrowseSourceState,
source: CatalogueSource, source: CatalogueSource?,
displayMode: LibraryDisplayMode, displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit, onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit, navigateUp: () -> Unit,
@ -44,7 +44,7 @@ fun BrowseSourceToolbar(
) { ) {
if (state.searchQuery == null) { if (state.searchQuery == null) {
BrowseSourceRegularToolbar( 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, isLocalSource = source is LocalSource,
displayMode = displayMode, displayMode = displayMode,
onDisplayModeChange = onDisplayModeChange, onDisplayModeChange = onDisplayModeChange,

View File

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

View File

@ -18,8 +18,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.R
@Composable @Composable
fun CategoryListItem( fun CategoryListItem(
@ -64,10 +66,10 @@ fun CategoryListItem(
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) { IconButton(onClick = onRename) {
Icon(imageVector = Icons.Outlined.Edit, contentDescription = "") Icon(imageVector = Icons.Outlined.Edit, contentDescription = stringResource(R.string.action_rename_category))
} }
IconButton(onClick = onDelete) { 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.foundation.text.KeyboardOptions
import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons 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.ArrowBack
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -55,7 +53,7 @@ fun AppBar(
subtitle: String? = null, subtitle: String? = null,
// Up button // Up button
navigateUp: (() -> Unit)? = null, navigateUp: (() -> Unit)? = null,
navigationIcon: ImageVector = Icons.Default.ArrowBack, navigationIcon: ImageVector = Icons.Outlined.ArrowBack,
// Menu // Menu
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
// Action mode // Action mode
@ -105,7 +103,7 @@ fun AppBar(
titleContent: @Composable () -> Unit, titleContent: @Composable () -> Unit,
// Up button // Up button
navigateUp: (() -> Unit)? = null, navigateUp: (() -> Unit)? = null,
navigationIcon: ImageVector = Icons.Default.ArrowBack, navigationIcon: ImageVector = Icons.Outlined.ArrowBack,
// Menu // Menu
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
// Action mode // Action mode
@ -125,7 +123,7 @@ fun AppBar(
if (isActionMode) { if (isActionMode) {
IconButton(onClick = onCancelActionMode) { IconButton(onClick = onCancelActionMode) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_cancel), contentDescription = stringResource(R.string.action_cancel),
) )
} }
@ -200,7 +198,7 @@ fun AppBarActions(
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>() val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
if (overflowActions.isNotEmpty()) { if (overflowActions.isNotEmpty()) {
IconButton(onClick = { showMenu = !showMenu }) { 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( DropdownMenu(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -67,7 +67,7 @@ fun HistoryDeleteDialog(
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
) )
@ -96,7 +96,7 @@ fun HistoryDeleteAllDialog(
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { 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.foundation.layout.padding
import androidx.compose.material.icons.Icons 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -79,7 +79,7 @@ fun LibraryScreen(
actions = listOf( actions = listOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.getting_started_guide, stringResId = R.string.getting_started_guide,
icon = Icons.Default.HelpOutline, icon = Icons.Outlined.HelpOutline,
onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") }, 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.FastScrollLazyVerticalGrid import eu.kanade.presentation.components.FastScrollLazyVerticalGrid
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -26,9 +27,9 @@ fun LazyLibraryGrid(
FastScrollLazyVerticalGrid( FastScrollLazyVerticalGrid(
columns = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns), columns = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns),
modifier = modifier, modifier = modifier,
contentPadding = contentPadding + PaddingValues(12.dp), contentPadding = contentPadding + PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
content = content, content = content,
) )
} }

View File

@ -1,21 +1,14 @@
package eu.kanade.presentation.library.components 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.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.items 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.runtime.Composable
import androidx.compose.ui.Modifier 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 androidx.compose.ui.util.fastAny
import eu.kanade.domain.library.model.LibraryManga import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.domain.manga.model.MangaCover import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.MangaComfortableGridItem
import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable @Composable
@ -44,76 +37,37 @@ fun LibraryComfortableGrid(
items = items, items = items,
contentType = { "library_comfortable_grid_item" }, contentType = { "library_comfortable_grid_item" },
) { libraryItem -> ) { libraryItem ->
LibraryComfortableGridItem( val manga = libraryItem.libraryManga.manga
item = libraryItem, MangaComfortableGridItem(
showDownloadBadge = showDownloadBadges,
showUnreadBadge = showUnreadBadges,
showLocalBadge = showLocalBadges,
showLanguageBadge = showLanguageBadges,
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
onClick = onClick, title = manga.title,
onLongClick = onLongClick, 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 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.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize 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.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.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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 androidx.compose.ui.util.fastAny
import eu.kanade.domain.library.model.LibraryManga 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 import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable @Composable
fun LibraryCompactGrid( fun LibraryCompactGrid(
items: List<LibraryItem>, items: List<LibraryItem>,
showTitle: Boolean,
showDownloadBadges: Boolean, showDownloadBadges: Boolean,
showUnreadBadges: Boolean, showUnreadBadges: Boolean,
showLocalBadges: Boolean, showLocalBadges: Boolean,
@ -53,92 +38,37 @@ fun LibraryCompactGrid(
items = items, items = items,
contentType = { "library_compact_grid_item" }, contentType = { "library_compact_grid_item" },
) { libraryItem -> ) { libraryItem ->
LibraryCompactGridItem( val manga = libraryItem.libraryManga.manga
item = libraryItem, MangaCompactGridItem(
showDownloadBadge = showDownloadBadges,
showUnreadBadge = showUnreadBadges,
showLocalBadge = showLocalBadges,
showLanguageBadge = showLanguageBadges,
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
onClick = onClick, title = manga.title.takeIf { showTitle },
onLongClick = onLongClick, 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 package eu.kanade.presentation.library.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.PaddingValues 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.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import eu.kanade.domain.library.model.LibraryManga import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.domain.manga.model.MangaCover import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.BadgeGroup
import eu.kanade.presentation.components.FastScrollLazyColumn import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.MangaCover.Square import eu.kanade.presentation.components.MangaListItem
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.selectedBackground
import eu.kanade.presentation.util.verticalPadding
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.LibraryItem
@ -47,7 +35,7 @@ fun LibraryList(
) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = contentPadding, contentPadding = contentPadding + PaddingValues(vertical = 8.dp),
) { ) {
item { item {
if (searchQuery.isNullOrEmpty().not()) { if (searchQuery.isNullOrEmpty().not()) {
@ -64,116 +52,25 @@ fun LibraryList(
items = items, items = items,
contentType = { "library_list_item" }, contentType = { "library_list_item" },
) { libraryItem -> ) { libraryItem ->
LibraryListItem( val manga = libraryItem.libraryManga.manga
item = libraryItem, MangaListItem(
showDownloadBadge = showDownloadBadges,
showUnreadBadge = showUnreadBadges,
showLocalBadge = showLocalBadges,
showLanguageBadge = showLanguageBadges,
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
onClick = onClick, title = manga.title,
onLongClick = onLongClick, 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, onGlobalSearchClicked = onGlobalSearchClicked,
) )
} }
LibraryDisplayMode.CompactGrid -> { LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
LibraryCompactGrid( LibraryCompactGrid(
items = library, items = library,
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
showDownloadBadges = showDownloadBadges, showDownloadBadges = showDownloadBadges,
showUnreadBadges = showUnreadBadges, showUnreadBadges = showUnreadBadges,
showLocalBadges = showLocalBadges, showLocalBadges = showLocalBadges,
@ -104,22 +105,6 @@ fun LibraryPager(
onGlobalSearchClicked = onGlobalSearchClicked, 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}") }, titleContent = { Text(text = "${state.selection.size}") },
actions = { actions = {
IconButton(onClick = onClickSelectAll) { IconButton(onClick = onClickSelectAll) {
Icon(Icons.Outlined.SelectAll, contentDescription = "search") Icon(Icons.Outlined.SelectAll, contentDescription = stringResource(R.string.action_select_all))
} }
IconButton(onClick = onClickInvertSelection) { IconButton(onClick = onClickInvertSelection) {
Icon(Icons.Outlined.FlipToBack, contentDescription = "invert") Icon(Icons.Outlined.FlipToBack, contentDescription = stringResource(R.string.action_select_inverse))
} }
}, },
isActionMode = true, isActionMode = true,

View File

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

View File

@ -37,7 +37,7 @@ fun DownloadCustomAmountDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
confirmButton = { confirmButton = {
@ -62,13 +62,13 @@ fun DownloadCustomAmountDialog(
onClick = { setAmount(amount - 10) }, onClick = { setAmount(amount - 10) },
enabled = amount > 0, enabled = amount > 0,
) { ) {
Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowLeft, contentDescription = "") Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowLeft, contentDescription = "-10")
} }
IconButton( IconButton(
onClick = { setAmount(amount - 1) }, onClick = { setAmount(amount - 1) },
enabled = amount > 0, enabled = amount > 0,
) { ) {
Icon(imageVector = Icons.Outlined.ChevronLeft, contentDescription = "") Icon(imageVector = Icons.Outlined.ChevronLeft, contentDescription = "-1")
} }
OutlinedTextField( OutlinedTextField(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -81,13 +81,13 @@ fun DownloadCustomAmountDialog(
onClick = { setAmount(amount + 1) }, onClick = { setAmount(amount + 1) },
enabled = amount < maxAmount, enabled = amount < maxAmount,
) { ) {
Icon(imageVector = Icons.Outlined.ChevronRight, contentDescription = "") Icon(imageVector = Icons.Outlined.ChevronRight, contentDescription = "+1")
} }
IconButton( IconButton(
onClick = { setAmount(amount + 10) }, onClick = { setAmount(amount + 10) },
enabled = amount < maxAmount, 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) } var textHeight by remember { mutableStateOf(0) }
if (bookmark) { if (bookmark) {
Icon( Icon(
imageVector = Icons.Default.Bookmark, imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked), contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), .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.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.material.icons.Icons 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.Edit
import androidx.compose.material.icons.outlined.Save import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Share import androidx.compose.material.icons.outlined.Share
@ -63,7 +63,7 @@ fun MangaCoverDialog(
) { ) {
IconButton(onClick = onDismissRequest) { IconButton(onClick = onDismissRequest) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_close), contentDescription = stringResource(R.string.action_close),
) )
} }

View File

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

View File

@ -23,18 +23,18 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons 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.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.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.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
@ -173,7 +173,7 @@ fun MangaActionRow(
} else { } else {
stringResource(R.string.add_to_library) 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, color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = onAddToLibraryClicked, onClick = onAddToLibraryClicked,
onLongClick = onEditCategory, onLongClick = onEditCategory,
@ -185,7 +185,7 @@ fun MangaActionRow(
} else { } else {
pluralStringResource(id = R.plurals.num_trackers, count = trackingCount, trackingCount) 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, color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
onClick = onTrackingClicked, onClick = onTrackingClicked,
) )
@ -193,7 +193,7 @@ fun MangaActionRow(
if (onWebViewClicked != null) { if (onWebViewClicked != null) {
MangaActionButton( MangaActionButton(
title = stringResource(R.string.action_web_view), title = stringResource(R.string.action_web_view),
icon = Icons.Default.Public, icon = Icons.Outlined.Public,
color = defaultActionButtonColor, color = defaultActionButtonColor,
onClick = onWebViewClicked, onClick = onWebViewClicked,
) )
@ -345,13 +345,13 @@ private fun MangaAndSourceTitlesLarge(
) { ) {
Icon( Icon(
imageVector = when (status) { imageVector = when (status) {
SManga.ONGOING.toLong() -> Icons.Default.Schedule SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
SManga.COMPLETED.toLong() -> Icons.Default.DoneAll SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
SManga.LICENSED.toLong() -> Icons.Default.AttachMoney SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
SManga.CANCELLED.toLong() -> Icons.Default.Close SManga.CANCELLED.toLong() -> Icons.Outlined.Close
SManga.ON_HIATUS.toLong() -> Icons.Default.Pause SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
else -> Icons.Default.Block else -> Icons.Outlined.Block
}, },
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
@ -375,7 +375,7 @@ private fun MangaAndSourceTitlesLarge(
DotSeparatorText() DotSeparatorText()
if (isStubSource) { if (isStubSource) {
Icon( Icon(
imageVector = Icons.Default.Warning, imageVector = Icons.Filled.Warning,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.padding(end = 4.dp) .padding(end = 4.dp)
@ -478,13 +478,13 @@ private fun MangaAndSourceTitlesSmall(
) { ) {
Icon( Icon(
imageVector = when (status) { imageVector = when (status) {
SManga.ONGOING.toLong() -> Icons.Default.Schedule SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
SManga.COMPLETED.toLong() -> Icons.Default.DoneAll SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
SManga.LICENSED.toLong() -> Icons.Default.AttachMoney SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
SManga.CANCELLED.toLong() -> Icons.Default.Close SManga.CANCELLED.toLong() -> Icons.Outlined.Close
SManga.ON_HIATUS.toLong() -> Icons.Default.Pause SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
else -> Icons.Default.Block else -> Icons.Outlined.Block
}, },
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
@ -508,7 +508,7 @@ private fun MangaAndSourceTitlesSmall(
DotSeparatorText() DotSeparatorText()
if (isStubSource) { if (isStubSource) {
Icon( Icon(
imageVector = Icons.Default.Warning, imageVector = Icons.Filled.Warning,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.padding(end = 4.dp) .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.WindowInsets
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.material.icons.Icons 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.filled.Close import androidx.compose.material.icons.outlined.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.Download import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.FilterList 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.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -71,7 +71,7 @@ fun MangaToolbar(
navigationIcon = { navigationIcon = {
IconButton(onClick = onBackClicked) { IconButton(onClick = onBackClicked) {
Icon( 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), contentDescription = stringResource(R.string.abc_action_bar_up_description),
) )
} }
@ -80,13 +80,13 @@ fun MangaToolbar(
if (isActionMode) { if (isActionMode) {
IconButton(onClick = onSelectAll) { IconButton(onClick = onSelectAll) {
Icon( Icon(
imageVector = Icons.Default.SelectAll, imageVector = Icons.Outlined.SelectAll,
contentDescription = stringResource(R.string.action_select_all), contentDescription = stringResource(R.string.action_select_all),
) )
} }
IconButton(onClick = onInvertSelection) { IconButton(onClick = onInvertSelection) {
Icon( Icon(
imageVector = Icons.Default.FlipToBack, imageVector = Icons.Outlined.FlipToBack,
contentDescription = stringResource(R.string.action_select_inverse), contentDescription = stringResource(R.string.action_select_inverse),
) )
} }
@ -161,7 +161,7 @@ fun MangaToolbar(
Box { Box {
IconButton(onClick = { onMoreExpanded(!moreExpanded) }) { IconButton(onClick = { onMoreExpanded(!moreExpanded) }) {
Icon( Icon(
imageVector = Icons.Default.MoreVert, imageVector = Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.abc_action_menu_overflow_description), contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
) )
} }

View File

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

View File

@ -1,7 +1,11 @@
package eu.kanade.presentation.more.settings 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.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.ui.model.AppTheme import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.core.preference.Preference as PreferenceData import eu.kanade.tachiyomi.core.preference.Preference as PreferenceData
@ -47,6 +51,8 @@ sealed class Preference {
val pref: PreferenceData<T>, val pref: PreferenceData<T>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", 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 icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: T) -> Boolean = { true }, override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
@ -55,6 +61,10 @@ sealed class Preference {
) : PreferenceItem<T>() { ) : PreferenceItem<T>() {
internal fun internalSet(newValue: Any) = pref.set(newValue as T) internal fun internalSet(newValue: Any) = pref.set(newValue as T)
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(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, val value: String,
override val title: String, override val title: String,
override val subtitle: String? = "%s", 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 icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }, override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
@ -78,7 +90,15 @@ sealed class Preference {
data class MultiSelectListPreference( data class MultiSelectListPreference(
val pref: PreferenceData<Set<String>>, val pref: PreferenceData<Set<String>>,
override val title: 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 icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Set<String>) -> 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.annotation.StringRes
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons 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.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -28,7 +28,7 @@ fun PreferenceScaffold(
if (onBackPressed != null) { if (onBackPressed != null) {
IconButton(onClick = onBackPressed) { IconButton(onClick = onBackPressed) {
Icon( Icon(
imageVector = Icons.Default.ArrowBack, imageVector = Icons.Outlined.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description), contentDescription = stringResource(R.string.abc_action_bar_up_description),
) )
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -177,28 +177,6 @@ class SettingsLibraryScreen : SearchableSettings {
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState() 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 included by libraryUpdateCategoriesPref.collectAsState()
val excluded by libraryUpdateCategoriesExcludePref.collectAsState() val excluded by libraryUpdateCategoriesExcludePref.collectAsState()
var showDialog by rememberSaveable { mutableStateOf(false) } var showDialog by rememberSaveable { mutableStateOf(false) }
@ -224,7 +202,6 @@ class SettingsLibraryScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = libraryUpdateIntervalPref, pref = libraryUpdateIntervalPref,
title = stringResource(R.string.pref_library_update_interval), title = stringResource(R.string.pref_library_update_interval),
subtitle = "%s",
entries = mapOf( entries = mapOf(
0 to stringResource(R.string.update_never), 0 to stringResource(R.string.update_never),
12 to stringResource(R.string.update_12hour), 12 to stringResource(R.string.update_12hour),
@ -242,8 +219,13 @@ class SettingsLibraryScreen : SearchableSettings {
pref = libraryUpdateDeviceRestrictionPref, pref = libraryUpdateDeviceRestrictionPref,
enabled = libraryUpdateInterval > 0, enabled = libraryUpdateInterval > 0,
title = stringResource(R.string.pref_library_update_restriction), title = stringResource(R.string.pref_library_update_restriction),
subtitle = stringResource(R.string.restrictions, deviceRestrictions), subtitle = stringResource(R.string.restrictions),
entries = deviceRestrictionEntries, 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 = { onValueChanged = {
// Post to event looper to allow the preference to be updated. // Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) } ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
@ -253,8 +235,11 @@ class SettingsLibraryScreen : SearchableSettings {
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryUpdateMangaRestrictionPref, pref = libraryUpdateMangaRestrictionPref,
title = stringResource(R.string.pref_library_update_manga_restriction), title = stringResource(R.string.pref_library_update_manga_restriction),
subtitle = mangaRestrictions, entries = mapOf(
entries = mangaRestrictionEntries, 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( Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.categories), title = stringResource(R.string.categories),
@ -341,7 +326,7 @@ class SettingsLibraryScreen : SearchableSettings {
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
confirmButton = { confirmButton = {

View File

@ -8,7 +8,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.ChromeReaderMode
import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.CollectionsBookmark import androidx.compose.material.icons.outlined.CollectionsBookmark
@ -98,7 +98,7 @@ object SettingsMainScreen : Screen {
navigationIcon = { navigationIcon = {
IconButton(onClick = backPress::invoke) { IconButton(onClick = backPress::invoke) {
Icon( Icon(
imageVector = Icons.Default.ArrowBack, imageVector = Icons.Outlined.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description), 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.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons 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.filled.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -95,8 +95,8 @@ class SettingsSearchScreen : Screen {
if (canPop) { if (canPop) {
IconButton(onClick = navigator::pop) { IconButton(onClick = navigator::pop) {
Icon( Icon(
imageVector = Icons.Default.ArrowBack, imageVector = Icons.Outlined.ArrowBack,
contentDescription = null, contentDescription = stringResource(R.string.abc_action_bar_up_description),
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
@ -131,7 +131,7 @@ class SettingsSearchScreen : Screen {
if (textFieldValue.text.isNotEmpty()) { if (textFieldValue.text.isNotEmpty()) {
IconButton(onClick = { textFieldValue = TextFieldValue() }) { IconButton(onClick = { textFieldValue = TextFieldValue() }) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Outlined.Close,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
) )

View File

@ -49,7 +49,6 @@ class SettingsSecurityScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = securityPreferences.lockAppAfter(), pref = securityPreferences.lockAppAfter(),
title = stringResource(R.string.lock_when_idle), title = stringResource(R.string.lock_when_idle),
subtitle = "%s",
enabled = authSupported && useAuth, enabled = authSupported && useAuth,
entries = LockAfterValues entries = LockAfterValues
.associateWith { .associateWith {
@ -72,7 +71,6 @@ class SettingsSecurityScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = securityPreferences.secureScreen(), pref = securityPreferences.secureScreen(),
title = stringResource(R.string.secure_screen), title = stringResource(R.string.secure_screen),
subtitle = "%s",
entries = SecurityPreferences.SecureScreenMode.values() entries = SecurityPreferences.SecureScreenMode.values()
.associateWith { stringResource(it.titleResId) }, .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.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons 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.Visibility
import androidx.compose.material.icons.filled.VisibilityOff 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.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@ -71,7 +71,7 @@ class SettingsTrackingScreen : SearchableSettings {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/help/guides/tracking/") }) { IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/help/guides/tracking/") }) {
Icon( Icon(
imageVector = Icons.Default.HelpOutline, imageVector = Icons.Outlined.HelpOutline,
contentDescription = stringResource(R.string.tracking_guide), contentDescription = stringResource(R.string.tracking_guide),
) )
} }
@ -199,7 +199,7 @@ class SettingsTrackingScreen : SearchableSettings {
) )
IconButton(onClick = onDismissRequest) { IconButton(onClick = onDismissRequest) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_close), contentDescription = stringResource(R.string.action_close),
) )
} }
@ -227,9 +227,9 @@ class SettingsTrackingScreen : SearchableSettings {
IconButton(onClick = { hidePassword = !hidePassword }) { IconButton(onClick = { hidePassword = !hidePassword }) {
Icon( Icon(
imageVector = if (hidePassword) { imageVector = if (hidePassword) {
Icons.Default.Visibility Icons.Filled.Visibility
} else { } else {
Icons.Default.VisibilityOff Icons.Filled.VisibilityOff
}, },
contentDescription = null, contentDescription = null,
) )
@ -317,7 +317,7 @@ class SettingsTrackingScreen : SearchableSettings {
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = onDismissRequest, onClick = onDismissRequest,
) { ) {
Text(text = stringResource(android.R.string.cancel)) Text(text = stringResource(R.string.action_cancel))
} }
Button( Button(
modifier = Modifier.weight(1f), 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.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.secondaryItemAlpha import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
@ -158,7 +159,7 @@ fun AppThemePreviewItem(
.padding(end = 4.dp) .padding(end = 4.dp)
.background( .background(
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
shape = RoundedCornerShape(9.dp), shape = MaterialTheme.shapes.small,
), ),
) )
@ -168,8 +169,8 @@ fun AppThemePreviewItem(
) { ) {
if (selected) { if (selected) {
Icon( Icon(
imageVector = Icons.Default.CheckCircle, imageVector = Icons.Filled.CheckCircle,
contentDescription = null, contentDescription = stringResource(R.string.selected),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) )
} }
@ -182,7 +183,7 @@ fun AppThemePreviewItem(
.padding(start = 8.dp, top = 2.dp) .padding(start = 8.dp, top = 2.dp)
.background( .background(
color = dividerColor, color = dividerColor,
shape = RoundedCornerShape(9.dp), shape = MaterialTheme.shapes.small,
) )
.fillMaxWidth(0.5f) .fillMaxWidth(0.5f)
.aspectRatio(MangaCover.Book.ratio), .aspectRatio(MangaCover.Book.ratio),
@ -242,7 +243,7 @@ fun AppThemePreviewItem(
.weight(1f) .weight(1f)
.background( .background(
color = MaterialTheme.colorScheme.onSurface, 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.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@ -71,7 +72,7 @@ fun EditTextPreferenceWidget(
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { 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.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton 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.isScrolledToEnd
import eu.kanade.presentation.util.isScrolledToStart import eu.kanade.presentation.util.isScrolledToStart
import eu.kanade.presentation.util.minimumTouchTargetSize import eu.kanade.presentation.util.minimumTouchTargetSize
import eu.kanade.tachiyomi.R
@Composable @Composable
fun <T> ListPreferenceWidget( fun <T> ListPreferenceWidget(
@ -40,7 +40,7 @@ fun <T> ListPreferenceWidget(
TextPreferenceWidget( TextPreferenceWidget(
title = title, title = title,
subtitle = subtitle?.format(entries[value]), subtitle = subtitle,
icon = icon, icon = icon,
onPreferenceClick = { showDialog(true) }, onPreferenceClick = { showDialog(true) },
) )
@ -73,7 +73,7 @@ fun <T> ListPreferenceWidget(
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { showDialog(false) }) { 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( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(8.dp)) .clip(MaterialTheme.shapes.small)
.selectable( .selectable(
selected = isSelected, selected = isSelected,
onClick = { if (!isSelected) onSelected() }, 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.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -23,6 +22,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.minimumTouchTargetSize import eu.kanade.presentation.util.minimumTouchTargetSize
import eu.kanade.tachiyomi.R
@Composable @Composable
fun MultiSelectListPreferenceWidget( fun MultiSelectListPreferenceWidget(
@ -34,7 +34,7 @@ fun MultiSelectListPreferenceWidget(
TextPreferenceWidget( TextPreferenceWidget(
title = preference.title, title = preference.title,
subtitle = preference.subtitle, subtitle = preference.subtitleProvider(values, preference.entries),
icon = preference.icon, icon = preference.icon,
onPreferenceClick = { showDialog(true) }, onPreferenceClick = { showDialog(true) },
) )
@ -62,7 +62,7 @@ fun MultiSelectListPreferenceWidget(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(8.dp)) .clip(MaterialTheme.shapes.small)
.selectable( .selectable(
selected = isSelected, selected = isSelected,
onClick = { onSelectionChanged() }, onClick = { onSelectionChanged() },
@ -99,7 +99,7 @@ fun MultiSelectListPreferenceWidget(
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showDialog(false) }) { 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( SwitchPreferenceWidget(
title = "Text preference with icon", title = "Text preference with icon",
subtitle = "Text preference summary", subtitle = "Text preference summary",
icon = Icons.Default.Preview, icon = Icons.Filled.Preview,
checked = true, checked = true,
onCheckedChanged = {}, onCheckedChanged = {},
) )

View File

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

View File

@ -10,9 +10,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -21,8 +20,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
import eu.kanade.tachiyomi.R
@Composable @Composable
fun TrackingPreferenceWidget( fun TrackingPreferenceWidget(
@ -45,7 +46,7 @@ fun TrackingPreferenceWidget(
Box( Box(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.background(color = Color(logoColor), shape = RoundedCornerShape(8.dp)) .background(color = Color(logoColor), shape = MaterialTheme.shapes.small)
.padding(4.dp), .padding(4.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
@ -65,12 +66,12 @@ fun TrackingPreferenceWidget(
) )
if (checked) { if (checked) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Outlined.Done,
modifier = Modifier modifier = Modifier
.padding(4.dp) .padding(4.dp)
.size(32.dp), .size(32.dp),
tint = Color(0xFF4CAF50), 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.layout.padding
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CheckBox import androidx.compose.material.icons.rounded.CheckBox
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
@ -79,7 +78,7 @@ fun <T> TriStateListDialog(
val state = selected[index] val state = selected[index]
Row( Row(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(8.dp)) .clip(MaterialTheme.shapes.small)
.clickable { .clickable {
selected[index] = when (state) { selected[index] = when (state) {
State.UNCHECKED -> State.CHECKED State.UNCHECKED -> State.CHECKED
@ -103,7 +102,13 @@ fun <T> TriStateListDialog(
} else { } else {
MaterialTheme.colorScheme.primary 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)) Text(text = itemLabel(item))
} }
@ -117,7 +122,7 @@ fun <T> TriStateListDialog(
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
confirmButton = { confirmButton = {

View File

@ -27,7 +27,7 @@ fun UpdatesDeleteConfirmationDialog(
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { 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.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FlipToBack import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
@ -215,7 +215,7 @@ private fun UpdatesAppBar(
actions = { actions = {
IconButton(onClick = onUpdateLibrary) { IconButton(onClick = onUpdateLibrary) {
Icon( Icon(
imageVector = Icons.Default.Refresh, imageVector = Icons.Outlined.Refresh,
contentDescription = stringResource(R.string.action_update_library), contentDescription = stringResource(R.string.action_update_library),
) )
} }
@ -225,13 +225,13 @@ private fun UpdatesAppBar(
actionModeActions = { actionModeActions = {
IconButton(onClick = onSelectAll) { IconButton(onClick = onSelectAll) {
Icon( Icon(
imageVector = Icons.Default.SelectAll, imageVector = Icons.Outlined.SelectAll,
contentDescription = stringResource(R.string.action_select_all), contentDescription = stringResource(R.string.action_select_all),
) )
} }
IconButton(onClick = onInvertSelection) { IconButton(onClick = onInvertSelection) {
Icon( Icon(
imageVector = Icons.Default.FlipToBack, imageVector = Icons.Outlined.FlipToBack,
contentDescription = stringResource(R.string.action_select_inverse), contentDescription = stringResource(R.string.action_select_inverse),
) )
} }

View File

@ -205,7 +205,7 @@ fun UpdatesUiItem(
var textHeight by remember { mutableStateOf(0) } var textHeight by remember { mutableStateOf(0) }
if (bookmark) { if (bookmark) {
Icon( Icon(
imageVector = Icons.Default.Bookmark, imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked), contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), .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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons 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.filled.ArrowForward import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -48,13 +48,13 @@ fun WebViewScreen(
title = state.pageTitle ?: initialTitle, title = state.pageTitle ?: initialTitle,
subtitle = state.content.getCurrentUrl(), subtitle = state.content.getCurrentUrl(),
navigateUp = onNavigateUp, navigateUp = onNavigateUp,
navigationIcon = Icons.Default.Close, navigationIcon = Icons.Outlined.Close,
actions = { actions = {
AppBarActions( AppBarActions(
listOf( listOf(
AppBar.Action( AppBar.Action(
title = stringResource(R.string.action_webview_back), title = stringResource(R.string.action_webview_back),
icon = Icons.Default.ArrowBack, icon = Icons.Outlined.ArrowBack,
onClick = { onClick = {
if (navigator.canGoBack) { if (navigator.canGoBack) {
navigator.navigateBack() navigator.navigateBack()
@ -64,7 +64,7 @@ fun WebViewScreen(
), ),
AppBar.Action( AppBar.Action(
title = stringResource(R.string.action_webview_forward), title = stringResource(R.string.action_webview_forward),
icon = Icons.Default.ArrowForward, icon = Icons.Outlined.ArrowForward,
onClick = { onClick = {
if (navigator.canGoForward) { if (navigator.canGoForward) {
navigator.navigateForward() navigator.navigateForward()

View File

@ -48,6 +48,8 @@ class DownloadCache(
private val scope = CoroutineScope(Dispatchers.IO) 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 * 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. * issues, as the cache is only used for UI feedback.
@ -241,56 +243,62 @@ class DownloadCache(
} }
renewalJob = scope.launchIO { renewalJob = scope.launchIO {
var sources = getSources() try {
notifier.onCacheProgress()
// Try to wait until extensions and sources have loaded var sources = getSources()
withTimeout(30.seconds) {
while (!extensionManager.isInitialized) {
delay(2.seconds)
}
while (sources.isEmpty()) { // Try to wait until extensions and sources have loaded
delay(2.seconds) withTimeout(30.seconds) {
sources = getSources() while (!extensionManager.isInitialized) {
} delay(2.seconds)
} }
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty() while (sources.isEmpty()) {
.associate { it.name to SourceDirectory(it) } delay(2.seconds)
.mapNotNullKeys { entry -> sources = getSources()
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() val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
notifyChanges() .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. * Status of download. Used for correct notification icon.
*/ */
@ -233,4 +244,14 @@ internal class DownloadNotifier(private val context: Context) {
errorThrown = true errorThrown = true
isDownloading = false 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 { private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile): Boolean {
if (!downloadPreferences.splitTallImages().get()) return true if (!downloadPreferences.splitTallImages().get()) return true
val filename = String.format("%03d", page.number) val filenamePrefix = String.format("%03d", page.number)
val imageFile = tmpDir.listFiles()?.find { it.name!!.startsWith(filename) } val imageFile = tmpDir.listFiles()?.firstOrNull { it.name.orEmpty().startsWith(filenamePrefix) }
?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number)) ?: 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. // check if the original page was previously splitted before then skip.
if (imageFile.name!!.contains("__")) return true if (imageFile.name!!.contains("__")) return true
return try { return try {
ImageUtil.splitTallImage(imageFile, imageFilePath) ImageUtil.splitTallImage(tmpDir, imageFile, filenamePrefix)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
false false

View File

@ -59,7 +59,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setLargeIcon(notificationBitmap) setLargeIcon(notificationBitmap)
setOngoing(true) setOngoing(true)
setOnlyAlertOnce(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 ID_DOWNLOAD_CHAPTER_COMPLETE = -203
const val CHANNEL_DOWNLOADER_ERROR = "downloader_error_channel" const val CHANNEL_DOWNLOADER_ERROR = "downloader_error_channel"
const val ID_DOWNLOAD_CHAPTER_ERROR = -202 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. * Notification channel and ids used by the library updater.
@ -159,6 +161,11 @@ object Notifications {
setGroup(GROUP_DOWNLOADER) setGroup(GROUP_DOWNLOADER)
setShowBadge(false) 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) { buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) {
setName(context.getString(R.string.channel_progress)) setName(context.getString(R.string.channel_progress))
setGroup(GROUP_BACKUP_RESTORE) setGroup(GROUP_BACKUP_RESTORE)

View File

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

View File

@ -100,7 +100,7 @@ internal class ExtensionGithubApi {
private fun List<ExtensionJsonObject>.toExtensions(): List<Extension.Available> { private fun List<ExtensionJsonObject>.toExtensions(): List<Extension.Available> {
return this return this
.filter { .filter {
val libVersion = it.version.substringBeforeLast('.').toDouble() val libVersion = it.extractLibVersion()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
} }
.map { .map {
@ -109,6 +109,7 @@ internal class ExtensionGithubApi {
pkgName = it.pkg, pkgName = it.pkg,
versionName = it.version, versionName = it.version,
versionCode = it.code, versionCode = it.code,
libVersion = it.extractLibVersion(),
lang = it.lang, lang = it.lang,
isNsfw = it.nsfw == 1, isNsfw = it.nsfw == 1,
hasReadme = it.hasReadme == 1, hasReadme = it.hasReadme == 1,
@ -142,6 +143,10 @@ internal class ExtensionGithubApi {
REPO_URL_PREFIX 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/" 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 pkgName: String
abstract val versionName: String abstract val versionName: String
abstract val versionCode: Long abstract val versionCode: Long
abstract val libVersion: Double
abstract val lang: String? abstract val lang: String?
abstract val isNsfw: Boolean abstract val isNsfw: Boolean
abstract val hasReadme: Boolean abstract val hasReadme: Boolean
@ -20,6 +21,7 @@ sealed class Extension {
override val pkgName: String, override val pkgName: String,
override val versionName: String, override val versionName: String,
override val versionCode: Long, override val versionCode: Long,
override val libVersion: Double,
override val lang: String, override val lang: String,
override val isNsfw: Boolean, override val isNsfw: Boolean,
override val hasReadme: Boolean, override val hasReadme: Boolean,
@ -37,6 +39,7 @@ sealed class Extension {
override val pkgName: String, override val pkgName: String,
override val versionName: String, override val versionName: String,
override val versionCode: Long, override val versionCode: Long,
override val libVersion: Double,
override val lang: String, override val lang: String,
override val isNsfw: Boolean, override val isNsfw: Boolean,
override val hasReadme: Boolean, override val hasReadme: Boolean,
@ -51,6 +54,7 @@ sealed class Extension {
override val pkgName: String, override val pkgName: String,
override val versionName: String, override val versionName: String,
override val versionCode: Long, override val versionCode: Long,
override val libVersion: Double,
val signatureHash: String, val signatureHash: String,
override val lang: String? = null, override val lang: String? = null,
override val isNsfw: Boolean = false, override val isNsfw: Boolean = false,

View File

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

View File

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

View File

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

View File

@ -29,10 +29,6 @@ class SourceSearchController(
@Composable @Composable
override fun ComposeContent() { 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( SourceSearchScreen(
presenter = presenter, presenter = presenter,
navigateUp = { router.popCurrentController() }, navigateUp = { router.popCurrentController() },
@ -46,8 +42,10 @@ class SourceSearchController(
}, },
onWebViewClick = f@{ onWebViewClick = f@{
val source = presenter.source as? HttpSource ?: return@f val source = presenter.source as? HttpSource ?: return@f
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name) activity?.let { context ->
context.startActivity(intent) 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.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority import logcat.LogPriority
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -39,6 +40,7 @@ class SourcesFilterPresenter(
logcat(LogPriority.ERROR, exception) logcat(LogPriority.ERROR, exception)
_events.send(Event.FailedFetchingLanguages) _events.send(Event.FailedFetchingLanguages)
} }
.stateIn(presenterScope)
.collectLatest(::collectLatestSourceLangMap) .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.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons 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.filled.PlayArrow
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Pause
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -152,7 +152,7 @@ class DownloadController :
IconButton(onClick = { onExpanded(!expanded) }) { IconButton(onClick = { onExpanded(!expanded) }) {
Icon( Icon(
imageVector = Icons.Outlined.MoreVert, imageVector = Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.label_more), contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
) )
} }
CascadeDropdownMenu( CascadeDropdownMenu(
@ -234,9 +234,9 @@ class DownloadController :
}, },
icon = { icon = {
val icon = if (isRunning) { val icon = if (isRunning) {
Icons.Default.Pause Icons.Outlined.Pause
} else { } else {
Icons.Default.PlayArrow Icons.Filled.PlayArrow
} }
Icon(imageVector = icon, contentDescription = null) 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.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
@ -126,7 +127,6 @@ class LibraryController(
settingsSheet = LibrarySettingsSheet(router) { group -> settingsSheet = LibrarySettingsSheet(router) { group ->
when (group) { when (group) {
is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged() is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged()
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
else -> {} // Handled via different mechanisms else -> {} // Handled via different mechanisms
} }
} }
@ -152,12 +152,10 @@ class LibraryController(
} }
private fun onFilterChanged() { private fun onFilterChanged() {
presenter.requestFilterUpdate() viewScope.launchUI {
activity?.invalidateOptionsMenu() presenter.requestFilterUpdate()
} activity?.invalidateOptionsMenu()
}
private fun onSortChanged() {
presenter.requestSortUpdate()
} }
fun search(query: String) { fun search(query: String) {
@ -180,7 +178,7 @@ class LibraryController(
* Clear all of the manga currently selected, and * Clear all of the manga currently selected, and
* invalidate the action mode to revert the top toolbar * invalidate the action mode to revert the top toolbar
*/ */
fun clearSelection() { private fun clearSelection() {
presenter.clearSelection() presenter.clearSelection()
} }

View File

@ -11,11 +11,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.core.prefs.CheckboxState import eu.kanade.core.prefs.CheckboxState
import eu.kanade.core.prefs.PreferenceMutableState 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.base.BasePreferences
import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.SetMangaCategories 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.Manga
import eu.kanade.domain.manga.model.MangaUpdate import eu.kanade.domain.manga.model.MangaUpdate
import eu.kanade.domain.manga.model.isLocal 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.category.visualName
import eu.kanade.presentation.library.LibraryState import eu.kanade.presentation.library.LibraryState
import eu.kanade.presentation.library.LibraryStateImpl 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.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNonCancellable import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart
import rx.Observable import kotlinx.coroutines.flow.receiveAsFlow
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.text.Collator import java.text.Collator
@ -79,7 +76,7 @@ typealias LibraryMap = Map<Long, List<LibraryItem>>
class LibraryPresenter( class LibraryPresenter(
private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl, private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl,
private val getLibraryManga: GetLibraryManga = Injekt.get(), 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 getCategories: GetCategories = Injekt.get(),
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val setReadStatus: SetReadStatus = Injekt.get(), private val setReadStatus: SetReadStatus = Injekt.get(),
@ -111,15 +108,8 @@ class LibraryPresenter(
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState() val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
val isIncognitoMode: Boolean by preferences.incognitoMode().asState() val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
/** private val _filterChanges: Channel<Unit> = Channel(Int.MAX_VALUE)
* Relay used to apply the UI filters to the last emission of the library. private val filterChanges = _filterChanges.receiveAsFlow().onStart { emit(Unit) }
*/
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 var librarySubscription: Job? = null private var librarySubscription: Job? = null
@ -129,30 +119,23 @@ class LibraryPresenter(
subscribeLibrary() subscribeLibrary()
} }
/**
* Subscribes to library if needed.
*/
fun subscribeLibrary() { 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 * - 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 * - 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 * - Fetch badges to maps and retrieve as needed instead of fetching all of them at once
*/ */
if (librarySubscription == null || librarySubscription!!.isCancelled) { if (librarySubscription == null || librarySubscription!!.isCancelled) {
librarySubscription = presenterScope.launchIO { librarySubscription = presenterScope.launchIO {
getLibraryFlow().asObservable() combine(getLibraryFlow(), getTracksPerManga.subscribe(), filterChanges) { library, tracks, _ ->
.combineLatest(getFilterObservable()) { lib, tracks -> library.mangaMap
lib.copy(mangaMap = applyFilters(lib.mangaMap, tracks)) .applyFilters(tracks)
} .applySort(library.categories)
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> }
lib.copy(mangaMap = applySort(lib.categories, lib.mangaMap))
}
.observeOn(AndroidSchedulers.mainThread())
.asFlow()
.collectLatest { .collectLatest {
state.isLoading = false state.isLoading = false
loadedManga = it.mangaMap loadedManga = it
} }
} }
} }
@ -160,21 +143,24 @@ class LibraryPresenter(
/** /**
* Applies library filters to the given map of manga. * 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 downloadedOnly = preferences.downloadedOnly().get()
val filterDownloaded = libraryPreferences.filterDownloaded().get() val filterDownloaded = libraryPreferences.filterDownloaded().get()
val filterUnread = libraryPreferences.filterUnread().get() val filterUnread = libraryPreferences.filterUnread().get()
val filterStarted = libraryPreferences.filterStarted().get() val filterStarted = libraryPreferences.filterStarted().get()
val filterBookmarked = libraryPreferences.filterBookmarked().get() val filterBookmarked = libraryPreferences.filterBookmarked().get()
val filterCompleted = libraryPreferences.filterCompleted().get() val filterCompleted = libraryPreferences.filterCompleted().get()
val loggedInServices = trackManager.services.filter { trackService -> trackService.isLogged }
val loggedInTrackServices = trackManager.services.filter { trackService -> trackService.isLogged }
.associate { trackService -> .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 -> val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{ item ->
if (!downloadedOnly && filterDownloaded == State.IGNORE.value) return@downloaded true if (!downloadedOnly && filterDownloaded == State.IGNORE.value) return@downloaded true
@ -237,25 +223,21 @@ class LibraryPresenter(
} }
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item -> 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 exclude = mangaTracks.filter { it in excludedTracks }
val containsInclude = loggedInServices.filterValues { it == State.INCLUDE.value } val include = mangaTracks.filter { it in includedTracks }
if (!containsExclude.any() && !containsInclude.any()) return@tracking true // TODO: Simplify the filter logic
if (includedTracks.isNotEmpty() && excludedTracks.isNotEmpty()) {
val exclude = trackedManga?.filterKeys { containsExclude.containsKey(it) }?.values ?: emptyList() return@tracking if (exclude.isNotEmpty()) false else include.isNotEmpty()
val include = trackedManga?.filterKeys { containsInclude.containsKey(it) }?.values ?: emptyList()
if (containsInclude.any() && containsExclude.any()) {
return@tracking if (exclude.isNotEmpty()) !exclude.any() else include.any()
} }
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 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. * 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 sortModes = categories.associate { it.id to it.sort }
val locale = Locale.getDefault() val locale = Locale.getDefault()
val collator = Collator.getInstance(locale).apply { val collator = Collator.getInstance(locale).apply {
strength = Collator.PRIMARY 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 sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
val sort = sortModes[i1.libraryManga.category]!! val sort = sortModes[i1.libraryManga.category]!!
when (sort.type) { when (sort.type) {
LibrarySort.Type.Alphabetical -> { LibrarySort.Type.Alphabetical -> {
collator.compare(i1.libraryManga.manga.title.lowercase(locale), i2.libraryManga.manga.title.lowercase(locale)) sortAlphabetically(i1, i2)
} }
LibrarySort.Type.LastRead -> { LibrarySort.Type.LastRead -> {
i1.libraryManga.lastRead.compareTo(i2.libraryManga.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) { val comparator = if (sortModes[entry.key]!!.isAscending) {
Comparator(sortFn) Comparator(sortFn)
} else { } else {
Collections.reverseOrder(sortFn) Collections.reverseOrder(sortFn)
} }
entry.value.sortedWith(comparator) entry.value.sortedWith(comparator.thenComparator(sortAlphabetically))
} }
} }
@ -342,13 +326,18 @@ class LibraryPresenter(
getLibraryManga.subscribe(), getLibraryManga.subscribe(),
libraryPreferences.downloadBadge().changes(), libraryPreferences.downloadBadge().changes(),
libraryPreferences.filterDownloaded().changes(), libraryPreferences.filterDownloaded().changes(),
preferences.downloadedOnly().changes(),
downloadCache.changes, downloadCache.changes,
) { libraryMangaList, downloadBadgePref, filterDownloadedPref, _ -> ) { libraryMangaList, downloadBadgePref, filterDownloadedPref, downloadedOnly, _ ->
libraryMangaList libraryMangaList
.map { libraryManga -> .map { libraryManga ->
val needsDownloadCounts = downloadBadgePref ||
filterDownloadedPref != State.IGNORE.value ||
downloadedOnly
// Display mode based on user preference: take it from global library setting or category // Display mode based on user preference: take it from global library setting or category
LibraryItem(libraryManga).apply { LibraryItem(libraryManga).apply {
downloadCount = if (downloadBadgePref || filterDownloadedPref == State.INCLUDE.value) { downloadCount = if (needsDownloadCounts) {
downloadManager.getDownloadCount(libraryManga.manga).toLong() downloadManager.getDownloadCount(libraryManga.manga).toLong()
} else { } else {
0 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. * Requests the library to be filtered.
*/ */
fun requestFilterUpdate() { suspend fun requestFilterUpdate() = withIOContext {
filterTriggerRelay.call(Unit) _filterChanges.send(Unit)
}
/**
* Requests the library to be sorted.
*/
fun requestSortUpdate() {
sortTriggerRelay.call(Unit)
} }
/** /**
@ -432,9 +384,9 @@ class LibraryPresenter(
*/ */
suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> { suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList() if (mangas.isEmpty()) return emptyList()
return mangas.toSet() return mangas
.map { getCategories.await(it.id) } .map { getCategories.await(it.id).toSet() }
.reduce { set1, set2 -> set1.intersect(set2).toMutableList() } .reduce { set1, set2 -> set1.intersect(set2) }
} }
/** /**
@ -444,9 +396,9 @@ class LibraryPresenter(
*/ */
suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> { suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList() if (mangas.isEmpty()) return emptyList()
val mangaCategories = mangas.toSet().map { getCategories.await(it.id) } val mangaCategories = mangas.map { getCategories.await(it.id).toSet() }
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2).toMutableList() } val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
return mangaCategories.flatten().distinct().subtract(common).toMutableList() return mangaCategories.flatten().distinct().subtract(common)
} }
/** /**
@ -524,10 +476,10 @@ class LibraryPresenter(
*/ */
fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) { fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
presenterScope.launchNonCancellable { presenterScope.launchNonCancellable {
mangaList.map { manga -> mangaList.forEach { manga ->
val categoryIds = getCategories.await(manga.id) val categoryIds = getCategories.await(manga.id)
.map { it.id } .map { it.id }
.subtract(removeCategories) .subtract(removeCategories.toSet())
.plus(addCategories) .plus(addCategories)
.toList() .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 { sealed class Dialog {
data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog() data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog()
data class DeleteManga(val manga: List<Manga>) : 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) activity?.toast(R.string.chapter_settings_updated)
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(R.string.action_cancel, null)
.create() .create()
} }

View File

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

View File

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

View File

@ -48,7 +48,7 @@ class SetTrackStatusDialog<T> : DialogController
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
listener.setStatus(item, selectedIndex) listener.setStatus(item, selectedIndex)
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(R.string.action_cancel, null)
.create() .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.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer 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.preference.toggle
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.createReaderThemeContext import eu.kanade.tachiyomi.util.system.createReaderThemeContext
@ -275,6 +276,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
*/ */
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_open_in_web_view -> {
openChapterInWebview()
}
R.id.action_bookmark -> { R.id.action_bookmark -> {
presenter.bookmarkCurrentChapter(true) presenter.bookmarkCurrentChapter(true)
invalidateOptionsMenu() invalidateOptionsMenu()
@ -665,6 +669,15 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
startPostponedEnterTransition() 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) { private fun showReadingModeToast(mode: Int) {
try { try {
val strings = resources.getStringArray(R.array.viewers_selector) val strings = resources.getStringArray(R.array.viewers_selector)

View File

@ -40,7 +40,7 @@ class ReaderPageSheet(
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
activity.setAsCover(page) activity.setAsCover(page)
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(R.string.action_cancel, null)
.show() .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.data.track.job.DelayedTrackingUpdateJob
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page 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.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
@ -606,6 +607,15 @@ class ReaderPresenter(
return viewerChaptersRelay.value?.currChapter 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. * Bookmarks the currently active chapter.
*/ */

View File

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