Compare commits

...

209 Commits

Author SHA1 Message Date
817418f7c9 Release v0.14.3 2023-01-07 12:09:27 -05:00
4eb2cd85b2 Update baseline profile 2023-01-07 12:03:17 -05:00
086eac5975 Translations update from Hosted Weblate (#8764)
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: Bujdosf <bujdos.f01@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: DarKCroX <DarKCroX@users.noreply.hosted.weblate.org>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: FTDaily <farrell05june2005@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: InfinityDouki56 <ced.paltep10@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jace Orwell <jaceorwell@gmail.com>
Co-authored-by: Jendrej <ejjendrej@gmail.com>
Co-authored-by: K. Sz. Bence <tudi20@protonmail.com>
Co-authored-by: Kaenova Mahendra Auditama <kaenova@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: The Initiator <eithansten@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: ZiomaleQ <r.partyka30@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: ayaao <myrgdream@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: lisienskenderi <lisienskenderi@hotmail.com>
Co-authored-by: stevenlele <stevenlele@outlook.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/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
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/he/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hu/
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/kk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ko/
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/pl/
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/sq/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
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: Abay Emes <abayemes@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Bujdosf <bujdos.f01@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: DarKCroX <DarKCroX@users.noreply.hosted.weblate.org>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: FTDaily <farrell05june2005@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jace Orwell <jaceorwell@gmail.com>
Co-authored-by: Jendrej <ejjendrej@gmail.com>
Co-authored-by: K. Sz. Bence <tudi20@protonmail.com>
Co-authored-by: Kaenova Mahendra Auditama <kaenova@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: The Initiator <eithansten@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: ZiomaleQ <r.partyka30@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: ayaao <myrgdream@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: lisienskenderi <lisienskenderi@hotmail.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
2023-01-07 11:54:23 -05:00
addd6bffbd Bump default user agent string and minimum WebView version 2023-01-07 11:51:36 -05:00
1e65313fa7 Open entry when long pressing during migration in source search
This matches the behavior from 0.13.6

Fixes #8176
2023-01-07 11:51:00 -05:00
c4c6e41c46 Fix downloaded badges appearing when filtering by downloaded
Fixes #8850
2023-01-07 10:32:14 -05:00
920ca405a2 Use MainScope for coroutines in ui package classes (#8845) 2023-01-07 10:07:09 -05:00
6d3a3b3f39 Adjust bookmarked chapter styling
No longer tints the title and subtitle text depending on bookmarked state
in favor of only showing a tinted bookmark icon regardless of read state.

Closes #8839
2023-01-07 10:02:41 -05:00
50d46fe7f6 Prioritize "all" ("Multi") lang in extensions lists
Fixes #8811
Fixes #8812
2023-01-05 22:34:24 -05:00
91e282d7e5 Show warning about installing extensions on MIUI
Related to #8834
2023-01-05 22:12:14 -05:00
a0f10f868e Handle file names with multiple ".cbz" occurrences properly
Fixes #8838
2023-01-05 21:59:18 -05:00
6a423f0650 Update toolbar query on genre search (#8837) 2023-01-05 17:02:27 -05:00
5cc84403e1 Debounce reindexing banner
Helps avoid showing it for short-lived jobs
2023-01-02 21:58:48 -05:00
ab61a65b4a Add worker info screen (#8774)
Mainly for debug purpose, might help with support.
2023-01-02 21:58:11 -05:00
01ec26842d Unify layout for new update and crash screens 2022-12-30 23:14:29 -05:00
bbf5817805 Allow 2 lines for tracker status text
Fixes #8805
2022-12-30 22:31:35 -05:00
50981cb102 Handle 1000+ pages properly in the downloader (#8818) 2022-12-30 22:20:14 -05:00
611ec8103c Handle 1000+ pages properly in the downloader (#8818) 2022-12-30 22:20:06 -05:00
12c672667c filter mangaupdates search (#8813) 2022-12-30 22:11:40 -05:00
db3c98fe72 Update OkHttp 2022-12-25 00:24:53 -05:00
f401574f5a Increase max library column size back to 10
Fixes #8798
2022-12-24 10:09:38 -05:00
3251fb36c8 Properly fix #8720 (#8797)
* Partially revert "Move library page EmptyScreens into list/grids"

This partially reverts commit 376bbeb724.

* Properly fix issue 8720
2022-12-24 10:02:38 -05:00
94a410f50f TrackDateRemoverScreen: Fix pop behavior after confirming removal (#8792) 2022-12-23 09:29:01 -05:00
a14c01c1de Update baseline profile 2022-12-21 22:48:39 -05:00
ca3b948628 Update plugin kotlinter to v3.13.0 (#8783)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-21 22:38:24 -05:00
a8230ad574 Fix browse search query display and keyboard focus (#8781) 2022-12-21 22:14:04 -05:00
8e1b5b4803 Pager: Bring back previous snapping behavior (#8776)
New default LazyList snap behavior is optimized for non-pager use.
2022-12-20 09:16:43 -05:00
8552838bda Update WorkManager (#8772) 2022-12-18 12:14:06 -05:00
46417fe427 Pass listing query to BrowseSourceScreen (#8763)
* Pass listing query to BrowseSourceScreen

* Don't use referential equality
2022-12-17 17:28:25 -05:00
dac04f2929 Translations update from Hosted Weblate (#8663)
Weblate translations

Co-authored-by: Abay Emes <abayemes@gmail.com>
Co-authored-by: Abou <aboozar.gh.r@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alex Maryson Jr <akamar87@gmail.com>
Co-authored-by: Ali Aljishi <ahj696@hotmail.com>
Co-authored-by: Bujdosf <bujdos.f01@gmail.com>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Diego <gonzalediego1@gmail.com>
Co-authored-by: Edi <mizumymommy@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: GuN4iK <maksimpradko59@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: Othmane El Alami <othmane.elalami@nupsol.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rostyslav Haitkulov <info@ubilling.net.ua>
Co-authored-by: Sana Thanks <thankssana4@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: aşina orkan göksel aşina <examplehuman@outlook.com>
Co-authored-by: blindmodz <sebareyes.1994@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: lb-fes <2241373229@qq.com>
Co-authored-by: michalani <michal.anisimow@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
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/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/eo/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fa/
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/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hu/
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/kk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ko/
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/pl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ro/
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/sr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/vi/
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: Abay Emes <abayemes@gmail.com>
Co-authored-by: Abou <aboozar.gh.r@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alex Maryson Jr <akamar87@gmail.com>
Co-authored-by: Ali Aljishi <ahj696@hotmail.com>
Co-authored-by: Bujdosf <bujdos.f01@gmail.com>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Diego <gonzalediego1@gmail.com>
Co-authored-by: Edi <mizumymommy@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: GuN4iK <maksimpradko59@gmail.com>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: Othmane El Alami <othmane.elalami@nupsol.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rostyslav Haitkulov <info@ubilling.net.ua>
Co-authored-by: Sana Thanks <thankssana4@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: aşina orkan göksel aşina <examplehuman@outlook.com>
Co-authored-by: blindmodz <sebareyes.1994@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: lb-fes <2241373229@qq.com>
Co-authored-by: michalani <michal.anisimow@gmail.com>
2022-12-17 14:53:39 -05:00
63da463e02 Clean up usages of listing UI models (#8762) 2022-12-17 14:51:03 -05:00
817e144ff6 BrowseSourceScreen: fix navigate up and filter sheet (#8761) 2022-12-17 13:21:12 -05:00
9d2d78ae5b AdaptiveSheet: Don't blindly consume back event (#8760) 2022-12-17 12:56:19 -05:00
c44db54d9f Fix snackbar blocking refreshing state in MangaScreen (#8759) 2022-12-17 12:06:49 -05:00
376bbeb724 Move library page EmptyScreens into list/grids
It does look awkward due to the lack of filled height within those list/grids though.

Fixes #8720
Fixes #8721
2022-12-17 12:06:02 -05:00
0e2bdb7863 Minor cleanup 2022-12-17 12:02:01 -05:00
235bc77457 Fix indexing notif not showing (#8758) 2022-12-17 10:32:49 -05:00
593172f891 Track Page progress with StateFlow (#8749)
* Update ReaderProgressIndicator documentation

ReaderProgressIndicator is not always determinate (cc554530, #5605).

* Track Page progress with StateFlow
2022-12-16 22:18:50 -05:00
e20c66b156 App state banner tweaks (#8746)
* Move download indexing notification to this banner group
* Animate state changes
2022-12-16 22:18:17 -05:00
5f4825465e Use actual indexes instead of existing order number when reordering categories
Fixes #8738
2022-12-15 23:06:05 -05:00
bc6a12a4f7 Sort global search source results properly
Fixes #8741
2022-12-14 23:20:51 -05:00
90db3acefd Don't start at last read page if chapter is completely marked as read
Fixes #8737
2022-12-14 23:04:30 -05:00
2f2f59279d Fix crash if tapping title when opening reader directly 2022-12-14 22:54:51 -05:00
4992f87cb1 Better handle status bar light/dark icons based on banner background color 2022-12-14 22:54:34 -05:00
7608cb0da3 Check ext lib version when checking for updates (#8740) 2022-12-14 13:49:10 -05:00
9dd9e741f3 Convert download cache/queue flows into SharedFlows
Fixes #8727
2022-12-12 22:37:37 -05:00
171db639ff Fix SetMangaViewerFlags (#8719)
Stop clearing old viewer flags when setting a flag
2022-12-11 16:12:41 -05:00
3ede42252c Remove unused resources 2022-12-11 10:22:14 -05:00
a94ca175e2 Update richtext to v0.16.0 (#8716)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-10 22:45:37 -05:00
3749cee28f Add Assistant content URLs
This is surfaced in recents on Pixel devices for example.
Docs: https://developer.android.com/guide/app-actions/assistant-sharing

Co-authored-by: Jays2Kings <Jays2Kings@users.noreply.github.com>
2022-12-10 12:08:39 -05:00
ca500da4d8 Adjust insets handling in tablet UI (#8711)
* Adds startBar slot in Scaffold to handle nav rail
* Consumes unneeded insets in settings
2022-12-10 10:02:13 -05:00
820ed6a468 Move system bar color set to the main composable (#8710)
This one doesn't check navbar location before adding a scrim, doesn't really
matter since now no body component is being drawn below the system bar.
2022-12-10 10:01:16 -05:00
7cbe18d325 Pull out settings sheet items as reusable composables 2022-12-09 22:23:26 -05:00
8937e22ce4 Add back option to hide Updates count (#8709)
Adds back the option to hide the updates count on the Updates tab
2022-12-09 17:25:06 -05:00
82a3a98a5a Adjust screen transitions (#8707)
* Fade transition between main navigation tabs
* Shared axis X between screen stacks

Activity transition is using a "close enough" shared axis X xml animation
2022-12-09 17:23:00 -05:00
d97eab0328 Move app state banner to the very top (#8706)
This moves the banners to the root composable and so eliminates the need to
track the app states in every screen.
2022-12-09 11:20:13 -05:00
a61e2799db Abstract ChapterSettingsDialog for reuse elsewhere 2022-12-08 23:15:50 -05:00
1009e15aa6 Reuse basic theme preview annotation 2022-12-08 22:45:17 -05:00
01c6e46a71 Show empty screen when a category is empty (#8690)
* Show empty screen when a category is empty

* Review changes

* Review changes #2

Co-authored-by: arkon <arkon@users.noreply.github.com>
2022-12-08 09:15:10 -05:00
ed5e013874 Use proper category when getting random item
Fixes #8700
2022-12-08 09:01:37 -05:00
f8e4153dbf Disable Jetifier 2022-12-07 23:06:25 -05:00
f7a92cf6ac Replace reader's Presenter with ViewModel (#8698)
includes:
* Use coroutines in more places
* Use domain Manga data class and effectively changing the state system
* Replace deprecated onBackPress method

Co-authored-by: arkon <arkon@users.noreply.github.com>
2022-12-07 23:00:01 -05:00
e748d91d4a Bump dependencies 2022-12-07 22:44:09 -05:00
2c4ddca38e Migrate Accompanist SwipeRefresh to Compose PullRefresh (#8106) 2022-12-07 22:40:57 -05:00
6ca32710be Cleanup Page status (#8696)
* Cleanup Page statusSubject and statusCallback

* Convert Page status from Int to enum
2022-12-07 18:28:38 -05:00
f05e251991 GlobalSearchScreen: Add unique key (#8693)
Avoids crash when an old screen is being replaced by a new one
2022-12-07 08:27:54 -05:00
a3f3f9d562 Avoid some crashes 2022-12-06 22:21:04 -05:00
410fcb73c5 Fix appbar back button in global search screen (#8689) 2022-12-06 22:20:57 -05:00
b6d6de6b9f Avoid crashing when clearing cookies for invalid source URLs
e.g. Komga sources with no URLs set
2022-12-05 22:18:19 -05:00
09cebf20f3 Handle intent after navigator is initialized
Fixes crash if opening from widget or notification when activity isn't already launched.
2022-12-05 17:16:16 -05:00
a8c732d67b Fix opening download notification only going to More tab 2022-12-05 16:09:55 -05:00
843c9c7e57 Fix migrate options dialog not being selected when tapping text 2022-12-05 15:12:16 -05:00
c88b79fa17 Minor cleanup 2022-12-05 14:14:50 -05:00
3f9820ac79 Always show library tabs and counts when searching
Closes #8680
2022-12-05 10:06:41 -05:00
c288e6b8fa Fix ANR when opening from notification/widget (#8683) 2022-12-05 09:00:30 -05:00
8945ef8880 Change source preference theming fix (#8679) 2022-12-05 00:10:11 -05:00
99a717f849 Hide webtoon reader scrollbars
Fixes #8676
2022-12-04 18:09:37 -05:00
4622b18c99 Fix local source detail JSON files not being read if .noxml was created
Fixes #8549
2022-12-04 14:00:23 -05:00
4f5270cb7d Fix unusable categories when content is filtered out
Fixes #8675
Effectively reverts #8633, which introduces weird edge cases
2022-12-04 13:39:53 -05:00
719d427956 Truncate long nav bar/rail items
Fixes #8670
2022-12-04 12:58:59 -05:00
d7a21771a5 Tweak manga cover dialog UI
Closes #8654, although it's just a workaround. The cover itself doesn't appear within the inset areas when zoomed.
2022-12-04 12:55:58 -05:00
be854b3e90 Fix appbar back button in Settings screen (#8674) 2022-12-04 10:27:14 -05:00
47f079891f Track sheet fixes (#8673)
* Fix Track sheet not being disposed properly

* Change insets handling
2022-12-04 10:27:02 -05:00
696dc59ea5 More domain model migrations 2022-12-03 22:54:18 -05:00
5f6666a438 Migrate Download to domain model (#8664) 2022-12-03 22:30:30 -05:00
f284a656d7 [skip ci] Update dessant/lock-threads action to v4 (#8666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-03 22:16:56 -05:00
1c3d566f8d Translations update from Hosted Weblate (#8622)
Weblate translations

Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Cypral <cypral@hotmail.fr>
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: Hatem Ghouthi <hatemghouthi@yahoo.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: Kaenova Mahendra Auditama <kaenova@gmail.com>
Co-authored-by: Kostiantyn Kopelets <kostyakopkop@gmail.com>
Co-authored-by: Luka Paun <croluxgame@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: SameDesu123 <jjunleegood@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: lb-fes <2241373229@qq.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
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/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
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/th/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/vi/
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: Cypral <cypral@hotmail.fr>
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: Hatem Ghouthi <hatemghouthi@yahoo.com>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: Kaenova Mahendra Auditama <kaenova@gmail.com>
Co-authored-by: Kostiantyn Kopelets <kostyakopkop@gmail.com>
Co-authored-by: Luka Paun <croluxgame@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: SameDesu123 <jjunleegood@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: lb-fes <2241373229@qq.com>
2022-12-03 14:48:32 -05:00
373463e995 Change Updates icon badge to show new updates count (#8659)
* Change Updates icon badge to show new updates count

* Fix reference

* review changes

* Lint
2022-12-03 14:44:30 -05:00
7be9b49143 Fix BrowseSourceScreen list/grid unnecessary reloads (#8661) 2022-12-03 14:43:52 -05:00
059a79debb [skip ci] Ignore WheelPickerCompose updates 2022-12-03 10:20:27 -05:00
1a70ebe7ea Fix crash when opening chapter from BrowseSourceScreen (#8657) 2022-12-03 00:26:11 -05:00
beda99bbe0 Replace RxJava in ReaderChapter and reader transitions 2022-12-02 23:36:33 -05:00
bb1e7816e1 Replace some usages of RxJava in reader 2022-12-02 23:11:42 -05:00
b0dc20e00c Remove some dead code 2022-12-02 22:48:08 -05:00
3d66eaea83 Merge Voyager screens (#8656)
* Merge Voyager screens

* cleanups
2022-12-02 22:35:30 -05:00
5313a5d5d2 Remove unnecessary base Nucleus classes
The reader still uses it, but we just move stuff to there.
2022-12-02 13:23:26 -05:00
5b189a909b Use Voyager on Source Preference screen (#8651) 2022-12-02 13:14:18 -05:00
75a687138d Migrate to Accompanist M3 theme adapter 2022-12-01 23:08:04 -05:00
ba91b483a0 Delayed Tracking Update related fix (#8642)
* Delayed Tracking Update related fix

* Lint
2022-12-01 23:01:24 -05:00
3a8b5e1b5e Fix default category name being shown with empty library 2022-12-01 23:00:34 -05:00
94d1b68598 Use Voyager on BrowseSource and SourceSearch screen (#8650)
Some navigation janks will be dealt with when the migration is complete
2022-11-30 23:05:11 -05:00
8eda4df71f Fix refreshing state for extensions tab
Fixes #8644
Also add an extra delay in case it's super fast.
2022-11-29 09:25:22 -05:00
8ad9337863 Fix Stub Source migration screen broken (#8643)
* Fix Stub Source migration screen broken

* Lint
2022-11-29 09:06:52 -05:00
cd13e187cf Use Voyager on Downloads screen (#8640) 2022-11-28 09:23:11 -05:00
bcc21e55bd Complete Settings migration to Voyager (#8639)
Now the Controller wrapper can be yeeted anytime
2022-11-28 09:21:18 -05:00
5fbecfd7b7 Don't remove queued downloads when deleting manga after chapter deletion 2022-11-27 17:12:45 -05:00
3480b45098 Minor cleanup 2022-11-27 17:12:45 -05:00
5076ab3049 Update dependency ch.acra:acra-http to v5.9.7 (#8636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-27 16:25:42 -05:00
44366ac058 Minor global search UI tweaks 2022-11-27 15:16:08 -05:00
4f2a794fba Remove dead code 2022-11-27 15:09:37 -05:00
fe6aa4358f Show toolbarTitle depending of size (#8633) 2022-11-27 14:57:52 -05:00
f99b62a069 Use Compose on Global/Migrate Search screen (#8631)
* Use Compose on Global/Migrate Search screen

- Refactor to use Voyager and Compose
- Use sealed class for state
- Somethings are broken/missing due to screens using different navigation libraries

* Review changes
2022-11-27 14:56:21 -05:00
ac1bed38f9 Show empty library message properly
Fixes #8632
The `library` map still contains the default category even when "empty".
2022-11-27 10:43:38 -05:00
217b03a292 Fix library not loading when not logged in to any tracker (#8629) 2022-11-26 21:37:22 -05:00
28bceffc6f Update aboutlib_version to v10.5.2 (#8626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-26 21:37:03 -05:00
09266a155c Update dependency gradle to v7.6 (#8630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-26 21:36:46 -05:00
3d7591feca Implement simple stats screen (#8068)
* Implement simple stats screen

* Review Changes

* Some other changes

* Remove unused

* Small changes

* Review Changes 2 + Cleanup

* Review Changes 3

* Cleanup leftovers

* Optimize imports
2022-11-26 15:50:26 -05:00
e14909fff4 Use Voyager on Library tab (#8620) 2022-11-26 15:48:57 -05:00
fe579c4865 Translations update from Hosted Weblate (#8567)
Weblate translations

Co-authored-by: Abay Emes <abayemes@gmail.com>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: CherryMonster222 <eljubeily+github@pm.me>
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: 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: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nepx <anandabaskara@outlook.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rostyslav Haitkulov <info@ubilling.net.ua>
Co-authored-by: Te quiero <ilytequiero@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: alex <hdhdhfhfbbffhhfhfjfjf@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ceb/
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/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
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/hr/
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/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/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ro/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
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: Abay Emes <abayemes@gmail.com>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: CherryMonster222 <eljubeily+github@pm.me>
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: 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: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nepx <anandabaskara@outlook.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rostyslav Haitkulov <info@ubilling.net.ua>
Co-authored-by: Te quiero <ilytequiero@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: alex <hdhdhfhfbbffhhfhfjfjf@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
2022-11-26 14:56:11 -05:00
37118088d4 Remove usage of PublishRelay in DownloadQueue 2022-11-26 10:07:51 -05:00
5c9e9bd2c4 Use Voyager between more screens 2022-11-26 09:34:06 -05:00
db35ba53b1 Use Voyager between supported screens in Extension package (#8616)
- Minor state behavior changes
2022-11-26 09:14:11 -05:00
758d223776 Disable generating ComicInfo.xml on download (#8619)
* Disable generating ComicInfo.xml on download

* Remove unused import
2022-11-26 09:13:08 -05:00
21a9bf2463 Add "jitpack" maven repo to pluginMangment (#8617) 2022-11-26 08:11:15 -05:00
a54d9912d0 Fix Kavita interceptor crashing app + minor cleanup 2022-11-25 23:03:42 -05:00
7e74949d38 Explicitly add READ_APP_SPECIFIC_LOCALES permission
Some devices are throwing a SecurityException (calling getApplicationLocales) for some reason.
2022-11-25 23:03:42 -05:00
a8c5780963 Use Voyager on Migrate Manga screen (#8611) 2022-11-24 22:25:36 -05:00
f4ac754d02 Use Voyager on Browse tab (#8605) 2022-11-23 22:28:25 -05:00
0347d3970a Cleanup [Downloader.ensureSuccessfulDownload] (#8602) 2022-11-23 09:23:29 -05:00
acc2312384 Use Voyager on Updates tab (#8603)
* Use Voyager on Updates tab

* Fix back press

* Fix selection
2022-11-23 09:22:20 -05:00
7d34ff214c Change settings screen to object (#8604) 2022-11-23 09:14:55 -05:00
e2179a6669 Avoid concurrency issues when reordering categories
Maybe fixes #8372
2022-11-22 23:12:23 -05:00
5c37347cec Delete empty source folder when deleting all downloads for a manga
It previously only attempted this after deleting a list of chapters, so it wasn't applicable
when deleting from Library or after unfavoriting an entry.

Closes #8594
2022-11-22 09:25:00 -05:00
ef3a6c80a7 Implement copying of Manga URL to Clipboard (#8587)
feat: Implement copying of Manga URL to Clipboard
2022-11-21 23:09:23 -05:00
2a2c6cee5f Allow zooming in WebView
Note that this does not force-enable zooming on pages with set viewports (which typically implies proper mobile scaling).
Closes #8588
2022-11-21 18:39:16 -05:00
7dff3cc6cb Remove unused resources (#8578) 2022-11-20 15:29:08 -05:00
8c1171a722 Don't attempt to check chapter download status for local chapters
Fixes #8541
2022-11-20 15:28:51 -05:00
2c850d0e33 Fix invert tapping dropdown not updating checked state in reader
Fixes #8566
Should ideally just Compose-ify it all some day.
2022-11-20 15:12:51 -05:00
f1b85ff39d Use Voyager on Extension Details screen (#8576) 2022-11-20 14:36:03 -05:00
b7fa25777d Update dependency com.github.requery:sqlite-android to v3.39.2 2022-11-20 14:30:11 -05:00
2d86f69caa Add reindex downloads description
Closes #8546
Also disable sound for the notification and cancel running indexing job if invalidating.
2022-11-20 14:29:56 -05:00
e22896a956 Use current timezone when setting tracker dates
Fixes #8553
2022-11-19 22:40:17 -05:00
be5802e473 Add back track icon onClick and title onLongClick actions
Closes #8565
Closes #8536
2022-11-19 22:37:48 -05:00
434c90d378 Translations update from Hosted Weblate (#8515)
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: Claudio Angileri <cloud019998@gmail.com>
Co-authored-by: Cường Bá <cuongba956@gmail.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: Hosted Weblate <hosted@weblate.org>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Kapiten Levi <kapitenlevi1@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: MXC48 <mxcvan@protonmail.com>
Co-authored-by: Moonlight! <kapitenlevi1@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Ronny Teixeira <ronnyteixeira15@hotmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
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/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/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/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
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/sq/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/vi/
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: Abay Emes <abayemes@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Claudio Angileri <cloud019998@gmail.com>
Co-authored-by: Cường Bá <cuongba956@gmail.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: ID-86 <id86dev@gmail.com>
Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Kapiten Levi <kapitenlevi1@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: MXC48 <mxcvan@protonmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Ronny Teixeira <ronnyteixeira15@hotmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
2022-11-19 10:00:28 -05:00
eb6ba96b57 Limit parallelism for Coil image loading
Reference: https://www.reddit.com/r/androiddev/comments/xbeizp/comment/io4ytdv/

Co-authored-by: ivaniskandar <ivaniskandar@users.noreply.github.com>
2022-11-18 22:57:54 -05:00
5325e590ec Fix url sharing
Maybe fixes #8539
Based on f52785cbbd

Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>
2022-11-18 22:49:54 -05:00
6ad6dae191 Bump dependencies 2022-11-18 22:41:06 -05:00
3f34fa1f58 Tweak library selection (#8513)
* Tweak library selection

Also use the new `fast*` extensions functions in other places of library presenter

* Cleanup
2022-11-18 22:33:38 -05:00
d12ea86b55 Add shecan DoH provider
Closes #8557
2022-11-18 22:28:08 -05:00
a8e45beb51 Bump image-decoder dependency
Corresponds with https://github.com/tachiyomiorg/image-decoder/pull/6
2022-11-18 22:28:08 -05:00
ba2a528886 Fix related to cancelling queued chapters (#8528)
Tachi removes the downloaded chapter (if it exists) when you just cancelled a download from queue.

PR fixes that

Also removes redundant return
2022-11-18 22:27:39 -05:00
d60367768b Fix monochrome launcher icon not applied when non-round shape is used (#8552) 2022-11-17 12:23:48 -05:00
db6528d3fa Show toast when no next chapter found in library
Closes #8522
Will probably become a snackbar at some point.
2022-11-14 22:47:07 -05:00
f5873d70c6 Don't rely on cache when deleting empty manga folders
In case the cache hasn't actually been indexed yet. Maybe fixes #8438.
2022-11-14 22:42:36 -05:00
10e349f76e Retain previous selected state when updating list states
Fixes #8417
2022-11-13 22:35:52 -05:00
b1ccebf329 Minor cleanup
Mostly just addressing comments from #8452
2022-11-13 12:24:59 -05:00
3407eb84c5 Make padding names neutral (#8531) 2022-11-13 12:11:51 -05:00
6017229d1b Clean up ComicInfo stuff a bit more 2022-11-13 12:01:19 -05:00
4f00af3173 Change long press on downloaded chapter icon to open menu
Seems like silently deleting things is confusing to some people.
2022-11-13 11:55:34 -05:00
9da232dcd8 Adjust download cache to ignore irrelevant files
Fixes #8530
2022-11-13 11:52:11 -05:00
acd43005df SearchToolbar: Better physical keyboard support (#8529)
Make enter keys behave like search key of on-screen keyboard
2022-11-13 10:59:23 -05:00
c31cf2a03a Bump test dependencies 2022-11-13 10:56:02 -05:00
51c964de3a Fix download not working on sd card (#8527)
Also create comicinfo file inside chapter folder instead of manga folder since it also contains some chapter specific data
2022-11-13 10:40:33 -05:00
dad24e785b Update leakcanary to v2.10 (#8521)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-13 10:38:32 -05:00
a908283e86 Update actions/dependency-review-action action to v3 (#8523)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-13 10:38:15 -05:00
d8725c7b7f Translations update from Hosted Weblate (#8398)
Weblate translations

Co-authored-by: Abay Emes <abayemes@gmail.com>
Co-authored-by: Abou <aboozar.gh.r@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Arisu <sylphtics@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Christian Elbrianno <crse@protonmail.ch>
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: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Howard Wu <wuhao_2000@outlook.com>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Komar <k99248169@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Muhamed Zahiri <muhamedzahiri@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rostyslav Haitkulov <info@ubilling.net.ua>
Co-authored-by: SHAWKIK ISLAM JOHA <shawkikislam@gmail.com>
Co-authored-by: Sebastian <klein.sebastian@outlook.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: alex <hdhdhfhfbbffhhfhfjfjf@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: arkon <eugcheung94@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/
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/fa/
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/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/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/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ro/
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/sq/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
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: Abay Emes <abayemes@gmail.com>
Co-authored-by: Abou <aboozar.gh.r@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Arisu <sylphtics@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Christian Elbrianno <crse@protonmail.ch>
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: Howard Wu <wuhao_2000@outlook.com>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Komar <k99248169@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Muhamed Zahiri <muhamedzahiri@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rostyslav Haitkulov <info@ubilling.net.ua>
Co-authored-by: SHAWKIK ISLAM JOHA <shawkikislam@gmail.com>
Co-authored-by: Sebastian <klein.sebastian@outlook.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: alex <hdhdhfhfbbffhhfhfjfjf@gmail.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: arkon <eugcheung94@gmail.com>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
2022-11-12 10:29:32 -05:00
262f8449b4 Resolve proper chapter URL for ComicInfo "Web" field
Requires extensions to be updated to lib 1.4 to have proper URLs for some of them, which will
happen soon in the future.
2022-11-12 09:54:32 -05:00
bdf035d60a Use Voyager on Source Filter screen (#8511) 2022-11-12 09:47:19 -05:00
0270878748 Use Voyager on Extension Filter screen (#8503)
- Use sealed class for state
- Minor changes
2022-11-11 16:57:31 -05:00
6ada3c90ff Clean up ComicInfo stuff a bit 2022-11-11 16:34:18 -05:00
4e628fe6de Create ComicInfo Metadata files on chapter download (#8033)
* generate ComicInfo files at the chapter root and inside CBZ archives on chapter download.

* Update app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt

Co-authored-by: Andreas <andreas.everos@gmail.com>

* Improvements suggested by @ghostbear

* now creates ComicInfo files in normal chapter folders as well
use manga directly instead of converting it to SManga
truncate old files before overwriting them

Co-authored-by: Andreas <6576096+ghostbear@users.noreply.github.com>

* remove empty line after resolving merge conflict

* fixes Serializer for class 'ComicInfo' is not found error

* some changes to comments and variable names

* Revert leftover changes to archiveChapter() function

* minor cleanup

* Changed Chapter to SChapter

Co-authored-by: Andreas <andreas.everos@gmail.com>
Co-authored-by: Andreas <6576096+ghostbear@users.noreply.github.com>
2022-11-11 16:16:37 -05:00
a8eebd824a Remove duplicate pinned sources setting
I guess it's simpler to just have 1 entry in the list (other than the last used duplicate).
This helps ensure that the list is as short as it can be.
2022-11-11 15:35:10 -05:00
afa0a0a0e2 Reword more references to "manga" in strings 2022-11-11 15:25:43 -05:00
92b039fac7 Add Kavita tracker (#7488)
* Added kavita tracker

* Changed api endpoint since tachiyomi has it's own. Moved some processing to backend

* Bugfix. Parsing to int instead of float

* Ignore DOH, update migration and cleanup

* Fix Unexpected JSON token
	modified:   app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt
	modified:   app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt
	modified:   app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaModels.kt

* Apply code format suggestions from code review

Co-authored-by: Andreas <andreas.everos@gmail.com>

* Apply simplified code suggestions from code review

Co-authored-by: Andreas <andreas.everos@gmail.com>

* Removed unused dtos

* Use setter instead of function to get apiurl

* Added Interceptor

* Handle not configured/not accesible sources

* Unused import

* Added kavita to new tracking settings screen

* Delete SettingsTrackingController.kt to solve conflict

* Review comments
* Removed break lines from log messages
* Fixed jwt typo

* Merged enhanced services compatibility warning message to be more generic.
* Updated Komga String res to use new formatted one
* Added Kavita String res to use formatted one

* Apply suggestions from code review - hardcoded strings to track name

Co-authored-by: Andreas <andreas.everos@gmail.com>

Co-authored-by: Andreas <andreas.everos@gmail.com>
2022-11-11 15:19:41 -05:00
acc65529a0 Replace numberpicker with wheelpicker (#8501)
* Replace numberpicker with wheelpicker

* cleanups
2022-11-11 15:02:45 -05:00
3061f198e9 Temporally Fix #8287 (#8493) 2022-11-11 15:01:48 -05:00
6fc1f4fc21 Reword download cache/indexing strings for consistency 2022-11-11 15:01:06 -05:00
a0f49b16c5 Remove "Download complete" notification
It wasn't really consistent with other notifications considering there's no
action to be taken in this state.
2022-11-10 23:08:19 -05:00
c6c4c1c393 Migrate to more domain model usages 2022-11-10 22:42:44 -05:00
811931ccc0 Minor cleanup 2022-11-10 22:23:34 -05:00
08d5633d81 Add option to invalidate download cache (#8491)
* Add option to invalidate download cache

* Review changes + lint
2022-11-10 22:15:35 -05:00
c76d5dd30c Tweak library continue reading button 2022-11-10 22:08:23 -05:00
340357d158 Voyager on More tab (#8498) 2022-11-10 22:08:18 -05:00
11ed47397d Remove top bar workaround (#8497)
Fixed upstream and we currently using small top bar which doesn't affected
2022-11-10 21:26:56 -05:00
6ce54eb845 Fix clearing database freezes the app (#8492) 2022-11-10 07:59:31 -05:00
d0236aaecf Update dependency androidx.compose:compose-bom to v2022.11.00 (#8490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-09 23:24:37 -05:00
00059848b4 Bump dependencies 2022-11-09 22:51:27 -05:00
e45f6d0c92 Use toShareIntent in WebViewActivity 2022-11-09 22:38:28 -05:00
18ccde082d Full Compose MangaController (#8452)
* Full Compose MangaController

* unique key

* Use StateScreenModel

* dismiss

* rebase fix

* toShareIntent
2022-11-09 22:31:56 -05:00
21bc0f1952 Don't use default Lenovo "browser" handler 2022-11-09 19:43:52 -05:00
a37be747e9 Update dependency com.bluelinelabs:conductor to v3.1.8 (#8487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-09 09:44:20 -05:00
bc3bb82651 Voyager on History tab (#8481) 2022-11-09 09:26:29 -05:00
ba00d9e5d2 Add "Play" button on manga in library (#8218)
* resume manga button in libarary

* work on resume button

* Backup

* work on opening the last read chapter

* backup

* renaming

* fab instead of image

* done with logic

* cleanup

* cleanup

* import cleanup

* cleanup...

* refactoring

* fixing logic

* fixing scopes

* Reworking design

* adding ability to turn on/off the feature

* cleanup

* refactoring, fixing logic, adding filter logic (partial)

* backup

* backup

* logic done

* backup before merge fix

* merge conflict....

* merge conflict...

* reworking ui logic

* removing unnecessary file

* refactoring

* refactoring

* review changes + minor parameter position movement

* commiting suggestion

Co-authored-by: arkon <arkon@users.noreply.github.com>

* fixing minor mistake

* moving ChapterFilter.kt

Co-authored-by: arkon <arkon@users.noreply.github.com>
2022-11-07 22:32:23 -05:00
bf9edda04c Use Voyager on Category screen (#8472) 2022-11-07 22:13:14 -05:00
9c9357639a Update dependency com.github.junrar:junrar to v7.5.4 (#8461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-05 22:42:28 -04:00
3733871d2f Don't show copied to clipboard toast on A13+ when copying backup restore error 2022-11-05 11:56:31 -04:00
54471a014f Get index of selected update list item based on chapterId
Fixes #8442
2022-11-05 11:56:08 -04:00
8749be518f Adjust read next history logic
Closes #8454
2022-11-05 10:37:32 -04:00
6d880c938a Retry the MAL request if the token is expired (#8437)
Retry the MAL request if the token expired.
2022-11-04 22:54:52 -04:00
34aa4eb291 Add back haptic feedback long tap to fav (#8418)
* Add back haptic feedback long tap to fav

- add back haptic when long tap on manga to add to library

* simplify

* Revert "simplify"

This reverts commit f4bd57315a3dbf35f5975233980304fa66807718.

* Revert "Add back haptic feedback long tap to fav"

This reverts commit 81486e30e9adf6a7e983b5e3f12bd5bc34083db1.

* cleanup
2022-11-04 22:52:28 -04:00
280b0f42db Toggle enabled source in bulk
Maybe fixes #8439
2022-11-04 09:39:23 -04:00
65387d0089 Bump default user agent string 2022-11-04 09:38:49 -04:00
d41c103a72 Increase visibility of selected item background in dark themes
Closes #8419
2022-11-04 09:38:38 -04:00
0b93b9e059 Add pseudolocales to dev builds 2022-11-03 09:47:27 -04:00
ea3f933e95 #8264: Enabled isPseudoLocalesEnabled for debug (#8367)
Enabled isPseudoLocalesEnabled for debug
2022-11-03 09:46:53 -04:00
b006fe3a22 Revert "Tweak how getChapterUrl works (#8392)" (#8427)
This reverts commit 1a25cea0d6.
2022-11-03 09:23:59 -04:00
37ff3b4920 Ignore gradle.properties.swp (#8425)
* gitignore

* e

* .

* suggestion changes
2022-11-03 09:23:43 -04:00
1e93d785e5 Remove redundant compiler args (#8405) 2022-11-01 20:13:30 -04:00
999bd4efee Center extension name in ExtensionDetailsScreen (#8407) 2022-11-01 12:03:31 -04:00
546 changed files with 25075 additions and 19719 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.2) - To the latest version of the app (stable is v0.14.3)
- 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.2" Example: "0.14.3"
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.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. - label: I have updated the app to version **[0.14.3](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.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. - label: I have updated the app to version **[0.14.3](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

@ -5,10 +5,8 @@
"schedule": ["every sunday"], "schedule": ["every sunday"],
"ignoreDeps": [ "ignoreDeps": [
"androidx.core:core-splashscreen", "androidx.core:core-splashscreen",
"androidx.work:work-runtime-ktx",
"info.android15.nucleus:nucleus-support-v7",
"info.android15.nucleus:nucleus",
"com.android.tools:r8", "com.android.tools:r8",
"com.google.guava:guava" "com.google.guava:guava",
"com.github.commandiron:WheelPickerCompose"
] ]
} }

View File

@ -25,7 +25,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
- name: Dependency Review - name: Dependency Review
uses: actions/dependency-review-action@v2 uses: actions/dependency-review-action@v3
- name: Set up JDK 11 - name: Set up JDK 11
uses: actions/setup-java@v3 uses: actions/setup-java@v3

View File

@ -12,7 +12,7 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v3 - uses: dessant/lock-threads@v4
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-inactive-days: '2' issue-inactive-days: '2'

5
.gitignore vendored
View File

@ -10,4 +10,7 @@
*/build */build
/build /build
*.apk *.apk
app/**/output.json app/**/output.json
# Unnecessary file
*.swp

View File

@ -1,5 +1,6 @@
import org.gradle.api.tasks.testing.logging.TestLogEvent import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jmailen.gradle.kotlinter.tasks.LintTask
plugins { plugins {
id("com.android.application") id("com.android.application")
@ -27,8 +28,8 @@ android {
applicationId = "eu.kanade.tachiyomi" applicationId = "eu.kanade.tachiyomi"
minSdk = AndroidConfig.minSdk minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk targetSdk = AndroidConfig.targetSdk
versionCode = 91 versionCode = 93
versionName = "0.14.2" versionName = "0.14.3"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -59,6 +60,7 @@ android {
named("debug") { named("debug") {
versionNameSuffix = "-${getCommitCount()}" versionNameSuffix = "-${getCommitCount()}"
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
isPseudoLocalesEnabled = true
} }
named("release") { named("release") {
isShrinkResources = true isShrinkResources = true
@ -99,7 +101,8 @@ android {
dimension = "default" dimension = "default"
} }
create("dev") { create("dev") {
resourceConfigurations.addAll(listOf("en", "xxhdpi")) // Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales
resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi"))
dimension = "default" dimension = "default"
} }
} }
@ -142,6 +145,8 @@ android {
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
isCoreLibraryDesugaringEnabled = true
} }
kotlinOptions { kotlinOptions {
@ -161,27 +166,29 @@ dependencies {
implementation(project(":core")) implementation(project(":core"))
implementation(project(":source-api")) implementation(project(":source-api"))
coreLibraryDesugaring(libs.desugar)
// Compose // Compose
implementation(platform(compose.bom)) implementation(platform(compose.bom))
implementation(compose.activity) implementation(compose.activity)
implementation(compose.foundation) implementation(compose.foundation)
implementation(compose.material3.core) implementation(compose.material3.core)
implementation(compose.material3.adapter) implementation(compose.material.core)
implementation(compose.material.icons) implementation(compose.material.icons)
implementation(compose.animation) implementation(compose.animation)
implementation(compose.animation.graphics) implementation(compose.animation.graphics)
implementation(compose.ui.tooling) implementation(compose.ui.tooling)
implementation(compose.ui.util) implementation(compose.ui.util)
implementation(compose.accompanist.webview) implementation(compose.accompanist.webview)
implementation(compose.accompanist.swiperefresh)
implementation(compose.accompanist.flowlayout) implementation(compose.accompanist.flowlayout)
implementation(compose.accompanist.permissions) implementation(compose.accompanist.permissions)
implementation(compose.accompanist.themeadapter)
implementation(compose.accompanist.systemuicontroller)
implementation(androidx.paging.runtime) implementation(androidx.paging.runtime)
implementation(androidx.paging.compose) implementation(androidx.paging.compose)
implementation(libs.bundles.sqlite) implementation(libs.bundles.sqlite)
implementation(androidx.sqlite)
implementation(libs.sqldelight.android.driver) implementation(libs.sqldelight.android.driver)
implementation(libs.sqldelight.coroutines) implementation(libs.sqldelight.coroutines)
implementation(libs.sqldelight.android.paging) implementation(libs.sqldelight.android.paging)
@ -234,15 +241,11 @@ dependencies {
// Preferences // Preferences
implementation(libs.preferencektx) implementation(libs.preferencektx)
// Model View Presenter
implementation(libs.bundles.nucleus)
// Dependency injection // Dependency injection
implementation(libs.injekt.core) implementation(libs.injekt.core)
// Image loading // Image loading
implementation(libs.bundles.coil) implementation(libs.bundles.coil)
implementation(libs.subsamplingscaleimageview) { implementation(libs.subsamplingscaleimageview) {
exclude(module = "image-decoder") exclude(module = "image-decoder")
} }
@ -260,17 +263,12 @@ dependencies {
exclude(group = "androidx.viewpager", module = "viewpager") exclude(group = "androidx.viewpager", module = "viewpager")
} }
implementation(libs.insetter) implementation(libs.insetter)
implementation(libs.markwon) implementation(libs.bundles.richtext)
implementation(libs.aboutLibraries.compose) implementation(libs.aboutLibraries.compose)
implementation(libs.cascade) implementation(libs.cascade)
implementation(libs.numberpicker)
implementation(libs.bundles.voyager) implementation(libs.bundles.voyager)
implementation(libs.wheelpicker)
// Conductor implementation(libs.materialmotion.core)
implementation(libs.bundles.conductor)
// FlowBinding
implementation(libs.bundles.flowbinding)
// Logging // Logging
implementation(libs.logcat) implementation(libs.logcat)
@ -312,7 +310,7 @@ tasks {
} }
} }
withType<org.jmailen.gradle.kotlinter.tasks.LintTask>().configureEach { withType<LintTask>().configureEach {
exclude { it.file.path.contains("generated[\\\\/]".toRegex()) } exclude { it.file.path.contains("generated[\\\\/]".toRegex()) }
} }
@ -320,10 +318,11 @@ tasks {
withType<KotlinCompile> { withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=coil.annotation.ExperimentalCoilApi", "-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi", "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi", "-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/transparent"/> <background android:drawable="@android:color/transparent"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
</adaptive-icon> </adaptive-icon>

View File

@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
<!-- Remove permission from Firebase dependency --> <!-- Remove permission from Firebase dependency -->
<uses-permission android:name="com.google.android.gms.permission.AD_ID" <uses-permission android:name="com.google.android.gms.permission.AD_ID"

File diff suppressed because it is too large Load Diff

View File

@ -34,3 +34,5 @@ class PreferenceMutableState<T>(
return { preference.set(it) } return { preference.set(it) }
} }
} }
fun <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope)

View File

@ -0,0 +1,148 @@
package eu.kanade.core.util
import androidx.compose.ui.util.fastForEach
import java.util.concurrent.ConcurrentHashMap
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
fun <T : R, R : Any> List<T>.insertSeparators(
generator: (T?, T?) -> R?,
): List<R> {
if (isEmpty()) return emptyList()
val newList = mutableListOf<R>()
for (i in -1..lastIndex) {
val before = getOrNull(i)
before?.let { newList.add(it) }
val after = getOrNull(i + 1)
val separator = generator.invoke(before, after)
separator?.let { newList.add(it) }
}
return newList
}
/**
* Returns a new map containing only the key entries of [transform] that are not null.
*/
inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): ConcurrentHashMap<R, V> {
val mutableMap = ConcurrentHashMap<R, V>()
forEach { element -> transform(element)?.let { mutableMap[it] = element.value } }
return mutableMap
}
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
if (shouldAdd) {
add(value)
} else {
remove(value)
}
}
/**
* Returns a list containing only elements matching the given [predicate].
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
contract { callsInPlace(predicate) }
val destination = ArrayList<T>()
fastForEach { if (predicate(it)) destination.add(it) }
return destination
}
/**
* Returns a list containing all elements not matching the given [predicate].
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
contract { callsInPlace(predicate) }
val destination = ArrayList<T>()
fastForEach { if (!predicate(it)) destination.add(it) }
return destination
}
/**
* Returns a list containing only the non-null results of applying the
* given [transform] function to each element in the original collection.
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
contract { callsInPlace(transform) }
val destination = ArrayList<R>()
fastForEach { element ->
transform(element)?.let { destination.add(it) }
}
return destination
}
/**
* Splits the original collection into pair of lists,
* where *first* list contains elements for which [predicate] yielded `true`,
* while *second* list contains elements for which [predicate] yielded `false`.
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastPartition(predicate: (T) -> Boolean): Pair<List<T>, List<T>> {
contract { callsInPlace(predicate) }
val first = ArrayList<T>()
val second = ArrayList<T>()
fastForEach {
if (predicate(it)) {
first.add(it)
} else {
second.add(it)
}
}
return Pair(first, second)
}
/**
* Returns the number of entries not matching the given [predicate].
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
contract { callsInPlace(predicate) }
var count = size
fastForEach { if (predicate(it)) --count }
return count
}
/**
* Returns a list containing only elements from the given collection
* having distinct keys returned by the given [selector] function.
*
* Among elements of the given collection with equal keys, only the first one will be present in the resulting list.
* The elements in the resulting list are in the same order as they were in the source collection.
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
contract { callsInPlace(selector) }
val set = HashSet<K>()
val list = ArrayList<T>()
fastForEach {
val key = selector(it)
if (set.add(key)) list.add(it)
}
return list
}

View File

@ -0,0 +1,16 @@
package eu.kanade.core.util
import android.content.Context
import eu.kanade.tachiyomi.R
import kotlin.time.Duration
fun Duration.toDurationString(context: Context, fallback: String): String {
return toComponents { days, hours, minutes, seconds, _ ->
buildList(4) {
if (days != 0L) add(context.getString(R.string.day_short, days))
if (hours != 0) add(context.getString(R.string.hour_short, hours))
if (minutes != 0 && (days == 0L || hours == 0)) add(context.getString(R.string.minute_short, minutes))
if (seconds != 0 && days == 0L && hours == 0) add(context.getString(R.string.seconds_short, seconds))
}.joinToString(" ").ifBlank { fallback }
}
}

View File

@ -1,16 +0,0 @@
package eu.kanade.core.util
fun <T : R, R : Any> List<T>.insertSeparators(
generator: (T?, T?) -> R?,
): List<R> {
if (isEmpty()) return emptyList()
val newList = mutableListOf<R>()
for (i in -1..lastIndex) {
val before = getOrNull(i)
before?.let { newList.add(it) }
val after = getOrNull(i + 1)
val separator = generator.invoke(before, after)
separator?.let { newList.add(it) }
}
return newList
}

View File

@ -13,7 +13,7 @@ private const val listOfStringsSeparator = ", "
val listOfStringsAdapter = object : ColumnAdapter<List<String>, String> { val listOfStringsAdapter = object : ColumnAdapter<List<String>, String> {
override fun decode(databaseValue: String) = override fun decode(databaseValue: String) =
if (databaseValue.isEmpty()) { if (databaseValue.isEmpty()) {
listOf() emptyList()
} else { } else {
databaseValue.split(listOfStringsSeparator) databaseValue.split(listOfStringsSeparator)
} }

View File

@ -107,16 +107,16 @@ private suspend fun CoroutineDispatcher.acquireTransactionThread(
try { try {
dispatch(EmptyCoroutineContext) { dispatch(EmptyCoroutineContext) {
runBlocking { runBlocking {
// Thread acquired, resume coroutine. // Thread acquired, resume coroutine
continuation.resume(coroutineContext[ContinuationInterceptor]!!) continuation.resume(coroutineContext[ContinuationInterceptor]!!)
controlJob.join() controlJob.join()
} }
} }
} catch (ex: RejectedExecutionException) { } catch (ex: RejectedExecutionException) {
// Couldn't acquire a thread, cancel coroutine. // Couldn't acquire a thread, cancel coroutine
continuation.cancel( continuation.cancel(
IllegalStateException( IllegalStateException(
"Unable to acquire a thread to perform the database transaction.", "Unable to acquire a thread to perform the database transaction",
ex, ex,
), ),
) )
@ -152,7 +152,7 @@ private class TransactionElement(
fun release() { fun release() {
val count = referenceCount.decrementAndGet() val count = referenceCount.decrementAndGet()
if (count < 0) { if (count < 0) {
throw IllegalStateException("Transaction was never started or was already released.") throw IllegalStateException("Transaction was never started or was already released")
} else if (count == 0) { } else if (count == 0) {
// Cancel the job that controls the transaction thread, causing it to be released. // Cancel the job that controls the transaction thread, causing it to be released.
transactionThreadControlJob.cancel() transactionThreadControlJob.cancel()

View File

@ -24,6 +24,10 @@ class HistoryRepositoryImpl(
} }
} }
override suspend fun getTotalReadDuration(): Long {
return handler.awaitOne { historyQueries.getReadDuration() }
}
override suspend fun resetHistory(historyId: Long) { override suspend fun resetHistory(historyId: Long) {
try { try {
handler.await { historyQueries.resetHistoryById(historyId) } handler.await { historyQueries.resetHistoryById(historyId) }

View File

@ -9,6 +9,10 @@ class TrackRepositoryImpl(
private val handler: DatabaseHandler, private val handler: DatabaseHandler,
) : TrackRepository { ) : TrackRepository {
override suspend fun getTrackById(id: Long): Track? {
return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, trackMapper) }
}
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> { override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
return handler.awaitList { return handler.awaitList {
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper) manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)

View File

@ -32,11 +32,10 @@ import eu.kanade.domain.download.interactor.DeleteDownload
import eu.kanade.domain.extension.interactor.GetExtensionLanguages import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.extension.interactor.GetExtensionSources 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.GetHistory import eu.kanade.domain.history.interactor.GetHistory
import eu.kanade.domain.history.interactor.GetNextUnreadChapters import eu.kanade.domain.history.interactor.GetNextChapters
import eu.kanade.domain.history.interactor.RemoveHistoryById import eu.kanade.domain.history.interactor.GetTotalReadDuration
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId import eu.kanade.domain.history.interactor.RemoveHistory
import eu.kanade.domain.history.interactor.UpsertHistory import eu.kanade.domain.history.interactor.UpsertHistory
import eu.kanade.domain.history.repository.HistoryRepository import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
@ -94,7 +93,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 { GetNextUnreadChapters(get(), get(), get()) } addFactory { GetNextChapters(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()) }
@ -119,11 +118,10 @@ class DomainModule : InjektModule {
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) } addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { DeleteAllHistory(get()) }
addFactory { GetHistory(get()) } addFactory { GetHistory(get()) }
addFactory { UpsertHistory(get()) } addFactory { UpsertHistory(get()) }
addFactory { RemoveHistoryById(get()) } addFactory { RemoveHistory(get()) }
addFactory { RemoveHistoryByMangaId(get()) } addFactory { GetTotalReadDuration(get()) }
addFactory { DeleteDownload(get(), get()) } addFactory { DeleteDownload(get(), get()) }
@ -132,7 +130,7 @@ class DomainModule : InjektModule {
addFactory { GetExtensionLanguages(get(), get()) } addFactory { GetExtensionLanguages(get(), get()) }
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) } addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
addFactory { GetUpdates(get(), get()) } addFactory { GetUpdates(get()) }
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) } addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) } addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }

View File

@ -5,46 +5,66 @@ import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.repository.CategoryRepository import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import logcat.LogPriority import logcat.LogPriority
import java.util.Collections
class ReorderCategory( class ReorderCategory(
private val categoryRepository: CategoryRepository, private val categoryRepository: CategoryRepository,
) { ) {
suspend fun await(categoryId: Long, newPosition: Int) = withNonCancellableContext { private val mutex = Mutex()
val categories = categoryRepository.getAll().filterNot(Category::isSystemCategory)
val currentIndex = categories.indexOfFirst { it.id == categoryId } suspend fun moveUp(category: Category): Result =
if (currentIndex == newPosition) { await(category, MoveTo.UP)
return@withNonCancellableContext Result.Unchanged
}
val reorderedCategories = categories.toMutableList() suspend fun moveDown(category: Category): Result =
val reorderedCategory = reorderedCategories.removeAt(currentIndex) await(category, MoveTo.DOWN)
reorderedCategories.add(newPosition, reorderedCategory)
val updates = reorderedCategories.mapIndexed { index, category -> private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
CategoryUpdate( mutex.withLock {
id = category.id, val categories = categoryRepository.getAll()
order = index.toLong(), .filterNot(Category::isSystemCategory)
) .toMutableList()
}
try { val currentIndex = categories.indexOfFirst { it.id == category.id }
categoryRepository.updatePartial(updates) if (currentIndex == -1) {
Result.Success return@withNonCancellableContext Result.Unchanged
} catch (e: Exception) { }
logcat(LogPriority.ERROR, e)
Result.InternalError(e) val newPosition = when (moveTo) {
MoveTo.UP -> currentIndex - 1
MoveTo.DOWN -> currentIndex + 1
}.toInt()
try {
Collections.swap(categories, currentIndex, newPosition)
val updates = categories.mapIndexed { index, category ->
CategoryUpdate(
id = category.id,
order = index.toLong(),
)
}
categoryRepository.updatePartial(updates)
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
Result.InternalError(e)
}
} }
} }
suspend fun await(category: Category, newPosition: Long): Result =
await(category.id, newPosition.toInt())
sealed class Result { sealed class Result {
object Success : Result() object Success : Result()
object Unchanged : Result() object Unchanged : Result()
data class InternalError(val error: Throwable) : Result() data class InternalError(val error: Throwable) : Result()
} }
private enum class MoveTo {
UP,
DOWN,
}
} }

View File

@ -12,7 +12,6 @@ data class Category(
val isSystemCategory: Boolean = id == UNCATEGORIZED_ID val isSystemCategory: Boolean = id == UNCATEGORIZED_ID
companion object { companion object {
const val UNCATEGORIZED_ID = 0L const val UNCATEGORIZED_ID = 0L
} }
} }

View File

@ -4,7 +4,6 @@ import eu.kanade.data.chapter.CleanupChapterName
import eu.kanade.data.chapter.NoChaptersException import eu.kanade.data.chapter.NoChaptersException
import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.toChapterUpdate import eu.kanade.domain.chapter.model.toChapterUpdate
import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.chapter.repository.ChapterRepository import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
@ -111,7 +110,7 @@ class SyncChaptersWithSource(
downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source) downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source)
if (shouldRenameChapter) { if (shouldRenameChapter) {
downloadManager.renameChapter(source, manga, dbChapter.toDbChapter(), chapter.toDbChapter()) downloadManager.renameChapter(source, manga, dbChapter, chapter)
} }
var toChangeChapter = dbChapter.copy( var toChangeChapter = dbChapter.copy(
name = chapter.name, name = chapter.name,

View File

@ -0,0 +1,82 @@
package eu.kanade.domain.chapter.model
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.TriStateFilter
import eu.kanade.domain.manga.model.isLocal
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.manga.ChapterItem
import eu.kanade.tachiyomi.util.chapter.getChapterSort
/**
* Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted.
*/
fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager): List<Chapter> {
val isLocalManga = manga.isLocal()
val unreadFilter = manga.unreadFilter
val downloadedFilter = manga.downloadedFilter
val bookmarkedFilter = manga.bookmarkedFilter
return filter { chapter ->
when (unreadFilter) {
TriStateFilter.DISABLED -> true
TriStateFilter.ENABLED_IS -> !chapter.read
TriStateFilter.ENABLED_NOT -> chapter.read
}
}
.filter { chapter ->
when (bookmarkedFilter) {
TriStateFilter.DISABLED -> true
TriStateFilter.ENABLED_IS -> chapter.bookmark
TriStateFilter.ENABLED_NOT -> !chapter.bookmark
}
}
.filter { chapter ->
val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
val downloadState = when {
downloaded -> Download.State.DOWNLOADED
else -> Download.State.NOT_DOWNLOADED
}
when (downloadedFilter) {
TriStateFilter.DISABLED -> true
TriStateFilter.ENABLED_IS -> downloadState == Download.State.DOWNLOADED || isLocalManga
TriStateFilter.ENABLED_NOT -> downloadState != Download.State.DOWNLOADED && !isLocalManga
}
}
.sortedWith(getChapterSort(manga))
}
/**
* Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted.
*/
fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> {
val isLocalManga = manga.isLocal()
val unreadFilter = manga.unreadFilter
val downloadedFilter = manga.downloadedFilter
val bookmarkedFilter = manga.bookmarkedFilter
return asSequence()
.filter { (chapter) ->
when (unreadFilter) {
TriStateFilter.DISABLED -> true
TriStateFilter.ENABLED_IS -> !chapter.read
TriStateFilter.ENABLED_NOT -> chapter.read
}
}
.filter { (chapter) ->
when (bookmarkedFilter) {
TriStateFilter.DISABLED -> true
TriStateFilter.ENABLED_IS -> chapter.bookmark
TriStateFilter.ENABLED_NOT -> !chapter.bookmark
}
}
.filter {
when (downloadedFilter) {
TriStateFilter.DISABLED -> true
TriStateFilter.ENABLED_IS -> it.isDownloaded || isLocalManga
TriStateFilter.ENABLED_NOT -> !it.isDownloaded && !isLocalManga
}
}
.sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) }
}

View File

@ -1,7 +1,6 @@
package eu.kanade.domain.download.interactor package eu.kanade.domain.download.interactor
import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
@ -14,7 +13,7 @@ class DeleteDownload(
suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext { suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext {
sourceManager.get(manga.source)?.let { source -> sourceManager.get(manga.source)?.let { source ->
downloadManager.deleteChapters(chapters.map { it.toDbChapter() }, manga, source) downloadManager.deleteChapters(chapters.toList(), manga, source)
} }
} }
} }

View File

@ -25,10 +25,7 @@ class GetExtensionLanguages(
} }
.distinct() .distinct()
.sortedWith( .sortedWith(
compareBy( compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
{ it !in enabledLanguage },
{ LocaleHelper.getDisplayName(it) },
),
) )
} }
} }

View File

@ -3,7 +3,6 @@ package eu.kanade.domain.extension.interactor
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -30,3 +29,9 @@ class GetExtensionSources(
} }
} }
} }
data class ExtensionSourceItem(
val source: Source,
val enabled: Boolean,
val labelAsName: Boolean,
)

View File

@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow
class GetHistory( class GetHistory(
private val repository: HistoryRepository, private val repository: HistoryRepository,
) { ) {
fun subscribe(query: String): Flow<List<HistoryWithRelations>> { fun subscribe(query: String): Flow<List<HistoryWithRelations>> {
return repository.getHistory(query) return repository.getHistory(query)
} }

View File

@ -0,0 +1,52 @@
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 GetNextChapters(
private val getChapterByMangaId: GetChapterByMangaId,
private val getManga: GetManga,
private val historyRepository: HistoryRepository,
) {
suspend fun await(onlyUnread: Boolean = true): List<Chapter> {
val history = historyRepository.getLastHistory() ?: return emptyList()
return await(history.mangaId, history.chapterId, onlyUnread)
}
suspend fun await(mangaId: Long, onlyUnread: Boolean = true): List<Chapter> {
val manga = getManga.await(mangaId) ?: return emptyList()
val chapters = getChapterByMangaId.await(mangaId)
.sortedWith(getChapterSort(manga, sortDescending = false))
return if (onlyUnread) {
chapters.filterNot { it.read }
} else {
chapters
}
}
suspend fun await(mangaId: Long, fromChapterId: Long, onlyUnread: Boolean = true): List<Chapter> {
val chapters = await(mangaId, onlyUnread)
val currChapterIndex = chapters.indexOfFirst { it.id == fromChapterId }
val nextChapters = chapters.subList(max(0, currChapterIndex), chapters.size)
if (onlyUnread) {
return nextChapters
}
// The "next chapter" is either:
// - The current chapter if it isn't completely read
// - The chapters after the current chapter if the current one is completely read
val fromChapter = chapters.getOrNull(currChapterIndex)
return if (fromChapter != null && !fromChapter.read) {
nextChapters
} else {
nextChapters.drop(1)
}
}
}

View File

@ -1,33 +0,0 @@
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

@ -2,11 +2,11 @@ package eu.kanade.domain.history.interactor
import eu.kanade.domain.history.repository.HistoryRepository import eu.kanade.domain.history.repository.HistoryRepository
class DeleteAllHistory( class GetTotalReadDuration(
private val repository: HistoryRepository, private val repository: HistoryRepository,
) { ) {
suspend fun await(): Boolean { suspend fun await(): Long {
return repository.deleteAllHistory() return repository.getTotalReadDuration()
} }
} }

View File

@ -3,11 +3,19 @@ package eu.kanade.domain.history.interactor
import eu.kanade.domain.history.model.HistoryWithRelations import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.domain.history.repository.HistoryRepository import eu.kanade.domain.history.repository.HistoryRepository
class RemoveHistoryById( class RemoveHistory(
private val repository: HistoryRepository, private val repository: HistoryRepository,
) { ) {
suspend fun awaitAll(): Boolean {
return repository.deleteAllHistory()
}
suspend fun await(history: HistoryWithRelations) { suspend fun await(history: HistoryWithRelations) {
repository.resetHistory(history.id) repository.resetHistory(history.id)
} }
suspend fun await(mangaId: Long) {
repository.resetHistoryByMangaId(mangaId)
}
} }

View File

@ -1,12 +0,0 @@
package eu.kanade.domain.history.interactor
import eu.kanade.domain.history.repository.HistoryRepository
class RemoveHistoryByMangaId(
private val repository: HistoryRepository,
) {
suspend fun await(mangaId: Long) {
repository.resetHistoryByMangaId(mangaId)
}
}

View File

@ -10,6 +10,8 @@ interface HistoryRepository {
suspend fun getLastHistory(): HistoryWithRelations? suspend fun getLastHistory(): HistoryWithRelations?
suspend fun getTotalReadDuration(): Long
suspend fun resetHistory(historyId: Long) suspend fun resetHistory(historyId: Long)
suspend fun resetHistoryByMangaId(mangaId: Long) suspend fun resetHistoryByMangaId(mangaId: Long)

View File

@ -32,7 +32,6 @@ data class LibrarySort(
object DateAdded : Type(0b00011100) object DateAdded : Type(0b00011100)
companion object { companion object {
fun valueOf(flag: Long): Type { fun valueOf(flag: Long): Type {
return types.find { type -> type.flag == flag and type.mask } ?: default.type return types.find { type -> type.flag == flag and type.mask } ?: default.type
} }
@ -49,7 +48,6 @@ data class LibrarySort(
object Descending : Direction(0b00000000) object Descending : Direction(0b00000000)
companion object { companion object {
fun valueOf(flag: Long): Direction { fun valueOf(flag: Long): Direction {
return directions.find { direction -> direction.flag == flag and direction.mask } ?: default.direction return directions.find { direction -> direction.flag == flag and direction.mask } ?: default.direction
} }

View File

@ -32,6 +32,8 @@ class LibraryPreferences(
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false) fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
fun showContinueReadingButton() = preferenceStore.getBoolean("display_continue_reading_button", false)
// region Filter // region Filter
fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value) fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
@ -58,8 +60,8 @@ class LibraryPreferences(
fun languageBadge() = preferenceStore.getBoolean("display_language_badge", false) fun languageBadge() = preferenceStore.getBoolean("display_language_badge", false)
fun showUpdatesNavBadge() = preferenceStore.getBoolean("library_update_show_tab_badge", false) fun newShowUpdatesCount() = preferenceStore.getBoolean("library_show_updates_count", true)
fun unreadUpdatesCount() = preferenceStore.getInt("library_unread_updates_count", 0) fun newUpdatesCount() = preferenceStore.getInt("library_unseen_updates_count", 0)
// endregion // endregion

View File

@ -10,19 +10,21 @@ class SetMangaViewerFlags(
) { ) {
suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) { suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id)
mangaRepository.update( mangaRepository.update(
MangaUpdate( MangaUpdate(
id = id, id = id,
viewerFlags = flag.setFlag(flag, ReadingModeType.MASK.toLong()), viewerFlags = manga.viewerFlags.setFlag(flag, ReadingModeType.MASK.toLong()),
), ),
) )
} }
suspend fun awaitSetOrientationType(id: Long, flag: Long) { suspend fun awaitSetOrientationType(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id)
mangaRepository.update( mangaRepository.update(
MangaUpdate( MangaUpdate(
id = id, id = id,
viewerFlags = flag.setFlag(flag, OrientationType.MASK.toLong()), viewerFlags = manga.viewerFlags.setFlag(flag, OrientationType.MASK.toLong()),
), ),
) )
} }

View File

@ -4,7 +4,6 @@ import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.MangaUpdate import eu.kanade.domain.manga.model.MangaUpdate
import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.domain.manga.model.isLocal import eu.kanade.domain.manga.model.isLocal
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.domain.manga.repository.MangaRepository import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
@ -46,11 +45,11 @@ class UpdateManga(
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null !manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
localManga.isLocal() -> Date().time localManga.isLocal() -> Date().time
localManga.hasCustomCover(coverCache) -> { localManga.hasCustomCover(coverCache) -> {
coverCache.deleteFromCache(localManga.toDbManga(), false) coverCache.deleteFromCache(localManga, false)
null null
} }
else -> { else -> {
coverCache.deleteFromCache(localManga.toDbManga(), false) coverCache.deleteFromCache(localManga, false)
Date().time Date().time
} }
} }

View File

@ -1,60 +1,175 @@
package eu.kanade.domain.manga.model package eu.kanade.domain.manga.model
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import nl.adaptivity.xmlutil.serialization.XmlElement
import nl.adaptivity.xmlutil.serialization.XmlSerialName import nl.adaptivity.xmlutil.serialization.XmlSerialName
import nl.adaptivity.xmlutil.serialization.XmlValue import nl.adaptivity.xmlutil.serialization.XmlValue
const val COMIC_INFO_FILE = "ComicInfo.xml"
/**
* Creates a ComicInfo instance based on the manga and chapter metadata.
*/
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo(
title = ComicInfo.Title(chapter.name),
series = ComicInfo.Series(manga.title),
web = ComicInfo.Web(chapterUrl),
summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) },
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
),
inker = null,
colorist = null,
letterer = null,
coverArtist = null,
tags = null,
)
fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
comicInfo.series?.let { title = it.value }
comicInfo.writer?.let { author = it.value }
comicInfo.summary?.let { description = it.value }
listOfNotNull(
comicInfo.genre?.value,
comicInfo.tags?.value,
)
.flatMap { it.split(", ") }
.distinct()
.joinToString(", ") { it.trim() }
.takeIf { it.isNotEmpty() }
?.let { genre = it }
listOfNotNull(
comicInfo.penciller?.value,
comicInfo.inker?.value,
comicInfo.colorist?.value,
comicInfo.letterer?.value,
comicInfo.coverArtist?.value,
)
.flatMap { it.split(", ") }
.distinct()
.joinToString(", ") { it.trim() }
.takeIf { it.isNotEmpty() }
?.let { artist = it }
status = ComicInfoPublishingStatus.toSMangaValue(comicInfo.publishingStatus?.value)
}
@Serializable @Serializable
@XmlSerialName("ComicInfo", "", "") @XmlSerialName("ComicInfo", "", "")
data class ComicInfo( data class ComicInfo(
val series: ComicInfoSeries?, val title: Title?,
val summary: ComicInfoSummary?, val series: Series?,
val writer: ComicInfoWriter?, val summary: Summary?,
val penciller: ComicInfoPenciller?, val writer: Writer?,
val inker: ComicInfoInker?, val penciller: Penciller?,
val colorist: ComicInfoColorist?, val inker: Inker?,
val letterer: ComicInfoLetterer?, val colorist: Colorist?,
val coverArtist: ComicInfoCoverArtist?, val letterer: Letterer?,
val genre: ComicInfoGenre?, val coverArtist: CoverArtist?,
val tags: ComicInfoTags?, val translator: Translator?,
) val genre: Genre?,
val tags: Tags?,
val web: Web?,
val publishingStatus: PublishingStatusTachiyomi?,
) {
@Suppress("UNUSED")
@XmlElement(false)
@XmlSerialName("xmlns:xsd", "", "")
val xmlSchema: String = "http://www.w3.org/2001/XMLSchema"
@Serializable @Suppress("UNUSED")
@XmlSerialName("Series", "", "") @XmlElement(false)
data class ComicInfoSeries(@XmlValue(true) val value: String = "") @XmlSerialName("xmlns:xsi", "", "")
val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance"
@Serializable @Serializable
@XmlSerialName("Summary", "", "") @XmlSerialName("Title", "", "")
data class ComicInfoSummary(@XmlValue(true) val value: String = "") data class Title(@XmlValue(true) val value: String = "")
@Serializable @Serializable
@XmlSerialName("Writer", "", "") @XmlSerialName("Series", "", "")
data class ComicInfoWriter(@XmlValue(true) val value: String = "") data class Series(@XmlValue(true) val value: String = "")
@Serializable @Serializable
@XmlSerialName("Penciller", "", "") @XmlSerialName("Summary", "", "")
data class ComicInfoPenciller(@XmlValue(true) val value: String = "") data class Summary(@XmlValue(true) val value: String = "")
@Serializable @Serializable
@XmlSerialName("Inker", "", "") @XmlSerialName("Writer", "", "")
data class ComicInfoInker(@XmlValue(true) val value: String = "") data class Writer(@XmlValue(true) val value: String = "")
@Serializable @Serializable
@XmlSerialName("Colorist", "", "") @XmlSerialName("Penciller", "", "")
data class ComicInfoColorist(@XmlValue(true) val value: String = "") data class Penciller(@XmlValue(true) val value: String = "")
@Serializable @Serializable
@XmlSerialName("Letterer", "", "") @XmlSerialName("Inker", "", "")
data class ComicInfoLetterer(@XmlValue(true) val value: String = "") data class Inker(@XmlValue(true) val value: String = "")
@Serializable @Serializable
@XmlSerialName("CoverArtist", "", "") @XmlSerialName("Colorist", "", "")
data class ComicInfoCoverArtist(@XmlValue(true) val value: String = "") data class Colorist(@XmlValue(true) val value: String = "")
@Serializable @Serializable
@XmlSerialName("Genre", "", "") @XmlSerialName("Letterer", "", "")
data class ComicInfoGenre(@XmlValue(true) val value: String = "") data class Letterer(@XmlValue(true) val value: String = "")
@Serializable @Serializable
@XmlSerialName("Tags", "", "") @XmlSerialName("CoverArtist", "", "")
data class ComicInfoTags(@XmlValue(true) val value: String = "") data class CoverArtist(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Translator", "", "")
data class Translator(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Genre", "", "")
data class Genre(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Tags", "", "")
data class Tags(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Web", "", "")
data class Web(@XmlValue(true) val value: String = "")
// The spec doesn't have a good field for this
@Serializable
@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "")
}
private enum class ComicInfoPublishingStatus(
val comicInfoValue: String,
val sMangaModelValue: Int,
) {
ONGOING("Ongoing", SManga.ONGOING),
COMPLETED("Completed", SManga.COMPLETED),
LICENSED("Licensed", SManga.LICENSED),
PUBLISHING_FINISHED("Publishing finished", SManga.PUBLISHING_FINISHED),
CANCELLED("Cancelled", SManga.CANCELLED),
ON_HIATUS("On hiatus", SManga.ON_HIATUS),
UNKNOWN("Unknown", SManga.UNKNOWN),
;
companion object {
fun toComicInfoValue(value: Long): String {
return values().firstOrNull { it.sMangaModelValue == value.toInt() }?.comicInfoValue
?: UNKNOWN.comicInfoValue
}
fun toSMangaValue(value: String?): Int {
return values().firstOrNull { it.comicInfoValue == value }?.sMangaModelValue
?: UNKNOWN.sMangaModelValue
}
}
}

View File

@ -1,17 +1,16 @@
package eu.kanade.domain.manga.model package eu.kanade.domain.manga.model
import eu.kanade.data.listOfStringsAdapter
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.Serializable import java.io.Serializable
import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
data class Manga( data class Manga(
val id: Long, val id: Long,
@ -49,6 +48,12 @@ data class Manga(
val bookmarkedFilterRaw: Long val bookmarkedFilterRaw: Long
get() = chapterFlags and CHAPTER_BOOKMARKED_MASK get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
val readingModeType: Long
get() = viewerFlags and ReadingModeType.MASK.toLong()
val orientationType: Long
get() = viewerFlags and OrientationType.MASK.toLong()
val unreadFilter: TriStateFilter val unreadFilter: TriStateFilter
get() = when (unreadFilterRaw) { get() = when (unreadFilterRaw) {
CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
@ -187,28 +192,6 @@ fun TriStateFilter.toTriStateGroupState(): ExtendedNavigationView.Item.TriStateG
} }
} }
// TODO: Remove when all deps are migrated
fun Manga.toDbManga(): DbManga = MangaImpl().also {
it.id = id
it.source = source
it.favorite = favorite
it.last_update = lastUpdate
it.date_added = dateAdded
it.viewer_flags = viewerFlags.toInt()
it.chapter_flags = chapterFlags.toInt()
it.cover_last_modified = coverLastModified
it.url = url
it.title = title
it.artist = artist
it.author = author
it.description = description
it.genre = genre?.let(listOfStringsAdapter::encode)
it.status = status.toInt()
it.thumbnail_url = thumbnailUrl
it.update_strategy = updateStrategy
it.initialized = initialized
}
fun Manga.toMangaUpdate(): MangaUpdate { fun Manga.toMangaUpdate(): MangaUpdate {
return MangaUpdate( return MangaUpdate(
id = id, id = id,

View File

@ -10,3 +10,13 @@ data class MangaCover(
val url: String?, val url: String?,
val lastModified: Long, val lastModified: Long,
) )
fun Manga.asMangaCover(): MangaCover {
return MangaCover(
mangaId = id,
sourceId = source,
isMangaFavorite = favorite,
url = thumbnailUrl,
lastModified = coverLastModified,
)
}

View File

@ -23,7 +23,6 @@ class GetEnabledSources(
preferences.lastUsedSource().changes(), preferences.lastUsedSource().changes(),
repository.getSources(), repository.getSources(),
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources -> ) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
val duplicatePins = preferences.duplicatePinnedSources().get()
sources sources
.filter { it.lang in enabledLanguages || it.id == LocalSource.ID } .filter { it.lang in enabledLanguages || it.id == LocalSource.ID }
.filterNot { it.id.toString() in disabledSources } .filterNot { it.id.toString() in disabledSources }
@ -35,10 +34,6 @@ class GetEnabledSources(
if (source.id == lastUsedSource) { if (source.id == lastUsedSource) {
toFlatten.add(source.copy(isUsedLast = true, pin = source.pin - Pin.Actual)) toFlatten.add(source.copy(isUsedLast = true, pin = source.pin - Pin.Actual))
} }
if (duplicatePins && Pin.Pinned in source.pin) {
toFlatten[0] = toFlatten[0].copy(pin = source.pin + Pin.Forced)
toFlatten.add(source.copy(pin = source.pin - Pin.Actual))
}
toFlatten toFlatten
} }
} }

View File

@ -25,10 +25,7 @@ class GetLanguagesWithSources(
sortedSources.groupBy { it.lang } sortedSources.groupBy { it.lang }
.toSortedMap( .toSortedMap(
compareBy( compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
{ it !in enabledLanguage },
{ LocaleHelper.getDisplayName(it) },
),
) )
} }
} }

View File

@ -18,6 +18,13 @@ class ToggleSource(
} }
} }
fun await(sourceIds: List<Long>, enable: Boolean) {
val transformedSourceIds = sourceIds.map { it.toString() }
preferences.disabledSources().getAndSet { disabled ->
if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds)
}
}
private fun isEnabled(sourceId: Long): Boolean { private fun isEnabled(sourceId: Long): Boolean {
return sourceId.toString() in preferences.disabledSources().get() return sourceId.toString() in preferences.disabledSources().get()
} }

View File

@ -33,7 +33,6 @@ data class Source(
val key: () -> String = { val key: () -> String = {
when { when {
isUsedLast -> "$id-lastused" isUsedLast -> "$id-lastused"
Pin.Forced in pin -> "$id-forced"
else -> "$id" else -> "$id"
} }
} }
@ -43,7 +42,6 @@ sealed class Pin(val code: Int) {
object Unpinned : Pin(0b00) object Unpinned : Pin(0b00)
object Pinned : Pin(0b01) object Pinned : Pin(0b01)
object Actual : Pin(0b10) object Actual : Pin(0b10)
object Forced : Pin(0b100)
} }
inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins { inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins {

View File

@ -10,6 +10,15 @@ class GetTracks(
private val trackRepository: TrackRepository, private val trackRepository: TrackRepository,
) { ) {
suspend fun awaitOne(id: Long): Track? {
return try {
trackRepository.getTrackById(id)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
null
}
}
suspend fun await(mangaId: Long): List<Track> { suspend fun await(mangaId: Long): List<Track> {
return try { return try {
trackRepository.getTracksByMangaId(mangaId) trackRepository.getTracksByMangaId(mangaId)

View File

@ -5,6 +5,8 @@ import kotlinx.coroutines.flow.Flow
interface TrackRepository { interface TrackRepository {
suspend fun getTrackById(id: Long): Track?
suspend fun getTracksByMangaId(mangaId: Long): List<Track> suspend fun getTracksByMangaId(mangaId: Long): List<Track>
fun getTracksAsFlow(): Flow<List<Track>> fun getTracksAsFlow(): Flow<List<Track>>

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.track.job package eu.kanade.domain.track.service
import android.content.Context import android.content.Context
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
@ -9,10 +9,10 @@ import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.track.interactor.GetTracks import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.InsertTrack import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
@ -25,7 +25,6 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val getManga = Injekt.get<GetManga>()
val getTracks = Injekt.get<GetTracks>() val getTracks = Injekt.get<GetTracks>()
val insertTrack = Injekt.get<InsertTrack>() val insertTrack = Injekt.get<InsertTrack>()
@ -34,10 +33,11 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
withIOContext { withIOContext {
val tracks = delayedTrackingStore.getItems().mapNotNull { val tracks = delayedTrackingStore.getItems().mapNotNull {
val manga = getManga.await(it.mangaId) ?: return@withIOContext val track = getTracks.awaitOne(it.trackId)
getTracks.await(manga.id) if (track == null) {
.find { track -> track.id == it.trackId } delayedTrackingStore.remove(it.trackId)
?.copy(lastChapterRead = it.lastChapterRead.toDouble()) }
track
} }
tracks.forEach { track -> tracks.forEach { track ->
@ -47,7 +47,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
service.update(track.toDbTrack(), true) service.update(track.toDbTrack(), true)
insertTrack.await(track) insertTrack.await(track)
} }
delayedTrackingStore.remove(track) delayedTrackingStore.remove(track.id)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
} }

View File

@ -0,0 +1,46 @@
package eu.kanade.domain.track.store
import android.content.Context
import androidx.core.content.edit
import eu.kanade.domain.track.model.Track
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class DelayedTrackingStore(context: Context) {
/**
* Preference file where queued tracking updates are stored.
*/
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
fun addItem(track: Track) {
val trackId = track.id.toString()
val lastChapterRead = preferences.getFloat(trackId, 0f)
if (track.lastChapterRead > lastChapterRead) {
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: ${track.lastChapterRead}" }
preferences.edit {
putFloat(trackId, track.lastChapterRead.toFloat())
}
}
}
fun remove(trackId: Long) {
preferences.edit {
remove(trackId.toString())
}
}
fun getItems(): List<DelayedTrackingItem> {
return preferences.all.mapNotNull {
DelayedTrackingItem(
trackId = it.key.toLong(),
lastChapterRead = it.value.toString().toFloat(),
)
}
}
data class DelayedTrackingItem(
val trackId: Long,
val lastChapterRead: Float,
)
}

View File

@ -1,24 +1,17 @@
package eu.kanade.domain.updates.interactor package eu.kanade.domain.updates.interactor
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.domain.updates.model.UpdatesWithRelations import eu.kanade.domain.updates.model.UpdatesWithRelations
import eu.kanade.domain.updates.repository.UpdatesRepository import eu.kanade.domain.updates.repository.UpdatesRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import java.util.Calendar import java.util.Calendar
class GetUpdates( class GetUpdates(
private val repository: UpdatesRepository, private val repository: UpdatesRepository,
private val preferences: LibraryPreferences,
) { ) {
fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> = subscribe(calendar.time.time) fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> = subscribe(calendar.time.time)
fun subscribe(after: Long): Flow<List<UpdatesWithRelations>> { fun subscribe(after: Long): Flow<List<UpdatesWithRelations>> {
return repository.subscribeAll(after) return repository.subscribeAll(after)
.onEach { updates ->
// Set unread chapter count for bottom bar badge
preferences.unreadUpdatesCount().set(updates.count { !it.read })
}
} }
} }

View File

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

View File

@ -1,213 +1,37 @@
package eu.kanade.presentation.browse package eu.kanade.presentation.browse
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
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.Row
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
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.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
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.collectAsLazyPagingItems
import eu.kanade.data.source.NoResultsException import eu.kanade.data.source.NoResultsException
import eu.kanade.domain.library.model.LibraryDisplayMode import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
import eu.kanade.presentation.browse.components.BrowseSourceList import eu.kanade.presentation.browse.components.BrowseSourceList
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.EmptyScreenAction import eu.kanade.presentation.components.EmptyScreenAction
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter import kotlinx.coroutines.flow.StateFlow
import eu.kanade.tachiyomi.ui.more.MoreController
@Composable
fun BrowseSourceScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
openFilterSheet: () -> Unit,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
onWebViewClick: () -> Unit,
incognitoMode: Boolean,
downloadedOnlyMode: Boolean,
) {
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
val snackbarHostState = remember { SnackbarHostState() }
val uriHandler = LocalUriHandler.current
val onHelpClick = {
uriHandler.openUri(LocalSource.HELP_URL)
}
Scaffold(
topBar = {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
BrowseSourceToolbar(
state = presenter,
source = presenter.source,
displayMode = presenter.displayMode,
onDisplayModeChange = { presenter.displayMode = it },
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
onSearch = { presenter.search(it) },
)
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
FilterChip(
selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Popular,
onClick = {
presenter.reset()
presenter.search(GetRemoteManga.QUERY_POPULAR)
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Favorite,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.popular))
},
)
if (presenter.source?.supportsLatest == true) {
FilterChip(
selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Latest,
onClick = {
presenter.reset()
presenter.search(GetRemoteManga.QUERY_LATEST)
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.NewReleases,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.latest))
},
)
}
if (presenter.filters.isNotEmpty()) {
FilterChip(
selected = presenter.currentFilter is BrowseSourcePresenter.Filter.UserInput,
onClick = openFilterSheet,
leadingIcon = {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.action_filter))
},
)
}
}
Divider()
AppStateBanners(downloadedOnlyMode, incognitoMode)
}
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
) { paddingValues ->
BrowseSourceContent(
state = presenter,
mangaList = mangaList,
getMangaState = { presenter.getManga(it) },
columns = columns,
displayMode = presenter.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = onWebViewClick,
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
onLocalSourceHelpClick = onHelpClick,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
}
@Composable
fun BrowseSourceFloatingActionButton(
modifier: Modifier = Modifier.navigationBarsPadding(),
isVisible: Boolean,
onFabClick: () -> Unit,
) {
AnimatedVisibility(visible = isVisible) {
ExtendedFloatingActionButton(
modifier = modifier,
text = { Text(text = stringResource(R.string.action_filter)) },
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
onClick = onFabClick,
)
}
}
@Composable @Composable
fun BrowseSourceContent( fun BrowseSourceContent(
state: BrowseSourceState, source: CatalogueSource?,
mangaList: LazyPagingItems<Manga>, mangaList: LazyPagingItems<StateFlow<Manga>>,
getMangaState: @Composable ((Manga) -> State<Manga>),
columns: GridCells, columns: GridCells,
displayMode: LibraryDisplayMode, displayMode: LibraryDisplayMode,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
@ -249,7 +73,7 @@ fun BrowseSourceContent(
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) { if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
EmptyScreen( EmptyScreen(
message = getErrorMessage(errorState), message = getErrorMessage(errorState),
actions = if (state.source is LocalSource) { actions = if (source is LocalSource) {
listOf( listOf(
EmptyScreenAction( EmptyScreenAction(
stringResId = R.string.local_source_help_guide, stringResId = R.string.local_source_help_guide,
@ -290,7 +114,6 @@ fun BrowseSourceContent(
LibraryDisplayMode.ComfortableGrid -> { LibraryDisplayMode.ComfortableGrid -> {
BrowseSourceComfortableGrid( BrowseSourceComfortableGrid(
mangaList = mangaList, mangaList = mangaList,
getMangaState = getMangaState,
columns = columns, columns = columns,
contentPadding = contentPadding, contentPadding = contentPadding,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,
@ -300,16 +123,14 @@ fun BrowseSourceContent(
LibraryDisplayMode.List -> { LibraryDisplayMode.List -> {
BrowseSourceList( BrowseSourceList(
mangaList = mangaList, mangaList = mangaList,
getMangaState = getMangaState,
contentPadding = contentPadding, contentPadding = contentPadding,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick, onMangaLongClick = onMangaLongClick,
) )
} }
else -> { LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
BrowseSourceCompactGrid( BrowseSourceCompactGrid(
mangaList = mangaList, mangaList = mangaList,
getMangaState = getMangaState,
columns = columns, columns = columns,
contentPadding = contentPadding, contentPadding = contentPadding,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,

View File

@ -1,41 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Filter
import eu.kanade.tachiyomi.ui.browse.source.browse.toItems
@Stable
interface BrowseSourceState {
val source: CatalogueSource?
var searchQuery: String?
val currentFilter: Filter
val isUserQuery: Boolean
val filters: FilterList
val filterItems: List<IFlexible<*>>
var dialog: BrowseSourcePresenter.Dialog?
}
fun BrowseSourceState(initialQuery: String?): BrowseSourceState {
return when (val filter = Filter.valueOf(initialQuery ?: "")) {
Filter.Latest, Filter.Popular -> BrowseSourceStateImpl(initialCurrentFilter = filter)
is Filter.UserInput -> BrowseSourceStateImpl(initialQuery = initialQuery, initialCurrentFilter = filter)
}
}
class BrowseSourceStateImpl(initialQuery: String? = null, initialCurrentFilter: Filter) : BrowseSourceState {
override var source: CatalogueSource? by mutableStateOf(null)
override var searchQuery: String? by mutableStateOf(initialQuery)
override var currentFilter: Filter by mutableStateOf(initialCurrentFilter)
override val isUserQuery: Boolean by derivedStateOf { currentFilter is Filter.UserInput && currentFilter.query.isNotEmpty() }
override var filters: FilterList by mutableStateOf(FilterList())
override val filterItems: List<IFlexible<*>> by derivedStateOf { filters.toItems() }
override var dialog: BrowseSourcePresenter.Dialog? by mutableStateOf(null)
}

View File

@ -23,7 +23,6 @@ import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Divider
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
@ -39,40 +38,43 @@ 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.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
import eu.kanade.presentation.browse.components.ExtensionIcon import eu.kanade.presentation.browse.components.ExtensionIcon
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.DIVIDER_ALPHA 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.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.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.padding
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsPresenter import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsState
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable @Composable
fun ExtensionDetailsScreen( fun ExtensionDetailsScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
presenter: ExtensionDetailsPresenter, state: ExtensionDetailsState,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit,
onClickReadme: () -> Unit,
onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit,
onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
) { ) {
val uriHandler = LocalUriHandler.current
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
@ -81,19 +83,19 @@ fun ExtensionDetailsScreen(
actions = { actions = {
AppBarActions( AppBarActions(
actions = buildList { actions = buildList {
if (presenter.extension?.isUnofficial == false) { if (state.extension?.isUnofficial == false) {
add( add(
AppBar.Action( AppBar.Action(
title = stringResource(R.string.whats_new), title = stringResource(R.string.whats_new),
icon = Icons.Outlined.History, icon = Icons.Outlined.History,
onClick = { uriHandler.openUri(presenter.getChangelogUrl()) }, onClick = onClickWhatsNew,
), ),
) )
add( add(
AppBar.Action( AppBar.Action(
title = stringResource(R.string.action_faq_and_guides), title = stringResource(R.string.action_faq_and_guides),
icon = Icons.Outlined.HelpOutline, icon = Icons.Outlined.HelpOutline,
onClick = { uriHandler.openUri(presenter.getReadmeUrl()) }, onClick = onClickReadme,
), ),
) )
} }
@ -101,15 +103,15 @@ fun ExtensionDetailsScreen(
listOf( listOf(
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(R.string.action_enable_all), title = stringResource(R.string.action_enable_all),
onClick = { presenter.toggleSources(true) }, onClick = onClickEnableAll,
), ),
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(R.string.action_disable_all), title = stringResource(R.string.action_disable_all),
onClick = { presenter.toggleSources(false) }, onClick = onClickDisableAll,
), ),
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(R.string.pref_clear_cookies), title = stringResource(R.string.pref_clear_cookies),
onClick = { presenter.clearCookies() }, onClick = onClickClearCookies,
), ),
), ),
) )
@ -120,77 +122,85 @@ fun ExtensionDetailsScreen(
) )
}, },
) { paddingValues -> ) { paddingValues ->
ExtensionDetails(paddingValues, presenter, onClickSourcePreferences) if (state.extension == null) {
EmptyScreen(
textResource = R.string.empty_screen,
modifier = Modifier.padding(paddingValues),
)
return@Scaffold
}
ExtensionDetails(
contentPadding = paddingValues,
extension = state.extension,
sources = state.sources,
onClickSourcePreferences = onClickSourcePreferences,
onClickUninstall = onClickUninstall,
onClickSource = onClickSource,
)
} }
} }
@Composable @Composable
private fun ExtensionDetails( private fun ExtensionDetails(
contentPadding: PaddingValues, contentPadding: PaddingValues,
presenter: ExtensionDetailsPresenter, extension: Extension.Installed,
sources: List<ExtensionSourceItem>,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
) { ) {
when { val context = LocalContext.current
presenter.isLoading -> LoadingScreen() var showNsfwWarning by remember { mutableStateOf(false) }
presenter.extension == null -> EmptyScreen(
textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding),
)
else -> {
val context = LocalContext.current
val extension = presenter.extension
var showNsfwWarning by remember { mutableStateOf(false) }
ScrollbarLazyColumn(
contentPadding = contentPadding,
) {
when {
extension.isUnofficial ->
item {
WarningBanner(R.string.unofficial_extension_message)
}
extension.isObsolete ->
item {
WarningBanner(R.string.obsolete_extension_message)
}
}
ScrollbarLazyColumn(
contentPadding = contentPadding,
) {
when {
extension.isUnofficial ->
item { item {
DetailsHeader( WarningBanner(R.string.unofficial_extension_message)
extension = extension,
onClickUninstall = { presenter.uninstallExtension() },
onClickAppInfo = {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", extension.pkgName, null)
context.startActivity(this)
}
},
onClickAgeRating = {
showNsfwWarning = true
},
)
} }
extension.isObsolete ->
items( item {
items = presenter.sources, WarningBanner(R.string.obsolete_extension_message)
key = { it.source.id },
) { source ->
SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(),
source = source,
onClickSourcePreferences = onClickSourcePreferences,
onClickSource = { presenter.toggleSource(it) },
)
} }
}
if (showNsfwWarning) {
NsfwWarningDialog(
onClickConfirm = {
showNsfwWarning = false
},
)
}
} }
item {
DetailsHeader(
extension = extension,
onClickUninstall = onClickUninstall,
onClickAppInfo = {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", extension.pkgName, null)
context.startActivity(this)
}
},
onClickAgeRating = {
showNsfwWarning = true
},
)
}
items(
items = sources,
key = { it.source.id },
) { source ->
SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(),
source = source,
onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource,
)
}
}
if (showNsfwWarning) {
NsfwWarningDialog(
onClickConfirm = {
showNsfwWarning = false
},
)
} }
} }
@ -208,10 +218,10 @@ private fun DetailsHeader(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding( .padding(
start = horizontalPadding, start = MaterialTheme.padding.medium,
end = horizontalPadding, end = MaterialTheme.padding.medium,
top = 16.dp, top = MaterialTheme.padding.medium,
bottom = 8.dp, bottom = MaterialTheme.padding.small,
), ),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
@ -225,6 +235,7 @@ private fun DetailsHeader(
Text( Text(
text = extension.name, text = extension.name,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
) )
val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.") val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
@ -239,8 +250,8 @@ private fun DetailsHeader(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding( .padding(
horizontal = horizontalPadding * 2, horizontal = MaterialTheme.padding.extraLarge,
vertical = 8.dp, vertical = MaterialTheme.padding.small,
), ),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -277,10 +288,10 @@ private fun DetailsHeader(
Row( Row(
modifier = Modifier.padding( modifier = Modifier.padding(
start = horizontalPadding, start = MaterialTheme.padding.medium,
end = horizontalPadding, end = MaterialTheme.padding.medium,
top = 8.dp, top = MaterialTheme.padding.small,
bottom = 16.dp, bottom = MaterialTheme.padding.medium,
), ),
) { ) {
OutlinedButton( OutlinedButton(

View File

@ -1,25 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
@Stable
interface ExtensionDetailsState {
val isLoading: Boolean
val extension: Extension.Installed?
val sources: List<ExtensionSourceItem>
}
fun ExtensionDetailsState(): ExtensionDetailsState {
return ExtensionDetailsStateImpl()
}
class ExtensionDetailsStateImpl : ExtensionDetailsState {
override var isLoading: Boolean by mutableStateOf(true)
override var extension: Extension.Installed? by mutableStateOf(null)
override var sources: List<ExtensionSourceItem> by mutableStateOf(emptyList())
}

View File

@ -4,28 +4,24 @@ 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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.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.FastScrollLazyColumn import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget 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.ExtensionFilterState
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun ExtensionFilterScreen( fun ExtensionFilterScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
presenter: ExtensionFilterPresenter, state: ExtensionFilterState.Success,
onClickToggle: (String) -> Unit,
) { ) {
val context = LocalContext.current
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
@ -35,50 +31,37 @@ fun ExtensionFilterScreen(
) )
}, },
) { contentPadding -> ) { contentPadding ->
when { if (state.isEmpty) {
presenter.isLoading -> LoadingScreen() EmptyScreen(
presenter.isEmpty -> EmptyScreen(
textResource = R.string.empty_screen, textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
else -> ExtensionFilterContent( return@Scaffold
contentPadding = contentPadding,
state = presenter,
onClickLang = {
presenter.toggleLanguage(it)
},
)
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest {
when (it) {
ExtensionFilterPresenter.Event.FailedFetchingLanguages -> {
context.toast(R.string.internal_error)
}
}
} }
ExtensionFilterContent(
contentPadding = contentPadding,
state = state,
onClickLang = onClickToggle,
)
} }
} }
@Composable @Composable
private fun ExtensionFilterContent( private fun ExtensionFilterContent(
contentPadding: PaddingValues, contentPadding: PaddingValues,
state: ExtensionFilterState, state: ExtensionFilterState.Success,
onClickLang: (String) -> Unit, onClickLang: (String) -> Unit,
) { ) {
val context = LocalContext.current
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
items( items(state.languages) { language ->
items = state.items,
) { model ->
val lang = model.lang
SwitchPreferenceWidget( SwitchPreferenceWidget(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
title = LocaleHelper.getSourceDisplayName(lang, LocalContext.current), title = LocaleHelper.getSourceDisplayName(language, context),
checked = model.enabled, checked = language in state.enabledLanguages,
onCheckedChanged = { onClickLang(lang) }, onCheckedChanged = { onClickLang(language) },
) )
} }
} }

View File

@ -1,25 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.extension.FilterUiModel
@Stable
interface ExtensionFilterState {
val isLoading: Boolean
val items: List<FilterUiModel>
val isEmpty: Boolean
}
fun ExtensionFilterState(): ExtensionFilterState {
return ExtensionFilterStateImpl()
}
class ExtensionFilterStateImpl : ExtensionFilterState {
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<FilterUiModel> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@ -2,6 +2,7 @@ package eu.kanade.presentation.browse
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable
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
@ -31,6 +32,7 @@ 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.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
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
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -40,24 +42,27 @@ import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.FastScrollLazyColumn import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.SwipeRefresh import eu.kanade.presentation.components.PullRefresh
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
import eu.kanade.presentation.theme.header import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.padding
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.secondaryItemAlpha import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.presentation.util.topPaddingValues import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsState
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable @Composable
fun ExtensionScreen( fun ExtensionScreen(
presenter: ExtensionsPresenter, state: ExtensionsState,
contentPadding: PaddingValues, contentPadding: PaddingValues,
searchQuery: String? = null,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit,
onInstallExtension: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit,
@ -68,20 +73,27 @@ fun ExtensionScreen(
onClickUpdateAll: () -> Unit, onClickUpdateAll: () -> Unit,
onRefresh: () -> Unit, onRefresh: () -> Unit,
) { ) {
SwipeRefresh( PullRefresh(
refreshing = presenter.isRefreshing, refreshing = state.isRefreshing,
onRefresh = onRefresh, onRefresh = onRefresh,
enabled = !presenter.isLoading, enabled = !state.isLoading,
) { ) {
when { when {
presenter.isLoading -> LoadingScreen() state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
presenter.isEmpty -> EmptyScreen( state.isEmpty -> {
textResource = R.string.empty_screen, val msg = if (!searchQuery.isNullOrEmpty()) {
modifier = Modifier.padding(contentPadding), R.string.no_results_found
) } else {
R.string.empty_screen
}
EmptyScreen(
textResource = msg,
modifier = Modifier.padding(contentPadding),
)
}
else -> { else -> {
ExtensionContent( ExtensionContent(
state = presenter, state = state,
contentPadding = contentPadding, contentPadding = contentPadding,
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel, onClickItemCancel = onClickItemCancel,
@ -111,10 +123,29 @@ private fun ExtensionContent(
onClickUpdateAll: () -> Unit, onClickUpdateAll: () -> Unit,
) { ) {
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) } var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
val showMiuiWarning = DeviceUtil.isMiui && !DeviceUtil.isMiuiOptimizationDisabled()
val uriHandler = LocalUriHandler.current
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding + topPaddingValues, contentPadding = if (showMiuiWarning) {
contentPadding
} else {
contentPadding + topSmallPaddingValues
},
) { ) {
if (showMiuiWarning) {
item {
WarningBanner(
textRes = R.string.ext_miui_warning,
modifier = Modifier
.padding(bottom = MaterialTheme.padding.small)
.clickable {
uriHandler.openUri("https://tachiyomi.org/extensions")
},
)
}
}
items( items(
items = state.items, items = state.items,
contentType = { contentType = {
@ -272,7 +303,7 @@ private fun ExtensionItemContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier.padding(start = horizontalPadding), modifier = modifier.padding(start = MaterialTheme.padding.medium),
) { ) {
Text( Text(
text = extension.name, text = extension.name,
@ -396,7 +427,7 @@ private fun ExtensionHeader(
action: @Composable RowScope.() -> Unit = {}, action: @Composable RowScope.() -> Unit = {},
) { ) {
Row( Row(
modifier = modifier.padding(horizontal = horizontalPadding), modifier = modifier.padding(horizontal = MaterialTheme.padding.medium),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(

View File

@ -1,27 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
interface ExtensionsState {
val isLoading: Boolean
val isRefreshing: Boolean
val items: List<ExtensionUiModel>
val updates: Int
val isEmpty: Boolean
}
fun ExtensionState(): ExtensionsState {
return ExtensionsStateImpl()
}
class ExtensionsStateImpl : ExtensionsState {
override var isLoading: Boolean by mutableStateOf(true)
override var isRefreshing: Boolean by mutableStateOf(false)
override var items: List<ExtensionUiModel> by mutableStateOf(emptyList())
override var updates: Int by mutableStateOf(0)
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@ -0,0 +1,112 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.padding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchState
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable
fun GlobalSearchScreen(
state: GlobalSearchState,
navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit,
) {
Scaffold(
topBar = { scrollBehavior ->
GlobalSearchToolbar(
searchQuery = state.searchQuery,
progress = state.progress,
total = state.total,
navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
GlobalSearchContent(
items = state.items,
contentPadding = paddingValues,
getManga = getManga,
onClickSource = onClickSource,
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
)
}
}
@Composable
fun GlobalSearchContent(
items: Map<CatalogueSource, SearchItemResult>,
contentPadding: PaddingValues,
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit,
) {
LazyColumn(
contentPadding = contentPadding,
) {
items.forEach { (source, result) ->
item {
GlobalSearchResultItem(
title = source.name,
subtitle = LocaleHelper.getDisplayName(source.lang),
onClick = { onClickSource(source) },
) {
when (result) {
is SearchItemResult.Error -> {
GlobalSearchErrorResultItem(message = result.throwable.message)
}
SearchItemResult.Loading -> {
GlobalSearchLoadingResultItem()
}
is SearchItemResult.Success -> {
if (result.isEmpty) {
Text(
text = stringResource(R.string.no_results_found),
modifier = Modifier
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
)
return@GlobalSearchResultItem
}
GlobalSearchCardRow(
titles = result.result,
getManga = { getManga(source, it) },
onClick = onClickItem,
onLongClick = onLongClickItem,
)
}
}
}
}
}
}
}

View File

@ -4,31 +4,24 @@ 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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
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.MigrateMangaState
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter.Event
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun MigrateMangaScreen( fun MigrateMangaScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
title: String?, title: String?,
presenter: MigrateMangaPresenter, state: MigrateMangaState,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit, onClickCover: (Manga) -> Unit,
) { ) {
val context = LocalContext.current
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
@ -38,30 +31,20 @@ fun MigrateMangaScreen(
) )
}, },
) { contentPadding -> ) { contentPadding ->
when { if (state.isEmpty) {
presenter.isLoading -> LoadingScreen() EmptyScreen(
presenter.isEmpty -> EmptyScreen(
textResource = R.string.empty_screen, textResource = R.string.empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
else -> { return@Scaffold
MigrateMangaContent(
contentPadding = contentPadding,
state = presenter,
onClickItem = onClickItem,
onClickCover = onClickCover,
)
}
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
Event.FailedFetchingFavorites -> {
context.toast(R.string.internal_error)
}
}
} }
MigrateMangaContent(
contentPadding = contentPadding,
state = state,
onClickItem = onClickItem,
onClickCover = onClickCover,
)
} }
} }
@ -75,7 +58,7 @@ private fun MigrateMangaContent(
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
items(state.items) { manga -> items(state.titles) { manga ->
MigrateMangaItem( MigrateMangaItem(
manga = manga, manga = manga,
onClickItem = onClickItem, onClickItem = onClickItem,

View File

@ -1,23 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.domain.manga.model.Manga
interface MigrateMangaState {
val isLoading: Boolean
val items: List<Manga>
val isEmpty: Boolean
}
fun MigrationMangaState(): MigrateMangaState {
return MigrateMangaStateImpl()
}
class MigrateMangaStateImpl : MigrateMangaState {
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<Manga> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@ -0,0 +1,101 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
import eu.kanade.presentation.browse.components.GlobalSearchEmptyResultItem
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchState
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable
fun MigrateSearchScreen(
navigateUp: () -> Unit,
state: MigrateSearchState,
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit,
) {
Scaffold(
topBar = { scrollBehavior ->
GlobalSearchToolbar(
searchQuery = state.searchQuery,
progress = state.progress,
total = state.total,
navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
MigrateSearchContent(
sourceId = state.manga?.source ?: -1,
items = state.items,
contentPadding = paddingValues,
getManga = getManga,
onClickSource = onClickSource,
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
)
}
}
@Composable
fun MigrateSearchContent(
sourceId: Long,
items: Map<CatalogueSource, SearchItemResult>,
contentPadding: PaddingValues,
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit,
) {
LazyColumn(
contentPadding = contentPadding,
) {
items.forEach { (source, result) ->
item {
GlobalSearchResultItem(
title = if (source.id == sourceId) "${source.name}" else source.name,
subtitle = LocaleHelper.getDisplayName(source.lang),
onClick = { onClickSource(source) },
) {
when (result) {
is SearchItemResult.Error -> {
GlobalSearchErrorResultItem(message = result.throwable.message)
}
SearchItemResult.Loading -> {
GlobalSearchLoadingResultItem()
}
is SearchItemResult.Success -> {
if (result.isEmpty) {
GlobalSearchEmptyResultItem()
return@GlobalSearchResultItem
}
GlobalSearchCardRow(
titles = result.result,
getManga = { getManga(source, it) },
onClick = onClickItem,
onLongClick = onLongClickItem,
)
}
}
}
}
}
}
}

View File

@ -34,40 +34,42 @@ import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
import eu.kanade.presentation.theme.header import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.padding
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.secondaryItemAlpha import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.presentation.util.topPaddingValues import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
@Composable @Composable
fun MigrateSourceScreen( fun MigrateSourceScreen(
presenter: MigrationSourcesPresenter, state: MigrateSourceState,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onToggleSortingDirection: () -> Unit,
onToggleSortingMode: () -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
when { when {
presenter.isLoading -> LoadingScreen() state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
presenter.isEmpty -> EmptyScreen( state.isEmpty -> EmptyScreen(
textResource = R.string.information_empty_library, textResource = R.string.information_empty_library,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
else -> else ->
MigrateSourceList( MigrateSourceList(
list = presenter.items, list = state.items,
contentPadding = contentPadding, contentPadding = contentPadding,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = { source -> onLongClickItem = { source ->
val sourceId = source.id.toString() val sourceId = source.id.toString()
context.copyToClipboard(sourceId, sourceId) context.copyToClipboard(sourceId, sourceId)
}, },
sortingMode = presenter.sortingMode, sortingMode = state.sortingMode,
onToggleSortingMode = { presenter.toggleSortingMode() }, onToggleSortingMode = onToggleSortingMode,
sortingDirection = presenter.sortingDirection, sortingDirection = state.sortingDirection,
onToggleSortingDirection = { presenter.toggleSortingDirection() }, onToggleSortingDirection = onToggleSortingDirection,
) )
} }
} }
@ -84,13 +86,13 @@ private fun MigrateSourceList(
onToggleSortingDirection: () -> Unit, onToggleSortingDirection: () -> Unit,
) { ) {
ScrollbarLazyColumn( ScrollbarLazyColumn(
contentPadding = contentPadding + topPaddingValues, contentPadding = contentPadding + topSmallPaddingValues,
) { ) {
stickyHeader(key = STICKY_HEADER_KEY_PREFIX) { stickyHeader(key = STICKY_HEADER_KEY_PREFIX) {
Row( Row(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
.padding(start = horizontalPadding), .padding(start = MaterialTheme.padding.medium),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
@ -152,7 +154,7 @@ private fun MigrateSourceItem(
content = { _, sourceLangString -> content = { _, sourceLangString ->
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = horizontalPadding) .padding(horizontal = MaterialTheme.padding.medium)
.weight(1f), .weight(1f),
) { ) {
Text( Text(

View File

@ -1,28 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.Source
interface MigrateSourceState {
val isLoading: Boolean
val items: List<Pair<Source, Long>>
val isEmpty: Boolean
val sortingMode: SetMigrateSorting.Mode
val sortingDirection: SetMigrateSorting.Direction
}
fun MigrateSourceState(): MigrateSourceState {
return MigrateSourceStateImpl()
}
class MigrateSourceStateImpl : MigrateSourceState {
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<Pair<Source, Long>> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
override var sortingMode: SetMigrateSorting.Mode by mutableStateOf(SetMigrateSorting.Mode.ALPHABETICAL)
override var sortingDirection: SetMigrateSorting.Direction by mutableStateOf(SetMigrateSorting.Direction.ASCENDING)
}

View File

@ -1,72 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalUriHandler
import androidx.paging.compose.collectAsLazyPagingItems
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.more.MoreController
@Composable
fun SourceSearchScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
onFabClick: () -> Unit,
onMangaClick: (Manga) -> Unit,
onWebViewClick: () -> Unit,
) {
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
val snackbarHostState = remember { SnackbarHostState() }
val uriHandler = LocalUriHandler.current
val onHelpClick = {
uriHandler.openUri(LocalSource.HELP_URL)
}
Scaffold(
topBar = { scrollBehavior ->
SearchToolbar(
searchQuery = presenter.searchQuery ?: "",
onChangeSearchQuery = { presenter.searchQuery = it },
onClickCloseSearch = navigateUp,
onSearch = { presenter.search(it) },
scrollBehavior = scrollBehavior,
)
},
floatingActionButton = {
BrowseSourceFloatingActionButton(
isVisible = presenter.filters.isNotEmpty(),
onFabClick = onFabClick,
)
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
) { paddingValues ->
BrowseSourceContent(
state = presenter,
mangaList = mangaList,
getMangaState = { presenter.getManga(it) },
columns = columns,
displayMode = presenter.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = onWebViewClick,
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
onLocalSourceHelpClick = onHelpClick,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaClick,
)
}
}

View File

@ -5,7 +5,6 @@ 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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.res.stringResource import androidx.compose.ui.res.stringResource
@ -14,24 +13,19 @@ 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.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget 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.SourcesFilterState
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun SourcesFilterScreen( fun SourcesFilterScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
presenter: SourcesFilterPresenter, state: SourcesFilterState.Success,
onClickLang: (String) -> Unit, onClickLanguage: (String) -> Unit,
onClickSource: (Source) -> Unit, onClickSource: (Source) -> Unit,
) { ) {
val context = LocalContext.current
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
@ -41,69 +35,55 @@ fun SourcesFilterScreen(
) )
}, },
) { contentPadding -> ) { contentPadding ->
when { if (state.isEmpty) {
presenter.isLoading -> LoadingScreen() EmptyScreen(
presenter.isEmpty -> EmptyScreen(
textResource = R.string.source_filter_empty_screen, textResource = R.string.source_filter_empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
else -> { return@Scaffold
SourcesFilterContent(
contentPadding = contentPadding,
state = presenter,
onClickLang = onClickLang,
onClickSource = onClickSource,
)
}
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
SourcesFilterPresenter.Event.FailedFetchingLanguages -> {
context.toast(R.string.internal_error)
}
}
} }
SourcesFilterContent(
contentPadding = contentPadding,
state = state,
onClickLanguage = onClickLanguage,
onClickSource = onClickSource,
)
} }
} }
@Composable @Composable
private fun SourcesFilterContent( private fun SourcesFilterContent(
contentPadding: PaddingValues, contentPadding: PaddingValues,
state: SourcesFilterState, state: SourcesFilterState.Success,
onClickLang: (String) -> Unit, onClickLanguage: (String) -> Unit,
onClickSource: (Source) -> Unit, onClickSource: (Source) -> Unit,
) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
items( state.items.forEach { (language, sources) ->
items = state.items, val enabled = language in state.enabledLanguages
contentType = { item(
when (it) { key = language.hashCode(),
is FilterUiModel.Header -> "header" contentType = "source-filter-header",
is FilterUiModel.Item -> "item" ) {
} SourcesFilterHeader(
},
key = {
when (it) {
is FilterUiModel.Header -> it.hashCode()
is FilterUiModel.Item -> "source-filter-${it.source.key()}"
}
},
) { model ->
when (model) {
is FilterUiModel.Header -> SourcesFilterHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
language = model.language, language = language,
enabled = model.enabled, enabled = enabled,
onClickItem = onClickLang, onClickItem = onClickLanguage,
) )
is FilterUiModel.Item -> SourcesFilterItem( }
if (!enabled) return@forEach
items(
items = sources,
key = { "source-filter-${it.key()}" },
contentType = { "source-filter-item" },
) { source ->
SourcesFilterItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
source = model.source, source = source,
enabled = model.enabled, enabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource, onClickItem = onClickSource,
) )
} }

View File

@ -1,23 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel
interface SourcesFilterState {
val isLoading: Boolean
val items: List<FilterUiModel>
val isEmpty: Boolean
}
fun SourcesFilterState(): SourcesFilterState {
return SourcesFilterStateImpl()
}
class SourcesFilterStateImpl : SourcesFilterState {
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<FilterUiModel> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@ -17,12 +17,10 @@ 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.LaunchedEffect
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.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.domain.source.model.Pin import eu.kanade.domain.source.model.Pin
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
@ -30,113 +28,69 @@ import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.theme.header import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.padding
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topPaddingValues import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter import eu.kanade.tachiyomi.ui.browse.source.SourcesState
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun SourcesScreen( fun SourcesScreen(
presenter: SourcesPresenter, state: SourcesState,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onClickItem: (Source, String) -> Unit, onClickItem: (Source, Listing) -> Unit,
onClickDisable: (Source) -> Unit,
onClickPin: (Source) -> Unit, onClickPin: (Source) -> Unit,
onLongClickItem: (Source) -> Unit,
) { ) {
val context = LocalContext.current
when { when {
presenter.isLoading -> LoadingScreen() state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
presenter.isEmpty -> EmptyScreen( state.isEmpty -> EmptyScreen(
textResource = R.string.source_empty_screen, textResource = R.string.source_empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
else -> { else -> {
SourceList( ScrollbarLazyColumn(
state = presenter, contentPadding = contentPadding + topSmallPaddingValues,
contentPadding = contentPadding, ) {
onClickItem = onClickItem, items(
onClickDisable = onClickDisable, items = state.items,
onClickPin = onClickPin, contentType = {
) when (it) {
} is SourceUiModel.Header -> "header"
} is SourceUiModel.Item -> "item"
LaunchedEffect(Unit) { }
presenter.events.collectLatest { event -> },
when (event) { key = {
SourcesPresenter.Event.FailedFetchingSources -> { when (it) {
context.toast(R.string.internal_error) is SourceUiModel.Header -> it.hashCode()
is SourceUiModel.Item -> "source-${it.source.key()}"
}
},
) { model ->
when (model) {
is SourceUiModel.Header -> {
SourceHeader(
modifier = Modifier.animateItemPlacement(),
language = model.language,
)
}
is SourceUiModel.Item -> SourceItem(
modifier = Modifier.animateItemPlacement(),
source = model.source,
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
onClickPin = onClickPin,
)
}
} }
} }
} }
} }
} }
@Composable
private fun SourceList(
state: SourcesState,
contentPadding: PaddingValues,
onClickItem: (Source, String) -> Unit,
onClickDisable: (Source) -> Unit,
onClickPin: (Source) -> Unit,
) {
ScrollbarLazyColumn(
contentPadding = contentPadding + topPaddingValues,
) {
items(
items = state.items,
contentType = {
when (it) {
is SourceUiModel.Header -> "header"
is SourceUiModel.Item -> "item"
}
},
key = {
when (it) {
is SourceUiModel.Header -> it.hashCode()
is SourceUiModel.Item -> "source-${it.source.key()}"
}
},
) { model ->
when (model) {
is SourceUiModel.Header -> {
SourceHeader(
modifier = Modifier.animateItemPlacement(),
language = model.language,
)
}
is SourceUiModel.Item -> SourceItem(
modifier = Modifier.animateItemPlacement(),
source = model.source,
onClickItem = onClickItem,
onLongClickItem = { state.dialog = SourcesPresenter.Dialog(it) },
onClickPin = onClickPin,
)
}
}
}
if (state.dialog != null) {
val source = state.dialog!!.source
SourceOptionsDialog(
source = source,
onClickPin = {
onClickPin(source)
state.dialog = null
},
onClickDisable = {
onClickDisable(source)
state.dialog = null
},
onDismiss = { state.dialog = null },
)
}
}
@Composable @Composable
private fun SourceHeader( private fun SourceHeader(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -146,7 +100,7 @@ private fun SourceHeader(
Text( Text(
text = LocaleHelper.getSourceDisplayName(language, context), text = LocaleHelper.getSourceDisplayName(language, context),
modifier = modifier modifier = modifier
.padding(horizontal = horizontalPadding, vertical = 8.dp), .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
style = MaterialTheme.typography.header, style = MaterialTheme.typography.header,
) )
} }
@ -155,18 +109,18 @@ private fun SourceHeader(
private fun SourceItem( private fun SourceItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
source: Source, source: Source,
onClickItem: (Source, String) -> Unit, onClickItem: (Source, Listing) -> Unit,
onLongClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit,
onClickPin: (Source) -> Unit, onClickPin: (Source) -> Unit,
) { ) {
BaseSourceItem( BaseSourceItem(
modifier = modifier, modifier = modifier,
source = source, source = source,
onClickItem = { onClickItem(source, GetRemoteManga.QUERY_POPULAR) }, onClickItem = { onClickItem(source, Listing.Popular) },
onLongClickItem = { onLongClickItem(source) }, onLongClickItem = { onLongClickItem(source) },
action = { action = {
if (source.supportsLatest) { if (source.supportsLatest) {
TextButton(onClick = { onClickItem(source, GetRemoteManga.QUERY_LATEST) }) { TextButton(onClick = { onClickItem(source, Listing.Latest) }) {
Text( Text(
text = stringResource(R.string.latest), text = stringResource(R.string.latest),
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
@ -201,7 +155,7 @@ private fun SourcePinButton(
} }
@Composable @Composable
private fun SourceOptionsDialog( fun SourceOptionsDialog(
source: Source, source: Source,
onClickPin: () -> Unit, onClickPin: () -> Unit,
onClickDisable: () -> Unit, onClickDisable: () -> Unit,

View File

@ -1,27 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
@Stable
interface SourcesState {
var dialog: SourcesPresenter.Dialog?
val isLoading: Boolean
val items: List<SourceUiModel>
val isEmpty: Boolean
}
fun SourcesState(): SourcesState {
return SourcesStateImpl()
}
class SourcesStateImpl : SourcesState {
override var dialog: SourcesPresenter.Dialog? by mutableStateOf(null)
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<SourceUiModel> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@ -4,11 +4,11 @@ import androidx.compose.foundation.combinedClickable
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.material3.MaterialTheme
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.unit.dp import eu.kanade.presentation.util.padding
import eu.kanade.presentation.util.horizontalPadding
@Composable @Composable
fun BaseBrowseItem( fun BaseBrowseItem(
@ -25,7 +25,7 @@ fun BaseBrowseItem(
onClick = onClickItem, onClick = onClickItem,
onLongClick = onLongClickItem, onLongClick = onLongClickItem,
) )
.padding(horizontal = horizontalPadding, vertical = 8.dp), .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
icon() icon()

View File

@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.padding
import eu.kanade.presentation.util.secondaryItemAlpha import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
@ -43,7 +43,7 @@ private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source ->
private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString -> private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString ->
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = horizontalPadding) .padding(horizontal = MaterialTheme.padding.medium)
.weight(1f), .weight(1f),
) { ) {
Text( Text(

View File

@ -62,7 +62,7 @@ fun SourceIcon(
} }
else -> { else -> {
Image( Image(
painter = painterResource(id = R.mipmap.ic_local_source), painter = painterResource(R.mipmap.ic_local_source),
contentDescription = null, contentDescription = null,
modifier = modifier.then(defaultModifier), modifier = modifier.then(defaultModifier),
) )

View File

@ -6,24 +6,22 @@ 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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
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.domain.manga.model.MangaCover
import eu.kanade.presentation.components.Badge import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.components.CommonMangaItemDefaults import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.MangaComfortableGridItem import eu.kanade.presentation.components.MangaComfortableGridItem
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun BrowseSourceComfortableGrid( fun BrowseSourceComfortableGrid(
mangaList: LazyPagingItems<Manga>, mangaList: LazyPagingItems<StateFlow<Manga>>,
getMangaState: @Composable ((Manga) -> State<Manga>),
columns: GridCells, columns: GridCells,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
@ -42,8 +40,7 @@ fun BrowseSourceComfortableGrid(
} }
items(mangaList.itemCount) { index -> items(mangaList.itemCount) { index ->
val initialManga = mangaList[index] ?: return@items val manga by mangaList[index]?.collectAsState() ?: return@items
val manga by getMangaState(initialManga)
BrowseSourceComfortableGridItem( BrowseSourceComfortableGridItem(
manga = manga, manga = manga,
onClick = { onMangaClick(manga) }, onClick = { onMangaClick(manga) },
@ -76,9 +73,7 @@ fun BrowseSourceComfortableGridItem(
), ),
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
coverBadgeStart = { coverBadgeStart = {
if (manga.favorite) { InLibraryBadge(enabled = manga.favorite)
Badge(text = stringResource(R.string.in_library))
}
}, },
onLongClick = onLongClick, onLongClick = onLongClick,
onClick = onClick, onClick = onClick,

View File

@ -6,24 +6,22 @@ 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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
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.domain.manga.model.MangaCover
import eu.kanade.presentation.components.Badge import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.components.CommonMangaItemDefaults import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.MangaCompactGridItem import eu.kanade.presentation.components.MangaCompactGridItem
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun BrowseSourceCompactGrid( fun BrowseSourceCompactGrid(
mangaList: LazyPagingItems<Manga>, mangaList: LazyPagingItems<StateFlow<Manga>>,
getMangaState: @Composable ((Manga) -> State<Manga>),
columns: GridCells, columns: GridCells,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
@ -42,8 +40,7 @@ fun BrowseSourceCompactGrid(
} }
items(mangaList.itemCount) { index -> items(mangaList.itemCount) { index ->
val initialManga = mangaList[index] ?: return@items val manga by mangaList[index]?.collectAsState() ?: return@items
val manga by getMangaState(initialManga)
BrowseSourceCompactGridItem( BrowseSourceCompactGridItem(
manga = manga, manga = manga,
onClick = { onMangaClick(manga) }, onClick = { onMangaClick(manga) },
@ -76,9 +73,7 @@ private fun BrowseSourceCompactGridItem(
), ),
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
coverBadgeStart = { coverBadgeStart = {
if (manga.favorite) { InLibraryBadge(enabled = manga.favorite)
Badge(text = stringResource(R.string.in_library))
}
}, },
onLongClick = onLongClick, onLongClick = onLongClick,
onClick = onClick, onClick = onClick,

View File

@ -2,26 +2,24 @@ package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
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 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.domain.manga.model.MangaCover
import eu.kanade.presentation.components.Badge import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.components.CommonMangaItemDefaults import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.LazyColumn import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.MangaListItem import eu.kanade.presentation.components.MangaListItem
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun BrowseSourceList( fun BrowseSourceList(
mangaList: LazyPagingItems<Manga>, mangaList: LazyPagingItems<StateFlow<Manga>>,
getMangaState: @Composable ((Manga) -> State<Manga>),
contentPadding: PaddingValues, contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit,
@ -35,9 +33,9 @@ fun BrowseSourceList(
} }
} }
items(mangaList) { initialManga -> items(mangaList) { mangaflow ->
initialManga ?: return@items mangaflow ?: return@items
val manga by getMangaState(initialManga) val manga by mangaflow.collectAsState()
BrowseSourceListItem( BrowseSourceListItem(
manga = manga, manga = manga,
onClick = { onMangaClick(manga) }, onClick = { onMangaClick(manga) },
@ -70,9 +68,7 @@ fun BrowseSourceListItem(
), ),
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
badge = { badge = {
if (manga.favorite) { InLibraryBadge(enabled = manga.favorite)
Badge(text = stringResource(R.string.in_library))
}
}, },
onLongClick = onLongClick, onLongClick = onLongClick,
onClick = onClick, onClick = onClick,

View File

@ -14,7 +14,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.domain.library.model.LibraryDisplayMode import eu.kanade.domain.library.model.LibraryDisplayMode
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.AppBarTitle
@ -27,7 +26,8 @@ import eu.kanade.tachiyomi.source.LocalSource
@Composable @Composable
fun BrowseSourceToolbar( fun BrowseSourceToolbar(
state: BrowseSourceState, searchQuery: String?,
onSearchQueryChange: (String?) -> Unit,
source: CatalogueSource?, source: CatalogueSource?,
displayMode: LibraryDisplayMode, displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit, onDisplayModeChange: (LibraryDisplayMode) -> Unit,
@ -44,8 +44,8 @@ fun BrowseSourceToolbar(
SearchToolbar( SearchToolbar(
navigateUp = navigateUp, navigateUp = navigateUp,
titleContent = { AppBarTitle(title) }, titleContent = { AppBarTitle(title) },
searchQuery = state.searchQuery, searchQuery = searchQuery,
onChangeSearchQuery = { state.searchQuery = it }, onChangeSearchQuery = onSearchQueryChange,
onSearch = onSearch, onSearch = onSearch,
onClickCloseSearch = navigateUp, onClickCloseSearch = navigateUp,
actions = { actions = {

View File

@ -0,0 +1,40 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.asMangaCover
import eu.kanade.presentation.util.padding
@Composable
fun GlobalSearchCardRow(
titles: List<Manga>,
getManga: @Composable (Manga) -> State<Manga>,
onClick: (Manga) -> Unit,
onLongClick: (Manga) -> Unit,
) {
LazyRow(
contentPadding = PaddingValues(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
items(titles) { title ->
val title by getManga(title)
GlobalSearchCard(
title = title.title,
cover = title.asMangaCover(),
isFavorite = title.favorite,
onClick = { onClick(title) },
onLongClick = { onLongClick(title) },
)
}
}
}

View File

@ -0,0 +1,111 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Error
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.padding
import eu.kanade.tachiyomi.R
@Composable
fun GlobalSearchResultItem(
title: String,
subtitle: String,
onClick: () -> Unit,
content: @Composable () -> Unit,
) {
Column {
Row(
modifier = Modifier
.padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.tiny,
)
.fillMaxWidth()
.clickable(onClick = onClick),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
)
Text(text = subtitle)
}
IconButton(onClick = onClick) {
Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null)
}
}
content()
}
}
@Composable
fun GlobalSearchEmptyResultItem() {
Text(
text = stringResource(R.string.no_results_found),
modifier = Modifier
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
)
}
@Composable
fun GlobalSearchLoadingResultItem() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MaterialTheme.padding.medium),
) {
CircularProgressIndicator(
modifier = Modifier
.size(16.dp)
.align(Alignment.Center),
strokeWidth = 2.dp,
)
}
}
@Composable
fun GlobalSearchErrorResultItem(message: String?) {
Column(
modifier = Modifier
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Icon(imageVector = Icons.Outlined.Error, contentDescription = null)
Spacer(Modifier.height(4.dp))
Text(
text = message ?: stringResource(R.string.unknown_error),
textAlign = TextAlign.Center,
)
}
}

View File

@ -0,0 +1,40 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import eu.kanade.presentation.components.SearchToolbar
@Composable
fun GlobalSearchToolbar(
searchQuery: String?,
progress: Int,
total: Int,
navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
) {
Box {
SearchToolbar(
searchQuery = searchQuery,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
onClickCloseSearch = navigateUp,
navigateUp = navigateUp,
scrollBehavior = scrollBehavior,
)
if (progress in 1 until total) {
LinearProgressIndicator(
progress = progress / total.toFloat(),
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth(),
)
}
}
}

View File

@ -0,0 +1,33 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.MangaComfortableGridItem
@Composable
fun GlobalSearchCard(
title: String,
cover: MangaCover,
isFavorite: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
) {
Box(modifier = Modifier.width(128.dp)) {
MangaComfortableGridItem(
title = title,
coverData = cover,
coverBadgeStart = {
InLibraryBadge(enabled = isFavorite)
},
coverAlpha = if (isFavorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
onClick = onClick,
onLongClick = onLongClick,
)
}
}

View File

@ -3,32 +3,30 @@ package eu.kanade.presentation.category
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.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.category.components.CategoryContent import eu.kanade.presentation.category.components.CategoryContent
import eu.kanade.presentation.category.components.CategoryCreateDialog
import eu.kanade.presentation.category.components.CategoryDeleteDialog
import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.CategoryRenameDialog
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.LoadingScreen
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.padding
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topPaddingValues import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.category.CategoryPresenter import eu.kanade.tachiyomi.ui.category.CategoryScreenState
import eu.kanade.tachiyomi.ui.category.CategoryPresenter.Dialog
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun CategoryScreen( fun CategoryScreen(
presenter: CategoryPresenter, state: CategoryScreenState.Success,
onClickCreate: () -> Unit,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
onClickMoveUp: (Category) -> Unit,
onClickMoveDown: (Category) -> Unit,
navigateUp: () -> Unit, navigateUp: () -> Unit,
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
@ -43,63 +41,26 @@ fun CategoryScreen(
floatingActionButton = { floatingActionButton = {
CategoryFloatingActionButton( CategoryFloatingActionButton(
lazyListState = lazyListState, lazyListState = lazyListState,
onCreate = { presenter.dialog = Dialog.Create }, onCreate = onClickCreate,
) )
}, },
) { paddingValues -> ) { paddingValues ->
val context = LocalContext.current if (state.isEmpty) {
when { EmptyScreen(
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(
textResource = R.string.information_empty_category, textResource = R.string.information_empty_category,
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
) )
else -> { return@Scaffold
CategoryContent(
state = presenter,
lazyListState = lazyListState,
paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding),
onMoveUp = { presenter.moveUp(it) },
onMoveDown = { presenter.moveDown(it) },
)
}
} }
val onDismissRequest = { presenter.dialog = null } CategoryContent(
when (val dialog = presenter.dialog) { categories = state.categories,
Dialog.Create -> { lazyListState = lazyListState,
CategoryCreateDialog( paddingValues = paddingValues + topSmallPaddingValues + PaddingValues(horizontal = MaterialTheme.padding.medium),
onDismissRequest = onDismissRequest, onClickRename = onClickRename,
onCreate = { presenter.createCategory(it) }, onClickDelete = onClickDelete,
) onMoveUp = onClickMoveUp,
} onMoveDown = onClickMoveDown,
is Dialog.Rename -> { )
CategoryRenameDialog(
onDismissRequest = onDismissRequest,
onRename = { presenter.renameCategory(dialog.category, it) },
category = dialog.category,
)
}
is Dialog.Delete -> {
CategoryDeleteDialog(
onDismissRequest = onDismissRequest,
onDelete = { presenter.deleteCategory(dialog.category) },
category = dialog.category,
)
}
else -> {}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
is CategoryPresenter.Event.CategoryWithNameAlreadyExists -> {
context.toast(R.string.error_category_exists)
}
is CategoryPresenter.Event.InternalError -> {
context.toast(R.string.internal_error)
}
}
}
}
} }
} }

View File

@ -1,28 +0,0 @@
package eu.kanade.presentation.category
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.domain.category.model.Category
import eu.kanade.tachiyomi.ui.category.CategoryPresenter
@Stable
interface CategoryState {
val isLoading: Boolean
var dialog: CategoryPresenter.Dialog?
val categories: List<Category>
val isEmpty: Boolean
}
fun CategoryState(): CategoryState {
return CategoryStateImpl()
}
class CategoryStateImpl : CategoryState {
override var isLoading: Boolean by mutableStateOf(true)
override var dialog: CategoryPresenter.Dialog? by mutableStateOf(null)
override var categories: List<Category> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { categories.isEmpty() }
}

View File

@ -8,19 +8,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.category.CategoryState
import eu.kanade.presentation.components.LazyColumn import eu.kanade.presentation.components.LazyColumn
import eu.kanade.tachiyomi.ui.category.CategoryPresenter.Dialog
@Composable @Composable
fun CategoryContent( fun CategoryContent(
state: CategoryState, categories: List<Category>,
lazyListState: LazyListState, lazyListState: LazyListState,
paddingValues: PaddingValues, paddingValues: PaddingValues,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
onMoveUp: (Category) -> Unit, onMoveUp: (Category) -> Unit,
onMoveDown: (Category) -> Unit, onMoveDown: (Category) -> Unit,
) { ) {
val categories = state.categories
LazyColumn( LazyColumn(
state = lazyListState, state = lazyListState,
contentPadding = paddingValues, contentPadding = paddingValues,
@ -37,8 +36,8 @@ fun CategoryContent(
canMoveDown = index != categories.lastIndex, canMoveDown = index != categories.lastIndex,
onMoveUp = onMoveUp, onMoveUp = onMoveUp,
onMoveDown = onMoveDown, onMoveDown = onMoveDown,
onRename = { state.dialog = Dialog.Rename(category) }, onRename = { onClickRename(category) },
onDelete = { state.dialog = Dialog.Delete(category) }, onDelete = { onClickDelete(category) },
) )
} }
} }

View File

@ -14,13 +14,14 @@ import androidx.compose.material.icons.outlined.Label
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
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.Text 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 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.padding
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@Composable @Composable
@ -41,14 +42,18 @@ fun CategoryListItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onRename() } .clickable { onRename() }
.padding(start = horizontalPadding, top = horizontalPadding, end = horizontalPadding), .padding(
start = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon(imageVector = Icons.Outlined.Label, contentDescription = "") Icon(imageVector = Icons.Outlined.Label, contentDescription = "")
Text( Text(
text = category.name, text = category.name,
modifier = Modifier modifier = Modifier
.padding(start = horizontalPadding), .padding(start = MaterialTheme.padding.medium),
) )
} }
Row { Row {

View File

@ -0,0 +1,344 @@
package eu.kanade.presentation.components
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.with
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.SwipeableState
import androidx.compose.material.rememberSwipeableState
import androidx.compose.material.swipeable
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.ScreenTransition
import eu.kanade.presentation.util.isTabletUi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.milliseconds
private const val SheetAnimationDuration = 500
private val SheetAnimationSpec = tween<Float>(durationMillis = SheetAnimationDuration)
private const val ScrimAnimationDuration = 350
private val ScrimAnimationSpec = tween<Float>(durationMillis = ScrimAnimationDuration)
@Composable
fun NavigatorAdaptiveSheet(
screen: Screen,
tonalElevation: Dp = 1.dp,
enableSwipeDismiss: (Navigator) -> Boolean = { true },
onDismissRequest: () -> Unit,
) {
Navigator(
screen = screen,
content = { sheetNavigator ->
AdaptiveSheet(
tonalElevation = tonalElevation,
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
onDismissRequest = onDismissRequest,
) {
ScreenTransition(
navigator = sheetNavigator,
transition = {
fadeIn(animationSpec = tween(220, delayMillis = 90)) with
fadeOut(animationSpec = tween(90))
},
)
BackHandler(
enabled = sheetNavigator.size > 1,
onBack = sheetNavigator::pop,
)
}
// Make sure screens are disposed no matter what
if (sheetNavigator.parent?.disposeBehavior?.disposeNestedNavigators == false) {
DisposableEffectIgnoringConfiguration {
onDispose {
sheetNavigator.items
.asReversed()
.forEach(sheetNavigator::dispose)
}
}
}
},
)
}
/**
* Sheet with adaptive position aligned to bottom on small screen, otherwise aligned to center
* and will not be able to dismissed with swipe gesture.
*
* Max width of the content is set to 460 dp.
*/
@Composable
fun AdaptiveSheet(
tonalElevation: Dp = 1.dp,
enableSwipeDismiss: Boolean = true,
onDismissRequest: () -> Unit,
content: @Composable (PaddingValues) -> Unit,
) {
val isTabletUi = isTabletUi()
AdaptiveSheetImpl(
isTabletUi = isTabletUi,
tonalElevation = tonalElevation,
enableSwipeDismiss = enableSwipeDismiss,
onDismissRequest = onDismissRequest,
) {
val contentPadding = if (isTabletUi) {
PaddingValues()
} else {
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
}
content(contentPadding)
}
}
@Composable
fun AdaptiveSheetImpl(
isTabletUi: Boolean,
tonalElevation: Dp,
enableSwipeDismiss: Boolean,
onDismissRequest: () -> Unit,
content: @Composable () -> Unit,
) {
val scope = rememberCoroutineScope()
if (isTabletUi) {
var targetAlpha by remember { mutableStateOf(0f) }
val alpha by animateFloatAsState(
targetValue = targetAlpha,
animationSpec = ScrimAnimationSpec,
)
val internalOnDismissRequest: () -> Unit = {
scope.launch {
targetAlpha = 0f
delay(ScrimAnimationSpec.durationMillis.milliseconds)
onDismissRequest()
}
}
BoxWithConstraints(
modifier = Modifier
.clickable(
enabled = true,
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = internalOnDismissRequest,
)
.fillMaxSize()
.alpha(alpha),
contentAlignment = Alignment.Center,
) {
Box(
modifier = Modifier
.matchParentSize()
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
)
Surface(
modifier = Modifier
.requiredWidthIn(max = 460.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {},
)
.systemBarsPadding()
.padding(vertical = 16.dp),
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = tonalElevation,
content = {
BackHandler(enabled = alpha > 0f, onBack = internalOnDismissRequest)
content()
},
)
LaunchedEffect(Unit) {
targetAlpha = 1f
}
}
} else {
val swipeState = rememberSwipeableState(
initialValue = 1,
animationSpec = SheetAnimationSpec,
)
val internalOnDismissRequest: () -> Unit = { if (swipeState.currentValue == 0) scope.launch { swipeState.animateTo(1) } }
BoxWithConstraints(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = internalOnDismissRequest,
)
.fillMaxSize(),
contentAlignment = Alignment.BottomCenter,
) {
val fullHeight = constraints.maxHeight.toFloat()
val anchors = mapOf(0f to 0, fullHeight to 1)
val scrimAlpha by animateFloatAsState(
targetValue = if (swipeState.targetValue == 1) 0f else 1f,
animationSpec = ScrimAnimationSpec,
)
Box(
modifier = Modifier
.matchParentSize()
.alpha(scrimAlpha)
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
)
Surface(
modifier = Modifier
.widthIn(max = 460.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {},
)
.nestedScroll(
remember(enableSwipeDismiss, anchors) {
swipeState.preUpPostDownNestedScrollConnection(
enabled = enableSwipeDismiss,
anchor = anchors,
)
},
)
.offset {
IntOffset(
0,
swipeState.offset.value.roundToInt(),
)
}
.swipeable(
enabled = enableSwipeDismiss,
state = swipeState,
anchors = anchors,
orientation = Orientation.Vertical,
resistance = null,
)
.windowInsetsPadding(
WindowInsets.systemBars
.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
)
.consumeWindowInsets(
WindowInsets.systemBars
.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
),
shape = MaterialTheme.shapes.extraLarge.copy(bottomStart = ZeroCornerSize, bottomEnd = ZeroCornerSize),
tonalElevation = tonalElevation,
content = {
BackHandler(enabled = swipeState.targetValue == 0, onBack = internalOnDismissRequest)
content()
},
)
LaunchedEffect(swipeState) {
scope.launch { swipeState.animateTo(0) }
snapshotFlow { swipeState.currentValue }
.drop(1)
.filter { it == 1 }
.collectLatest {
delay(ScrimAnimationSpec.durationMillis.milliseconds)
onDismissRequest()
}
}
}
}
}
/**
* Yoinked from Swipeable.kt with modifications to disable
*/
private fun <T> SwipeableState<T>.preUpPostDownNestedScrollConnection(
enabled: Boolean = true,
anchor: Map<Float, T>,
) = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
return if (enabled && delta < 0 && source == NestedScrollSource.Drag) {
performDrag(delta).toOffset()
} else {
Offset.Zero
}
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset {
return if (enabled && source == NestedScrollSource.Drag) {
performDrag(available.toFloat()).toOffset()
} else {
Offset.Zero
}
}
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = Offset(available.x, available.y).toFloat()
return if (enabled && toFling < 0 && offset.value > anchor.keys.minOrNull()!!) {
performFling(velocity = toFling)
// since we go to the anchor with tween settling, consume all for the best UX
available
} else {
Velocity.Zero
}
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return if (enabled) {
performFling(velocity = Offset(available.x, available.y).toFloat())
available
} else {
Velocity.Zero
}
}
private fun Float.toOffset(): Offset = Offset(0f, this)
private fun Offset.toFloat(): Float = this.y
}

View File

@ -0,0 +1,93 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun AlertDialogContent(
buttons: @Composable () -> Unit,
modifier: Modifier = Modifier,
icon: (@Composable () -> Unit)? = null,
title: (@Composable () -> Unit)? = null,
text: @Composable (() -> Unit)? = null,
) {
Column(
modifier = modifier
.sizeIn(minWidth = MinWidth, maxWidth = MaxWidth)
.padding(DialogPadding),
) {
icon?.let {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.secondary) {
Box(
Modifier
.padding(IconPadding)
.align(Alignment.CenterHorizontally),
) {
icon()
}
}
}
title?.let {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
val textStyle = MaterialTheme.typography.headlineSmall
ProvideTextStyle(textStyle) {
Box(
// Align the title to the center when an icon is present.
Modifier
.padding(TitlePadding)
.align(
if (icon == null) {
Alignment.Start
} else {
Alignment.CenterHorizontally
},
),
) {
title()
}
}
}
}
text?.let {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
val textStyle = MaterialTheme.typography.bodyMedium
ProvideTextStyle(textStyle) {
Box(
Modifier
.weight(weight = 1f, fill = false)
.padding(TextPadding)
.align(Alignment.Start),
) {
text()
}
}
}
}
Box(modifier = Modifier.align(Alignment.End)) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
val textStyle = MaterialTheme.typography.labelLarge
ProvideTextStyle(value = textStyle, content = buttons)
}
}
}
}
// Paddings for each of the dialog's parts.
private val DialogPadding = PaddingValues(all = 24.dp)
private val IconPadding = PaddingValues(bottom = 16.dp)
private val TitlePadding = PaddingValues(bottom = 16.dp)
private val TextPadding = PaddingValues(bottom = 24.dp)
private val MinWidth = 280.dp
private val MaxWidth = 560.dp

View File

@ -44,6 +44,7 @@ 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
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.presentation.util.runOnEnterKeyPressed
import eu.kanade.presentation.util.secondaryItemAlpha import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -62,9 +63,6 @@ fun AppBar(
actionModeCounter: Int = 0, actionModeCounter: Int = 0,
onCancelActionMode: () -> Unit = {}, onCancelActionMode: () -> Unit = {},
actionModeActions: @Composable RowScope.() -> Unit = {}, actionModeActions: @Composable RowScope.() -> Unit = {},
// Banners
downloadedOnlyMode: Boolean = false,
incognitoMode: Boolean = false,
scrollBehavior: TopAppBarScrollBehavior? = null, scrollBehavior: TopAppBarScrollBehavior? = null,
) { ) {
@ -92,8 +90,6 @@ fun AppBar(
}, },
isActionMode = isActionMode, isActionMode = isActionMode,
onCancelActionMode = onCancelActionMode, onCancelActionMode = onCancelActionMode,
downloadedOnlyMode = downloadedOnlyMode,
incognitoMode = incognitoMode,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
} }
@ -111,9 +107,6 @@ fun AppBar(
// Action mode // Action mode
isActionMode: Boolean = false, isActionMode: Boolean = false,
onCancelActionMode: () -> Unit = {}, onCancelActionMode: () -> Unit = {},
// Banners
downloadedOnlyMode: Boolean = false,
incognitoMode: Boolean = false,
scrollBehavior: TopAppBarScrollBehavior? = null, scrollBehavior: TopAppBarScrollBehavior? = null,
) { ) {
@ -149,8 +142,6 @@ fun AppBar(
), ),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
AppStateBanners(downloadedOnlyMode, incognitoMode)
} }
} }
@ -235,8 +226,6 @@ fun SearchToolbar(
onSearch: (String) -> Unit = {}, onSearch: (String) -> Unit = {},
onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) }, onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) },
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
incognitoMode: Boolean = false,
downloadedOnlyMode: Boolean = false,
scrollBehavior: TopAppBarScrollBehavior? = null, scrollBehavior: TopAppBarScrollBehavior? = null,
visualTransformation: VisualTransformation = VisualTransformation.None, visualTransformation: VisualTransformation = VisualTransformation.None,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@ -251,25 +240,27 @@ fun SearchToolbar(
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val searchAndClearFocus: () -> Unit = f@{
if (searchQuery.isBlank()) return@f
onSearch(searchQuery)
focusManager.clearFocus()
keyboardController?.hide()
}
BasicTextField( BasicTextField(
value = searchQuery, value = searchQuery,
onValueChange = onChangeSearchQuery, onValueChange = onChangeSearchQuery,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.focusRequester(focusRequester), .focusRequester(focusRequester)
.runOnEnterKeyPressed(action = searchAndClearFocus),
textStyle = MaterialTheme.typography.titleMedium.copy( textStyle = MaterialTheme.typography.titleMedium.copy(
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 18.sp, fontSize = 18.sp,
), ),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(onSearch = { searchAndClearFocus() }),
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,
@ -324,12 +315,11 @@ fun SearchToolbar(
key("actions") { actions() } key("actions") { actions() }
}, },
isActionMode = false, isActionMode = false,
downloadedOnlyMode = downloadedOnlyMode,
incognitoMode = incognitoMode,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
LaunchedEffect(searchClickCount) { LaunchedEffect(searchClickCount) {
if (searchQuery == null) return@LaunchedEffect if (searchQuery == null) return@LaunchedEffect
if (searchClickCount == 0 && searchQuery.isNotEmpty()) return@LaunchedEffect
try { try {
focusRequester.requestFocus() focusRequester.requestFocus()
} catch (_: Throwable) { } catch (_: Throwable) {

View File

@ -1,19 +1,46 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
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.requiredSize
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.CircularProgressIndicator
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.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
val DownloadedOnlyBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.tertiary
val IncognitoModeBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.primary
val IndexingBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.secondary
@Composable @Composable
fun WarningBanner( fun WarningBanner(
@StringRes textRes: Int, @StringRes textRes: Int,
@ -35,26 +62,77 @@ fun WarningBanner(
fun AppStateBanners( fun AppStateBanners(
downloadedOnlyMode: Boolean, downloadedOnlyMode: Boolean,
incognitoMode: Boolean, incognitoMode: Boolean,
indexing: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier = modifier) { val density = LocalDensity.current
if (downloadedOnlyMode) { val mainInsets = WindowInsets.statusBars
DownloadedOnlyModeBanner() val mainInsetsTop = mainInsets.getTop(density)
} SubcomposeLayout(modifier = modifier) { constraints ->
if (incognitoMode) { val indexingPlaceable = subcompose(0) {
IncognitoModeBanner() AnimatedVisibility(
visible = indexing,
enter = expandVertically(),
exit = shrinkVertically(),
) {
IndexingDownloadBanner(
modifier = Modifier.windowInsetsPadding(mainInsets),
)
}
}.fastMap { it.measure(constraints) }
val indexingHeight = indexingPlaceable.fastMaxBy { it.height }?.height ?: 0
val downloadedOnlyPlaceable = subcompose(1) {
AnimatedVisibility(
visible = downloadedOnlyMode,
enter = expandVertically(),
exit = shrinkVertically(),
) {
val top = (mainInsetsTop - indexingHeight).coerceAtLeast(0)
DownloadedOnlyModeBanner(
modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)),
)
}
}.fastMap { it.measure(constraints) }
val downloadedOnlyHeight = downloadedOnlyPlaceable.fastMaxBy { it.height }?.height ?: 0
val incognitoPlaceable = subcompose(2) {
AnimatedVisibility(
visible = incognitoMode,
enter = expandVertically(),
exit = shrinkVertically(),
) {
val top = (mainInsetsTop - indexingHeight - downloadedOnlyHeight).coerceAtLeast(0)
IncognitoModeBanner(
modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)),
)
}
}.fastMap { it.measure(constraints) }
val incognitoHeight = incognitoPlaceable.fastMaxBy { it.height }?.height ?: 0
layout(constraints.maxWidth, indexingHeight + downloadedOnlyHeight + incognitoHeight) {
indexingPlaceable.fastForEach {
it.place(0, 0)
}
downloadedOnlyPlaceable.fastForEach {
it.place(0, indexingHeight)
}
incognitoPlaceable.fastForEach {
it.place(0, indexingHeight + downloadedOnlyHeight)
}
} }
} }
} }
@Composable @Composable
private fun DownloadedOnlyModeBanner() { private fun DownloadedOnlyModeBanner(modifier: Modifier = Modifier) {
Text( Text(
text = stringResource(R.string.label_downloaded_only), text = stringResource(R.string.label_downloaded_only),
modifier = Modifier modifier = Modifier
.background(color = MaterialTheme.colorScheme.tertiary) .background(DownloadedOnlyBannerBackgroundColor)
.fillMaxWidth() .fillMaxWidth()
.padding(4.dp), .padding(4.dp)
.then(modifier),
color = MaterialTheme.colorScheme.onTertiary, color = MaterialTheme.colorScheme.onTertiary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
@ -62,15 +140,48 @@ private fun DownloadedOnlyModeBanner() {
} }
@Composable @Composable
private fun IncognitoModeBanner() { private fun IncognitoModeBanner(modifier: Modifier = Modifier) {
Text( Text(
text = stringResource(R.string.pref_incognito_mode), text = stringResource(R.string.pref_incognito_mode),
modifier = Modifier modifier = Modifier
.background(color = MaterialTheme.colorScheme.primary) .background(IncognitoModeBannerBackgroundColor)
.fillMaxWidth() .fillMaxWidth()
.padding(4.dp), .padding(4.dp)
.then(modifier),
color = MaterialTheme.colorScheme.onPrimary, color = MaterialTheme.colorScheme.onPrimary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
) )
} }
@Composable
private fun IndexingDownloadBanner(modifier: Modifier = Modifier) {
val density = LocalDensity.current
Row(
modifier = Modifier
.background(color = IndexingBannerBackgroundColor)
.fillMaxWidth()
.padding(8.dp)
.then(modifier),
horizontalArrangement = Arrangement.Center,
) {
var textHeight by remember { mutableStateOf(0.dp) }
CircularProgressIndicator(
modifier = Modifier.requiredSize(textHeight),
color = MaterialTheme.colorScheme.onSecondary,
strokeWidth = textHeight / 8,
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.download_notifier_cache_renewal),
color = MaterialTheme.colorScheme.onSecondary,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelMedium,
onTextLayout = {
with(density) {
textHeight = it.size.height.toDp()
}
},
)
}
}

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
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.Text import androidx.compose.material3.Text
import androidx.compose.material3.TriStateCheckbox import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -23,7 +24,7 @@ import androidx.compose.ui.res.stringResource
import eu.kanade.core.prefs.CheckboxState import eu.kanade.core.prefs.CheckboxState
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.util.horizontalPadding import eu.kanade.presentation.util.padding
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@Composable @Composable
@ -95,8 +96,7 @@ fun ChangeCategoryDialog(
val index = selection.indexOf(it) val index = selection.indexOf(it)
if (index != -1) { if (index != -1) {
val mutableList = selection.toMutableList() val mutableList = selection.toMutableList()
mutableList.removeAt(index) mutableList[index] = it.next()
mutableList.add(index, it.next())
selection = mutableList.toList() selection = mutableList.toList()
} }
} }
@ -123,7 +123,7 @@ fun ChangeCategoryDialog(
Text( Text(
text = checkbox.value.visualName, text = checkbox.value.visualName,
modifier = Modifier.padding(horizontal = horizontalPadding), modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
) )
} }
} }

View File

@ -95,7 +95,7 @@ private fun NotDownloadedIndicator(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_download_chapter_24dp), painter = painterResource(R.drawable.ic_download_chapter_24dp),
contentDescription = stringResource(R.string.manga_download), contentDescription = stringResource(R.string.manga_download),
modifier = Modifier.size(IndicatorSize), modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
@ -188,7 +188,7 @@ private fun DownloadedIndicator(
.size(IconButtonTokens.StateLayerSize) .size(IconButtonTokens.StateLayerSize)
.commonClickable( .commonClickable(
enabled = enabled, enabled = enabled,
onLongClick = { onClick(ChapterDownloadAction.DELETE) }, onLongClick = { isMenuExpanded = true },
onClick = { isMenuExpanded = true }, onClick = { isMenuExpanded = true },
), ),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,

View File

@ -12,10 +12,17 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -41,6 +48,10 @@ object CommonMangaItemDefaults {
const val BrowseFavoriteCoverAlpha = 0.34f const val BrowseFavoriteCoverAlpha = 0.34f
} }
private val ContinueReadingButtonSize = 32.dp
private val ContinueReadingButtonGridPadding = 6.dp
private val ContinueReadingButtonListSpacing = 8.dp
private const val GridSelectedCoverAlpha = 0.76f private const val GridSelectedCoverAlpha = 0.76f
/** /**
@ -53,10 +64,11 @@ fun MangaCompactGridItem(
title: String? = null, title: String? = null,
coverData: eu.kanade.domain.manga.model.MangaCover, coverData: eu.kanade.domain.manga.model.MangaCover,
coverAlpha: Float = 1f, coverAlpha: Float = 1f,
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null, coverBadgeStart: @Composable (RowScope.() -> Unit)? = null,
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null, coverBadgeEnd: @Composable (RowScope.() -> Unit)? = null,
onLongClick: () -> Unit, onLongClick: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
onClickContinueReading: (() -> Unit)? = null,
) { ) {
GridItemSelectable( GridItemSelectable(
isSelected = isSelected, isSelected = isSelected,
@ -76,7 +88,17 @@ fun MangaCompactGridItem(
badgesEnd = coverBadgeEnd, badgesEnd = coverBadgeEnd,
content = { content = {
if (title != null) { if (title != null) {
CoverTextOverlay(title = title) CoverTextOverlay(
title = title,
onClickContinueReading = onClickContinueReading,
)
} else if (onClickContinueReading != null) {
ContinueReadingButton(
modifier = Modifier
.padding(ContinueReadingButtonGridPadding)
.align(Alignment.BottomEnd),
onClickContinueReading = onClickContinueReading,
)
} }
}, },
) )
@ -87,7 +109,10 @@ fun MangaCompactGridItem(
* Title overlay for [MangaCompactGridItem] * Title overlay for [MangaCompactGridItem]
*/ */
@Composable @Composable
private fun BoxScope.CoverTextOverlay(title: String) { private fun BoxScope.CoverTextOverlay(
title: String,
onClickContinueReading: (() -> Unit)? = null,
) {
Box( Box(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp)) .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
@ -101,19 +126,33 @@ private fun BoxScope.CoverTextOverlay(title: String) {
.fillMaxWidth() .fillMaxWidth()
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter),
) )
GridItemTitle( Row(
modifier = Modifier modifier = Modifier.align(Alignment.BottomStart),
.padding(8.dp) verticalAlignment = Alignment.Bottom,
.align(Alignment.BottomStart), ) {
title = title, GridItemTitle(
style = MaterialTheme.typography.titleSmall.copy( modifier = Modifier
color = Color.White, .weight(1f)
shadow = Shadow( .padding(8.dp),
color = Color.Black, title = title,
blurRadius = 4f, style = MaterialTheme.typography.titleSmall.copy(
color = Color.White,
shadow = Shadow(
color = Color.Black,
blurRadius = 4f,
),
), ),
), )
) if (onClickContinueReading != null) {
ContinueReadingButton(
modifier = Modifier.padding(
end = ContinueReadingButtonGridPadding,
bottom = ContinueReadingButtonGridPadding,
),
onClickContinueReading = onClickContinueReading,
)
}
}
} }
/** /**
@ -129,6 +168,7 @@ fun MangaComfortableGridItem(
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null, coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
onLongClick: () -> Unit, onLongClick: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
onClickContinueReading: (() -> Unit)? = null,
) { ) {
GridItemSelectable( GridItemSelectable(
isSelected = isSelected, isSelected = isSelected,
@ -147,6 +187,16 @@ fun MangaComfortableGridItem(
}, },
badgesStart = coverBadgeStart, badgesStart = coverBadgeStart,
badgesEnd = coverBadgeEnd, badgesEnd = coverBadgeEnd,
content = {
if (onClickContinueReading != null) {
ContinueReadingButton(
modifier = Modifier
.padding(ContinueReadingButtonGridPadding)
.align(Alignment.BottomEnd),
onClickContinueReading = onClickContinueReading,
)
}
},
) )
GridItemTitle( GridItemTitle(
modifier = Modifier.padding(4.dp), modifier = Modifier.padding(4.dp),
@ -259,7 +309,7 @@ private fun Modifier.selectedOutline(
} }
return this then modifierElementOf( return this then modifierElementOf(
params = isSelected.hashCode() + color.hashCode(), key = isSelected.hashCode() + color.hashCode(),
create = { SelectedOutlineNode(isSelected, color) }, create = { SelectedOutlineNode(isSelected, color) },
update = { update = {
it.selected = isSelected it.selected = isSelected
@ -282,9 +332,10 @@ fun MangaListItem(
title: String, title: String,
coverData: eu.kanade.domain.manga.model.MangaCover, coverData: eu.kanade.domain.manga.model.MangaCover,
coverAlpha: Float = 1f, coverAlpha: Float = 1f,
badge: @Composable RowScope.() -> Unit, badge: @Composable (RowScope.() -> Unit),
onLongClick: () -> Unit, onLongClick: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
onClickContinueReading: (() -> Unit)? = null,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -313,5 +364,35 @@ fun MangaListItem(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
BadgeGroup(content = badge) BadgeGroup(content = badge)
if (onClickContinueReading != null) {
ContinueReadingButton(
modifier = Modifier.padding(start = ContinueReadingButtonListSpacing),
onClickContinueReading = onClickContinueReading,
)
}
}
}
@Composable
private fun ContinueReadingButton(
modifier: Modifier = Modifier,
onClickContinueReading: () -> Unit,
) {
Box(modifier = modifier) {
FilledIconButton(
onClick = onClickContinueReading,
modifier = Modifier.size(ContinueReadingButtonSize),
shape = MaterialTheme.shapes.small,
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer),
),
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "",
modifier = Modifier.size(16.dp),
)
}
} }
} }

View File

@ -64,8 +64,7 @@ fun DeleteLibraryMangaDialog(
val onCheck = { val onCheck = {
val index = list.indexOf(state) val index = list.indexOf(state)
val mutableList = list.toMutableList() val mutableList = list.toMutableList()
mutableList.removeAt(index) mutableList[index] = state.next() as CheckboxState.State<Int>
mutableList.add(index, state.next() as CheckboxState.State<Int>)
list = mutableList.toList() list = mutableList.toList()
} }

View File

@ -1,17 +1,44 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.compose.material3.MaterialTheme import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.DividerDefaults
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.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
const val DIVIDER_ALPHA = 0.2f const val DIVIDER_ALPHA = 0.2f
@Composable @Composable
fun Divider( fun Divider(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
color: Color = DividerDefaults.color,
) { ) {
androidx.compose.material3.Divider( Box(
modifier = modifier, modifier
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA), .fillMaxWidth()
.height(1.dp)
.background(color = color)
.alpha(DIVIDER_ALPHA),
)
}
@Composable
fun VerticalDivider(
modifier: Modifier = Modifier,
color: Color = DividerDefaults.color,
) {
Box(
modifier
.fillMaxHeight()
.width(1.dp)
.background(color = color)
.alpha(DIVIDER_ALPHA),
) )
} }

View File

@ -1,6 +1,5 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -23,14 +22,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.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.stringResource 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.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.ThemePreviews
import eu.kanade.presentation.util.secondaryItemAlpha import eu.kanade.presentation.util.secondaryItemAlpha
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import kotlin.random.Random import kotlin.random.Random
@ -55,68 +51,45 @@ fun EmptyScreen(
actions: List<EmptyScreenAction>? = null, actions: List<EmptyScreenAction>? = null,
) { ) {
val face = remember { getRandomErrorFace() } val face = remember { getRandomErrorFace() }
Layout( Column(
content = { modifier = modifier
Column( .fillMaxSize()
modifier = Modifier .padding(horizontal = 24.dp),
.layoutId("face") horizontalAlignment = Alignment.CenterHorizontally,
.padding(horizontal = 24.dp), verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, ) {
) { Text(
Text( text = face,
text = face, modifier = Modifier.secondaryItemAlpha(),
modifier = Modifier.secondaryItemAlpha(), style = MaterialTheme.typography.displayMedium,
style = MaterialTheme.typography.displayMedium, )
)
Text( Text(
text = message, text = message,
modifier = Modifier.paddingFromBaseline(top = 24.dp).secondaryItemAlpha(), modifier = Modifier.paddingFromBaseline(top = 24.dp).secondaryItemAlpha(),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
}
if (!actions.isNullOrEmpty()) { if (!actions.isNullOrEmpty()) {
Row( Row(
modifier = Modifier modifier = Modifier
.layoutId("actions") .padding(
.padding( top = 24.dp,
top = 24.dp, start = 24.dp,
start = horizontalPadding, end = 24.dp,
end = horizontalPadding, ),
), horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp), ) {
) { actions.forEach {
actions.forEach { ActionButton(
ActionButton( modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f), title = stringResource(it.stringResId),
title = stringResource(it.stringResId), icon = it.icon,
icon = it.icon, onClick = it.onClick,
onClick = it.onClick, )
)
}
} }
} }
},
modifier = modifier.fillMaxSize(),
) { measurables, constraints ->
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val facePlaceable = measurables.fastFirstOrNull { it.layoutId == "face" }!!
.measure(looseConstraints)
val actionsPlaceable = measurables.fastFirstOrNull { it.layoutId == "actions" }
?.measure(looseConstraints)
layout(constraints.maxWidth, constraints.maxHeight) {
val faceY = (constraints.maxHeight - facePlaceable.height) / 2
facePlaceable.placeRelative(
x = (constraints.maxWidth - facePlaceable.width) / 2,
y = faceY,
)
actionsPlaceable?.placeRelative(
x = (constraints.maxWidth - actionsPlaceable.width) / 2,
y = faceY + facePlaceable.height,
)
} }
} }
} }
@ -146,17 +119,7 @@ private fun ActionButton(
} }
} }
@Preview( @ThemePreviews
name = "Light",
widthDp = 400,
heightDp = 400,
)
@Preview(
name = "Dark",
widthDp = 400,
heightDp = 400,
uiMode = UI_MODE_NIGHT_YES,
)
@Composable @Composable
private fun NoActionPreview() { private fun NoActionPreview() {
TachiyomiTheme { TachiyomiTheme {
@ -168,17 +131,7 @@ private fun NoActionPreview() {
} }
} }
@Preview( @ThemePreviews
name = "Light",
widthDp = 400,
heightDp = 400,
)
@Preview(
name = "Dark",
widthDp = 400,
heightDp = 400,
uiMode = UI_MODE_NIGHT_YES,
)
@Composable @Composable
private fun WithActionPreview() { private fun WithActionPreview() {
TachiyomiTheme { TachiyomiTheme {
@ -208,8 +161,6 @@ data class EmptyScreenAction(
val onClick: () -> Unit, val onClick: () -> Unit,
) )
private val horizontalPadding = 24.dp
private val ERROR_FACES = listOf( private val ERROR_FACES = listOf(
"(・o・;)", "(・o・;)",
"Σ(ಠ_ಠ)", "Σ(ಠ_ಠ)",

View File

@ -0,0 +1,141 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Newspaper
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.ThemePreviews
import eu.kanade.presentation.util.padding
import eu.kanade.presentation.util.secondaryItemAlpha
@Composable
fun InfoScaffold(
icon: ImageVector,
headingText: String,
subtitleText: String,
acceptText: String,
onAcceptClick: () -> Unit,
rejectText: String,
onRejectClick: () -> Unit,
content: @Composable ColumnScope.() -> Unit,
) {
Scaffold(
bottomBar = {
val strokeWidth = Dp.Hairline
val borderColor = MaterialTheme.colorScheme.outline
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.drawBehind {
drawLine(
borderColor,
Offset(0f, 0f),
Offset(size.width, 0f),
strokeWidth.value,
)
}
.windowInsetsPadding(NavigationBarDefaults.windowInsets)
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
) {
androidx.compose.material3.Button(
modifier = Modifier.fillMaxWidth(),
onClick = onAcceptClick,
) {
Text(text = acceptText)
}
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = onRejectClick,
) {
Text(text = rejectText)
}
}
},
) { paddingValues ->
// Status bar scrim
Box(
modifier = Modifier
.zIndex(2f)
.secondaryItemAlpha()
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth()
.height(paddingValues.calculateTopPadding()),
)
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(paddingValues)
.padding(top = 48.dp)
.padding(horizontal = MaterialTheme.padding.medium),
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier
.padding(bottom = MaterialTheme.padding.small)
.size(48.dp),
tint = MaterialTheme.colorScheme.primary,
)
Text(
text = headingText,
style = MaterialTheme.typography.headlineLarge,
)
Text(
text = subtitleText,
modifier = Modifier
.secondaryItemAlpha()
.padding(vertical = MaterialTheme.padding.small),
style = MaterialTheme.typography.titleSmall,
)
content()
}
}
}
@ThemePreviews
@Composable
private fun InfoScaffoldPreview() {
TachiyomiTheme {
InfoScaffold(
icon = Icons.Outlined.Newspaper,
headingText = "Heading",
subtitleText = "Subtitle",
acceptText = "Accept",
onAcceptClick = {},
rejectText = "Reject",
onRejectClick = {},
) {
Text("Hello world")
}
}
}

View File

@ -0,0 +1,27 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import eu.kanade.presentation.util.padding
@Composable
fun ListGroupHeader(
modifier: Modifier = Modifier,
text: String,
) {
Text(
text = text,
modifier = modifier
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodyMedium,
)
}

View File

@ -8,9 +8,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@Composable @Composable
fun LoadingScreen() { fun LoadingScreen(modifier: Modifier = Modifier) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
CircularProgressIndicator() CircularProgressIndicator()

View File

@ -2,6 +2,7 @@ package eu.kanade.presentation.components
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -16,10 +17,10 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
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.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
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.outlined.BookmarkAdd import androidx.compose.material.icons.outlined.BookmarkAdd
@ -95,7 +96,11 @@ fun MangaBottomActionMenu(
} }
Row( Row(
modifier = Modifier modifier = Modifier
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()) .padding(
WindowInsets.navigationBars
.only(WindowInsetsSides.Bottom)
.asPaddingValues(),
)
.padding(horizontal = 8.dp, vertical = 12.dp), .padding(horizontal = 8.dp, vertical = 12.dp),
) { ) {
if (onBookmarkClicked != null) { if (onBookmarkClicked != null) {
@ -213,16 +218,16 @@ private fun RowScope.Button(
fun LibraryBottomActionMenu( fun LibraryBottomActionMenu(
visible: Boolean, visible: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onChangeCategoryClicked: (() -> Unit)?, onChangeCategoryClicked: () -> Unit,
onMarkAsReadClicked: (() -> Unit)?, onMarkAsReadClicked: () -> Unit,
onMarkAsUnreadClicked: (() -> Unit)?, onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: ((DownloadAction) -> Unit)?, onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: (() -> Unit)?, onDeleteClicked: () -> Unit,
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = expandVertically(expandFrom = Alignment.Bottom), enter = expandVertically(animationSpec = tween(delayMillis = 300)),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom), exit = shrinkVertically(animationSpec = tween()),
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Surface( Surface(
@ -244,36 +249,33 @@ fun LibraryBottomActionMenu(
} }
Row( Row(
modifier = Modifier modifier = Modifier
.navigationBarsPadding() .windowInsetsPadding(
WindowInsets.navigationBars
.only(WindowInsetsSides.Bottom),
)
.padding(horizontal = 8.dp, vertical = 12.dp), .padding(horizontal = 8.dp, vertical = 12.dp),
) { ) {
if (onChangeCategoryClicked != null) { Button(
Button( title = stringResource(R.string.action_move_category),
title = stringResource(R.string.action_move_category), icon = Icons.Outlined.Label,
icon = Icons.Outlined.Label, toConfirm = confirm[0],
toConfirm = confirm[0], onLongClick = { onLongClickItem(0) },
onLongClick = { onLongClickItem(0) }, onClick = onChangeCategoryClicked,
onClick = onChangeCategoryClicked, )
) Button(
} title = stringResource(R.string.action_mark_as_read),
if (onMarkAsReadClicked != null) { icon = Icons.Outlined.DoneAll,
Button( toConfirm = confirm[1],
title = stringResource(R.string.action_mark_as_read), onLongClick = { onLongClickItem(1) },
icon = Icons.Outlined.DoneAll, onClick = onMarkAsReadClicked,
toConfirm = confirm[1], )
onLongClick = { onLongClickItem(1) }, Button(
onClick = onMarkAsReadClicked, title = stringResource(R.string.action_mark_as_unread),
) icon = Icons.Outlined.RemoveDone,
} toConfirm = confirm[2],
if (onMarkAsUnreadClicked != null) { onLongClick = { onLongClickItem(2) },
Button( onClick = onMarkAsUnreadClicked,
title = stringResource(R.string.action_mark_as_unread), )
icon = Icons.Outlined.RemoveDone,
toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) },
onClick = onMarkAsUnreadClicked,
)
}
if (onDownloadClicked != null) { if (onDownloadClicked != null) {
var downloadExpanded by remember { mutableStateOf(false) } var downloadExpanded by remember { mutableStateOf(false) }
Button( Button(
@ -292,15 +294,13 @@ fun LibraryBottomActionMenu(
) )
} }
} }
if (onDeleteClicked != null) { Button(
Button( title = stringResource(R.string.action_delete),
title = stringResource(R.string.action_delete), icon = Icons.Outlined.Delete,
icon = Icons.Outlined.Delete, toConfirm = confirm[4],
toConfirm = confirm[4], onLongClick = { onLongClickItem(4) },
onLongClick = { onLongClickItem(4) }, onClick = onDeleteClicked,
onClick = onDeleteClicked, )
)
}
} }
} }
} }

View File

@ -0,0 +1,48 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* M3 Navbar with no horizontal spacer
*
* @see [androidx.compose.material3.NavigationBar]
*/
@Composable
fun NavigationBar(
modifier: Modifier = Modifier,
containerColor: Color = NavigationBarDefaults.containerColor,
contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
tonalElevation: Dp = NavigationBarDefaults.Elevation,
windowInsets: WindowInsets = NavigationBarDefaults.windowInsets,
content: @Composable RowScope.() -> Unit,
) {
androidx.compose.material3.Surface(
color = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
modifier = modifier,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(windowInsets)
.height(80.dp)
.selectableGroup(),
content = content,
)
}
}

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