Compare commits

...

61 Commits

Author SHA1 Message Date
3222247969 Release v0.14.2 2022-10-31 17:38:56 -04:00
dd6c9ce2fe Avoid crashing if multiple entries exist for same URL/source
Related to #8331. We'll need to revisit some of the get/insert logic to make sure this doesn't actually happen,
but at least it'll stop crashing for now.
2022-10-31 17:38:56 -04:00
7446b28ff1 Translations update from Hosted Weblate (#8342)
Weblate translations

Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Gediminas Murauskas <muziejusinfo@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Hiroshi <borlonjhayron1119@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: Jhayron Borlon <borlonjhayron1119@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: SHA 2048 <ajrtcuev@gmail.com>
Co-authored-by: SHAWKIK ISLAM JOHA <shawkikislam@gmail.com>
Co-authored-by: Shyntia Tan <shyntia.tan@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Unai <uesandi@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/eu/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/he/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
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/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/nb_NO/
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/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/
Translation: Tachiyomi/Tachiyomi 0.x

Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Gediminas Murauskas <muziejusinfo@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Hiroshi <borlonjhayron1119@gmail.com>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: SHA 2048 <ajrtcuev@gmail.com>
Co-authored-by: SHAWKIK ISLAM JOHA <shawkikislam@gmail.com>
Co-authored-by: Shyntia Tan <shyntia.tan@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Unai <uesandi@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
2022-10-31 17:38:47 -04:00
38c6702b8f Perform haptic feedback where appropriate (#8378) 2022-10-31 17:23:00 -04:00
afcf4b2988 Fix resetting filter resets browse pager (#8394)
Fix resetinf filter resets browse pager
2022-10-31 17:22:53 -04:00
ebb96a6ff4 Use selectedBackground for other list items to match with others (#8379)
* Use `selectedBackground` for other list items to match with others

* Remove unused imports
2022-10-31 17:20:31 -04:00
8b0affe9bd Set softWrap to true again for Pill text (#8391) 2022-10-31 17:20:24 -04:00
1a25cea0d6 Tweak how getChapterUrl works (#8392) 2022-10-31 13:05:27 -04:00
2ecbcdf4bd Reword "title"/"titles -> "entry"/"entries" (#8390) 2022-10-31 11:24:47 -04:00
642b392d44 Fix crash in ReaderReadingModeSettings when reverse portrait orientation is set 2022-10-30 23:10:59 -04:00
8dce7b3e9e Disable ChapterHeader & ChapterDownloadIndicator click when in selection mode (#8350)
* Disable `ChapterHeader` click when in selection mode

* Disable `ChapterDownloadIndicator` click when in selection mode

* Review changes

* Merge remote-tracking branch 'origin/master' into patch-7

* Merge remote-tracking branch 'origin/master' into patch-7

* Revert back to old implementation
2022-10-30 22:57:56 -04:00
33e90d6449 Clean up library download chapters logic
We can probably clean up the same logic in the manga controller at some point too, but that stuff's messy.
Also fixes the spacing issue that the new icon introduced.
2022-10-30 22:56:07 -04:00
50b17d5d34 Add different download options within the Library (#8267)
* feat: add download options to library

* feat: use max instead of min

* feat: remove download all option

* feat: applied requested changes + rename some functions

* feat: merge downloadAllUnreadChapters and downloadUnreadChapters into one function

* Apply suggestions from code review

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

* feat: apply lint suggestions + fix code

feat: apply lint suggestions + fix code

* feat: revert onClickDownload back to onDownloadClicked

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2022-10-30 22:27:48 -04:00
7818885406 Use proper content color for filter icon in library toolbar 2022-10-30 22:19:02 -04:00
26af7ccc77 Use BOM for kotlinx.coroutines dependencies 2022-10-30 19:38:18 -04:00
5d1f79012e Fix some crashes
- Delay the initial emission of updates/sources/extensions lists instead of using a state flow. This hopefully avoids rapid initial recompositions that cause the LazyColumn key duplication crashes. (Closes #8371)
- Fix a NPE in BrowseSourcePresenter
2022-10-30 18:43:16 -04:00
cac80daa71 Set source properly when creating manga entries
Fixes #8333
2022-10-30 17:40:17 -04:00
fc184f1cfa Clean up download ahead logic
- Remove redundant chapter sorting logic when fetching next chapter(s)
- Remove redundant download queue checks (it'll handle already queued or downloaded items)
- Trigger download ahead when read >= 25% of chapter rather than 20%
- Rely on download cache when checking if next chapter is downloaded to avoid jank (fixes #8328)
2022-10-30 16:59:33 -04:00
725fcbba0e Add warning about F-Droid build support in More screen 2022-10-30 16:00:19 -04:00
bdeb209d43 Downgrade to org.jetbrains.kotlinx:kotlinx-serialization-json 1.4.0
Fixes data class casting issue seen when multiple sources have the same shadowed classes.
2022-10-30 15:29:51 -04:00
a078f1ab1b Refactor search toolbar and fix browse source (#8360) 2022-10-30 13:34:47 -04:00
86c3d8c064 Use Compose fast* functions in more places 2022-10-30 12:27:12 -04:00
156191af44 Tabs: Don't explicitly set text color in the text (#8365)
The container already provides color option for both states
2022-10-30 12:04:46 -04:00
57bba9e5ab Fix Layout Inspector's Compose tree for dev flavor (#8363) 2022-10-30 11:42:06 -04:00
dd1923fe88 Remove redundant preference composables 2022-10-30 11:37:02 -04:00
df773ee15c Refactor overflow menus into a composable 2022-10-30 11:06:41 -04:00
f5451a6881 Add ability to open random manga (#8232)
* Add ability to open random manga

* Use `getMangaForCategory` instead

* Put it in overflow menu instead of using EFAB

* Partial review changes

* Merge remote-tracking branch 'refs/remotes/origin/patch-6' into patch-6

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/library/LibraryScreen.kt

* Merge remote-tracking branch 'refs/remotes/origin/patch-6' into patch-6

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/library/LibraryScreen.kt

* Wording changes
2022-10-30 10:57:33 -04:00
fcec1581b7 Fix share menu item not appearing for entries not in library 2022-10-30 10:48:25 -04:00
11cc789e36 Center global search prompt properly in library list mode 2022-10-30 10:48:25 -04:00
16f9fb2f40 Rebase Scaffold fork (#8353)
This adds content window insets supports that will be passed to
all components used except top and bottom bar.
2022-10-30 09:59:50 -04:00
6bfaa85e84 MoreScreen: Add navbar padding (#8349) 2022-10-29 23:10:18 -04:00
8f43fb9530 Update voyager to v1.0.0-rc06 (#8346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-29 23:05:51 -04:00
04d2a3399b Restore chapter description alpha (#8345)
Restore "Darken the description colors"

Restores #3858, with new values based on current standards (0.78f rather than 0.62f)

I wanted to accomplish this without having to call a const, but that felt like a logical solution as well

Of course, if you got cleaner methods do tell, thanks
2022-10-29 22:58:18 -04:00
054bf8ec5d MangaScreen: Apply bottom content padding to large screen info column (#8347) 2022-10-29 22:57:19 -04:00
8417f5a63c Add more context to obsolete extension warning 2022-10-29 16:35:32 -04:00
26b46cace0 Few UI changes (#8299)
Co-authored-by: arkon <arkon@users.noreply.github.com>
2022-10-29 16:28:25 -04:00
0849111247 Use remember var delegates in more places 2022-10-29 16:14:49 -04:00
f9c25b350e New Pager implementation (#8323)
Minimal implementation using new Compose SnapFlingBehavior
2022-10-29 12:32:55 -04:00
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
178 changed files with 2778 additions and 2684 deletions

View File

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

View File

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

View File

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

View File

@ -27,8 +27,8 @@ android {
applicationId = "eu.kanade.tachiyomi"
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
versionCode = 89
versionName = "0.14.0"
versionCode = 91
versionName = "0.14.2"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -113,7 +113,6 @@ android {
"META-INF/README.md",
"META-INF/NOTICE",
"META-INF/*.kotlin_module",
"META-INF/*.version",
))
}
@ -176,8 +175,6 @@ dependencies {
implementation(compose.accompanist.webview)
implementation(compose.accompanist.swiperefresh)
implementation(compose.accompanist.flowlayout)
implementation(compose.accompanist.pager.core)
implementation(compose.accompanist.pager.indicators)
implementation(compose.accompanist.permissions)
implementation(androidx.paging.runtime)
@ -190,6 +187,8 @@ dependencies {
implementation(libs.sqldelight.android.paging)
implementation(kotlinx.reflect)
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
// AndroidX libraries
@ -298,6 +297,11 @@ androidComponents {
variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev"))
}
}
onVariants(selector().withFlavor("default" to "standard")) {
// Only excluding in standard flavor because this breaks
// Layout Inspector's Compose tree
it.packaging.resources.excludes.add("META-INF/*.version")
}
}
tasks {
@ -329,6 +333,19 @@ tasks {
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_metrics"
)
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_metrics"
)
}
}
preBuild {

View File

@ -34,7 +34,7 @@ import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.extension.interactor.GetExtensionsByType
import eu.kanade.domain.history.interactor.DeleteAllHistory
import eu.kanade.domain.history.interactor.GetHistory
import eu.kanade.domain.history.interactor.GetNextChapter
import eu.kanade.domain.history.interactor.GetNextUnreadChapters
import eu.kanade.domain.history.interactor.RemoveHistoryById
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
import eu.kanade.domain.history.interactor.UpsertHistory
@ -63,6 +63,7 @@ import eu.kanade.domain.source.repository.SourceDataRepository
import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.domain.track.interactor.DeleteTrack
import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.GetTracksPerManga
import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.repository.TrackRepository
import eu.kanade.domain.updates.interactor.GetUpdates
@ -93,7 +94,7 @@ class DomainModule : InjektModule {
addFactory { GetLibraryManga(get()) }
addFactory { GetMangaWithChapters(get(), get()) }
addFactory { GetManga(get()) }
addFactory { GetNextChapter(get(), get(), get(), get()) }
addFactory { GetNextUnreadChapters(get(), get(), get()) }
addFactory { ResetViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) }
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
@ -104,6 +105,7 @@ class DomainModule : InjektModule {
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { DeleteTrack(get()) }
addFactory { GetTracksPerManga(get()) }
addFactory { GetTracks(get()) }
addFactory { InsertTrack(get()) }

View File

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

View File

@ -1,51 +0,0 @@
package eu.kanade.domain.history.interactor
import eu.kanade.domain.chapter.interactor.GetChapter
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.util.chapter.getChapterSort
class GetNextChapter(
private val getChapter: GetChapter,
private val getChapterByMangaId: GetChapterByMangaId,
private val getManga: GetManga,
private val historyRepository: HistoryRepository,
) {
suspend fun await(): Chapter? {
val history = historyRepository.getLastHistory() ?: return null
return await(history.mangaId, history.chapterId)
}
suspend fun await(mangaId: Long, chapterId: Long): Chapter? {
val chapter = getChapter.await(chapterId)!!
val manga = getManga.await(mangaId)!!
if (!chapter.read) return chapter
val chapters = getChapterByMangaId.await(mangaId)
.sortedWith(getChapterSort(manga, sortDescending = false))
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
return when (manga.sorting) {
Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1)
Manga.CHAPTER_SORTING_NUMBER -> {
val chapterNumber = chapter.chapterNumber
((currChapterIndex + 1) until chapters.size)
.map { chapters[it] }
.firstOrNull {
it.chapterNumber > chapterNumber && it.chapterNumber <= chapterNumber + 1
}
}
Manga.CHAPTER_SORTING_UPLOAD_DATE -> {
chapters.drop(currChapterIndex + 1)
.firstOrNull { it.dateUpload >= chapter.dateUpload }
}
else -> throw NotImplementedError("Invalid chapter sorting method: ${manga.sorting}")
}
}
}

View File

@ -0,0 +1,33 @@
package eu.kanade.domain.history.interactor
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.tachiyomi.util.chapter.getChapterSort
import kotlin.math.max
class GetNextUnreadChapters(
private val getChapterByMangaId: GetChapterByMangaId,
private val getManga: GetManga,
private val historyRepository: HistoryRepository,
) {
suspend fun await(): Chapter? {
val history = historyRepository.getLastHistory() ?: return null
return await(history.mangaId, history.chapterId).firstOrNull()
}
suspend fun await(mangaId: Long): List<Chapter> {
val manga = getManga.await(mangaId) ?: return emptyList()
return getChapterByMangaId.await(mangaId)
.sortedWith(getChapterSort(manga, sortDescending = false))
.filterNot { it.read }
}
suspend fun await(mangaId: Long, fromChapterId: Long): List<Chapter> {
val unreadChapters = await(mangaId)
val currChapterIndex = unreadChapters.indexOfFirst { it.id == fromChapterId }
return unreadChapters.subList(max(0, currChapterIndex), unreadChapters.size)
}
}

View File

@ -7,11 +7,11 @@ class NetworkToLocalManga(
private val mangaRepository: MangaRepository,
) {
suspend fun await(manga: Manga, sourceId: Long): Manga {
val localManga = getManga(manga.url, sourceId)
suspend fun await(manga: Manga): Manga {
val localManga = getManga(manga.url, manga.source)
return when {
localManga == null -> {
val id = insertManga(manga.copy(source = sourceId))
val id = insertManga(manga)
manga.copy(id = id!!)
}
!localManga.favorite -> {

View File

@ -232,7 +232,7 @@ fun Manga.toMangaUpdate(): MangaUpdate {
)
}
fun SManga.toDomainManga(): Manga {
fun SManga.toDomainManga(sourceId: Long): Manga {
return Manga.create().copy(
url = url,
title = title,
@ -244,6 +244,7 @@ fun SManga.toDomainManga(): Manga {
thumbnailUrl = thumbnail_url,
updateStrategy = update_strategy,
initialized = initialized,
source = sourceId,
)
}

View File

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

View File

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

View File

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

View File

@ -4,12 +4,9 @@ import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.util.DisplayMetrics
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@ -55,9 +52,11 @@ import eu.kanade.presentation.components.DIVIDER_ALPHA
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
@ -195,23 +194,6 @@ private fun ExtensionDetails(
}
}
@Composable
private fun WarningBanner(@StringRes textRes: Int) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.error)
.padding(16.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(textRes),
color = MaterialTheme.colorScheme.onError,
style = MaterialTheme.typography.bodyMedium,
)
}
}
@Composable
private fun DetailsHeader(
extension: Extension,
@ -380,15 +362,14 @@ private fun SourceSwitchPreference(
) {
val context = LocalContext.current
PreferenceRow(
TextPreferenceWidget(
modifier = modifier,
title = if (source.labelAsName) {
source.source.toString()
} else {
LocaleHelper.getSourceDisplayName(source.source.lang, context)
},
onClick = { onClickSource(source.source.id) },
action = {
widget = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
@ -402,9 +383,14 @@ private fun SourceSwitchPreference(
}
}
Switch(checked = source.enabled, onCheckedChange = null)
Switch(
checked = source.enabled,
onCheckedChange = null,
modifier = Modifier.padding(start = TrailingWidgetBuffer),
)
}
},
onPreferenceClick = { onClickSource(source.source.id) },
)
}

View File

@ -3,7 +3,6 @@ package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
@ -11,10 +10,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper
@ -42,15 +41,13 @@ fun ExtensionFilterScreen(
textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
SourceFilterContent(
contentPadding = contentPadding,
state = presenter,
onClickLang = {
presenter.toggleLanguage(it)
},
)
}
else -> ExtensionFilterContent(
contentPadding = contentPadding,
state = presenter,
onClickLang = {
presenter.toggleLanguage(it)
},
)
}
}
LaunchedEffect(Unit) {
@ -65,40 +62,24 @@ fun ExtensionFilterScreen(
}
@Composable
private fun SourceFilterContent(
private fun ExtensionFilterContent(
contentPadding: PaddingValues,
state: ExtensionFilterState,
onClickLang: (String) -> Unit,
) {
LazyColumn(
FastScrollLazyColumn(
contentPadding = contentPadding,
) {
items(
items = state.items,
) { model ->
ExtensionFilterItem(
val lang = model.lang
SwitchPreferenceWidget(
modifier = Modifier.animateItemPlacement(),
lang = model.lang,
enabled = model.enabled,
onClickItem = onClickLang,
title = LocaleHelper.getSourceDisplayName(lang, LocalContext.current),
checked = model.enabled,
onCheckedChanged = { onClickLang(lang) },
)
}
}
}
@Composable
private fun ExtensionFilterItem(
modifier: Modifier,
lang: String,
enabled: Boolean,
onClickItem: (String) -> Unit,
) {
PreferenceRow(
modifier = modifier,
title = LocaleHelper.getSourceDisplayName(lang, LocalContext.current),
action = {
Switch(checked = enabled, onCheckedChange = null)
},
onClick = { onClickItem(lang) },
)
}

View File

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

View File

@ -10,9 +10,9 @@ import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.manga.components.BaseMangaListItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter
@ -72,7 +72,7 @@ private fun MigrateMangaContent(
onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit,
) {
ScrollbarLazyColumn(
FastScrollLazyColumn(
contentPadding = contentPadding,
) {
items(state.items) { manga ->

View File

@ -6,12 +6,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.paging.compose.collectAsLazyPagingItems
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.components.BrowseSourceSearchToolbar
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.more.MoreController
@ -38,13 +36,11 @@ fun SourceSearchScreen(
Scaffold(
topBar = { scrollBehavior ->
BrowseSourceSearchToolbar(
SearchToolbar(
searchQuery = presenter.searchQuery ?: "",
onSearchQueryChanged = { presenter.searchQuery = it },
placeholderText = stringResource(R.string.action_search_hint),
navigateUp = navigateUp,
onResetClick = { presenter.searchQuery = "" },
onSearchClick = { presenter.search(it) },
onChangeSearchQuery = { presenter.searchQuery = it },
onClickCloseSearch = navigateUp,
onSearch = { presenter.search(it) },
scrollBehavior = scrollBehavior,
)
},

View File

@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
@ -14,10 +13,10 @@ import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterPresenter
@ -76,7 +75,7 @@ private fun SourcesFilterContent(
onClickLang: (String) -> Unit,
onClickSource: (Source) -> Unit,
) {
ScrollbarLazyColumn(
FastScrollLazyColumn(
contentPadding = contentPadding,
) {
items(
@ -95,14 +94,12 @@ private fun SourcesFilterContent(
},
) { model ->
when (model) {
is FilterUiModel.Header -> {
SourcesFilterHeader(
modifier = Modifier.animateItemPlacement(),
language = model.language,
enabled = model.enabled,
onClickItem = onClickLang,
)
}
is FilterUiModel.Header -> SourcesFilterHeader(
modifier = Modifier.animateItemPlacement(),
language = model.language,
enabled = model.enabled,
onClickItem = onClickLang,
)
is FilterUiModel.Item -> SourcesFilterItem(
modifier = Modifier.animateItemPlacement(),
source = model.source,
@ -121,13 +118,11 @@ private fun SourcesFilterHeader(
enabled: Boolean,
onClickItem: (String) -> Unit,
) {
PreferenceRow(
SwitchPreferenceWidget(
modifier = modifier,
title = LocaleHelper.getSourceDisplayName(language, LocalContext.current),
action = {
Switch(checked = enabled, onCheckedChange = null)
},
onClick = { onClickItem(language) },
checked = enabled,
onCheckedChanged = { onClickItem(language) },
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,10 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ViewList
import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
@ -15,14 +12,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.presentation.browse.BrowseSourceState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.RadioMenuItem
import eu.kanade.presentation.components.SearchToolbar
@ -33,7 +28,7 @@ import eu.kanade.tachiyomi.source.LocalSource
@Composable
fun BrowseSourceToolbar(
state: BrowseSourceState,
source: CatalogueSource,
source: CatalogueSource?,
displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit,
@ -42,59 +37,21 @@ fun BrowseSourceToolbar(
onSearch: (String) -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null,
) {
if (state.searchQuery == null) {
BrowseSourceRegularToolbar(
title = if (state.isUserQuery) state.currentFilter.query else source.name,
isLocalSource = source is LocalSource,
displayMode = displayMode,
onDisplayModeChange = onDisplayModeChange,
navigateUp = navigateUp,
onSearchClick = { state.searchQuery = if (state.isUserQuery) state.currentFilter.query else "" },
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
scrollBehavior = scrollBehavior,
)
} else {
val cancelSearch = { state.searchQuery = null }
BrowseSourceSearchToolbar(
searchQuery = state.searchQuery!!,
onSearchQueryChanged = { state.searchQuery = it },
placeholderText = stringResource(R.string.action_search_hint),
navigateUp = cancelSearch,
onResetClick = { state.searchQuery = "" },
onSearchClick = {
onSearch(it)
cancelSearch()
},
scrollBehavior = scrollBehavior,
)
}
}
// Avoid capturing unstable source in actions lambda
val title = source?.name
val isLocalSource = source is LocalSource
@Composable
fun BrowseSourceRegularToolbar(
title: String,
isLocalSource: Boolean,
displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit,
onSearchClick: () -> Unit,
onWebViewClick: () -> Unit,
onHelpClick: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
) {
AppBar(
SearchToolbar(
navigateUp = navigateUp,
title = title,
titleContent = { AppBarTitle(title) },
searchQuery = state.searchQuery,
onChangeSearchQuery = { state.searchQuery = it },
onSearch = onSearch,
onClickCloseSearch = navigateUp,
actions = {
var selectingDisplayMode by remember { mutableStateOf(false) }
AppBarActions(
actions = listOf(
AppBar.Action(
title = stringResource(R.string.action_search),
icon = Icons.Outlined.Search,
onClick = onSearchClick,
),
AppBar.Action(
title = stringResource(R.string.action_display_mode),
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
@ -123,18 +80,21 @@ fun BrowseSourceRegularToolbar(
text = { Text(text = stringResource(R.string.action_display_comfortable_grid)) },
isChecked = displayMode == LibraryDisplayMode.ComfortableGrid,
) {
selectingDisplayMode = false
onDisplayModeChange(LibraryDisplayMode.ComfortableGrid)
}
RadioMenuItem(
text = { Text(text = stringResource(R.string.action_display_grid)) },
isChecked = displayMode == LibraryDisplayMode.CompactGrid,
) {
selectingDisplayMode = false
onDisplayModeChange(LibraryDisplayMode.CompactGrid)
}
RadioMenuItem(
text = { Text(text = stringResource(R.string.action_display_list)) },
isChecked = displayMode == LibraryDisplayMode.List,
) {
selectingDisplayMode = false
onDisplayModeChange(LibraryDisplayMode.List)
}
}
@ -142,34 +102,3 @@ fun BrowseSourceRegularToolbar(
scrollBehavior = scrollBehavior,
)
}
@Composable
fun BrowseSourceSearchToolbar(
searchQuery: String,
onSearchQueryChanged: (String) -> Unit,
placeholderText: String?,
navigateUp: () -> Unit,
onResetClick: () -> Unit,
onSearchClick: (String) -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
SearchToolbar(
searchQuery = searchQuery,
onChangeSearchQuery = onSearchQueryChanged,
placeholderText = placeholderText,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
onSearchClick(searchQuery)
focusManager.clearFocus()
keyboardController?.hide()
},
),
onClickCloseSearch = navigateUp,
onClickResetSearch = onResetClick,
scrollBehavior = scrollBehavior,
)
}

View File

@ -6,8 +6,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@ -22,7 +24,7 @@ fun CategoryCreateDialog(
onDismissRequest: () -> Unit,
onCreate: (String) -> Unit,
) {
val (name, onNameChange) = remember { mutableStateOf("") }
var name by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
AlertDialog(
@ -48,7 +50,7 @@ fun CategoryCreateDialog(
modifier = Modifier
.focusRequester(focusRequester),
value = name,
onValueChange = onNameChange,
onValueChange = { name = it },
label = {
Text(text = stringResource(R.string.name))
},
@ -70,7 +72,7 @@ fun CategoryRenameDialog(
onRename: (String) -> Unit,
category: Category,
) {
val (name, onNameChange) = remember { mutableStateOf(category.name) }
var name by remember { mutableStateOf(category.name) }
val focusRequester = remember { FocusRequester() }
AlertDialog(
@ -96,7 +98,7 @@ fun CategoryRenameDialog(
modifier = Modifier
.focusRequester(focusRequester),
value = name,
onValueChange = onNameChange,
onValueChange = { name = it },
label = {
Text(text = stringResource(R.string.name))
},
@ -130,7 +132,7 @@ fun CategoryDeleteDialog(
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
title = {

View File

@ -1,13 +1,11 @@
package eu.kanade.presentation.category.components
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.util.isScrolledToEnd
@ -23,8 +21,6 @@ fun CategoryFloatingActionButton(
text = { Text(text = stringResource(R.string.action_add)) },
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") },
onClick = onCreate,
modifier = Modifier
.navigationBarsPadding(),
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
)
}

View File

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

View File

@ -1,22 +1,18 @@
package eu.kanade.presentation.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -30,6 +26,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -38,8 +35,11 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -55,7 +55,7 @@ fun AppBar(
subtitle: String? = null,
// Up button
navigateUp: (() -> Unit)? = null,
navigationIcon: ImageVector = Icons.Default.ArrowBack,
navigationIcon: ImageVector = Icons.Outlined.ArrowBack,
// Menu
actions: @Composable RowScope.() -> Unit = {},
// Action mode
@ -105,7 +105,7 @@ fun AppBar(
titleContent: @Composable () -> Unit,
// Up button
navigateUp: (() -> Unit)? = null,
navigationIcon: ImageVector = Icons.Default.ArrowBack,
navigationIcon: ImageVector = Icons.Outlined.ArrowBack,
// Menu
actions: @Composable RowScope.() -> Unit = {},
// Action mode
@ -125,7 +125,7 @@ fun AppBar(
if (isActionMode) {
IconButton(onClick = onCancelActionMode) {
Icon(
imageVector = Icons.Default.Close,
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_cancel),
)
}
@ -142,7 +142,6 @@ fun AppBar(
},
title = titleContent,
actions = actions,
windowInsets = WindowInsets.statusBars,
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
elevation = if (isActionMode) 3.dp else 0.dp,
@ -200,7 +199,7 @@ fun AppBarActions(
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
if (overflowActions.isNotEmpty()) {
IconButton(onClick = { showMenu = !showMenu }) {
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.label_more))
Icon(Icons.Outlined.MoreVert, contentDescription = stringResource(R.string.abc_action_menu_overflow_description))
}
DropdownMenu(
@ -220,15 +219,22 @@ fun AppBarActions(
}
}
/**
* @param searchEnabled Set to false if you don't want to show search action.
* @param searchQuery If null, use normal toolbar.
* @param placeholderText If null, [R.string.action_search_hint] is used.
*/
@Composable
fun SearchToolbar(
searchQuery: String,
onChangeSearchQuery: (String) -> Unit,
titleContent: @Composable () -> Unit = {},
navigateUp: (() -> Unit)? = null,
searchEnabled: Boolean = true,
searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit,
placeholderText: String? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
onClickCloseSearch: () -> Unit,
onClickResetSearch: () -> Unit,
onSearch: (String) -> Unit = {},
onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) },
actions: @Composable RowScope.() -> Unit = {},
incognitoMode: Boolean = false,
downloadedOnlyMode: Boolean = false,
scrollBehavior: TopAppBarScrollBehavior? = null,
@ -236,9 +242,15 @@ fun SearchToolbar(
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val focusRequester = remember { FocusRequester() }
var searchClickCount by remember { mutableStateOf(0) }
AppBar(
titleContent = {
if (searchQuery == null) return@AppBar titleContent()
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
BasicTextField(
value = searchQuery,
onValueChange = onChangeSearchQuery,
@ -250,8 +262,14 @@ fun SearchToolbar(
fontWeight = FontWeight.Normal,
fontSize = 18.sp,
),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
onSearch(searchQuery)
focusManager.clearFocus()
keyboardController?.hide()
},
),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
visualTransformation = visualTransformation,
@ -265,7 +283,7 @@ fun SearchToolbar(
visualTransformation = visualTransformation,
interactionSource = interactionSource,
placeholder = {
if (!placeholderText.isNullOrEmpty()) {
(placeholderText ?: stringResource(R.string.action_search_hint)).let { placeholderText ->
Text(
modifier = Modifier.secondaryItemAlpha(),
text = placeholderText,
@ -282,22 +300,41 @@ fun SearchToolbar(
},
)
},
navigationIcon = Icons.Outlined.ArrowBack,
navigateUp = onClickCloseSearch,
navigateUp = if (searchQuery == null) navigateUp else onClickCloseSearch,
actions = {
AnimatedVisibility(visible = searchQuery.isNotEmpty()) {
IconButton(onClick = onClickResetSearch) {
Icon(Icons.Outlined.Close, contentDescription = stringResource(R.string.action_reset))
key("search") {
val onClick = {
searchClickCount++
onChangeSearchQuery("")
}
if (!searchEnabled) {
// Don't show search action
} else if (searchQuery == null) {
IconButton(onClick) {
Icon(Icons.Outlined.Search, contentDescription = stringResource(R.string.action_search))
}
} else if (searchQuery.isNotEmpty()) {
IconButton(onClick) {
Icon(Icons.Outlined.Close, contentDescription = stringResource(R.string.action_reset))
}
}
}
key("actions") { actions() }
},
isActionMode = false,
downloadedOnlyMode = downloadedOnlyMode,
incognitoMode = incognitoMode,
scrollBehavior = scrollBehavior,
)
LaunchedEffect(focusRequester) {
focusRequester.requestFocus()
LaunchedEffect(searchClickCount) {
if (searchQuery == null) return@LaunchedEffect
try {
focusRequester.requestFocus()
} catch (_: Throwable) {
// TextField is gone
}
}
}

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.components
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@ -13,6 +14,23 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R
@Composable
fun WarningBanner(
@StringRes textRes: Int,
modifier: Modifier = Modifier,
) {
Text(
text = stringResource(textRes),
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.error)
.padding(16.dp),
color = MaterialTheme.colorScheme.onError,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
)
}
@Composable
fun AppStateBanners(
downloadedOnlyMode: Boolean,

View File

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

View File

@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ErrorOutline
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
@ -26,6 +26,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
@ -43,26 +45,41 @@ enum class ChapterDownloadAction {
@Composable
fun ChapterDownloadIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int,
onClick: (ChapterDownloadAction) -> Unit,
) {
when (val downloadState = downloadStateProvider()) {
Download.State.NOT_DOWNLOADED -> NotDownloadedIndicator(modifier = modifier, onClick = onClick)
Download.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
enabled = enabled,
modifier = modifier,
onClick = onClick,
)
Download.State.QUEUE, Download.State.DOWNLOADING -> DownloadingIndicator(
enabled = enabled,
modifier = modifier,
downloadState = downloadState,
downloadProgressProvider = downloadProgressProvider,
onClick = onClick,
)
Download.State.DOWNLOADED -> DownloadedIndicator(modifier = modifier, onClick = onClick)
Download.State.ERROR -> ErrorIndicator(modifier = modifier, onClick = onClick)
Download.State.DOWNLOADED -> DownloadedIndicator(
enabled = enabled,
modifier = modifier,
onClick = onClick,
)
Download.State.ERROR -> ErrorIndicator(
enabled = enabled,
modifier = modifier,
onClick = onClick,
)
}
}
@Composable
private fun NotDownloadedIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
@ -70,6 +87,7 @@ private fun NotDownloadedIndicator(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
onLongClick = { onClick(ChapterDownloadAction.START_NOW) },
onClick = { onClick(ChapterDownloadAction.START) },
)
@ -78,7 +96,7 @@ private fun NotDownloadedIndicator(
) {
Icon(
painter = painterResource(id = R.drawable.ic_download_chapter_24dp),
contentDescription = null,
contentDescription = stringResource(R.string.manga_download),
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -87,6 +105,7 @@ private fun NotDownloadedIndicator(
@Composable
private fun DownloadingIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
downloadState: Download.State,
downloadProgressProvider: () -> Int,
@ -97,6 +116,7 @@ private fun DownloadingIndicator(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
onLongClick = { onClick(ChapterDownloadAction.CANCEL) },
onClick = { isMenuExpanded = true },
),
@ -148,7 +168,7 @@ private fun DownloadingIndicator(
)
}
Icon(
imageVector = Icons.Default.ArrowDownward,
imageVector = Icons.Outlined.ArrowDownward,
contentDescription = null,
modifier = ArrowModifier,
tint = arrowColor,
@ -158,6 +178,7 @@ private fun DownloadingIndicator(
@Composable
private fun DownloadedIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
@ -166,13 +187,14 @@ private fun DownloadedIndicator(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
onLongClick = { onClick(ChapterDownloadAction.DELETE) },
onClick = { isMenuExpanded = true },
),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.CheckCircle,
imageVector = Icons.Filled.CheckCircle,
contentDescription = null,
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
@ -191,6 +213,7 @@ private fun DownloadedIndicator(
@Composable
private fun ErrorIndicator(
enabled: Boolean,
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
@ -198,14 +221,15 @@ private fun ErrorIndicator(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
onLongClick = { onClick(ChapterDownloadAction.START) },
onClick = { onClick(ChapterDownloadAction.START) },
),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.ErrorOutline,
contentDescription = null,
imageVector = Icons.Outlined.ErrorOutline,
contentDescription = stringResource(R.string.chapter_error),
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.error,
)
@ -213,11 +237,18 @@ private fun ErrorIndicator(
}
private fun Modifier.commonClickable(
enabled: Boolean,
onLongClick: () -> Unit,
onClick: () -> Unit,
) = composed {
val haptic = LocalHapticFeedback.current
this.combinedClickable(
onLongClick = onLongClick,
enabled = enabled,
onLongClick = {
onLongClick()
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = onClick,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },

View File

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

View File

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

View File

@ -0,0 +1,17 @@
package eu.kanade.presentation.components
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
const val DIVIDER_ALPHA = 0.2f
@Composable
fun Divider(
modifier: Modifier = Modifier,
) {
androidx.compose.material3.Divider(
modifier = modifier,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA),
)
}

View File

@ -0,0 +1,66 @@
package eu.kanade.presentation.components
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.tachiyomi.R
@Composable
fun DownloadDropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit,
includeDownloadAllOption: Boolean = true,
) {
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_1)) },
onClick = {
onDownloadClicked(DownloadAction.NEXT_1_CHAPTER)
onDismissRequest()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_5)) },
onClick = {
onDownloadClicked(DownloadAction.NEXT_5_CHAPTERS)
onDismissRequest()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_10)) },
onClick = {
onDownloadClicked(DownloadAction.NEXT_10_CHAPTERS)
onDismissRequest()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_custom)) },
onClick = {
onDownloadClicked(DownloadAction.CUSTOM)
onDismissRequest()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_unread)) },
onClick = {
onDownloadClicked(DownloadAction.UNREAD_CHAPTERS)
onDismissRequest()
},
)
if (includeDownloadAllOption) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_all)) },
onClick = {
onDownloadClicked(DownloadAction.ALL_CHAPTERS)
onDismissRequest()
},
)
}
}
}

View File

@ -1,18 +1,29 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.RadioButtonChecked
import androidx.compose.material.icons.outlined.RadioButtonUnchecked
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import eu.kanade.tachiyomi.R
import me.saket.cascade.CascadeColumnScope
import me.saket.cascade.CascadeDropdownMenu
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
@Composable
@ -20,6 +31,7 @@ fun DropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(8.dp, (-56).dp),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit,
) {
@ -27,7 +39,7 @@ fun DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
offset = DpOffset(8.dp, (-8).dp),
offset = offset,
properties = properties,
content = content,
)
@ -46,15 +58,39 @@ fun RadioMenuItem(
if (isChecked) {
Icon(
imageVector = Icons.Outlined.RadioButtonChecked,
contentDescription = "",
contentDescription = stringResource(R.string.selected),
tint = MaterialTheme.colorScheme.primary,
)
} else {
Icon(
imageVector = Icons.Outlined.RadioButtonUnchecked,
contentDescription = "",
contentDescription = stringResource(R.string.not_selected),
)
}
},
)
}
@Composable
fun OverflowMenu(
content: @Composable CascadeColumnScope.(() -> Unit) -> Unit,
) {
var moreExpanded by remember { mutableStateOf(false) }
val closeMenu = { moreExpanded = false }
Box {
IconButton(onClick = { moreExpanded = !moreExpanded }) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
)
}
CascadeDropdownMenu(
expanded = moreExpanded,
onDismissRequest = closeMenu,
offset = DpOffset(8.dp, (-56).dp),
) {
content(closeMenu)
}
}
}

View File

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

View File

@ -11,8 +11,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@ -29,6 +29,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirstOrNull
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.R
@ -100,9 +101,9 @@ fun EmptyScreen(
modifier = modifier.fillMaxSize(),
) { measurables, constraints ->
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val facePlaceable = measurables.first { it.layoutId == "face" }
val facePlaceable = measurables.fastFirstOrNull { it.layoutId == "face" }!!
.measure(looseConstraints)
val actionsPlaceable = measurables.firstOrNull { it.layoutId == "actions" }
val actionsPlaceable = measurables.fastFirstOrNull { it.layoutId == "actions" }
?.measure(looseConstraints)
layout(constraints.maxWidth, constraints.maxHeight) {
@ -187,12 +188,12 @@ private fun WithActionPreview() {
actions = listOf(
EmptyScreenAction(
stringResId = R.string.action_retry,
icon = Icons.Default.Refresh,
icon = Icons.Outlined.Refresh,
onClick = {},
),
EmptyScreenAction(
stringResId = R.string.getting_started_guide,
icon = Icons.Default.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = {},
),
),

View File

@ -22,13 +22,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BookmarkAdd
import androidx.compose.material.icons.filled.BookmarkRemove
import androidx.compose.material.icons.filled.DoneAll
import androidx.compose.material.icons.filled.RemoveDone
import androidx.compose.material.icons.outlined.BookmarkAdd
import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -37,8 +37,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@ -48,6 +50,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -98,7 +101,7 @@ fun MangaBottomActionMenu(
if (onBookmarkClicked != null) {
Button(
title = stringResource(R.string.action_bookmark),
icon = Icons.Default.BookmarkAdd,
icon = Icons.Outlined.BookmarkAdd,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
onClick = onBookmarkClicked,
@ -107,7 +110,7 @@ fun MangaBottomActionMenu(
if (onRemoveBookmarkClicked != null) {
Button(
title = stringResource(R.string.action_remove_bookmark),
icon = Icons.Default.BookmarkRemove,
icon = Icons.Outlined.BookmarkRemove,
toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) },
onClick = onRemoveBookmarkClicked,
@ -116,7 +119,7 @@ fun MangaBottomActionMenu(
if (onMarkAsReadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_read),
icon = Icons.Default.DoneAll,
icon = Icons.Outlined.DoneAll,
toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) },
onClick = onMarkAsReadClicked,
@ -125,7 +128,7 @@ fun MangaBottomActionMenu(
if (onMarkAsUnreadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_unread),
icon = Icons.Default.RemoveDone,
icon = Icons.Outlined.RemoveDone,
toConfirm = confirm[3],
onLongClick = { onLongClickItem(3) },
onClick = onMarkAsUnreadClicked,
@ -170,6 +173,7 @@ private fun RowScope.Button(
toConfirm: Boolean,
onLongClick: () -> Unit,
onClick: () -> Unit,
content: (@Composable () -> Unit)? = null,
) {
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
Column(
@ -201,6 +205,7 @@ private fun RowScope.Button(
style = MaterialTheme.typography.labelSmall,
)
}
content?.invoke()
}
}
@ -211,7 +216,7 @@ fun LibraryBottomActionMenu(
onChangeCategoryClicked: (() -> Unit)?,
onMarkAsReadClicked: (() -> Unit)?,
onMarkAsUnreadClicked: (() -> Unit)?,
onDownloadClicked: (() -> Unit)?,
onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: (() -> Unit)?,
) {
AnimatedVisibility(
@ -254,7 +259,7 @@ fun LibraryBottomActionMenu(
if (onMarkAsReadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_read),
icon = Icons.Default.DoneAll,
icon = Icons.Outlined.DoneAll,
toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) },
onClick = onMarkAsReadClicked,
@ -263,20 +268,29 @@ fun LibraryBottomActionMenu(
if (onMarkAsUnreadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_unread),
icon = Icons.Default.RemoveDone,
icon = Icons.Outlined.RemoveDone,
toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) },
onClick = onMarkAsUnreadClicked,
)
}
if (onDownloadClicked != null) {
var downloadExpanded by remember { mutableStateOf(false) }
Button(
title = stringResource(R.string.action_download),
icon = Icons.Outlined.Download,
toConfirm = confirm[3],
onLongClick = { onLongClickItem(3) },
onClick = onDownloadClicked,
)
onClick = { downloadExpanded = !downloadExpanded },
) {
val onDismissRequest = { downloadExpanded = false }
DownloadDropdownMenu(
expanded = downloadExpanded,
onDismissRequest = onDismissRequest,
onDownloadClicked = onDownloadClicked,
includeDownloadAllOption = false,
)
}
}
if (onDeleteClicked != null) {
Button(

View File

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

View File

@ -0,0 +1,182 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.util.fastMaxBy
import kotlinx.coroutines.flow.distinctUntilChanged
@Composable
fun HorizontalPager(
count: Int,
modifier: Modifier = Modifier,
state: PagerState = rememberPagerState(),
key: ((page: Int) -> Any)? = null,
contentPadding: PaddingValues = PaddingValues(),
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
userScrollEnabled: Boolean = true,
content: @Composable BoxScope.(page: Int) -> Unit,
) {
Pager(
count = count,
modifier = modifier,
state = state,
isVertical = false,
key = key,
contentPadding = contentPadding,
verticalAlignment = verticalAlignment,
userScrollEnabled = userScrollEnabled,
content = content,
)
}
@Composable
private fun Pager(
count: Int,
modifier: Modifier,
state: PagerState,
isVertical: Boolean,
key: ((page: Int) -> Any)?,
contentPadding: PaddingValues,
userScrollEnabled: Boolean,
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
content: @Composable BoxScope.(page: Int) -> Unit,
) {
LaunchedEffect(count) {
state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0)
}
LaunchedEffect(state) {
snapshotFlow { state.mostVisiblePageLayoutInfo?.index }
.distinctUntilChanged()
.collect { state.updateCurrentPageBasedOnLazyListState() }
}
if (isVertical) {
LazyColumn(
modifier = modifier,
state = state.lazyListState,
contentPadding = contentPadding,
horizontalAlignment = horizontalAlignment,
verticalArrangement = Arrangement.aligned(verticalAlignment),
userScrollEnabled = userScrollEnabled,
flingBehavior = rememberSnapFlingBehavior(lazyListState = state.lazyListState),
) {
items(
count = count,
key = key,
) { page ->
Box(
modifier = Modifier
.fillParentMaxHeight()
.wrapContentSize(),
) {
content(this, page)
}
}
}
} else {
LazyRow(
modifier = modifier,
state = state.lazyListState,
contentPadding = contentPadding,
verticalAlignment = verticalAlignment,
horizontalArrangement = Arrangement.aligned(horizontalAlignment),
userScrollEnabled = userScrollEnabled,
flingBehavior = rememberSnapFlingBehavior(lazyListState = state.lazyListState),
) {
items(
count = count,
key = key,
) { page ->
Box(
modifier = Modifier
.fillParentMaxWidth()
.wrapContentSize(),
) {
content(this, page)
}
}
}
}
}
@Composable
fun rememberPagerState(
initialPage: Int = 0,
) = rememberSaveable(saver = PagerState.Saver) {
PagerState(currentPage = initialPage)
}
@Stable
class PagerState(
currentPage: Int = 0,
) {
init { check(currentPage >= 0) { "currentPage cannot be less than zero" } }
val lazyListState = LazyListState(firstVisibleItemIndex = currentPage)
private var _currentPage by mutableStateOf(currentPage)
var currentPage: Int
get() = _currentPage
set(value) {
if (value != _currentPage) {
_currentPage = value
}
}
val mostVisiblePageLayoutInfo: LazyListItemInfo?
get() {
val layoutInfo = lazyListState.layoutInfo
return layoutInfo.visibleItemsInfo.fastMaxBy {
val start = maxOf(it.offset, 0)
val end = minOf(
it.offset + it.size,
layoutInfo.viewportEndOffset - layoutInfo.afterContentPadding,
)
end - start
}
}
fun updateCurrentPageBasedOnLazyListState() {
mostVisiblePageLayoutInfo?.let {
currentPage = it.index
}
}
suspend fun animateScrollToPage(page: Int) {
lazyListState.animateScrollToItem(index = page)
}
suspend fun scrollToPage(page: Int) {
lazyListState.scrollToItem(index = page)
updateCurrentPageBasedOnLazyListState()
}
companion object {
val Saver: Saver<PagerState, *> = listSaver(
save = { listOf(it.currentPage) },
restore = { PagerState(it[0]) },
)
}
}

View File

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

View File

@ -1,167 +0,0 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.horizontalPadding
const val DIVIDER_ALPHA = 0.2f
@Composable
fun Divider(
modifier: Modifier = Modifier,
) {
androidx.compose.material3.Divider(
modifier = modifier,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA),
)
}
@Composable
fun PreferenceRow(
modifier: Modifier = Modifier,
title: String,
painter: Painter? = null,
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
subtitle: String? = null,
action: @Composable (() -> Unit)? = null,
) {
val height = if (subtitle != null) 72.dp else 56.dp
val titleTextStyle = MaterialTheme.typography.bodyLarge
val subtitleTextStyle = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f),
)
Row(
modifier = modifier
.fillMaxWidth()
.heightIn(min = height)
.combinedClickable(
onLongClick = onLongClick,
onClick = onClick,
),
verticalAlignment = Alignment.CenterVertically,
) {
if (painter != null) {
Icon(
painter = painter,
modifier = Modifier
.padding(start = horizontalPadding, end = 16.dp)
.size(24.dp),
tint = MaterialTheme.colorScheme.primary,
contentDescription = null,
)
}
Column(
Modifier
.padding(horizontal = 16.dp)
.weight(1f),
) {
Text(
text = title,
style = titleTextStyle,
)
if (subtitle != null) {
Text(
modifier = Modifier.padding(top = 4.dp),
text = subtitle,
style = subtitleTextStyle,
)
}
}
if (action != null) {
Box(
Modifier
.widthIn(min = 56.dp)
.padding(end = horizontalPadding),
) {
action()
}
}
}
}
@Composable
fun SwitchPreference(
modifier: Modifier = Modifier,
checked: Boolean,
onClick: () -> Unit,
title: String,
subtitle: String? = null,
painter: Painter? = null,
) {
PreferenceRow(
modifier = modifier,
title = title,
subtitle = subtitle,
painter = painter,
action = { Switch(checked = checked, onCheckedChange = null) },
onClick = onClick,
)
}
@Composable
fun SwitchPreference(
modifier: Modifier = Modifier,
preference: PreferenceMutableState<Boolean>,
title: String,
subtitle: String? = null,
painter: Painter? = null,
) {
SwitchPreference(
modifier = modifier,
title = title,
subtitle = subtitle,
painter = painter,
checked = preference.value,
onClick = { preference.value = !preference.value },
)
}
@Preview
@Composable
private fun PreferencesPreview() {
TachiyomiTheme {
Column {
PreferenceRow(
title = "Plain",
subtitle = "Subtitle",
)
Divider()
SwitchPreference(
title = "Switch (on)",
subtitle = "Subtitle",
checked = true,
onClick = {},
)
SwitchPreference(
title = "Switch (off)",
subtitle = "Subtitle",
checked = false,
onClick = {},
)
}
}
}

View File

@ -19,9 +19,11 @@ package eu.kanade.presentation.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.contentColorFor
@ -37,6 +39,11 @@ import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy
import kotlin.math.max
/**
* <a href="https://material.io/design/layout/understanding-layout.html" class="external" target="_blank">Material Design layout</a>.
@ -59,6 +66,7 @@ import androidx.compose.ui.unit.dp
* * Pass scroll behavior to top bar by default
* * Remove height constraint for expanded app bar
* * Also take account of fab height when providing inner padding
* * Fixes for fab and snackbar horizontal placements when [contentWindowInsets] is used
*
* @param modifier the [Modifier] to be applied to this scaffold
* @param topBar top app bar of the screen, typically a [SmallTopAppBar]
@ -72,6 +80,9 @@ import androidx.compose.ui.unit.dp
* @param contentColor the preferred color for content inside this scaffold. Defaults to either the
* matching content color for [containerColor], or to the current [LocalContentColor] if
* [containerColor] is not a color from the theme.
* @param contentWindowInsets window insets to be passed to content slot via PaddingValues params.
* Scaffold will take the insets into account from the top/bottom only if the topBar/ bottomBar
* are not present, as the scaffold expect topBar/bottomBar to handle insets instead
* @param content content of the screen. The lambda receives a [PaddingValues] that should be
* applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
* properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
@ -89,6 +100,7 @@ fun Scaffold(
floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = MaterialTheme.colorScheme.background,
contentColor: Color = contentColorFor(containerColor),
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit,
) {
androidx.compose.material3.Surface(
@ -104,6 +116,7 @@ fun Scaffold(
bottomBar = bottomBar,
content = content,
snackbar = snackbarHost,
contentWindowInsets = contentWindowInsets,
fab = floatingActionButton,
)
}
@ -129,6 +142,7 @@ private fun ScaffoldLayout(
content: @Composable (PaddingValues) -> Unit,
snackbar: @Composable () -> Unit,
fab: @Composable () -> Unit,
contentWindowInsets: WindowInsets,
bottomBar: @Composable () -> Unit,
) {
SubcomposeLayout { constraints ->
@ -143,37 +157,51 @@ private fun ScaffoldLayout(
val topBarConstraints = looseConstraints.copy(maxHeight = Constraints.Infinity)
layout(layoutWidth, layoutHeight) {
val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).map {
val leftInset = contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
val rightInset = contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
// Tachiyomi: layoutWidth after horizontal insets
val insetLayoutWidth = layoutWidth - leftInset - rightInset
val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
it.measure(topBarConstraints)
}
val topBarHeight = topBarPlaceables.maxByOrNull { it.height }?.height ?: 0
val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).map {
val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
it.measure(looseConstraints)
}
val snackbarHeight = snackbarPlaceables.maxByOrNull { it.height }?.height ?: 0
val snackbarWidth = snackbarPlaceables.maxByOrNull { it.width }?.width ?: 0
val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0
// Tachiyomi: Calculate insets for snackbar placement offset
val snackbarLeft = if (snackbarPlaceables.isNotEmpty()) {
(insetLayoutWidth - snackbarWidth) / 2 + leftInset
} else {
0
}
val fabPlaceables =
subcompose(ScaffoldLayoutContent.Fab, fab).mapNotNull { measurable ->
measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 }
subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable ->
measurable.measure(looseConstraints)
}
val fabHeight = fabPlaceables.maxByOrNull { it.height }?.height ?: 0
val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
val fabPlacement = if (fabPlaceables.isNotEmpty()) {
val fabWidth = fabPlaceables.maxByOrNull { it.width }!!.width
val fabPlacement = if (fabPlaceables.isNotEmpty() && fabWidth != 0 && fabHeight != 0) {
// FAB distance from the left of the layout, taking into account LTR / RTL
// Tachiyomi: Calculate insets for fab placement offset
val fabLeftOffset = if (fabPosition == FabPosition.End) {
if (layoutDirection == LayoutDirection.Ltr) {
layoutWidth - FabSpacing.roundToPx() - fabWidth
layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset
} else {
FabSpacing.roundToPx()
FabSpacing.roundToPx() + leftInset
}
} else {
(layoutWidth - fabWidth) / 2
leftInset + ((insetLayoutWidth - fabWidth) / 2)
}
FabPlacement(
@ -190,75 +218,63 @@ private fun ScaffoldLayout(
LocalFabPlacement provides fabPlacement,
content = bottomBar,
)
}.map { it.measure(looseConstraints) }
}.fastMap { it.measure(looseConstraints) }
val bottomBarHeight = bottomBarPlaceables.maxByOrNull { it.height }?.height ?: 0
val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
val fabOffsetFromBottom = fabPlacement?.let {
if (bottomBarHeight == 0) {
it.height + FabSpacing.roundToPx()
} else {
// Total height is the bottom bar height + the FAB height + the padding
// between the FAB and bottom bar
bottomBarHeight + it.height + FabSpacing.roundToPx()
}
max(bottomBarHeight ?: 0, bottomInset) + it.height + FabSpacing.roundToPx()
}
val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight)
snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight ?: bottomInset)
} else {
0
}
/**
* Tachiyomi: Also take account of fab height when providing inner padding
*/
val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
val insets = WindowInsets.Companion.safeDrawing
.asPaddingValues(this@SubcomposeLayout)
val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout)
val fabOffsetDp = fabOffsetFromBottom?.toDp() ?: 0.dp
val bottomBarHeightPx = bottomBarHeight ?: 0
val innerPadding = PaddingValues(
top =
if (topBarHeight == 0) {
if (topBarPlaceables.isEmpty()) {
insets.calculateTopPadding()
} else {
topBarHeight.toDp()
},
bottom =
(
if (bottomBarHeight == 0) {
insets.calculateBottomPadding()
} else {
bottomBarHeight.toDp()
}
) + fabHeight.toDp(),
start = insets.calculateLeftPadding((this@SubcomposeLayout).layoutDirection),
end = insets.calculateRightPadding((this@SubcomposeLayout).layoutDirection),
bottom = // Tachiyomi: Also take account of fab height when providing inner padding
if (bottomBarPlaceables.isEmpty() || bottomBarHeightPx == 0) {
max(insets.calculateBottomPadding(), fabOffsetDp)
} else {
max(bottomBarHeightPx.toDp(), fabOffsetDp)
},
start = insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection),
end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection),
)
content(innerPadding)
}.map { it.measure(looseConstraints) }
}.fastMap { it.measure(looseConstraints) }
// Placing to control drawing order to match default elevation of each placeable
bodyContentPlaceables.forEach {
bodyContentPlaceables.fastForEach {
it.place(0, 0)
}
topBarPlaceables.forEach {
topBarPlaceables.fastForEach {
it.place(0, 0)
}
snackbarPlaceables.forEach {
snackbarPlaceables.fastForEach {
it.place(
(layoutWidth - snackbarWidth) / 2,
snackbarLeft,
layoutHeight - snackbarOffsetFromBottom,
)
}
// The bottom bar is always at the bottom of the layout
bottomBarPlaceables.forEach {
it.place(0, layoutHeight - bottomBarHeight)
bottomBarPlaceables.fastForEach {
it.place(0, layoutHeight - (bottomBarHeight ?: 0))
}
// Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
fabPlacement?.let { placement ->
fabPlaceables.forEach {
it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
}
fabPlaceables.fastForEach {
it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0))
}
}
}

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
@ -16,8 +17,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
import kotlinx.coroutines.launch
@ -27,7 +26,6 @@ fun TabbedScreen(
tabs: List<TabContent>,
startIndex: Int? = null,
searchQuery: String? = null,
@StringRes placeholderRes: Int? = null,
onChangeSearchQuery: (String?) -> Unit = {},
incognitoMode: Boolean,
downloadedOnlyMode: Boolean,
@ -43,28 +41,16 @@ fun TabbedScreen(
Scaffold(
topBar = {
if (searchQuery == null) {
AppBar(
title = stringResource(titleRes),
actions = {
AppBarActions(tabs[state.currentPage].actions)
},
)
} else {
SearchToolbar(
searchQuery = searchQuery,
placeholderText = placeholderRes?.let { stringResource(it) },
onChangeSearchQuery = {
onChangeSearchQuery(it)
},
onClickCloseSearch = {
onChangeSearchQuery(null)
},
onClickResetSearch = {
onChangeSearchQuery("")
},
)
}
val tab = tabs[state.currentPage]
val searchEnabled = tab.searchEnabled
SearchToolbar(
titleContent = { AppBarTitle(stringResource(titleRes)) },
searchEnabled = searchEnabled,
searchQuery = if (searchEnabled) searchQuery else null,
onChangeSearchQuery = onChangeSearchQuery,
actions = { AppBarActions(tab.actions) },
)
},
) { contentPadding ->
Column(
@ -82,9 +68,8 @@ fun TabbedScreen(
Tab(
selected = state.currentPage == index,
onClick = { scope.launch { state.animateScrollToPage(index) } },
text = {
TabText(stringResource(tab.titleRes), tab.badgeNumber, state.currentPage == index)
},
text = { TabText(text = stringResource(tab.titleRes), badgeCount = tab.badgeNumber) },
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
)
}
}
@ -110,6 +95,7 @@ fun TabbedScreen(
data class TabContent(
@StringRes val titleRes: Int,
val badgeNumber: Int? = null,
val searchEnabled: Boolean = false,
val actions: List<AppBar.Action> = emptyList(),
val content: @Composable (contentPadding: PaddingValues) -> Unit,
)

View File

@ -30,17 +30,13 @@ fun TabIndicator(currentTabPosition: TabPosition) {
fun TabText(
text: String,
badgeCount: Int? = null,
isCurrentPage: Boolean,
) {
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = text,
color = if (isCurrentPage) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground,
)
Text(text = text)
if (badgeCount != null) {
Pill(
text = "$badgeCount",

View File

@ -7,10 +7,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.history.HistoryUiModel
import eu.kanade.presentation.util.plus
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.DateFormat
@ -27,7 +26,7 @@ fun HistoryContent(
val relativeTime: Int = remember { preferences.relativeTime().get() }
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
ScrollbarLazyColumn(
FastScrollLazyColumn(
contentPadding = contentPadding,
) {
items(

View File

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

View File

@ -1,19 +1,13 @@
package eu.kanade.presentation.history.components
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DeleteSweep
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter
@ -26,54 +20,12 @@ fun HistoryToolbar(
incognitoMode: Boolean,
downloadedOnlyMode: Boolean,
) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
if (state.searchQuery == null) {
HistoryRegularToolbar(
onClickSearch = { state.searchQuery = "" },
onClickDelete = { state.dialog = HistoryPresenter.Dialog.DeleteAll },
incognitoMode = incognitoMode,
downloadedOnlyMode = downloadedOnlyMode,
scrollBehavior = scrollBehavior,
)
} else {
SearchToolbar(
searchQuery = state.searchQuery!!,
onChangeSearchQuery = { state.searchQuery = it },
placeholderText = stringResource(R.string.action_search_hint),
onClickCloseSearch = { state.searchQuery = null },
onClickResetSearch = { state.searchQuery = "" },
incognitoMode = incognitoMode,
downloadedOnlyMode = downloadedOnlyMode,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Search,
),
keyboardActions = KeyboardActions(
onSearch = {
focusManager.clearFocus()
keyboardController?.hide()
},
),
)
}
}
@Composable
fun HistoryRegularToolbar(
onClickSearch: () -> Unit,
onClickDelete: () -> Unit,
incognitoMode: Boolean,
downloadedOnlyMode: Boolean,
scrollBehavior: TopAppBarScrollBehavior,
) {
AppBar(
title = stringResource(R.string.history),
SearchToolbar(
titleContent = { AppBarTitle(stringResource(R.string.history)) },
searchQuery = state.searchQuery,
onChangeSearchQuery = { state.searchQuery = it },
actions = {
IconButton(onClick = onClickSearch) {
Icon(Icons.Outlined.Search, contentDescription = stringResource(R.string.action_search))
}
IconButton(onClick = onClickDelete) {
IconButton(onClick = { state.dialog = HistoryPresenter.Dialog.DeleteAll }) {
Icon(Icons.Outlined.DeleteSweep, contentDescription = stringResource(R.string.pref_clear_history))
}
},

View File

@ -2,11 +2,14 @@ package eu.kanade.presentation.library
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.util.fastAll
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.library.model.display
import eu.kanade.domain.manga.model.isLocal
@ -17,6 +20,7 @@ import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.library.components.LibraryContent
import eu.kanade.presentation.library.components.LibraryToolbar
import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
@ -29,14 +33,17 @@ fun LibraryScreen(
onChangeCategoryClicked: () -> Unit,
onMarkAsReadClicked: () -> Unit,
onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit,
onDeleteClicked: () -> Unit,
onClickUnselectAll: () -> Unit,
onClickSelectAll: () -> Unit,
onClickInvertSelection: () -> Unit,
onClickFilter: () -> Unit,
onClickRefresh: (Category?) -> Boolean,
onClickOpenRandomManga: () -> Unit,
) {
val haptic = LocalHapticFeedback.current
Scaffold(
topBar = { scrollBehavior ->
val title by presenter.getToolbarTitle()
@ -51,6 +58,7 @@ fun LibraryScreen(
onClickInvertSelection = onClickInvertSelection,
onClickFilter = onClickFilter,
onClickRefresh = { onClickRefresh(null) },
onClickOpenRandomManga = onClickOpenRandomManga,
scrollBehavior = scrollBehavior.takeIf { !tabVisible }, // For scroll overlay when no tab
)
},
@ -60,7 +68,7 @@ fun LibraryScreen(
onChangeCategoryClicked = onChangeCategoryClicked,
onMarkAsReadClicked = onMarkAsReadClicked,
onMarkAsUnreadClicked = onMarkAsUnreadClicked,
onDownloadClicked = onDownloadClicked.takeIf { presenter.selection.none { it.manga.isLocal() } },
onDownloadClicked = onDownloadClicked.takeIf { presenter.selection.fastAll { !it.manga.isLocal() } },
onDeleteClicked = onDeleteClicked,
)
},
@ -79,7 +87,7 @@ fun LibraryScreen(
actions = listOf(
EmptyScreenAction(
stringResId = R.string.getting_started_guide,
icon = Icons.Default.HelpOutline,
icon = Icons.Outlined.HelpOutline,
onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
),
),
@ -97,7 +105,10 @@ fun LibraryScreen(
onChangeCurrentPage = { presenter.activeCategory = it },
onMangaClicked = onMangaClicked,
onToggleSelection = { presenter.toggleSelection(it) },
onToggleRangeSelection = { presenter.toggleRangeSelection(it) },
onToggleRangeSelection = {
presenter.toggleRangeSelection(it)
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onRefresh = onClickRefresh,
onGlobalSearchClicked = onGlobalSearchClicked,
getNumberOfMangaForCategory = { presenter.getMangaCountForCategory(it) },

View File

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

View File

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

View File

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

View File

@ -15,12 +15,12 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import com.google.accompanist.pager.rememberPagerState
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.presentation.components.SwipeRefresh
import eu.kanade.presentation.components.rememberPagerState
import eu.kanade.presentation.library.LibraryState
import eu.kanade.tachiyomi.ui.library.LibraryItem
import kotlinx.coroutines.delay
@ -68,12 +68,13 @@ fun LibraryContent(
if (isLibraryEmpty.not() && showPageTabs && categories.size > 1) {
LibraryTabs(
state = pagerState,
categories = categories,
currentPageIndex = pagerState.currentPage,
showMangaCount = showMangaCount,
getNumberOfMangaForCategory = getNumberOfMangaForCategory,
isDownloadOnly = isDownloadOnly,
isIncognitoMode = isIncognitoMode,
onTabItemClick = { scope.launch { pagerState.animateScrollToPage(it) } },
)
}

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

View File

@ -10,11 +10,11 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.presentation.components.HorizontalPager
import eu.kanade.presentation.components.PagerState
import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable
@ -72,9 +72,10 @@ fun LibraryPager(
onGlobalSearchClicked = onGlobalSearchClicked,
)
}
LibraryDisplayMode.CompactGrid -> {
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
LibraryCompactGrid(
items = library,
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
showDownloadBadges = showDownloadBadges,
showUnreadBadges = showUnreadBadges,
showLocalBadges = showLocalBadges,
@ -104,22 +105,6 @@ fun LibraryPager(
onGlobalSearchClicked = onGlobalSearchClicked,
)
}
LibraryDisplayMode.CoverOnlyGrid -> {
LibraryCoverOnlyGrid(
items = library,
showDownloadBadges = showDownloadBadges,
showUnreadBadges = showUnreadBadges,
showLocalBadges = showLocalBadges,
showLanguageBadges = showLanguageBadges,
columns = columns,
contentPadding = contentPadding,
selection = selectedManga,
onClick = onClickManga,
onLongClick = onLongClickManga,
searchQuery = searchQuery,
onGlobalSearchClicked = onGlobalSearchClicked,
)
}
}
}
}

View File

@ -1,56 +1,54 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.PagerState
import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.TabIndicator
import eu.kanade.presentation.components.TabText
import kotlinx.coroutines.launch
@Composable
fun LibraryTabs(
state: PagerState,
categories: List<Category>,
currentPageIndex: Int,
showMangaCount: Boolean,
isDownloadOnly: Boolean,
isIncognitoMode: Boolean,
getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>,
onTabItemClick: (Int) -> Unit,
) {
val scope = rememberCoroutineScope()
Column {
ScrollableTabRow(
selectedTabIndex = state.currentPage,
selectedTabIndex = currentPageIndex,
edgePadding = 0.dp,
indicator = { TabIndicator(it[state.currentPage]) },
indicator = { TabIndicator(it[currentPageIndex]) },
// TODO: use default when width is fixed upstream
// https://issuetracker.google.com/issues/242879624
divider = {},
) {
categories.forEachIndexed { index, category ->
val count by if (showMangaCount) {
getNumberOfMangaForCategory(category.id)
} else {
remember { mutableStateOf<Int?>(null) }
}
Tab(
selected = state.currentPage == index,
onClick = { scope.launch { state.animateScrollToPage(index) } },
selected = currentPageIndex == index,
onClick = { onTabItemClick(index) },
text = {
TabText(category.visualName, count, state.currentPage == index)
TabText(
text = category.visualName,
badgeCount = if (showMangaCount) {
getNumberOfMangaForCategory(category.id)
} else {
null
}?.value,
)
},
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
)
}
}

View File

@ -2,14 +2,11 @@ package eu.kanade.presentation.library.components
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
@ -19,13 +16,11 @@ import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.OverflowMenu
import eu.kanade.presentation.components.Pill
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.library.LibraryState
@ -43,6 +38,7 @@ fun LibraryToolbar(
onClickInvertSelection: () -> Unit,
onClickFilter: () -> Unit,
onClickRefresh: () -> Unit,
onClickOpenRandomManga: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
) = when {
state.selectionMode -> LibrarySelectionToolbar(
@ -53,38 +49,16 @@ fun LibraryToolbar(
onClickSelectAll = onClickSelectAll,
onClickInvertSelection = onClickInvertSelection,
)
state.searchQuery != null -> {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
SearchToolbar(
searchQuery = state.searchQuery!!,
onChangeSearchQuery = { state.searchQuery = it },
onClickCloseSearch = { state.searchQuery = null },
onClickResetSearch = { state.searchQuery = "" },
scrollBehavior = scrollBehavior,
incognitoMode = incognitoMode,
downloadedOnlyMode = downloadedOnlyMode,
placeholderText = stringResource(R.string.action_search_hint),
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Search,
),
keyboardActions = KeyboardActions(
onSearch = {
focusManager.clearFocus()
keyboardController?.hide()
},
),
)
}
else -> LibraryRegularToolbar(
title = title,
hasFilters = state.hasActiveFilters,
incognitoMode = incognitoMode,
downloadedOnlyMode = downloadedOnlyMode,
onClickSearch = { state.searchQuery = "" },
searchQuery = state.searchQuery,
onChangeSearchQuery = { state.searchQuery = it },
onClickFilter = onClickFilter,
onClickRefresh = onClickRefresh,
onClickOpenRandomManga = onClickOpenRandomManga,
scrollBehavior = scrollBehavior,
)
}
@ -95,14 +69,15 @@ fun LibraryRegularToolbar(
hasFilters: Boolean,
incognitoMode: Boolean,
downloadedOnlyMode: Boolean,
onClickSearch: () -> Unit,
searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit,
onClickFilter: () -> Unit,
onClickRefresh: () -> Unit,
onClickOpenRandomManga: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
) {
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
AppBar(
SearchToolbar(
titleContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
@ -120,15 +95,29 @@ fun LibraryRegularToolbar(
}
}
},
searchQuery = searchQuery,
onChangeSearchQuery = onChangeSearchQuery,
actions = {
IconButton(onClick = onClickSearch) {
Icon(Icons.Outlined.Search, contentDescription = stringResource(R.string.action_search))
}
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
IconButton(onClick = onClickFilter) {
Icon(Icons.Outlined.FilterList, contentDescription = stringResource(R.string.action_filter), tint = filterTint)
}
IconButton(onClick = onClickRefresh) {
Icon(Icons.Outlined.Refresh, contentDescription = stringResource(R.string.pref_category_library_update))
OverflowMenu { closeMenu ->
DropdownMenuItem(
text = { Text(text = stringResource(R.string.pref_category_library_update)) },
onClick = {
onClickRefresh()
closeMenu()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_open_random_manga)) },
onClick = {
onClickOpenRandomManga()
closeMenu()
},
)
}
},
incognitoMode = incognitoMode,
@ -150,10 +139,10 @@ fun LibrarySelectionToolbar(
titleContent = { Text(text = "${state.selection.size}") },
actions = {
IconButton(onClick = onClickSelectAll) {
Icon(Icons.Outlined.SelectAll, contentDescription = "search")
Icon(Icons.Outlined.SelectAll, contentDescription = stringResource(R.string.action_select_all))
}
IconButton(onClick = onClickInvertSelection) {
Icon(Icons.Outlined.FlipToBack, contentDescription = "invert")
Icon(Icons.Outlined.FlipToBack, contentDescription = stringResource(R.string.action_select_inverse))
}
},
isActionMode = true,

View File

@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
@ -35,6 +34,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@ -43,6 +43,9 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.ExtendedFloatingActionButton
@ -204,7 +207,7 @@ private fun MangaScreenSmallImpl(
val chapters = remember(state) { state.processedChapters.toList() }
val internalOnBackPressed = {
if (chapters.any { it.selected }) {
if (chapters.fastAny { it.selected }) {
onAllChapterSelected(false)
} else {
onBackClicked()
@ -213,8 +216,6 @@ private fun MangaScreenSmallImpl(
BackHandler(onBack = internalOnBackPressed)
Scaffold(
modifier = Modifier
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal).asPaddingValues()),
topBar = {
val firstVisibleItemIndex by remember {
derivedStateOf { chapterListState.firstVisibleItemIndex }
@ -260,24 +261,22 @@ private fun MangaScreenSmallImpl(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
AnimatedVisibility(
visible = chapters.any { !it.chapter.read } && chapters.none { it.selected },
visible = chapters.fastAny { !it.chapter.read } && chapters.fastAll { !it.selected },
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
val id = if (chapters.any { it.chapter.read }) {
val id = if (chapters.fastAny { it.chapter.read }) {
R.string.action_resume
} else {
R.string.action_start
}
Text(text = stringResource(id))
},
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
modifier = Modifier
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
)
}
},
@ -287,17 +286,21 @@ private fun MangaScreenSmallImpl(
SwipeRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = chapters.none { it.selected },
enabled = chapters.fastAll { !it.selected },
indicatorPadding = contentPadding,
) {
val layoutDirection = LocalLayoutDirection.current
VerticalFastScroller(
listState = chapterListState,
topContentPadding = topPadding,
endContentPadding = contentPadding.calculateEndPadding(layoutDirection),
) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
state = chapterListState,
contentPadding = PaddingValues(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding(),
),
) {
@ -351,6 +354,7 @@ private fun MangaScreenSmallImpl(
contentType = MangaScreenItem.CHAPTER_HEADER,
) {
ChapterHeader(
enabled = chapters.fastAll { !it.selected },
chapterCount = chapters.size,
onClick = onFilterClicked,
)
@ -410,11 +414,11 @@ fun MangaScreenLargeImpl(
val chapters = remember(state) { state.processedChapters.toList() }
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) }
var topBarHeight by remember { mutableStateOf(0) }
SwipeRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
enabled = chapters.none { it.selected },
enabled = chapters.fastAll { !it.selected },
indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection),
top = with(density) { topBarHeight.toDp() },
@ -424,7 +428,7 @@ fun MangaScreenLargeImpl(
val chapterListState = rememberLazyListState()
val internalOnBackPressed = {
if (chapters.any { it.selected }) {
if (chapters.fastAny { it.selected }) {
onAllChapterSelected(false)
} else {
onBackClicked()
@ -433,12 +437,11 @@ fun MangaScreenLargeImpl(
BackHandler(onBack = internalOnBackPressed)
Scaffold(
modifier = Modifier.padding(insetPadding),
topBar = {
MangaToolbar(
modifier = Modifier.onSizeChanged { onTopBarHeightChanged(it.height) },
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
title = state.manga.title,
titleAlphaProvider = { if (chapters.any { it.selected }) 1f else 0f },
titleAlphaProvider = { if (chapters.fastAny { it.selected }) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.manga.chaptersFiltered(),
incognitoMode = state.isIncognitoMode,
@ -473,33 +476,36 @@ fun MangaScreenLargeImpl(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
AnimatedVisibility(
visible = chapters.any { !it.chapter.read } && chapters.none { it.selected },
visible = chapters.fastAny { !it.chapter.read } && chapters.fastAll { !it.selected },
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
val id = if (chapters.any { it.chapter.read }) {
val id = if (chapters.fastAny { it.chapter.read }) {
R.string.action_resume
} else {
R.string.action_start
}
Text(text = stringResource(id))
},
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
modifier = Modifier
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
)
}
},
) { contentPadding ->
TwoPanelBox(
modifier = Modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
),
startContent = {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState()),
.verticalScroll(rememberScrollState())
.padding(bottom = contentPadding.calculateBottomPadding()),
) {
MangaInfoBox(
isTabletUi = true,
@ -548,6 +554,7 @@ fun MangaScreenLargeImpl(
contentType = MangaScreenItem.CHAPTER_HEADER,
) {
ChapterHeader(
enabled = chapters.fastAll { !it.selected },
chapterCount = chapters.size,
onClick = onFilterButtonClicked,
)
@ -582,29 +589,29 @@ private fun SharedMangaBottomActionMenu(
visible = selected.isNotEmpty(),
modifier = modifier.fillMaxWidth(fillFraction),
onBookmarkClicked = {
onMultiBookmarkClicked.invoke(selected.map { it.chapter }, true)
}.takeIf { selected.any { !it.chapter.bookmark } },
onMultiBookmarkClicked.invoke(selected.fastMap { it.chapter }, true)
}.takeIf { selected.fastAny { !it.chapter.bookmark } },
onRemoveBookmarkClicked = {
onMultiBookmarkClicked.invoke(selected.map { it.chapter }, false)
}.takeIf { selected.all { it.chapter.bookmark } },
onMultiBookmarkClicked.invoke(selected.fastMap { it.chapter }, false)
}.takeIf { selected.fastAll { it.chapter.bookmark } },
onMarkAsReadClicked = {
onMultiMarkAsReadClicked(selected.map { it.chapter }, true)
}.takeIf { selected.any { !it.chapter.read } },
onMultiMarkAsReadClicked(selected.fastMap { it.chapter }, true)
}.takeIf { selected.fastAny { !it.chapter.read } },
onMarkAsUnreadClicked = {
onMultiMarkAsReadClicked(selected.map { it.chapter }, false)
}.takeIf { selected.any { it.chapter.read || it.chapter.lastPageRead > 0L } },
onMultiMarkAsReadClicked(selected.fastMap { it.chapter }, false)
}.takeIf { selected.fastAny { it.chapter.read || it.chapter.lastPageRead > 0L } },
onMarkPreviousAsReadClicked = {
onMarkPreviousAsReadClicked(selected[0].chapter)
}.takeIf { selected.size == 1 },
onDownloadClicked = {
onDownloadChapter!!(selected.toList(), ChapterDownloadAction.START)
}.takeIf {
onDownloadChapter != null && selected.any { it.downloadState != Download.State.DOWNLOADED }
onDownloadChapter != null && selected.fastAny { it.downloadState != Download.State.DOWNLOADED }
},
onDeleteClicked = {
onMultiDeleteClicked(selected.map { it.chapter })
onMultiDeleteClicked(selected.fastMap { it.chapter })
}.takeIf {
onDownloadChapter != null && selected.any { it.downloadState == Download.State.DOWNLOADED }
onDownloadChapter != null && selected.fastAny { it.downloadState == Download.State.DOWNLOADED }
},
)
}
@ -629,6 +636,7 @@ private fun LazyListScope.sharedChapterItems(
read = chapterItem.chapter.read,
bookmark = chapterItem.chapter.bookmark,
selected = chapterItem.selected,
downloadIndicatorEnabled = chapters.fastAll { !it.selected },
downloadStateProvider = { chapterItem.downloadState },
downloadProgressProvider = { chapterItem.downloadProgress },
onLongClick = {
@ -660,7 +668,7 @@ private fun onChapterItemClick(
) {
when {
chapterItem.selected -> onToggleSelection(false)
chapters.any { it.selected } -> onToggleSelection(true)
chapters.fastAny { it.selected } -> onToggleSelection(true)
else -> onChapterClicked(chapterItem.chapter)
}
}

View File

@ -16,13 +16,17 @@ import eu.kanade.tachiyomi.R
@Composable
fun ChapterHeader(
enabled: Boolean,
chapterCount: Int?,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.clickable(
enabled = enabled,
onClick = onClick,
)
.padding(horizontal = 16.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -23,7 +22,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@ -32,6 +30,8 @@ import androidx.compose.ui.unit.sp
import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.ChapterDownloadIndicator
import eu.kanade.presentation.util.ReadItemAlpha
import eu.kanade.presentation.util.SecondaryItemAlpha
import eu.kanade.presentation.util.selectedBackground
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
@ -45,6 +45,7 @@ fun MangaChapterListItem(
read: Boolean,
bookmark: Boolean,
selected: Boolean,
downloadIndicatorEnabled: Boolean,
downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int,
onLongClick: () -> Unit,
@ -53,7 +54,7 @@ fun MangaChapterListItem(
) {
Row(
modifier = modifier
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent)
.selectedBackground(selected)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
@ -67,12 +68,13 @@ fun MangaChapterListItem(
MaterialTheme.colorScheme.onSurface
}
val textAlpha = remember(read) { if (read) ReadItemAlpha else 1f }
val textSubtitleAlpha = remember(read) { if (read) ReadItemAlpha else SecondaryItemAlpha }
Row(verticalAlignment = Alignment.CenterVertically) {
var textHeight by remember { mutableStateOf(0) }
if (bookmark) {
Icon(
imageVector = Icons.Default.Bookmark,
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
@ -91,7 +93,7 @@ fun MangaChapterListItem(
)
}
Spacer(modifier = Modifier.height(6.dp))
Row(modifier = Modifier.alpha(textAlpha)) {
Row(modifier = Modifier.alpha(textSubtitleAlpha)) {
ProvideTextStyle(
value = MaterialTheme.typography.bodyMedium
.copy(color = textColor, fontSize = 12.sp),
@ -127,6 +129,7 @@ fun MangaChapterListItem(
// Download view
if (onDownloadClick != null) {
ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,

View File

@ -14,7 +14,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Share
@ -24,11 +24,14 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.updatePadding
@ -63,7 +66,7 @@ fun MangaCoverDialog(
) {
IconButton(onClick = onDismissRequest) {
Icon(
imageVector = Icons.Default.Close,
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_close),
)
}
@ -82,9 +85,15 @@ fun MangaCoverDialog(
}
if (onEditClick != null) {
Box {
val (expanded, onExpand) = remember { mutableStateOf(false) }
var expanded by remember { mutableStateOf(false) }
IconButton(
onClick = { if (isCustomCover) onExpand(true) else onEditClick(EditCoverAction.EDIT) },
onClick = {
if (isCustomCover) {
expanded = true
} else {
onEditClick(EditCoverAction.EDIT)
}
},
) {
Icon(
imageVector = Icons.Outlined.Edit,
@ -93,20 +102,21 @@ fun MangaCoverDialog(
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { onExpand(false) },
onDismissRequest = { expanded = false },
offset = DpOffset(8.dp, 0.dp),
) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_edit)) },
onClick = {
onEditClick(EditCoverAction.EDIT)
onExpand(false)
expanded = false
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_delete)) },
onClick = {
onEditClick(EditCoverAction.DELETE)
onExpand(false)
expanded = false
},
)
}

View File

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

View File

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

View File

@ -2,16 +2,13 @@ package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.statusBars
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.FlipToBack
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -30,7 +27,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.DownloadDropdownMenu
import eu.kanade.presentation.components.OverflowMenu
import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.presentation.theme.active
import eu.kanade.tachiyomi.R
@ -71,7 +69,7 @@ fun MangaToolbar(
navigationIcon = {
IconButton(onClick = onBackClicked) {
Icon(
imageVector = if (isActionMode) Icons.Default.Close else Icons.Default.ArrowBack,
imageVector = if (isActionMode) Icons.Outlined.Close else Icons.Outlined.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
}
@ -80,13 +78,13 @@ fun MangaToolbar(
if (isActionMode) {
IconButton(onClick = onSelectAll) {
Icon(
imageVector = Icons.Default.SelectAll,
imageVector = Icons.Outlined.SelectAll,
contentDescription = stringResource(R.string.action_select_all),
)
}
IconButton(onClick = onInvertSelection) {
Icon(
imageVector = Icons.Default.FlipToBack,
imageVector = Icons.Outlined.FlipToBack,
contentDescription = stringResource(R.string.action_select_inverse),
)
}
@ -101,53 +99,11 @@ fun MangaToolbar(
)
}
val onDismissRequest = { onDownloadExpanded(false) }
DropdownMenu(
DownloadDropdownMenu(
expanded = downloadExpanded,
onDismissRequest = onDismissRequest,
) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_1)) },
onClick = {
onClickDownload(DownloadAction.NEXT_1_CHAPTER)
onDismissRequest()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_5)) },
onClick = {
onClickDownload(DownloadAction.NEXT_5_CHAPTERS)
onDismissRequest()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_10)) },
onClick = {
onClickDownload(DownloadAction.NEXT_10_CHAPTERS)
onDismissRequest()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_custom)) },
onClick = {
onClickDownload(DownloadAction.CUSTOM)
onDismissRequest()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_unread)) },
onClick = {
onClickDownload(DownloadAction.UNREAD_CHAPTERS)
onDismissRequest()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.download_all)) },
onClick = {
onClickDownload(DownloadAction.ALL_CHAPTERS)
onDismissRequest()
},
)
}
onDownloadClicked = onClickDownload,
)
}
}
@ -156,49 +112,39 @@ fun MangaToolbar(
Icon(Icons.Outlined.FilterList, contentDescription = stringResource(R.string.action_filter), tint = filterTint)
}
if (onClickEditCategory != null && onClickMigrate != null) {
val (moreExpanded, onMoreExpanded) = remember { mutableStateOf(false) }
Box {
IconButton(onClick = { onMoreExpanded(!moreExpanded) }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
)
}
val onDismissRequest = { onMoreExpanded(false) }
DropdownMenu(
expanded = moreExpanded,
onDismissRequest = onDismissRequest,
) {
if (onClickEditCategory != null || onClickMigrate != null || onClickShare != null) {
OverflowMenu { closeMenu ->
if (onClickEditCategory != null) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_edit_categories)) },
onClick = {
onClickEditCategory()
onDismissRequest()
closeMenu()
},
)
}
if (onClickMigrate != null) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_migrate)) },
onClick = {
onClickMigrate()
onDismissRequest()
closeMenu()
},
)
}
if (onClickShare != null) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_share)) },
onClick = {
onClickShare()
closeMenu()
},
)
if (onClickShare != null) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_share)) },
onClick = {
onClickShare()
onDismissRequest()
},
)
}
}
}
}
}
},
windowInsets = WindowInsets.statusBars,
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)

View File

@ -1,5 +1,9 @@
package eu.kanade.presentation.more
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CloudOff
@ -21,6 +25,7 @@ import androidx.compose.ui.res.vectorResource
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.R
@ -32,6 +37,7 @@ import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
@Composable
fun MoreScreen(
presenter: MorePresenter,
isFDroid: Boolean,
onClickDownloadQueue: () -> Unit,
onClickCategories: () -> Unit,
onClickBackupAndRestore: () -> Unit,
@ -43,8 +49,21 @@ fun MoreScreen(
ScrollbarLazyColumn(
modifier = Modifier.statusBarsPadding(),
contentPadding = TachiyomiBottomNavigationView.withBottomNavPadding(),
contentPadding = TachiyomiBottomNavigationView.withBottomNavPadding(
WindowInsets.navigationBars.asPaddingValues(),
),
) {
if (isFDroid) {
item {
WarningBanner(
textRes = R.string.fdroid_warning,
modifier = Modifier.clickable {
uriHandler.openUri("https://tachiyomi.org/help/faq/#how-do-i-migrate-from-the-f-droid-version")
},
)
}
}
item {
LogoHeader()
}

View File

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

View File

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

View File

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

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import cafe.adriel.voyager.core.model.rememberScreenModel
@ -81,7 +82,7 @@ class ClearDatabaseScreen : Screen {
},
dismissButton = {
TextButton(onClick = model::hideConfirmation) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
text = {
@ -240,14 +241,14 @@ private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenMod
fun selectAll() = mutableState.update { state ->
if (state !is State.Ready) return@update state
state.copy(selection = state.items.map { it.id })
state.copy(selection = state.items.fastMap { it.id })
}
fun invertSelection() = mutableState.update { state ->
if (state !is State.Ready) return@update state
state.copy(
selection = state.items
.map { it.id }
.fastMap { it.id }
.filterNot { it in state.selection },
)
}

View File

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

View File

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

View File

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

View File

@ -10,13 +10,13 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastMap
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.domain.category.interactor.GetCategories
@ -27,7 +27,6 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -101,9 +100,11 @@ class SettingsDownloadScreen : SearchableSettings {
return Preference.PreferenceItem.ListPreference(
pref = currentDirPref,
title = stringResource(R.string.pref_download_directory),
subtitle = remember(currentDir) {
UniFile.fromUri(context, currentDir.toUri())?.filePath
} ?: stringResource(R.string.invalid_location, currentDir),
subtitleProvider = { value, _ ->
remember(value) {
UniFile.fromUri(context, value.toUri())?.filePath
} ?: stringResource(R.string.invalid_location, value)
},
entries = mapOf(
defaultDirPair,
customDirEntryKey to stringResource(R.string.custom_dir),
@ -173,25 +174,10 @@ class SettingsDownloadScreen : SearchableSettings {
downloadPreferences: DownloadPreferences,
categories: () -> List<Category>,
): Preference.PreferenceItem.MultiSelectListPreference {
val none = stringResource(R.string.none)
val pref = downloadPreferences.removeExcludeCategories()
val entries = categories().associate { it.id.toString() to it.visualName }
val subtitle by produceState(initialValue = "") {
pref.changes()
.stateIn(this)
.collect { mutable ->
value = mutable
.mapNotNull { id -> entries[id] }
.sortedBy { entries.values.indexOf(it) }
.joinToString()
.ifEmpty { none }
}
}
return Preference.PreferenceItem.MultiSelectListPreference(
pref = pref,
pref = downloadPreferences.removeExcludeCategories(),
title = stringResource(R.string.pref_remove_exclude_categories),
subtitle = subtitle,
entries = entries,
entries = categories().associate { it.id.toString() to it.visualName },
)
}
@ -219,8 +205,8 @@ class SettingsDownloadScreen : SearchableSettings {
itemLabel = { it.visualName },
onDismissRequest = { showDialog = false },
onValueChanged = { newIncluded, newExcluded ->
downloadNewChapterCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet())
downloadNewChapterCategoriesExcludePref.set(newExcluded.map { it.id.toString() }.toSet())
downloadNewChapterCategoriesPref.set(newIncluded.fastMap { it.id.toString() }.toSet())
downloadNewChapterCategoriesExcludePref.set(newExcluded.fastMap { it.id.toString() }.toSet())
showDialog = false
},
)

View File

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

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastMap
import androidx.core.content.ContextCompat
import cafe.adriel.voyager.navigator.currentOrThrow
import com.bluelinelabs.conductor.Router
@ -124,9 +125,9 @@ class SettingsLibraryScreen : SearchableSettings {
// For default category
val ids = listOf(libraryPreferences.defaultCategory().defaultValue()) +
allCategories.map { it.id.toInt() }
allCategories.fastMap { it.id.toInt() }
val labels = listOf(stringResource(R.string.default_category_summary)) +
allCategories.map { it.visualName(context) }
allCategories.fastMap { it.visualName(context) }
return Preference.PreferenceGroup(
title = stringResource(R.string.categories),
@ -177,28 +178,6 @@ class SettingsLibraryScreen : SearchableSettings {
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
val deviceRestrictionEntries = mapOf(
DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered),
DEVICE_CHARGING to stringResource(R.string.charging),
DEVICE_BATTERY_NOT_LOW to stringResource(R.string.battery_not_low),
)
val deviceRestrictions = libraryUpdateDeviceRestrictionPref.collectAsState()
.value
.sorted()
.map { deviceRestrictionEntries.getOrElse(it) { it } }
.let { if (it.isEmpty()) stringResource(R.string.none) else it.joinToString() }
val mangaRestrictionEntries = mapOf(
MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
MANGA_NON_READ to stringResource(R.string.pref_update_only_started),
MANGA_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
)
val mangaRestrictions = libraryUpdateMangaRestrictionPref.collectAsState()
.value
.map { mangaRestrictionEntries.getOrElse(it) { it } }
.let { if (it.isEmpty()) stringResource(R.string.none) else it.joinToString() }
val included by libraryUpdateCategoriesPref.collectAsState()
val excluded by libraryUpdateCategoriesExcludePref.collectAsState()
var showDialog by rememberSaveable { mutableStateOf(false) }
@ -224,7 +203,6 @@ class SettingsLibraryScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = libraryUpdateIntervalPref,
title = stringResource(R.string.pref_library_update_interval),
subtitle = "%s",
entries = mapOf(
0 to stringResource(R.string.update_never),
12 to stringResource(R.string.update_12hour),
@ -242,8 +220,13 @@ class SettingsLibraryScreen : SearchableSettings {
pref = libraryUpdateDeviceRestrictionPref,
enabled = libraryUpdateInterval > 0,
title = stringResource(R.string.pref_library_update_restriction),
subtitle = stringResource(R.string.restrictions, deviceRestrictions),
entries = deviceRestrictionEntries,
subtitle = stringResource(R.string.restrictions),
entries = mapOf(
DEVICE_ONLY_ON_WIFI to stringResource(R.string.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(R.string.network_not_metered),
DEVICE_CHARGING to stringResource(R.string.charging),
DEVICE_BATTERY_NOT_LOW to stringResource(R.string.battery_not_low),
),
onValueChanged = {
// Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
@ -253,8 +236,11 @@ class SettingsLibraryScreen : SearchableSettings {
Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryUpdateMangaRestrictionPref,
title = stringResource(R.string.pref_library_update_manga_restriction),
subtitle = mangaRestrictions,
entries = mangaRestrictionEntries,
entries = mapOf(
MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
MANGA_NON_READ to stringResource(R.string.pref_update_only_started),
MANGA_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
),
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.categories),
@ -341,7 +327,7 @@ class SettingsLibraryScreen : SearchableSettings {
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,43 +44,42 @@ internal fun BasePreferenceWidget(
widget: @Composable (() -> Unit)? = null,
) {
val highlighted = LocalPreferenceHighlighted.current
Box(modifier = Modifier.highlightBackground(highlighted)) {
Row(
modifier = modifier
.sizeIn(minHeight = 56.dp)
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
Row(
modifier = modifier
.highlightBackground(highlighted)
.sizeIn(minHeight = 56.dp)
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
if (icon != null) {
Box(
modifier = Modifier.padding(start = PrefsHorizontalPadding, end = 8.dp),
content = { icon() },
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = PrefsVerticalPadding),
) {
if (icon != null) {
Box(
modifier = Modifier.padding(start = PrefsHorizontalPadding, end = 8.dp),
content = { icon() },
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = PrefsVerticalPadding),
) {
if (!title.isNullOrBlank()) {
Text(
modifier = Modifier.padding(horizontal = PrefsHorizontalPadding),
text = title,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
style = MaterialTheme.typography.titleLarge,
fontSize = TitleFontSize,
)
}
subcomponent?.invoke(this)
}
if (widget != null) {
Box(
modifier = Modifier.padding(end = PrefsHorizontalPadding),
content = { widget() },
if (!title.isNullOrBlank()) {
Text(
modifier = Modifier.padding(horizontal = PrefsHorizontalPadding),
text = title,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
style = MaterialTheme.typography.titleLarge,
fontSize = TitleFontSize,
)
}
subcomponent?.invoke(this)
}
if (widget != null) {
Box(
modifier = Modifier.padding(end = PrefsHorizontalPadding),
content = { widget() },
)
}
}
}

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.window.DialogProperties
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.launch
@Composable
@ -27,18 +28,18 @@ fun EditTextPreferenceWidget(
value: String,
onConfirm: suspend (String) -> Boolean,
) {
val (isDialogShown, showDialog) = remember { mutableStateOf(false) }
var isDialogShown by remember { mutableStateOf(false) }
TextPreferenceWidget(
title = title,
subtitle = subtitle?.format(value),
icon = icon,
onPreferenceClick = { showDialog(true) },
onPreferenceClick = { isDialogShown = true },
)
if (isDialogShown) {
val scope = rememberCoroutineScope()
val onDismissRequest = { showDialog(false) }
val onDismissRequest = { isDialogShown = false }
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(value))
}
@ -71,7 +72,7 @@ fun EditTextPreferenceWidget(
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
Text(text = stringResource(R.string.action_cancel))
}
},
)

View File

@ -6,15 +6,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -26,6 +27,7 @@ import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.isScrolledToEnd
import eu.kanade.presentation.util.isScrolledToStart
import eu.kanade.presentation.util.minimumTouchTargetSize
import eu.kanade.tachiyomi.R
@Composable
fun <T> ListPreferenceWidget(
@ -36,18 +38,18 @@ fun <T> ListPreferenceWidget(
entries: Map<out T, String>,
onValueChange: (T) -> Unit,
) {
val (isDialogShown, showDialog) = remember { mutableStateOf(false) }
var isDialogShown by remember { mutableStateOf(false) }
TextPreferenceWidget(
title = title,
subtitle = subtitle?.format(entries[value]),
subtitle = subtitle,
icon = icon,
onPreferenceClick = { showDialog(true) },
onPreferenceClick = { isDialogShown = true },
)
if (isDialogShown) {
AlertDialog(
onDismissRequest = { showDialog(false) },
onDismissRequest = { isDialogShown = false },
title = { Text(text = title) },
text = {
Box {
@ -61,7 +63,7 @@ fun <T> ListPreferenceWidget(
isSelected = isSelected,
onSelected = {
onValueChange(current.key!!)
showDialog(false)
isDialogShown = false
},
)
}
@ -72,8 +74,8 @@ fun <T> ListPreferenceWidget(
}
},
confirmButton = {
TextButton(onClick = { showDialog(false) }) {
Text(text = stringResource(android.R.string.cancel))
TextButton(onClick = { isDialogShown = false }) {
Text(text = stringResource(R.string.action_cancel))
}
},
)
@ -89,7 +91,7 @@ private fun DialogRow(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.clip(MaterialTheme.shapes.small)
.selectable(
selected = isSelected,
onClick = { if (!isSelected) onSelected() },

View File

@ -5,15 +5,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -23,6 +24,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.minimumTouchTargetSize
import eu.kanade.tachiyomi.R
@Composable
fun MultiSelectListPreferenceWidget(
@ -30,13 +32,13 @@ fun MultiSelectListPreferenceWidget(
values: Set<String>,
onValuesChange: (Set<String>) -> Unit,
) {
val (isDialogShown, showDialog) = remember { mutableStateOf(false) }
var isDialogShown by remember { mutableStateOf(false) }
TextPreferenceWidget(
title = preference.title,
subtitle = preference.subtitle,
subtitle = preference.subtitleProvider(values, preference.entries),
icon = preference.icon,
onPreferenceClick = { showDialog(true) },
onPreferenceClick = { isDialogShown = true },
)
if (isDialogShown) {
@ -46,7 +48,7 @@ fun MultiSelectListPreferenceWidget(
.toMutableStateList()
}
AlertDialog(
onDismissRequest = { showDialog(false) },
onDismissRequest = { isDialogShown = false },
title = { Text(text = preference.title) },
text = {
LazyColumn {
@ -62,7 +64,7 @@ fun MultiSelectListPreferenceWidget(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.clip(MaterialTheme.shapes.small)
.selectable(
selected = isSelected,
onClick = { onSelectionChanged() },
@ -91,15 +93,15 @@ fun MultiSelectListPreferenceWidget(
TextButton(
onClick = {
onValuesChange(selected.toMutableSet())
showDialog(false)
isDialogShown = false
},
) {
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { showDialog(false) }) {
Text(text = stringResource(android.R.string.cancel))
TextButton(onClick = { isDialogShown = false }) {
Text(text = stringResource(R.string.action_cancel))
}
},
)

View File

@ -14,6 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview
@Composable
fun SwitchPreferenceWidget(
modifier: Modifier = Modifier,
title: String,
subtitle: String? = null,
icon: ImageVector? = null,
@ -21,6 +22,7 @@ fun SwitchPreferenceWidget(
onCheckedChanged: (Boolean) -> Unit,
) {
TextPreferenceWidget(
modifier = modifier,
title = title,
subtitle = subtitle,
icon = icon,
@ -44,7 +46,7 @@ private fun SwitchPreferenceWidgetPreview() {
SwitchPreferenceWidget(
title = "Text preference with icon",
subtitle = "Text preference summary",
icon = Icons.Default.Preview,
icon = Icons.Filled.Preview,
checked = true,
onCheckedChanged = {},
)

View File

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

View File

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

View File

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

View File

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

View File

@ -2,15 +2,12 @@ package eu.kanade.presentation.updates
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FlipToBack
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBarScrollBehavior
@ -23,17 +20,17 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.MangaBottomActionMenu
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SwipeRefresh
import eu.kanade.presentation.components.VerticalFastScroller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
@ -124,7 +121,6 @@ private fun UpdateScreenContent(
onClickCover: (UpdatesItem) -> Unit,
) {
val context = LocalContext.current
val updatesListState = rememberLazyListState()
val scope = rememberCoroutineScope()
var isRefreshing by remember { mutableStateOf(false) }
@ -143,34 +139,26 @@ private fun UpdateScreenContent(
enabled = presenter.selectionMode.not(),
indicatorPadding = contentPadding,
) {
VerticalFastScroller(
listState = updatesListState,
topContentPadding = contentPadding.calculateTopPadding(),
endContentPadding = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
FastScrollLazyColumn(
contentPadding = contentPadding,
) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
state = updatesListState,
contentPadding = contentPadding,
) {
if (presenter.lastUpdated > 0L) {
updatesLastUpdatedItem(presenter.lastUpdated)
}
updatesUiItems(
uiModels = presenter.uiModels,
selectionMode = presenter.selectionMode,
onUpdateSelected = presenter::toggleSelection,
onClickCover = onClickCover,
onClickUpdate = {
val intent = ReaderActivity.newIntent(context, it.update.mangaId, it.update.chapterId)
context.startActivity(intent)
},
onDownloadChapter = presenter::downloadChapters,
relativeTime = presenter.relativeTime,
dateFormat = presenter.dateFormat,
)
if (presenter.lastUpdated > 0L) {
updatesLastUpdatedItem(presenter.lastUpdated)
}
updatesUiItems(
uiModels = presenter.uiModels,
selectionMode = presenter.selectionMode,
onUpdateSelected = presenter::toggleSelection,
onClickCover = onClickCover,
onClickUpdate = {
val intent = ReaderActivity.newIntent(context, it.update.mangaId, it.update.chapterId)
context.startActivity(intent)
},
onDownloadChapter = presenter::downloadChapters,
relativeTime = presenter.relativeTime,
dateFormat = presenter.dateFormat,
)
}
}
@ -215,7 +203,7 @@ private fun UpdatesAppBar(
actions = {
IconButton(onClick = onUpdateLibrary) {
Icon(
imageVector = Icons.Default.Refresh,
imageVector = Icons.Outlined.Refresh,
contentDescription = stringResource(R.string.action_update_library),
)
}
@ -225,13 +213,13 @@ private fun UpdatesAppBar(
actionModeActions = {
IconButton(onClick = onSelectAll) {
Icon(
imageVector = Icons.Default.SelectAll,
imageVector = Icons.Outlined.SelectAll,
contentDescription = stringResource(R.string.action_select_all),
)
}
IconButton(onClick = onInvertSelection) {
Icon(
imageVector = Icons.Default.FlipToBack,
imageVector = Icons.Outlined.FlipToBack,
contentDescription = stringResource(R.string.action_select_inverse),
)
}
@ -255,24 +243,24 @@ private fun UpdatesBottomBar(
modifier = Modifier.fillMaxWidth(),
onBookmarkClicked = {
onMultiBookmarkClicked.invoke(selected, true)
}.takeIf { selected.any { !it.update.bookmark } },
}.takeIf { selected.fastAny { !it.update.bookmark } },
onRemoveBookmarkClicked = {
onMultiBookmarkClicked.invoke(selected, false)
}.takeIf { selected.all { it.update.bookmark } },
}.takeIf { selected.fastAll { it.update.bookmark } },
onMarkAsReadClicked = {
onMultiMarkAsReadClicked(selected, true)
}.takeIf { selected.any { !it.update.read } },
}.takeIf { selected.fastAny { !it.update.read } },
onMarkAsUnreadClicked = {
onMultiMarkAsReadClicked(selected, false)
}.takeIf { selected.any { it.update.read } },
}.takeIf { selected.fastAny { it.update.read } },
onDownloadClicked = {
onDownloadChapter(selected, ChapterDownloadAction.START)
}.takeIf {
selected.any { it.downloadStateProvider() != Download.State.DOWNLOADED }
selected.fastAny { it.downloadStateProvider() != Download.State.DOWNLOADED }
},
onDeleteClicked = {
onMultiDeleteClicked(selected)
}.takeIf { selected.any { it.downloadStateProvider() == Download.State.DOWNLOADED } },
}.takeIf { selected.fastAny { it.downloadStateProvider() == Download.State.DOWNLOADED } },
)
}

View File

@ -1,7 +1,6 @@
package eu.kanade.presentation.updates
import android.text.format.DateUtils
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -28,7 +27,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
@ -43,6 +41,7 @@ import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.util.ReadItemAlpha
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.selectedBackground
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesItem
@ -135,6 +134,7 @@ fun LazyListScope.updatesUiItems(
onDownloadChapter = {
if (selectionMode.not()) onDownloadChapter(listOf(updatesItem), it)
},
downloadIndicatorEnabled = selectionMode.not(),
downloadStateProvider = updatesItem.downloadStateProvider,
downloadProgressProvider = updatesItem.downloadProgressProvider,
)
@ -153,13 +153,14 @@ fun UpdatesUiItem(
onClickCover: () -> Unit,
onDownloadChapter: (ChapterDownloadAction) -> Unit,
// Download Indicator
downloadIndicatorEnabled: Boolean,
downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int,
) {
val haptic = LocalHapticFeedback.current
Row(
modifier = modifier
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent)
.selectedBackground(selected)
.combinedClickable(
onClick = onClick,
onLongClick = {
@ -205,7 +206,7 @@ fun UpdatesUiItem(
var textHeight by remember { mutableStateOf(0) }
if (bookmark) {
Icon(
imageVector = Icons.Default.Bookmark,
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
@ -225,6 +226,7 @@ fun UpdatesUiItem(
}
}
ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,

View File

@ -12,3 +12,4 @@ val verticalPadding = vertical
val topPaddingValues = PaddingValues(top = vertical)
const val ReadItemAlpha = .38f
const val SecondaryItemAlpha = .78f

View File

@ -29,7 +29,7 @@ fun Modifier.selectedBackground(isSelected: Boolean): Modifier = composed {
}
}
fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(.78f)
fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(SecondaryItemAlpha)
fun Modifier.clickableNoIndication(
onLongClick: (() -> Unit)? = null,

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