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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, private val mangaRepository: MangaRepository,
) { ) {
suspend fun await(manga: Manga, sourceId: Long): Manga { suspend fun await(manga: Manga): Manga {
val localManga = getManga(manga.url, sourceId) val localManga = getManga(manga.url, manga.source)
return when { return when {
localManga == null -> { localManga == null -> {
val id = insertManga(manga.copy(source = sourceId)) val id = insertManga(manga)
manga.copy(id = id!!) manga.copy(id = id!!)
} }
!localManga.favorite -> { !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( return Manga.create().copy(
url = url, url = url,
title = title, title = title,
@ -244,6 +244,7 @@ fun SManga.toDomainManga(): Manga {
thumbnailUrl = thumbnail_url, thumbnailUrl = thumbnail_url,
updateStrategy = update_strategy, updateStrategy = update_strategy,
initialized = initialized, 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>> { fun subscribe(mangaId: Long): Flow<List<Track>> {
return trackRepository.getTracksByMangaIdAsFlow(mangaId) return trackRepository.getTracksByMangaIdAsFlow(mangaId)
} }

View File

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

View File

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

View File

@ -4,12 +4,9 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.Settings import android.provider.Settings
import android.util.DisplayMetrics import android.util.DisplayMetrics
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row 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.Divider
import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn 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.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension 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 @Composable
private fun DetailsHeader( private fun DetailsHeader(
extension: Extension, extension: Extension,
@ -380,15 +362,14 @@ private fun SourceSwitchPreference(
) { ) {
val context = LocalContext.current val context = LocalContext.current
PreferenceRow( TextPreferenceWidget(
modifier = modifier, modifier = modifier,
title = if (source.labelAsName) { title = if (source.labelAsName) {
source.source.toString() source.source.toString()
} else { } else {
LocaleHelper.getSourceDisplayName(source.source.lang, context) LocaleHelper.getSourceDisplayName(source.source.lang, context)
}, },
onClick = { onClickSource(source.source.id) }, widget = {
action = {
Row( Row(
verticalAlignment = Alignment.CenterVertically, 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.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -11,10 +10,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen 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.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterPresenter import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
@ -42,8 +41,7 @@ fun ExtensionFilterScreen(
textResource = R.string.empty_screen, textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
else -> { else -> ExtensionFilterContent(
SourceFilterContent(
contentPadding = contentPadding, contentPadding = contentPadding,
state = presenter, state = presenter,
onClickLang = { onClickLang = {
@ -52,7 +50,6 @@ fun ExtensionFilterScreen(
) )
} }
} }
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
presenter.events.collectLatest { presenter.events.collectLatest {
when (it) { when (it) {
@ -65,40 +62,24 @@ fun ExtensionFilterScreen(
} }
@Composable @Composable
private fun SourceFilterContent( private fun ExtensionFilterContent(
contentPadding: PaddingValues, contentPadding: PaddingValues,
state: ExtensionFilterState, state: ExtensionFilterState,
onClickLang: (String) -> Unit, onClickLang: (String) -> Unit,
) { ) {
LazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
items( items(
items = state.items, items = state.items,
) { model -> ) { model ->
ExtensionFilterItem( val lang = model.lang
SwitchPreferenceWidget(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
lang = model.lang,
enabled = model.enabled,
onClickItem = onClickLang,
)
}
}
}
@Composable
private fun ExtensionFilterItem(
modifier: Modifier,
lang: String,
enabled: Boolean,
onClickItem: (String) -> Unit,
) {
PreferenceRow(
modifier = modifier,
title = LocaleHelper.getSourceDisplayName(lang, LocalContext.current), title = LocaleHelper.getSourceDisplayName(lang, LocalContext.current),
action = { checked = model.enabled,
Switch(checked = enabled, onCheckedChange = null) onCheckedChanged = { onClickLang(lang) },
},
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.layout.size
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@ -368,7 +368,7 @@ private fun ExtensionItemActions(
} else { } else {
IconButton(onClick = { onClickItemCancel(extension) }) { IconButton(onClick = { onClickItemCancel(extension) }) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_cancel), contentDescription = stringResource(R.string.action_cancel),
) )
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,10 @@
package eu.kanade.presentation.browse.components 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.Icons
import androidx.compose.material.icons.filled.ViewList import androidx.compose.material.icons.filled.ViewList
import androidx.compose.material.icons.filled.ViewModule import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material.icons.outlined.Help import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -15,14 +12,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import eu.kanade.domain.library.model.LibraryDisplayMode import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.presentation.browse.BrowseSourceState import eu.kanade.presentation.browse.BrowseSourceState
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.RadioMenuItem import eu.kanade.presentation.components.RadioMenuItem
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
@ -33,7 +28,7 @@ import eu.kanade.tachiyomi.source.LocalSource
@Composable @Composable
fun BrowseSourceToolbar( fun BrowseSourceToolbar(
state: BrowseSourceState, state: BrowseSourceState,
source: CatalogueSource, source: CatalogueSource?,
displayMode: LibraryDisplayMode, displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit, onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit, navigateUp: () -> Unit,
@ -42,59 +37,21 @@ fun BrowseSourceToolbar(
onSearch: (String) -> Unit, onSearch: (String) -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null, scrollBehavior: TopAppBarScrollBehavior? = null,
) { ) {
if (state.searchQuery == null) { // Avoid capturing unstable source in actions lambda
BrowseSourceRegularToolbar( val title = source?.name
title = if (state.isUserQuery) state.currentFilter.query else source.name, val isLocalSource = source is LocalSource
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,
)
}
}
@Composable SearchToolbar(
fun BrowseSourceRegularToolbar(
title: String,
isLocalSource: Boolean,
displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit,
onSearchClick: () -> Unit,
onWebViewClick: () -> Unit,
onHelpClick: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
) {
AppBar(
navigateUp = navigateUp, navigateUp = navigateUp,
title = title, titleContent = { AppBarTitle(title) },
searchQuery = state.searchQuery,
onChangeSearchQuery = { state.searchQuery = it },
onSearch = onSearch,
onClickCloseSearch = navigateUp,
actions = { actions = {
var selectingDisplayMode by remember { mutableStateOf(false) } var selectingDisplayMode by remember { mutableStateOf(false) }
AppBarActions( AppBarActions(
actions = listOf( actions = listOf(
AppBar.Action(
title = stringResource(R.string.action_search),
icon = Icons.Outlined.Search,
onClick = onSearchClick,
),
AppBar.Action( AppBar.Action(
title = stringResource(R.string.action_display_mode), title = stringResource(R.string.action_display_mode),
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule, 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)) }, text = { Text(text = stringResource(R.string.action_display_comfortable_grid)) },
isChecked = displayMode == LibraryDisplayMode.ComfortableGrid, isChecked = displayMode == LibraryDisplayMode.ComfortableGrid,
) { ) {
selectingDisplayMode = false
onDisplayModeChange(LibraryDisplayMode.ComfortableGrid) onDisplayModeChange(LibraryDisplayMode.ComfortableGrid)
} }
RadioMenuItem( RadioMenuItem(
text = { Text(text = stringResource(R.string.action_display_grid)) }, text = { Text(text = stringResource(R.string.action_display_grid)) },
isChecked = displayMode == LibraryDisplayMode.CompactGrid, isChecked = displayMode == LibraryDisplayMode.CompactGrid,
) { ) {
selectingDisplayMode = false
onDisplayModeChange(LibraryDisplayMode.CompactGrid) onDisplayModeChange(LibraryDisplayMode.CompactGrid)
} }
RadioMenuItem( RadioMenuItem(
text = { Text(text = stringResource(R.string.action_display_list)) }, text = { Text(text = stringResource(R.string.action_display_list)) },
isChecked = displayMode == LibraryDisplayMode.List, isChecked = displayMode == LibraryDisplayMode.List,
) { ) {
selectingDisplayMode = false
onDisplayModeChange(LibraryDisplayMode.List) onDisplayModeChange(LibraryDisplayMode.List)
} }
} }
@ -142,34 +102,3 @@ fun BrowseSourceRegularToolbar(
scrollBehavior = scrollBehavior, 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.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
@ -22,7 +24,7 @@ fun CategoryCreateDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onCreate: (String) -> Unit, onCreate: (String) -> Unit,
) { ) {
val (name, onNameChange) = remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
AlertDialog( AlertDialog(
@ -48,7 +50,7 @@ fun CategoryCreateDialog(
modifier = Modifier modifier = Modifier
.focusRequester(focusRequester), .focusRequester(focusRequester),
value = name, value = name,
onValueChange = onNameChange, onValueChange = { name = it },
label = { label = {
Text(text = stringResource(R.string.name)) Text(text = stringResource(R.string.name))
}, },
@ -70,7 +72,7 @@ fun CategoryRenameDialog(
onRename: (String) -> Unit, onRename: (String) -> Unit,
category: Category, category: Category,
) { ) {
val (name, onNameChange) = remember { mutableStateOf(category.name) } var name by remember { mutableStateOf(category.name) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
AlertDialog( AlertDialog(
@ -96,7 +98,7 @@ fun CategoryRenameDialog(
modifier = Modifier modifier = Modifier
.focusRequester(focusRequester), .focusRequester(focusRequester),
value = name, value = name,
onValueChange = onNameChange, onValueChange = { name = it },
label = { label = {
Text(text = stringResource(R.string.name)) Text(text = stringResource(R.string.name))
}, },
@ -130,7 +132,7 @@ fun CategoryDeleteDialog(
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel)) Text(text = stringResource(R.string.action_cancel))
} }
}, },
title = { title = {

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.annotation.StringRes
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -13,6 +14,23 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R 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 @Composable
fun AppStateBanners( fun AppStateBanners(
downloadedOnlyMode: Boolean, downloadedOnlyMode: Boolean,

View File

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

View File

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

View File

@ -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 package eu.kanade.presentation.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.icons.Icons 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.RadioButtonChecked
import androidx.compose.material.icons.outlined.RadioButtonUnchecked import androidx.compose.material.icons.outlined.RadioButtonUnchecked
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import eu.kanade.tachiyomi.R
import me.saket.cascade.CascadeColumnScope
import me.saket.cascade.CascadeDropdownMenu
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
@Composable @Composable
@ -20,6 +31,7 @@ fun DropdownMenu(
expanded: Boolean, expanded: Boolean,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(8.dp, (-56).dp),
properties: PopupProperties = PopupProperties(focusable = true), properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
@ -27,7 +39,7 @@ fun DropdownMenu(
expanded = expanded, expanded = expanded,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp), modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
offset = DpOffset(8.dp, (-8).dp), offset = offset,
properties = properties, properties = properties,
content = content, content = content,
) )
@ -46,15 +58,39 @@ fun RadioMenuItem(
if (isChecked) { if (isChecked) {
Icon( Icon(
imageVector = Icons.Outlined.RadioButtonChecked, imageVector = Icons.Outlined.RadioButtonChecked,
contentDescription = "", contentDescription = stringResource(R.string.selected),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) )
} else { } else {
Icon( Icon(
imageVector = Icons.Outlined.RadioButtonUnchecked, imageVector = Icons.Outlined.RadioButtonUnchecked,
contentDescription = "", contentDescription = stringResource(R.string.not_selected),
) )
} }
}, },
) )
} }
@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)) Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = onDismissRequest) { TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel)) Text(text = stringResource(R.string.action_cancel))
} }
TextButton( TextButton(
onClick = { onClick = {

View File

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

View File

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

View File

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

View File

@ -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.IntrinsicSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -28,7 +27,7 @@ fun Pill(
androidx.compose.material3.Surface( androidx.compose.material3.Surface(
modifier = modifier modifier = modifier
.padding(start = 4.dp), .padding(start = 4.dp),
shape = RoundedCornerShape(100), shape = MaterialTheme.shapes.extraLarge,
color = color, color = color,
contentColor = contentColor, contentColor = contentColor,
tonalElevation = elevation, tonalElevation = elevation,
@ -43,7 +42,6 @@ fun Pill(
text = text, text = text,
fontSize = fontSize, fontSize = fontSize,
maxLines = 1, 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.PaddingValues
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues 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.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.contentColorFor 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.Constraints
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp 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>. * <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 * * Pass scroll behavior to top bar by default
* * Remove height constraint for expanded app bar * * Remove height constraint for expanded app bar
* * Also take account of fab height when providing inner padding * * 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 modifier the [Modifier] to be applied to this scaffold
* @param topBar top app bar of the screen, typically a [SmallTopAppBar] * @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 * @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 * matching content color for [containerColor], or to the current [LocalContentColor] if
* [containerColor] is not a color from the theme. * [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 * @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 * 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 * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
@ -89,6 +100,7 @@ fun Scaffold(
floatingActionButtonPosition: FabPosition = FabPosition.End, floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = MaterialTheme.colorScheme.background, containerColor: Color = MaterialTheme.colorScheme.background,
contentColor: Color = contentColorFor(containerColor), contentColor: Color = contentColorFor(containerColor),
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit, content: @Composable (PaddingValues) -> Unit,
) { ) {
androidx.compose.material3.Surface( androidx.compose.material3.Surface(
@ -104,6 +116,7 @@ fun Scaffold(
bottomBar = bottomBar, bottomBar = bottomBar,
content = content, content = content,
snackbar = snackbarHost, snackbar = snackbarHost,
contentWindowInsets = contentWindowInsets,
fab = floatingActionButton, fab = floatingActionButton,
) )
} }
@ -129,6 +142,7 @@ private fun ScaffoldLayout(
content: @Composable (PaddingValues) -> Unit, content: @Composable (PaddingValues) -> Unit,
snackbar: @Composable () -> Unit, snackbar: @Composable () -> Unit,
fab: @Composable () -> Unit, fab: @Composable () -> Unit,
contentWindowInsets: WindowInsets,
bottomBar: @Composable () -> Unit, bottomBar: @Composable () -> Unit,
) { ) {
SubcomposeLayout { constraints -> SubcomposeLayout { constraints ->
@ -143,37 +157,51 @@ private fun ScaffoldLayout(
val topBarConstraints = looseConstraints.copy(maxHeight = Constraints.Infinity) val topBarConstraints = looseConstraints.copy(maxHeight = Constraints.Infinity)
layout(layoutWidth, layoutHeight) { 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) 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) it.measure(looseConstraints)
} }
val snackbarHeight = snackbarPlaceables.maxByOrNull { it.height }?.height ?: 0 val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
val snackbarWidth = snackbarPlaceables.maxByOrNull { it.width }?.width ?: 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 = val fabPlaceables =
subcompose(ScaffoldLayoutContent.Fab, fab).mapNotNull { measurable -> subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable ->
measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 } 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 fabPlacement = if (fabPlaceables.isNotEmpty() && fabWidth != 0 && fabHeight != 0) {
val fabWidth = fabPlaceables.maxByOrNull { it.width }!!.width
// FAB distance from the left of the layout, taking into account LTR / RTL // 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) { val fabLeftOffset = if (fabPosition == FabPosition.End) {
if (layoutDirection == LayoutDirection.Ltr) { if (layoutDirection == LayoutDirection.Ltr) {
layoutWidth - FabSpacing.roundToPx() - fabWidth layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset
} else { } else {
FabSpacing.roundToPx() FabSpacing.roundToPx() + leftInset
} }
} else { } else {
(layoutWidth - fabWidth) / 2 leftInset + ((insetLayoutWidth - fabWidth) / 2)
} }
FabPlacement( FabPlacement(
@ -190,75 +218,63 @@ private fun ScaffoldLayout(
LocalFabPlacement provides fabPlacement, LocalFabPlacement provides fabPlacement,
content = bottomBar, 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 { val fabOffsetFromBottom = fabPlacement?.let {
if (bottomBarHeight == 0) { max(bottomBarHeight ?: 0, bottomInset) + it.height + FabSpacing.roundToPx()
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()
}
} }
val snackbarOffsetFromBottom = if (snackbarHeight != 0) { val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight) snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight ?: bottomInset)
} else { } else {
0 0
} }
/**
* Tachiyomi: Also take account of fab height when providing inner padding
*/
val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) { val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
val insets = WindowInsets.Companion.safeDrawing val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout)
.asPaddingValues(this@SubcomposeLayout) val fabOffsetDp = fabOffsetFromBottom?.toDp() ?: 0.dp
val bottomBarHeightPx = bottomBarHeight ?: 0
val innerPadding = PaddingValues( val innerPadding = PaddingValues(
top = top =
if (topBarHeight == 0) { if (topBarPlaceables.isEmpty()) {
insets.calculateTopPadding() insets.calculateTopPadding()
} else { } else {
topBarHeight.toDp() topBarHeight.toDp()
}, },
bottom = bottom = // Tachiyomi: Also take account of fab height when providing inner padding
( if (bottomBarPlaceables.isEmpty() || bottomBarHeightPx == 0) {
if (bottomBarHeight == 0) { max(insets.calculateBottomPadding(), fabOffsetDp)
insets.calculateBottomPadding()
} else { } else {
bottomBarHeight.toDp() max(bottomBarHeightPx.toDp(), fabOffsetDp)
} },
) + fabHeight.toDp(), start = insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection),
start = insets.calculateLeftPadding((this@SubcomposeLayout).layoutDirection), end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection),
end = insets.calculateRightPadding((this@SubcomposeLayout).layoutDirection),
) )
content(innerPadding) content(innerPadding)
}.map { it.measure(looseConstraints) } }.fastMap { it.measure(looseConstraints) }
// Placing to control drawing order to match default elevation of each placeable // Placing to control drawing order to match default elevation of each placeable
bodyContentPlaceables.forEach { bodyContentPlaceables.fastForEach {
it.place(0, 0) it.place(0, 0)
} }
topBarPlaceables.forEach { topBarPlaceables.fastForEach {
it.place(0, 0) it.place(0, 0)
} }
snackbarPlaceables.forEach { snackbarPlaceables.fastForEach {
it.place( it.place(
(layoutWidth - snackbarWidth) / 2, snackbarLeft,
layoutHeight - snackbarOffsetFromBottom, layoutHeight - snackbarOffsetFromBottom,
) )
} }
// The bottom bar is always at the bottom of the layout // The bottom bar is always at the bottom of the layout
bottomBarPlaceables.forEach { bottomBarPlaceables.fastForEach {
it.place(0, layoutHeight - bottomBarHeight) it.place(0, layoutHeight - (bottomBarHeight ?: 0))
} }
// Explicitly not using placeRelative here as `leftOffset` already accounts for RTL // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
fabPlacement?.let { placement -> fabPlaceables.fastForEach {
fabPlaceables.forEach { it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0))
it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,13 @@
package eu.kanade.presentation.history.components 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.Icons
import androidx.compose.material.icons.outlined.DeleteSweep import androidx.compose.material.icons.outlined.DeleteSweep
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable 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.res.stringResource
import androidx.compose.ui.text.input.ImeAction import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter
@ -26,54 +20,12 @@ fun HistoryToolbar(
incognitoMode: Boolean, incognitoMode: Boolean,
downloadedOnlyMode: 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( SearchToolbar(
searchQuery = state.searchQuery!!, titleContent = { AppBarTitle(stringResource(R.string.history)) },
searchQuery = state.searchQuery,
onChangeSearchQuery = { state.searchQuery = it }, 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),
actions = { actions = {
IconButton(onClick = onClickSearch) { IconButton(onClick = { state.dialog = HistoryPresenter.Dialog.DeleteAll }) {
Icon(Icons.Outlined.Search, contentDescription = stringResource(R.string.action_search))
}
IconButton(onClick = onClickDelete) {
Icon(Icons.Outlined.DeleteSweep, contentDescription = stringResource(R.string.pref_clear_history)) 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.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.util.fastAll
import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.domain.library.model.display import eu.kanade.domain.library.model.display
import eu.kanade.domain.manga.model.isLocal 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.components.Scaffold
import eu.kanade.presentation.library.components.LibraryContent import eu.kanade.presentation.library.components.LibraryContent
import eu.kanade.presentation.library.components.LibraryToolbar import eu.kanade.presentation.library.components.LibraryToolbar
import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibraryPresenter import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
@ -29,14 +33,17 @@ fun LibraryScreen(
onChangeCategoryClicked: () -> Unit, onChangeCategoryClicked: () -> Unit,
onMarkAsReadClicked: () -> Unit, onMarkAsReadClicked: () -> Unit,
onMarkAsUnreadClicked: () -> Unit, onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: () -> Unit, onDownloadClicked: (DownloadAction) -> Unit,
onDeleteClicked: () -> Unit, onDeleteClicked: () -> Unit,
onClickUnselectAll: () -> Unit, onClickUnselectAll: () -> Unit,
onClickSelectAll: () -> Unit, onClickSelectAll: () -> Unit,
onClickInvertSelection: () -> Unit, onClickInvertSelection: () -> Unit,
onClickFilter: () -> Unit, onClickFilter: () -> Unit,
onClickRefresh: (Category?) -> Boolean, onClickRefresh: (Category?) -> Boolean,
onClickOpenRandomManga: () -> Unit,
) { ) {
val haptic = LocalHapticFeedback.current
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
val title by presenter.getToolbarTitle() val title by presenter.getToolbarTitle()
@ -51,6 +58,7 @@ fun LibraryScreen(
onClickInvertSelection = onClickInvertSelection, onClickInvertSelection = onClickInvertSelection,
onClickFilter = onClickFilter, onClickFilter = onClickFilter,
onClickRefresh = { onClickRefresh(null) }, onClickRefresh = { onClickRefresh(null) },
onClickOpenRandomManga = onClickOpenRandomManga,
scrollBehavior = scrollBehavior.takeIf { !tabVisible }, // For scroll overlay when no tab scrollBehavior = scrollBehavior.takeIf { !tabVisible }, // For scroll overlay when no tab
) )
}, },
@ -60,7 +68,7 @@ fun LibraryScreen(
onChangeCategoryClicked = onChangeCategoryClicked, onChangeCategoryClicked = onChangeCategoryClicked,
onMarkAsReadClicked = onMarkAsReadClicked, onMarkAsReadClicked = onMarkAsReadClicked,
onMarkAsUnreadClicked = onMarkAsUnreadClicked, onMarkAsUnreadClicked = onMarkAsUnreadClicked,
onDownloadClicked = onDownloadClicked.takeIf { presenter.selection.none { it.manga.isLocal() } }, onDownloadClicked = onDownloadClicked.takeIf { presenter.selection.fastAll { !it.manga.isLocal() } },
onDeleteClicked = onDeleteClicked, onDeleteClicked = onDeleteClicked,
) )
}, },
@ -79,7 +87,7 @@ fun LibraryScreen(
actions = listOf( actions = listOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.getting_started_guide, stringResId = R.string.getting_started_guide,
icon = Icons.Default.HelpOutline, icon = Icons.Outlined.HelpOutline,
onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") }, onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
), ),
), ),
@ -97,7 +105,10 @@ fun LibraryScreen(
onChangeCurrentPage = { presenter.activeCategory = it }, onChangeCurrentPage = { presenter.activeCategory = it },
onMangaClicked = onMangaClicked, onMangaClicked = onMangaClicked,
onToggleSelection = { presenter.toggleSelection(it) }, onToggleSelection = { presenter.toggleSelection(it) },
onToggleRangeSelection = { presenter.toggleRangeSelection(it) }, onToggleRangeSelection = {
presenter.toggleRangeSelection(it)
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onRefresh = onClickRefresh, onRefresh = onClickRefresh,
onGlobalSearchClicked = onGlobalSearchClicked, onGlobalSearchClicked = onGlobalSearchClicked,
getNumberOfMangaForCategory = { presenter.getMangaCountForCategory(it) }, 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.FastScrollLazyVerticalGrid import eu.kanade.presentation.components.FastScrollLazyVerticalGrid
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -26,9 +27,9 @@ fun LazyLibraryGrid(
FastScrollLazyVerticalGrid( FastScrollLazyVerticalGrid(
columns = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns), columns = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns),
modifier = modifier, modifier = modifier,
contentPadding = contentPadding + PaddingValues(12.dp), contentPadding = contentPadding + PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer),
content = content, content = content,
) )
} }

View File

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

View File

@ -1,35 +1,20 @@
package eu.kanade.presentation.library.components package eu.kanade.presentation.library.components
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import eu.kanade.domain.library.model.LibraryManga import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.MangaCompactGridItem
import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable @Composable
fun LibraryCompactGrid( fun LibraryCompactGrid(
items: List<LibraryItem>, items: List<LibraryItem>,
showTitle: Boolean,
showDownloadBadges: Boolean, showDownloadBadges: Boolean,
showUnreadBadges: Boolean, showUnreadBadges: Boolean,
showLocalBadges: Boolean, showLocalBadges: Boolean,
@ -53,92 +38,37 @@ fun LibraryCompactGrid(
items = items, items = items,
contentType = { "library_compact_grid_item" }, contentType = { "library_compact_grid_item" },
) { libraryItem -> ) { libraryItem ->
LibraryCompactGridItem( val manga = libraryItem.libraryManga.manga
item = libraryItem, MangaCompactGridItem(
showDownloadBadge = showDownloadBadges,
showUnreadBadge = showUnreadBadges,
showLocalBadge = showLocalBadges,
showLanguageBadge = showLanguageBadges,
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
onClick = onClick, title = manga.title.takeIf { showTitle },
onLongClick = onLongClick, coverData = MangaCover(
mangaId = manga.id,
sourceId = manga.source,
isMangaFavorite = manga.favorite,
url = manga.thumbnailUrl,
lastModified = manga.coverLastModified,
),
coverBadgeStart = {
DownloadsBadge(
enabled = showDownloadBadges,
item = libraryItem,
)
UnreadBadge(
enabled = showUnreadBadges,
item = libraryItem,
) )
}
}
}
@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 = { coverBadgeEnd = {
onLongClick(libraryManga) LanguageBadge(
showLanguage = showLanguageBadges,
showLocal = showLocalBadges,
item = libraryItem,
)
}, },
), onLongClick = { onLongClick(libraryItem.libraryManga) },
mangaCover = eu.kanade.domain.manga.model.MangaCover( onClick = { onClick(libraryItem.libraryManga) },
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.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import com.google.accompanist.pager.rememberPagerState
import eu.kanade.core.prefs.PreferenceMutableState import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.domain.library.model.LibraryDisplayMode import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.domain.library.model.LibraryManga import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.presentation.components.SwipeRefresh import eu.kanade.presentation.components.SwipeRefresh
import eu.kanade.presentation.components.rememberPagerState
import eu.kanade.presentation.library.LibraryState import eu.kanade.presentation.library.LibraryState
import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.LibraryItem
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -68,12 +68,13 @@ fun LibraryContent(
if (isLibraryEmpty.not() && showPageTabs && categories.size > 1) { if (isLibraryEmpty.not() && showPageTabs && categories.size > 1) {
LibraryTabs( LibraryTabs(
state = pagerState,
categories = categories, categories = categories,
currentPageIndex = pagerState.currentPage,
showMangaCount = showMangaCount, showMangaCount = showMangaCount,
getNumberOfMangaForCategory = getNumberOfMangaForCategory, getNumberOfMangaForCategory = getNumberOfMangaForCategory,
isDownloadOnly = isDownloadOnly, isDownloadOnly = isDownloadOnly,
isIncognitoMode = isIncognitoMode, 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 package eu.kanade.presentation.library.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import eu.kanade.domain.library.model.LibraryManga import eu.kanade.domain.library.model.LibraryManga
import eu.kanade.domain.manga.model.MangaCover import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.BadgeGroup
import eu.kanade.presentation.components.FastScrollLazyColumn import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.MangaCover.Square import eu.kanade.presentation.components.MangaListItem
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.selectedBackground
import eu.kanade.presentation.util.verticalPadding
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.LibraryItem
@ -47,11 +36,14 @@ fun LibraryList(
) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = contentPadding, contentPadding = contentPadding + PaddingValues(vertical = 8.dp),
) { ) {
item { item {
if (searchQuery.isNullOrEmpty().not()) { if (searchQuery.isNullOrEmpty().not()) {
TextButton(onClick = onGlobalSearchClicked) { TextButton(
modifier = Modifier.fillMaxWidth(),
onClick = onGlobalSearchClicked,
) {
Text( Text(
text = stringResource(R.string.action_global_search_query, searchQuery!!), text = stringResource(R.string.action_global_search_query, searchQuery!!),
modifier = Modifier.zIndex(99f), modifier = Modifier.zIndex(99f),
@ -64,116 +56,25 @@ fun LibraryList(
items = items, items = items,
contentType = { "library_list_item" }, contentType = { "library_list_item" },
) { libraryItem -> ) { libraryItem ->
LibraryListItem( val manga = libraryItem.libraryManga.manga
item = libraryItem, MangaListItem(
showDownloadBadge = showDownloadBadges,
showUnreadBadge = showUnreadBadges,
showLocalBadge = showLocalBadges,
showLanguageBadge = showLanguageBadges,
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
onClick = onClick,
onLongClick = onLongClick,
)
}
}
}
@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, title = manga.title,
cover = MangaCover( coverData = MangaCover(
manga.id, mangaId = manga.id,
manga.source, sourceId = manga.source,
manga.favorite, isMangaFavorite = manga.favorite,
manga.thumbnailUrl, url = manga.thumbnailUrl,
manga.coverLastModified, lastModified = manga.coverLastModified,
), ),
onClick = { onClick(libraryManga) }, badge = {
onLongClick = { onLongClick(libraryManga) }, DownloadsBadge(enabled = showDownloadBadges, item = libraryItem)
) { UnreadBadge(enabled = showUnreadBadges, item = libraryItem)
DownloadsBadge(enabled = showDownloadBadge, item = item) LanguageBadge(showLanguage = showLanguageBadges, showLocal = showLocalBadges, item = libraryItem)
UnreadBadge(enabled = showUnreadBadge, item = item) },
LanguageBadge(showLanguage = showLanguageBadge, showLocal = showLocalBadge, item = item) onLongClick = { onLongClick(libraryItem.libraryManga) },
onClick = { onClick(libraryItem.libraryManga) },
)
}
} }
} }
@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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration 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.core.prefs.PreferenceMutableState
import eu.kanade.domain.library.model.LibraryDisplayMode import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.domain.library.model.LibraryManga 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 import eu.kanade.tachiyomi.ui.library.LibraryItem
@Composable @Composable
@ -72,9 +72,10 @@ fun LibraryPager(
onGlobalSearchClicked = onGlobalSearchClicked, onGlobalSearchClicked = onGlobalSearchClicked,
) )
} }
LibraryDisplayMode.CompactGrid -> { LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
LibraryCompactGrid( LibraryCompactGrid(
items = library, items = library,
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
showDownloadBadges = showDownloadBadges, showDownloadBadges = showDownloadBadges,
showUnreadBadges = showUnreadBadges, showUnreadBadges = showUnreadBadges,
showLocalBadges = showLocalBadges, showLocalBadges = showLocalBadges,
@ -104,22 +105,6 @@ fun LibraryPager(
onGlobalSearchClicked = onGlobalSearchClicked, onGlobalSearchClicked = onGlobalSearchClicked,
) )
} }
LibraryDisplayMode.CoverOnlyGrid -> {
LibraryCoverOnlyGrid(
items = library,
showDownloadBadges = showDownloadBadges,
showUnreadBadges = showUnreadBadges,
showLocalBadges = showLocalBadges,
showLanguageBadges = showLanguageBadges,
columns = columns,
contentPadding = contentPadding,
selection = selectedManga,
onClick = onClickManga,
onLongClick = onLongClickManga,
searchQuery = searchQuery,
onGlobalSearchClicked = onGlobalSearchClicked,
)
}
} }
} }
} }

View File

@ -1,56 +1,54 @@
package eu.kanade.presentation.library.components package eu.kanade.presentation.library.components
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.PagerState
import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.category.visualName import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.components.AppStateBanners import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.Divider import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.TabIndicator import eu.kanade.presentation.components.TabIndicator
import eu.kanade.presentation.components.TabText import eu.kanade.presentation.components.TabText
import kotlinx.coroutines.launch
@Composable @Composable
fun LibraryTabs( fun LibraryTabs(
state: PagerState,
categories: List<Category>, categories: List<Category>,
currentPageIndex: Int,
showMangaCount: Boolean, showMangaCount: Boolean,
isDownloadOnly: Boolean, isDownloadOnly: Boolean,
isIncognitoMode: Boolean, isIncognitoMode: Boolean,
getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>, getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>,
onTabItemClick: (Int) -> Unit,
) { ) {
val scope = rememberCoroutineScope()
Column { Column {
ScrollableTabRow( ScrollableTabRow(
selectedTabIndex = state.currentPage, selectedTabIndex = currentPageIndex,
edgePadding = 0.dp, edgePadding = 0.dp,
indicator = { TabIndicator(it[state.currentPage]) }, indicator = { TabIndicator(it[currentPageIndex]) },
// TODO: use default when width is fixed upstream // TODO: use default when width is fixed upstream
// https://issuetracker.google.com/issues/242879624 // https://issuetracker.google.com/issues/242879624
divider = {}, divider = {},
) { ) {
categories.forEachIndexed { index, category -> categories.forEachIndexed { index, category ->
val count by if (showMangaCount) { Tab(
selected = currentPageIndex == index,
onClick = { onTabItemClick(index) },
text = {
TabText(
text = category.visualName,
badgeCount = if (showMangaCount) {
getNumberOfMangaForCategory(category.id) getNumberOfMangaForCategory(category.id)
} else { } else {
remember { mutableStateOf<Int?>(null) } null
} }?.value,
Tab( )
selected = state.currentPage == index,
onClick = { scope.launch { state.animateScrollToPage(index) } },
text = {
TabText(category.visualName, count, state.currentPage == index)
}, },
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.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row 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.Icons
import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack import androidx.compose.material.icons.outlined.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.material.icons.outlined.SelectAll
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
@ -19,13 +16,11 @@ import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.OverflowMenu
import eu.kanade.presentation.components.Pill import eu.kanade.presentation.components.Pill
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.library.LibraryState import eu.kanade.presentation.library.LibraryState
@ -43,6 +38,7 @@ fun LibraryToolbar(
onClickInvertSelection: () -> Unit, onClickInvertSelection: () -> Unit,
onClickFilter: () -> Unit, onClickFilter: () -> Unit,
onClickRefresh: () -> Unit, onClickRefresh: () -> Unit,
onClickOpenRandomManga: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior?, scrollBehavior: TopAppBarScrollBehavior?,
) = when { ) = when {
state.selectionMode -> LibrarySelectionToolbar( state.selectionMode -> LibrarySelectionToolbar(
@ -53,38 +49,16 @@ fun LibraryToolbar(
onClickSelectAll = onClickSelectAll, onClickSelectAll = onClickSelectAll,
onClickInvertSelection = onClickInvertSelection, 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( else -> LibraryRegularToolbar(
title = title, title = title,
hasFilters = state.hasActiveFilters, hasFilters = state.hasActiveFilters,
incognitoMode = incognitoMode, incognitoMode = incognitoMode,
downloadedOnlyMode = downloadedOnlyMode, downloadedOnlyMode = downloadedOnlyMode,
onClickSearch = { state.searchQuery = "" }, searchQuery = state.searchQuery,
onChangeSearchQuery = { state.searchQuery = it },
onClickFilter = onClickFilter, onClickFilter = onClickFilter,
onClickRefresh = onClickRefresh, onClickRefresh = onClickRefresh,
onClickOpenRandomManga = onClickOpenRandomManga,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
} }
@ -95,14 +69,15 @@ fun LibraryRegularToolbar(
hasFilters: Boolean, hasFilters: Boolean,
incognitoMode: Boolean, incognitoMode: Boolean,
downloadedOnlyMode: Boolean, downloadedOnlyMode: Boolean,
onClickSearch: () -> Unit, searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit,
onClickFilter: () -> Unit, onClickFilter: () -> Unit,
onClickRefresh: () -> Unit, onClickRefresh: () -> Unit,
onClickOpenRandomManga: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior?, scrollBehavior: TopAppBarScrollBehavior?,
) { ) {
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current SearchToolbar(
AppBar(
titleContent = { titleContent = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
@ -120,15 +95,29 @@ fun LibraryRegularToolbar(
} }
} }
}, },
searchQuery = searchQuery,
onChangeSearchQuery = onChangeSearchQuery,
actions = { actions = {
IconButton(onClick = onClickSearch) { val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
Icon(Icons.Outlined.Search, contentDescription = stringResource(R.string.action_search))
}
IconButton(onClick = onClickFilter) { IconButton(onClick = onClickFilter) {
Icon(Icons.Outlined.FilterList, contentDescription = stringResource(R.string.action_filter), tint = filterTint) 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, incognitoMode = incognitoMode,
@ -150,10 +139,10 @@ fun LibrarySelectionToolbar(
titleContent = { Text(text = "${state.selection.size}") }, titleContent = { Text(text = "${state.selection.size}") },
actions = { actions = {
IconButton(onClick = onClickSelectAll) { IconButton(onClick = onClickSelectAll) {
Icon(Icons.Outlined.SelectAll, contentDescription = "search") Icon(Icons.Outlined.SelectAll, contentDescription = stringResource(R.string.action_select_all))
} }
IconButton(onClick = onClickInvertSelection) { IconButton(onClick = onClickInvertSelection) {
Icon(Icons.Outlined.FlipToBack, contentDescription = "invert") Icon(Icons.Outlined.FlipToBack, contentDescription = stringResource(R.string.action_select_inverse))
} }
}, },
isActionMode = true, isActionMode = true,

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -23,7 +22,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow 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.ChapterDownloadAction
import eu.kanade.presentation.components.ChapterDownloadIndicator import eu.kanade.presentation.components.ChapterDownloadIndicator
import eu.kanade.presentation.util.ReadItemAlpha 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.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
@ -45,6 +45,7 @@ fun MangaChapterListItem(
read: Boolean, read: Boolean,
bookmark: Boolean, bookmark: Boolean,
selected: Boolean, selected: Boolean,
downloadIndicatorEnabled: Boolean,
downloadStateProvider: () -> Download.State, downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int, downloadProgressProvider: () -> Int,
onLongClick: () -> Unit, onLongClick: () -> Unit,
@ -53,7 +54,7 @@ fun MangaChapterListItem(
) { ) {
Row( Row(
modifier = modifier modifier = modifier
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent) .selectedBackground(selected)
.combinedClickable( .combinedClickable(
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
@ -67,12 +68,13 @@ fun MangaChapterListItem(
MaterialTheme.colorScheme.onSurface MaterialTheme.colorScheme.onSurface
} }
val textAlpha = remember(read) { if (read) ReadItemAlpha else 1f } val textAlpha = remember(read) { if (read) ReadItemAlpha else 1f }
val textSubtitleAlpha = remember(read) { if (read) ReadItemAlpha else SecondaryItemAlpha }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
var textHeight by remember { mutableStateOf(0) } var textHeight by remember { mutableStateOf(0) }
if (bookmark) { if (bookmark) {
Icon( Icon(
imageVector = Icons.Default.Bookmark, imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked), contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
@ -91,7 +93,7 @@ fun MangaChapterListItem(
) )
} }
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
Row(modifier = Modifier.alpha(textAlpha)) { Row(modifier = Modifier.alpha(textSubtitleAlpha)) {
ProvideTextStyle( ProvideTextStyle(
value = MaterialTheme.typography.bodyMedium value = MaterialTheme.typography.bodyMedium
.copy(color = textColor, fontSize = 12.sp), .copy(color = textColor, fontSize = 12.sp),
@ -127,6 +129,7 @@ fun MangaChapterListItem(
// Download view // Download view
if (onDownloadClick != null) { if (onDownloadClick != null) {
ChapterDownloadIndicator( ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp), modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider, downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider, 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.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Save import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Share import androidx.compose.material.icons.outlined.Share
@ -24,11 +24,14 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@ -63,7 +66,7 @@ fun MangaCoverDialog(
) { ) {
IconButton(onClick = onDismissRequest) { IconButton(onClick = onDismissRequest) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.action_close), contentDescription = stringResource(R.string.action_close),
) )
} }
@ -82,9 +85,15 @@ fun MangaCoverDialog(
} }
if (onEditClick != null) { if (onEditClick != null) {
Box { Box {
val (expanded, onExpand) = remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
IconButton( IconButton(
onClick = { if (isCustomCover) onExpand(true) else onEditClick(EditCoverAction.EDIT) }, onClick = {
if (isCustomCover) {
expanded = true
} else {
onEditClick(EditCoverAction.EDIT)
}
},
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.Edit, imageVector = Icons.Outlined.Edit,
@ -93,20 +102,21 @@ fun MangaCoverDialog(
} }
DropdownMenu( DropdownMenu(
expanded = expanded, expanded = expanded,
onDismissRequest = { onExpand(false) }, onDismissRequest = { expanded = false },
offset = DpOffset(8.dp, 0.dp),
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_edit)) }, text = { Text(text = stringResource(R.string.action_edit)) },
onClick = { onClick = {
onEditClick(EditCoverAction.EDIT) onEditClick(EditCoverAction.EDIT)
onExpand(false) expanded = false
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_delete)) }, text = { Text(text = stringResource(R.string.action_delete)) },
onClick = { onClick = {
onEditClick(EditCoverAction.DELETE) onEditClick(EditCoverAction.DELETE)
onExpand(false) expanded = false
}, },
) )
} }

View File

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

View File

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

View File

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

View File

@ -1,5 +1,9 @@
package eu.kanade.presentation.more 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.foundation.layout.statusBarsPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CloudOff 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.AppStateBanners
import eu.kanade.presentation.components.Divider import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.ScrollbarLazyColumn 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.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -32,6 +37,7 @@ import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
@Composable @Composable
fun MoreScreen( fun MoreScreen(
presenter: MorePresenter, presenter: MorePresenter,
isFDroid: Boolean,
onClickDownloadQueue: () -> Unit, onClickDownloadQueue: () -> Unit,
onClickCategories: () -> Unit, onClickCategories: () -> Unit,
onClickBackupAndRestore: () -> Unit, onClickBackupAndRestore: () -> Unit,
@ -43,8 +49,21 @@ fun MoreScreen(
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.statusBarsPadding(), 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 { item {
LogoHeader() LogoHeader()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,9 +44,9 @@ internal fun BasePreferenceWidget(
widget: @Composable (() -> Unit)? = null, widget: @Composable (() -> Unit)? = null,
) { ) {
val highlighted = LocalPreferenceHighlighted.current val highlighted = LocalPreferenceHighlighted.current
Box(modifier = Modifier.highlightBackground(highlighted)) {
Row( Row(
modifier = modifier modifier = modifier
.highlightBackground(highlighted)
.sizeIn(minHeight = 56.dp) .sizeIn(minHeight = 56.dp)
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() }) .clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
.fillMaxWidth(), .fillMaxWidth(),
@ -82,7 +82,6 @@ internal fun BasePreferenceWidget(
) )
} }
} }
}
} }
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = composed { internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = composed {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,15 +2,12 @@ package eu.kanade.presentation.updates
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.PaddingValues 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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FlipToBack import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
@ -23,17 +20,17 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource 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.AppBar
import eu.kanade.presentation.components.ChapterDownloadAction import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.EmptyScreen 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.LoadingScreen
import eu.kanade.presentation.components.MangaBottomActionMenu import eu.kanade.presentation.components.MangaBottomActionMenu
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SwipeRefresh import eu.kanade.presentation.components.SwipeRefresh
import eu.kanade.presentation.components.VerticalFastScroller
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
@ -124,7 +121,6 @@ private fun UpdateScreenContent(
onClickCover: (UpdatesItem) -> Unit, onClickCover: (UpdatesItem) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val updatesListState = rememberLazyListState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var isRefreshing by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) }
@ -143,14 +139,7 @@ private fun UpdateScreenContent(
enabled = presenter.selectionMode.not(), enabled = presenter.selectionMode.not(),
indicatorPadding = contentPadding, indicatorPadding = contentPadding,
) { ) {
VerticalFastScroller( FastScrollLazyColumn(
listState = updatesListState,
topContentPadding = contentPadding.calculateTopPadding(),
endContentPadding = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
state = updatesListState,
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
if (presenter.lastUpdated > 0L) { if (presenter.lastUpdated > 0L) {
@ -172,7 +161,6 @@ private fun UpdateScreenContent(
) )
} }
} }
}
val onDismissDialog = { presenter.dialog = null } val onDismissDialog = { presenter.dialog = null }
when (val dialog = presenter.dialog) { when (val dialog = presenter.dialog) {
@ -215,7 +203,7 @@ private fun UpdatesAppBar(
actions = { actions = {
IconButton(onClick = onUpdateLibrary) { IconButton(onClick = onUpdateLibrary) {
Icon( Icon(
imageVector = Icons.Default.Refresh, imageVector = Icons.Outlined.Refresh,
contentDescription = stringResource(R.string.action_update_library), contentDescription = stringResource(R.string.action_update_library),
) )
} }
@ -225,13 +213,13 @@ private fun UpdatesAppBar(
actionModeActions = { actionModeActions = {
IconButton(onClick = onSelectAll) { IconButton(onClick = onSelectAll) {
Icon( Icon(
imageVector = Icons.Default.SelectAll, imageVector = Icons.Outlined.SelectAll,
contentDescription = stringResource(R.string.action_select_all), contentDescription = stringResource(R.string.action_select_all),
) )
} }
IconButton(onClick = onInvertSelection) { IconButton(onClick = onInvertSelection) {
Icon( Icon(
imageVector = Icons.Default.FlipToBack, imageVector = Icons.Outlined.FlipToBack,
contentDescription = stringResource(R.string.action_select_inverse), contentDescription = stringResource(R.string.action_select_inverse),
) )
} }
@ -255,24 +243,24 @@ private fun UpdatesBottomBar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onBookmarkClicked = { onBookmarkClicked = {
onMultiBookmarkClicked.invoke(selected, true) onMultiBookmarkClicked.invoke(selected, true)
}.takeIf { selected.any { !it.update.bookmark } }, }.takeIf { selected.fastAny { !it.update.bookmark } },
onRemoveBookmarkClicked = { onRemoveBookmarkClicked = {
onMultiBookmarkClicked.invoke(selected, false) onMultiBookmarkClicked.invoke(selected, false)
}.takeIf { selected.all { it.update.bookmark } }, }.takeIf { selected.fastAll { it.update.bookmark } },
onMarkAsReadClicked = { onMarkAsReadClicked = {
onMultiMarkAsReadClicked(selected, true) onMultiMarkAsReadClicked(selected, true)
}.takeIf { selected.any { !it.update.read } }, }.takeIf { selected.fastAny { !it.update.read } },
onMarkAsUnreadClicked = { onMarkAsUnreadClicked = {
onMultiMarkAsReadClicked(selected, false) onMultiMarkAsReadClicked(selected, false)
}.takeIf { selected.any { it.update.read } }, }.takeIf { selected.fastAny { it.update.read } },
onDownloadClicked = { onDownloadClicked = {
onDownloadChapter(selected, ChapterDownloadAction.START) onDownloadChapter(selected, ChapterDownloadAction.START)
}.takeIf { }.takeIf {
selected.any { it.downloadStateProvider() != Download.State.DOWNLOADED } selected.fastAny { it.downloadStateProvider() != Download.State.DOWNLOADED }
}, },
onDeleteClicked = { onDeleteClicked = {
onMultiDeleteClicked(selected) 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 package eu.kanade.presentation.updates
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -28,7 +27,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback 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.components.RelativeDateHeader
import eu.kanade.presentation.util.ReadItemAlpha import eu.kanade.presentation.util.ReadItemAlpha
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.selectedBackground
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesItem import eu.kanade.tachiyomi.ui.recent.updates.UpdatesItem
@ -135,6 +134,7 @@ fun LazyListScope.updatesUiItems(
onDownloadChapter = { onDownloadChapter = {
if (selectionMode.not()) onDownloadChapter(listOf(updatesItem), it) if (selectionMode.not()) onDownloadChapter(listOf(updatesItem), it)
}, },
downloadIndicatorEnabled = selectionMode.not(),
downloadStateProvider = updatesItem.downloadStateProvider, downloadStateProvider = updatesItem.downloadStateProvider,
downloadProgressProvider = updatesItem.downloadProgressProvider, downloadProgressProvider = updatesItem.downloadProgressProvider,
) )
@ -153,13 +153,14 @@ fun UpdatesUiItem(
onClickCover: () -> Unit, onClickCover: () -> Unit,
onDownloadChapter: (ChapterDownloadAction) -> Unit, onDownloadChapter: (ChapterDownloadAction) -> Unit,
// Download Indicator // Download Indicator
downloadIndicatorEnabled: Boolean,
downloadStateProvider: () -> Download.State, downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int, downloadProgressProvider: () -> Int,
) { ) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
Row( Row(
modifier = modifier modifier = modifier
.background(if (selected) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent) .selectedBackground(selected)
.combinedClickable( .combinedClickable(
onClick = onClick, onClick = onClick,
onLongClick = { onLongClick = {
@ -205,7 +206,7 @@ fun UpdatesUiItem(
var textHeight by remember { mutableStateOf(0) } var textHeight by remember { mutableStateOf(0) }
if (bookmark) { if (bookmark) {
Icon( Icon(
imageVector = Icons.Default.Bookmark, imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked), contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
@ -225,6 +226,7 @@ fun UpdatesUiItem(
} }
} }
ChapterDownloadIndicator( ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp), modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider, downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider, downloadProgressProvider = downloadProgressProvider,

View File

@ -12,3 +12,4 @@ val verticalPadding = vertical
val topPaddingValues = PaddingValues(top = vertical) val topPaddingValues = PaddingValues(top = vertical)
const val ReadItemAlpha = .38f 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( fun Modifier.clickableNoIndication(
onLongClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null,

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