Compare commits

..

195 Commits

Author SHA1 Message Date
f3e1fb7664
update gitignore 2025-05-30 17:15:05 +01:00
Jobobby04
cc934607c8 SpotlessApply 2025-05-24 21:07:29 -04:00
Weblate (bot)
5074e68b9c
Translations update from Hosted Weblate (#1442)
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/my/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/uk/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hant/
Translation: Mihon/TachiyomiSY
Translation: Mihon/TachiyomiSY Plurals

Co-authored-by: Alex Maryson Jr <akamar87@gmail.com>
Co-authored-by: B4LiN7 <87660017+B4LiN7@users.noreply.github.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: FateXBlood <fatexblood@gmail.com>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Hualiang <642615676@qq.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Kosťantin Horovij <lg096066587039@gmail.com>
Co-authored-by: Lapis (Bas77) <sebastianramli77@gmail.com>
Co-authored-by: LordTenebrous <danielmorenoperez836@gmail.com>
Co-authored-by: Mohamed kh <mohamedkhamekhami@gmail.com>
Co-authored-by: Sky children of the Light <tu25261@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: akir45 <akkn0708@gmail.com>
Co-authored-by: edgole <test.backache009@aleeas.com>
Co-authored-by: fl0k1 <michele.carnova@gmail.com>
Co-authored-by: naikhon <naikhon5@gmail.com>
Co-authored-by: qaugji <asteeky9@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Co-authored-by: Георгій Обушенков <heorhii.obushenkov@gmail.com>
Co-authored-by: ابومسلم <linuxmint1978@gmail.com>
2025-05-24 20:47:35 -04:00
多能豆
ade41f113d
Fix E-Hentai Jump/Seek match for detailed date (#1450) 2025-05-24 20:44:55 -04:00
Jobobby04
95dc82594f More guards against edited data 2025-05-24 20:44:27 -04:00
NGB-Was-Taken
80e585fa91
Change log file extension to .txt (#1449) 2025-05-24 20:19:17 -04:00
Jobobby04
9f110f9db8 Bump version so migation actually runs 2025-05-24 20:18:14 -04:00
NGB-Was-Taken
71470b9e02
Remove the unused mark duplicate as read preference. (#1448)
* Remove the unused mark duplicate as read preference.

* Migrate the old preference to new preference
2025-05-24 20:16:44 -04:00
renovate[bot]
4fd24accac
Update dependency net.zetetic:sqlcipher-android to v4.9.0 (#1447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-24 20:15:17 -04:00
NGB-Was-Taken
31312fecac
Fixes screen staying on in library tab. (#1451) 2025-05-24 20:13:43 -04:00
Mend Renovate
b80d057922 Update dependency androidx.compose:compose-bom to v2025.05.01 (#2133)
(cherry picked from commit 92b376d9af93da988b695c36c4775d5e6947c048)
2025-05-24 20:12:57 -04:00
Mend Renovate
f01d8bc835 Update dependency gradle to v8.14.1 (#2138)
(cherry picked from commit 1a2f09a622017dc5b201eadc6acc667487cf3d4d)
2025-05-24 20:12:48 -04:00
Jobobby04
ddffe71a22 SpotlessApply 2025-05-24 20:12:38 -04:00
AntsyLich
649a19ec57 Fix content cut off in home screen
Closes #2141

(cherry picked from commit 209e982fe4f1da5d1d49cfbfdd178625ee3c70f4)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt
2025-05-24 20:10:18 -04:00
Mend Renovate
e82fd99a09 Update plugin org.gradle.toolchains.foojay-resolver-convention to v1 (#2130)
(cherry picked from commit 0109102901f942502807e2a7a9f1a58f951c763f)
2025-05-24 20:09:57 -04:00
Mend Renovate
67a9b8e2a0 Update dependency com.pinterest.ktlint:ktlint-cli to v1.6.0 (#2129)
(cherry picked from commit 4117a5167470ae36b4172721b4e4d40373f2a3c6)
2025-05-24 20:09:51 -04:00
Mend Renovate
2000f947c3 Update xml.serialization.version to v0.91.1 (#2112)
(cherry picked from commit 4090a61d08964a8e82f943ac4eb81a52c110005f)
2025-05-24 20:09:45 -04:00
Jobobby04
f992ada0a8 SpotlessApply 2025-05-16 21:16:09 -04:00
Jobobby04
f876cdb037 Use MediaServer in Json for NHentai
Co-authored-by: 4521 <18432684+az4521@users.noreply.github.com>
2025-05-16 20:34:03 -04:00
Jobobby04
f919d42370 Hello Discord 2025-05-16 12:02:37 -04:00
Mend Renovate
ab5ff00c39 Update kotlin monorepo to v2.1.21 (#2102)
(cherry picked from commit 625c85cbd68086d605553b2d5ab9cc9cc9460688)
2025-05-15 13:50:17 -04:00
Mend Renovate
422738af56 Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.4.0 (#2104)
(cherry picked from commit 737ceeea576074cffcd2e96933aceffdbcc0a03a)
2025-05-15 13:50:08 -04:00
Mend Renovate
81751fc9ce Update dependency io.coil-kt.coil3:coil-bom to v3.2.0 (#2101)
(cherry picked from commit 7933c9eeb7b28ecc2ae31fa337654a80f2371b85)
2025-05-15 13:49:58 -04:00
Jobobby04
9b6c5effc9 Minor refactors 2025-05-15 13:38:03 -04:00
Jobobby04
129841d5c2 SpotlessApply and up version code due to database migration 2025-05-11 20:23:10 -04:00
AntsyLich
24d2460697 Disable reader's 'Keep screen on' setting by default (#2095)
(cherry picked from commit f0de8f973b331ebad6e1844aea7864f97f237941)

# Conflicts:
#	CHANGELOG.md
2025-05-11 20:18:36 -04:00
AntsyLich
ef3d9626c1 Add full predictive back support (#2085)
Co-authored-by: p
(cherry picked from commit c12bdbae8e7bc14da8966e45a3c450913e32129f)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt
2025-05-11 20:18:11 -04:00
Mend Renovate
5c7b3c6c3b Update markdown to v0.34.0 (#2086)
(cherry picked from commit 33d407ee9c2196f25aaee0978b6808b9c393841e)
2025-05-11 20:10:50 -04:00
AntsyLich
6257888735 Update voyager to v1.1.0-beta03 (#2087)
(cherry picked from commit ef8c3ca119dfbdfcb0588bde080a7c6203133024)
2025-05-11 20:10:43 -04:00
FlaminSarge
f5e714f794 Add advanced option to always update manga title from source (#1182)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 8b45ef0e5d5d368e0925df9816ae423defaed4d9)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
2025-05-11 20:10:28 -04:00
AwkwardPeak7
3091f63504 Fix pressing Enter while searching also triggering navigation back on physical keyboards (#2077)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 86ebf5581598f28feab4090ac3bf627f54b511d7)

# Conflicts:
#	CHANGELOG.md
2025-05-11 20:00:59 -04:00
Mend Renovate
39755cccdc Update lifecycle.version to v2.9.0 (#2080)
(cherry picked from commit ddf282b10364fc71c5af4cfe77684a8ac0beaa5c)
2025-05-11 20:00:40 -04:00
Mend Renovate
9caf706ca3 Update sqlite to v2.5.1 (#2078)
(cherry picked from commit 744b809d458ba1bfee923f76493505768c261231)
2025-05-11 20:00:32 -04:00
Mend Renovate
6ba6a7c8d9 Update dependency androidx.compose:compose-bom to v2025.05.00 (#2079)
(cherry picked from commit cd2ce44efafbaf19e43ba342d417e4d553a8a0d7)
2025-05-11 20:00:26 -04:00
Mend Renovate
0a4a0e4c4c Update dependency com.android.tools.build:gradle to v8.10.0 (#2072)
(cherry picked from commit cae7c3dc588055dee5c9a99c331710e038b17794)
2025-05-11 20:00:19 -04:00
Mend Renovate
b48d1e521a Update aboutlib.version to v12.1.2 (#2073)
(cherry picked from commit c0074402e7cc5917272459e7ebfb269e11af4c76)
2025-05-11 20:00:08 -04:00
AntsyLich
211d090a2d Add autofill support to tracker login dialog and update processing text (#2069)
(cherry picked from commit 7deeabe844d41d2b5e918ad747ddd548163c9fe3)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:59:44 -04:00
AntsyLich
b6e5943e15 Fix downloader stopping after failing to create download directory of a manga (#2068)
(cherry picked from commit 536393a6d9941fac282f10b825aa611b91e1fcdb)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
2025-05-11 19:59:22 -04:00
AntsyLich
78f6a34339 Fix Pill not following the local text style
Closes #2009

(cherry picked from commit f8cb506137a3619f828dac94557b5448b2a7fa24)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:48:31 -04:00
AntsyLich
de967ae149 Cleanup MarkdownRender
Co-authored-by: p
(cherry picked from commit 98230ed30f04fe754fd4bd407356c8c03d8d8719)
2025-05-11 19:48:04 -04:00
Mend Renovate
4d075ff190 Update dependency androidx.compose:compose-bom to v2025.04.01 (#2040)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit d721a4321bdc6fafdd32e7bfd451b61b2bdd66b7)
2025-05-11 19:47:58 -04:00
Mend Renovate
076e2961c6 Update aboutlib.version to v12.1.0 (#2052)
(cherry picked from commit 1ac4b72cfe09be10cbde45633fbd84ffd703fc70)
2025-05-11 19:47:52 -04:00
Mend Renovate
7149de1bc3 Update dependency io.mockk:mockk to v1.14.2 (#2057)
(cherry picked from commit 9331f2b93f907bf3f7c95eb850b5befce90b68c6)
2025-05-11 19:47:42 -04:00
Mend Renovate
091f2f583a Update dependency org.jsoup:jsoup to v1.20.1 (#2058)
(cherry picked from commit 99c2a999735e04d026b937b953cdd5f19b2e7b1f)
2025-05-11 19:47:36 -04:00
Mend Renovate
1c0ef2ca98 Update aboutlib.version (#2046)
(cherry picked from commit 001716e34b0c533cfd0318be5048d32bc107931b)
2025-05-11 19:47:29 -04:00
NarwhalHorns
2a845bd7b5 Fix empty layout not appearing in browse source screen in some cases (#2043)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 37e19edf8a5f6a15a95f160390cbcf22d8133380)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt
2025-05-11 19:47:22 -04:00
AntsyLich
afe326006f Switch default user agent to Android Chrome (#2048)
(cherry picked from commit 8b7f35598833917c89f8ae53cca10578fd880d67)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:44:45 -04:00
Mend Renovate
4b80154b09 Update dependency com.google.firebase:firebase-bom to v33.13.0 (#2047)
(cherry picked from commit 0b777336739b6f91ebfe45772c2f139d4e60c555)
2025-05-11 19:44:30 -04:00
Mend Renovate
d6b230b8f1 Update dependency androidx.work:work-runtime to v2.10.1 (#2041)
(cherry picked from commit 9be558d6c0e0e81c1f37fc3d01b19872879f2daa)
2025-05-11 19:44:23 -04:00
Mend Renovate
d02a2cbd29 Update dependency gradle to v8.14 (#2049)
(cherry picked from commit 0c8c5dbba6bf60fbc4e3e4dad912e2be8ce25a79)
2025-05-11 19:44:17 -04:00
AntsyLich
17d225b0d9 Fix crash when trying use source sort filter without a pre-selection (#2036)
(cherry picked from commit 1c982c2a01c1bba5ec4a955c9bf61cb346c752e7)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:44:10 -04:00
Mend Renovate
6cbbaccaf4 Update dependency com.android.tools.build:gradle to v8.9.2 (#2033)
(cherry picked from commit eeab61fc94e1a9486eba42fd79a8169473ab6fde)
2025-05-11 19:43:51 -04:00
Mend Renovate
94cc4027c2 Update aboutlib.version to v12 (major) (#2016)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit a036407c75d83ef0ba5350cb1825b615361316c3)
2025-05-11 19:43:44 -04:00
AntsyLich
03ae6ed2b0 Update dependency com.mohamedrejeb.richeditor:richeditor-compose to v1.0.0-rc11
(cherry picked from commit 615d93f780b415e505fb2159f4cbdf4694749f82)
2025-05-11 19:43:35 -04:00
AntsyLich
fa8c232a69 Fix content under source browse screen top appbar is interactable (#2026)
(cherry picked from commit 9750c1e4bd6b931e71b7b348abbe2638a8cf317b)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:43:27 -04:00
Secozzi
0386ce998a Update markdown to 0.33.0 and tweak visuals (#2024)
- Update markdown to 0.33.0
- Use github flavour for github changelog
- Fix bullet list alignment

(cherry picked from commit e2915a1f69340cad515962de8a0b9d11ecff8d42)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:43:12 -04:00
AwkwardPeak7
273f73e9a2 Remove Okhttp networking from WebView Screen (#2020)
(cherry picked from commit df2b4c754bab9dd96fe2199b9f6df62d87b7175e)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:42:33 -04:00
KokaKiwi
5e20e54649 Fix reader not updating progress (#2007)
The condition for updating progress is wrong since fefa8f84982b537ca930438f7976087844d5bb9c

(cherry picked from commit 6632a122288cc9733844c8dce1ee51b520c0a32e)
2025-05-11 19:42:06 -04:00
Joseph Madamba
b8c3f9dcce Update Facebook and Reddit icon (#1994)
(cherry picked from commit 0cb1925cf158155665f3173bccb93f39d84b71e0)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:41:59 -04:00
ArthurKun
802b6508fa Replace Modifier.composed with Composable Modifier (#1959)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit a31b3b7bbf2c5164baf76ac4b36f1d27c5d43135)
2025-05-11 19:41:36 -04:00
AwkwardPeak7
b6409b05e7 Include source headers when opening failed images from reader (#2004)
(cherry picked from commit fea85241afac5a849aa418d01710f5cdc0c25b54)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:41:27 -04:00
Secozzi
129f355b9c Use simpler markdown flavour in manga description (#2000)
(cherry picked from commit e273a26c9b7f0a9dd9f8847cfc65e69453fa5905)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:41:01 -04:00
AwkwardPeak7
9ffacb80e3 Fix duplicate requests in WebView due to empty reasonPhrase (#2003)
(cherry picked from commit 818e6931c6bc89e0bb111e77418542a88f8db37c)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:40:27 -04:00
AwkwardPeak7
85726db45d Add option to keep read manga when clearing database (#1979)
(cherry picked from commit ecc6ede0815a89b7f8288e47c607c57bacc47e71)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt
#	data/src/main/sqldelight/tachiyomi/data/mangas.sq
2025-05-11 19:40:06 -04:00
AwkwardPeak7
746b1b051c Surface image loading error in Reader (#1981)
(cherry picked from commit fefa8f84982b537ca930438f7976087844d5bb9c)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt
2025-05-11 19:36:53 -04:00
AwkwardPeak7
59887eed80 Change Page.State to sealed interface (#1988)
(cherry picked from commit f1e2efcb37e2c623b769e979fa1c7e9e5ad7117d)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt
2025-05-11 19:35:07 -04:00
Mend Renovate
b8267f1fef Update dependency androidx.core:core-ktx to v1.16.0 (#1990)
(cherry picked from commit 180318f57d82529c0040a2d310a679a493d2b9f3)
2025-05-11 19:12:53 -04:00
Mend Renovate
8c62bb6d6d Update plugin org.gradle.toolchains.foojay-resolver-convention to v0.10.0 (#1992)
(cherry picked from commit ed749de8066ac056bc0caa505739f1b7e45dea48)
2025-05-11 19:12:46 -04:00
Mend Renovate
751e04b87f Update markdown to v0.33.0-rc01 (#1999)
(cherry picked from commit bb33b0029ef630a609dd160ed278d5e8274f316d)
2025-05-11 19:12:40 -04:00
Mend Renovate
9f0161ed70 Update dependency androidx.compose:compose-bom to v2025.04.00 (#1989)
(cherry picked from commit 3a19e449b13c078d7c0b5762f0ba84a162cc7f71)
2025-05-11 19:12:33 -04:00
Mend Renovate
7b2c341386 Update dependency com.squareup.okio:okio to v3.11.0 (#1991)
(cherry picked from commit 818edf2776fd7706cd1a829a6bdc963a436f06d6)
2025-05-11 19:12:22 -04:00
Mend Renovate
c8b29ecf1c Update dependency io.mockk:mockk to v1.14.0 (#1987)
(cherry picked from commit 47d2646751d40a155724f658ba6461e8a2d57aad)
2025-05-11 19:12:15 -04:00
Mend Renovate
c30381c6ec Update dependency androidx.sqlite:sqlite-framework to v2.5.0 (#1986)
(cherry picked from commit a1a7d67afb16cc206f1d6c6d47da52b7705a417d)
2025-05-11 19:12:08 -04:00
Mend Renovate
f489531543 Update dependency com.diffplug.spotless:spotless-plugin-gradle to v7.0.3 (#1977)
(cherry picked from commit 2090a380e0e9ab4f74fd2e5e74e6c2807e96f23d)
2025-05-11 19:12:01 -04:00
Mend Renovate
4bbe795626 Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.2 (#1978)
(cherry picked from commit 8e5cfe9d0acf854c9387ed3405f0fe3cc9140733)
2025-05-11 19:11:54 -04:00
Cuong-Tran
8aa3dca95f Fix navigation issue after migrating a duplicated entry from History tab
(cherry picked from commit d9c4b56336c21db96a835630a48c46ee7a480342)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
2025-05-11 19:11:40 -04:00
NarwhalHorns
5e0f730159 Display total chapters on duplicates list items (#1963)
(cherry picked from commit 12abd9938b7c235d6a1c02391624703476c1f339)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
#	data/src/main/java/tachiyomi/data/manga/MangaMapper.kt
2025-05-11 19:03:47 -04:00
Mend Renovate
f1aed0d8b9 Update dependency androidx.compose:compose-bom to v2025.03.01 (#1927)
(cherry picked from commit c1225a5ef96f5f77bd337e0481935cbef75cc711)
2025-05-11 18:57:57 -04:00
AntsyLich
a3465c31c9 Update non-library manga data when browsing (#1967)
(cherry picked from commit a594ad392d4793f3a5cb2b709d29b2feab6120d3)

# Conflicts:
#	CHANGELOG.md
2025-05-11 18:57:50 -04:00
Mend Renovate
053c48613b Update xml.serialization.version to v0.91.0 (#1956)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 80de0328190bd3adac8e034c420e6a91d3d7cfc9)
2025-05-11 18:55:30 -04:00
NarwhalHorns
615adc567b Display all similarly named duplicates in duplicate manga dialogue (#1861)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 0d35b6fdafbf5451a2743ea9bcfc735bf49374a7)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
2025-05-11 18:14:26 -04:00
AntsyLich
b0f645d906 Deduplicate entries when browsing (#1957)
(cherry picked from commit f81da3dcce9afba883b6a3accdd3bf4ea21cfa81)

# Conflicts:
#	CHANGELOG.md
#	data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt
2025-05-11 18:05:21 -04:00
Mend Renovate
023c78d0e8 Update serialization.version to v1.8.1 (#1953)
(cherry picked from commit 2ce9fa0271c16449475adb07eb6338a497e11e3c)
2025-05-11 17:58:51 -04:00
AntsyLich
824550175a Remove feature flag from Nord theme (#1951)
(cherry picked from commit 5d2110f3fb1aa6b15f62af0dcd3378cfbe475b7a)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt
2025-05-11 17:32:37 -04:00
Secozzi
ad53c0de83 Add markdown support for manga descriptions (#1948)
(cherry picked from commit 4e68339783b47b0780e1b9aee643404339d35ed1)

# Conflicts:
#	CHANGELOG.md
#	gradle/libs.versions.toml
2025-05-11 17:31:41 -04:00
AntsyLich
c8039739d5 Significantly improve browsing speed (near instantaneous) (#1946)
(cherry picked from commit c8ffabc84a096207c1997ab69fc86176f3b53f00)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/domain/manga/model/Manga.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt
#	data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt
#	data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt
#	data/src/main/sqldelight/tachiyomi/data/mangas.sq
#	domain/src/main/java/tachiyomi/domain/manga/interactor/NetworkToLocalManga.kt
#	domain/src/main/java/tachiyomi/domain/manga/repository/MangaRepository.kt
#	domain/src/main/java/tachiyomi/domain/source/repository/SourceRepository.kt
2025-05-11 17:24:33 -04:00
Bartu Özen
26674136e6 Fix app bar action tooltips blocking clicks (#1928)
(cherry picked from commit 77e79233ab054d16bb5dc04a040d0d86a326136f)

# Conflicts:
#	CHANGELOG.md
2025-05-11 16:50:49 -04:00
AntsyLich
9972fa1053 Fix mark existing duplicate read chapters as read option not working in some cases (#1944)
(cherry picked from commit 8a21148578af3c1538e9ab2b1fe5bdf05b4e35c9)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
2025-05-11 16:50:02 -04:00
AntsyLich
ae3f974d8c Fix user notes not restoring when manga doesn't exist in DB (#1945)
(cherry picked from commit e91db86faef8d6b17961a1b73fbf07f0d2c8975d)

# Conflicts:
#	CHANGELOG.md
2025-05-11 16:44:09 -04:00
Mend Renovate
027f179a4b Update kotlin monorepo to v2.1.20 (#1883)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 556290f2d35f739bb4bddc012739acf10b92708d)
2025-05-11 16:43:30 -04:00
Mend Renovate
e80cb1795e Update dependency com.android.tools.build:gradle to v8.9.1 (#1913)
(cherry picked from commit 8b947919acde9932808e666b8bb6a2df9613a67f)
2025-05-11 16:43:22 -04:00
Mend Renovate
66fe599498 Update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.4 (#1926)
(cherry picked from commit b62a9b40eb8ecb4c0c9c861d66c9afc427bc6bbe)
2025-05-11 16:43:15 -04:00
AntsyLich
c9e6e321b3 Update editor config for 'sq' and 'sqm' file [skip ci]
(cherry picked from commit a6b532ee57d24e1ad83f1daea415cad1f313b49c)
2025-05-11 16:43:02 -04:00
kunet
fb3c996904 Add user manga notes (#428)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 8fbe630308b962043c7b59422878c94f80156e9f)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
#	data/src/main/sqldelight/tachiyomi/migrations/5.sqm
#	domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt
2025-05-11 16:42:33 -04:00
perokhe
70b25825ec Fix page number not appearing when opening chapter (#1936)
(cherry picked from commit 132d77aa9947f891f90f1afcdcb24e20ce515438)

# Conflicts:
#	CHANGELOG.md
2025-05-11 16:26:43 -04:00
Cuong-Tran
290e8f5a1e Fix benchmark build (#1938)
(cherry picked from commit b00bbe91beb942f2ac18765be6c78b6f318cc66d)
2025-05-11 16:25:36 -04:00
Jayman Rana
f6b1440bf2 Fix backup sharing from notifications not working when app is in background (#1929)
(cherry picked from commit 3e5d3d099fed5feb6a6807196bea5fed72973fe9)

# Conflicts:
#	CHANGELOG.md
2025-05-11 16:25:27 -04:00
perokhe
77a4919656 Fix next chapter button occasionally jumping to the last page of the current chapter (#1920)
(cherry picked from commit 941dde341eb11703eadae543f351c9284617541c)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt
2025-05-11 16:25:14 -04:00
Ian Hunter
84d901b8a3 Add more Kaomoji for empty/error screens (#1909)
(cherry picked from commit d4aaf6521e86e8509d3971854c46b8520cef7f59)

# Conflicts:
#	CHANGELOG.md
2025-05-11 16:22:24 -04:00
Mend Renovate
d27ed2580f Update dependency com.google.firebase:firebase-bom to v33.11.0 (#1890)
(cherry picked from commit f7046a503bea421a0310f8d2064888aea0a07d11)
2025-05-11 16:22:08 -04:00
MajorTanya
87ea971be0 Fix Bangumi search including novels (#1885)
(cherry picked from commit 953c4e7bc056ed8b9eebe1b111677a4616c4d694)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.kt
2025-05-11 16:22:01 -04:00
Weblate (bot)
91ea70b335 Translations update from Hosted Weblate (#1877)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/uk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: scb261 <scb261261@gmail.com>
(cherry picked from commit 1d6dc1e8b0de08af1370c04eff480e4555095c55)
2025-05-11 16:21:27 -04:00
AntsyLich
2e94e152c2 Use current time as build time for preview builds (#1876)
(cherry picked from commit 935f1fcf3f8e4f9da4774d932b65ae77b44cc773)

# Conflicts:
#	app/build.gradle.kts
2025-05-11 16:21:22 -04:00
Weblate (bot)
eece46fb0f Translations update from Hosted Weblate (#1550)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/bn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/gl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/he/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hu/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/kk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/pl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ro/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/am/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/as/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/be/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bg/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ca/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ceb/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cs/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/el/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/gl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hu/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ka/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/km/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/lt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ml/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/mr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/my/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ro/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sa/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sah/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sc/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sq/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ta/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/th/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/uz/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Abay Emes <abayemes@gmail.com>
Co-authored-by: Acelith <joel.jon@moix.me>
Co-authored-by: Ahmad Ansori Palembani <palembani@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Champ0999 <il.migliore0999@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Doministo <doministo@seznam.cz>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eji-san <ejierubani@gmail.com>
Co-authored-by: FateXBlood <fatexblood@gmail.com>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Harshit Prajapati <harshitprajapati7666@gmail.com>
Co-authored-by: Hasanur Rahman Biplob <hrbiplob10@gmail.com>
Co-authored-by: Horace Johnson <horacejohnson99@gmail.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Co-authored-by: Jakub Fabijan <jakubfabijan@tuta.io>
Co-authored-by: Kerim Demirkaynak <aschannel111@gmail.com>
Co-authored-by: Koanrade <konrad.nowicki91@gmail.com>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: MD_Abdulla072 <md.abdullacse20@gmail.com>
Co-authored-by: Matyáš Caras <matyas@caras.wtf>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mochammad Nopal Attasya <meleboy22@gmail.com>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: NormalRandomPeople <normal.scribe833@silomails.com>
Co-authored-by: Pecs1 <mynameisnoname897@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Piyoka Smith <piyoka5697@ahaks.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Rom Savidor <romsavidor@gmail.com>
Co-authored-by: Saft Octavian <saftoctavian@gmail.com>
Co-authored-by: Shiratori <kuromaruhatake@gmail.com>
Co-authored-by: Siebrenvde <siebren@siebrenvde.dev>
Co-authored-by: Sixten Lund <arbitraryindices@users.noreply.hosted.weblate.org>
Co-authored-by: Sorawit Jannareubate <moszaduck007@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Temuri Doghonadze <temuri.doghonadze@gmail.com>
Co-authored-by: TheKingTermux <50316075+TheKingTermux@users.noreply.github.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Co-authored-by: dianisaac <muhandreop@gmail.com>
Co-authored-by: f0roots <f0rootss@gmail.com>
Co-authored-by: kevans <albapazpi@gmail.com>
Co-authored-by: staxhinho <staxhinho@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
(cherry picked from commit b3726572381abfa1eaaf31cf7f3b685b390f60bf)

# Conflicts:
#	i18n/src/commonMain/moko-resources/de/strings.xml
#	i18n/src/commonMain/moko-resources/kk/strings.xml
#	i18n/src/commonMain/moko-resources/lt/strings.xml
#	i18n/src/commonMain/moko-resources/ru/strings.xml
2025-05-11 16:19:26 -04:00
AntsyLich
34736bc26e For release builds use last commit time as build time (#1873)
(cherry picked from commit dae7d179662ff6d6654e7c10e57f1aeeaf579de8)

# Conflicts:
#	app/build.gradle.kts
2025-05-11 16:17:52 -04:00
renovate[bot]
82cf385f9d
Update dependency net.zetetic:sqlcipher-android to v4.8.0 (#1429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 16:14:08 -04:00
renovate[bot]
682dea2fb1
Update koin to v4.0.4 (#1428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 16:12:00 -04:00
renovate[bot]
c10588d183
Update tachiyomiorg/issue-moderator-action action to v2.6.1 (#1388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 16:11:44 -04:00
Jobobby04
6db1637770 Fix library flags test 2025-05-11 14:57:14 -04:00
Jobobby04
5742d2e3fe Release 1.12.0 2025-05-11 14:24:22 -04:00
BrutuZ
c2d0308ac0
Populate Author field and clear Description on a couple of delegated (#1432) 2025-05-11 14:16:43 -04:00
Callum Wong
84c7da5a7d
Add QR code scan button for sync API key (#1430)
* Add dependency com.journeyapps:zxing-android-embedded:4.3.0

* Add widget parameter to EditTextPreferenceWidget

* Add QR code scanner icon button to sync API key preference which launches a ScanContract

* Remove screenOrientation property from CaptureActivity manifest

* Allow scanning both normal and inverted codes

* store values and make code more concise

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

* Import local context

---------

Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>
2025-05-11 14:15:05 -04:00
cfouche
274350c118
Change for t1 for better hit rate (#1425) 2025-05-11 14:12:44 -04:00
Weblate (bot)
6bd978eef1
Translations update from Hosted Weblate (#1422)
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/uk/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hant/
Translation: Mihon/TachiyomiSY
Translation: Mihon/TachiyomiSY Plurals

Co-authored-by: Alex Maryson Jr <akamar87@gmail.com>
Co-authored-by: B4LiN7 <87660017+B4LiN7@users.noreply.github.com>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Hualiang <642615676@qq.com>
Co-authored-by: Kosťantin Horovij <lg096066587039@gmail.com>
Co-authored-by: Sky children of the Light <tu25261@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: edgole <test.backache009@aleeas.com>
Co-authored-by: fl0k1 <michele.carnova@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Co-authored-by: Георгій Обушенков <heorhii.obushenkov@gmail.com>
Co-authored-by: ابومسلم <linuxmint1978@gmail.com>
2025-05-11 13:49:55 -04:00
Weblate (bot)
e0f40fad8c
Translations update from Hosted Weblate (#1408)
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/as/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hant/
Translation: Mihon/TachiyomiSY
Translation: Mihon/TachiyomiSY Plurals

Co-authored-by: Champ0999 <il.migliore0999@gmail.com>
Co-authored-by: Corrado Belmonte <corrado.spam@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Eji-san <ejierubani@gmail.com>
Co-authored-by: FateXBlood <fatexblood@gmail.com>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Nam Pai <namhg911@gmail.com>
Co-authored-by: Renan Sarto <app@renansg.com>
Co-authored-by: Sepultrex <sepultrex@gmail.com>
Co-authored-by: Shiratori <kuromaruhatake@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Tim Schneeberger <tim.schneeberger@outlook.de>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: dianisaac <muhandreop@gmail.com>
Co-authored-by: quangpao <ddquangbao@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
2025-03-18 18:17:56 -04:00
renovate[bot]
5647665782
Update dependency com.google.oauth-client:google-oauth-client to v1.39.0 (#1410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-18 18:16:40 -04:00
Jobobby04
df99e7ee49 SpotlessApply 2025-03-18 18:04:23 -04:00
cfouche
dbd4437474
Update base URL and host for Pururin to pururin.me (#1415) 2025-03-18 17:43:03 -04:00
AntsyLich
9c198d0c33 Seperate mark duplicate read chapters as read behaviors as options (#1870)
(cherry picked from commit 8a3b6107755c768924a31c2b58d705296133839c)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
2025-03-18 17:37:58 -04:00
AntsyLich
d62a8a138c Add back option to hide unread chapter badge in library (#1871)
(cherry picked from commit ac432e2e941f4689caad246bab6aa7d303c83bfa)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
2025-03-18 17:31:02 -04:00
Cuong-Tran
f8a57ec98c Add back build tools version to sign-android-release (#1842)
(cherry picked from commit 7028b8673a6b78dc6ccc19f5b3242bf1b37ca908)
2025-03-18 17:29:07 -04:00
Mend Renovate
aa6339df06 Update dependency org.jsoup:jsoup to v1.19.1 (#1822)
(cherry picked from commit 2dc8cf000b871b8ffe07016d76a4bc7114d6ea49)
2025-03-18 17:28:57 -04:00
Mend Renovate
3fbbfbd9cb Update dependency androidx.compose:compose-bom to v2025.03.00 (#1857)
(cherry picked from commit f76a3ad15ad3954c512c20d99337a207f2e2d37a)
2025-03-18 17:28:49 -04:00
AntsyLich
31d6bf1967 Remove closed issue/pr auto lock workflow [skip ci]
(cherry picked from commit f33aa1ac9223393d0921df2902e4b59589ab7d2d)

# Conflicts:
#	.github/workflows/lock.yml
2025-03-18 17:28:41 -04:00
MajorTanya
226b3f2ff4 Add app ID to debug info (#1847)
This will avoid the need to know which forks has which version numbers
and avoid confusion in support.

(cherry picked from commit eddf07f9ac31bab57d06515e42df9c854bc50eed)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt
2025-03-18 17:27:55 -04:00
AntsyLich
ac8dab75fe Make option to mark duplicate chapter as read apply when reading (#1839)
(cherry picked from commit 22b5fb58ff8e89635d646f8fa29256b53c41ffbf)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
2025-03-18 17:27:12 -04:00
AntsyLich
aad2bf4645 Make more sliders discrete and ensure they don't look out of place (#1840)
Also cleanup the underlying code

(cherry picked from commit 4f06c1cc09d15245b26b8a862738cb6a859fedcc)

# Conflicts:
#	CHANGELOG.md
2025-03-18 17:24:05 -04:00
AntsyLich
7f71296e1c Change label of setting to always use SSIV in long strip reader (#1834)
(cherry picked from commit 85d168ed5e201134558cc843aba896306617c9ca)

# Conflicts:
#	CHANGELOG.md
2025-03-18 16:57:58 -04:00
AntsyLich
9137170fb8 Bump default user agent (#1833)
(cherry picked from commit d3691cc2563815490683cc69cbc3260e4561906c)

# Conflicts:
#	CHANGELOG.md
2025-03-18 16:57:37 -04:00
FlaminSarge
0af667c9aa Attempt to fix crash when migrating or removing entries from library (#1828)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 563bc02113a5ebc53650fdfdd13f408284a0cdc8)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt
#	domain/src/main/java/tachiyomi/domain/manga/interactor/GetLibraryManga.kt
2025-03-18 16:57:00 -04:00
NarwhalHorns
8dc6a95ce6 Display staff information on Anilist tracker search results (#1810)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit b702603965044cfe3ee852f8d0c970b6eb93b97a)

# Conflicts:
#	CHANGELOG.md
2025-03-18 16:55:40 -04:00
Roshan Varughese
1eb64d117e Fix an issue where tracker reading progress is changed to a lower value (#1795)
(cherry picked from commit 2e2f1ed82d63a93ebf87ee8494434c1bad2e268c)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
2025-03-18 16:55:21 -04:00
Mend Renovate
8f48a80bc4 Update dependency com.android.tools.build:gradle to v8.9.0 (#1824)
(cherry picked from commit b2765a00d285040531619a287d5144718959dd49)
2025-03-18 16:54:37 -04:00
NarwhalHorns
e76dd7fab0 Update track search preview (#1825)
(cherry picked from commit 0e6d6c087e5a4d889b9153b390d8335d7add1e87)
2025-03-18 16:54:29 -04:00
Smol Ame
b53a9ce5ae Tweak and adjust issue template (#1817)
Co-authored-by: BrutuZ <brutuz@users.noreply.github.com>
(cherry picked from commit 4f7122d6f09f87930ccd7dae7c557f4b236bbc4b)
2025-03-18 16:54:22 -04:00
Mend Renovate
952f26929c Update dependency io.mockk:mockk to v1.13.17 (#1786)
(cherry picked from commit b763d3e2c24caac6898981395aece2984b3e03a3)
2025-03-18 16:54:12 -04:00
AwkwardPeak7
9ff048e683 Fix webview crash caused by 793d7fb (#1819)
(cherry picked from commit 9957fff2fbb6dad6f9df89bb2c16db34d9e4da96)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
2025-03-04 11:33:14 -05:00
Jobobby04
a64fe8121b Guard against NPE in edit info dialog 2025-03-02 14:03:33 -05:00
Jobobby04
4db7a32075 Fix migration delete downloaded not registering properly 2025-03-02 14:01:39 -05:00
Jobobby04
20ee5ea3e1 Fix database migration 2025-03-02 13:42:54 -05:00
Jobobby04
d9200ef006 Build fix 2025-03-02 13:34:37 -05:00
AwkwardPeak7
dfde271f7f Spoof or remove X-Requested-With header from webview (#1812)
(cherry picked from commit 793d7fbe40c87ed233da8cc99d544d01024ed4f5)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
#	core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt
2025-03-02 13:18:06 -05:00
Mend Renovate
5346eac037 Update dependency com.google.firebase:firebase-bom to v33.10.0 (#1789)
(cherry picked from commit b12ee027ea8941cb29d0f83085481c75eb862ed4)
2025-03-02 13:14:32 -05:00
Smol Ame
95e151be4b Update Issue Request Template (#1808)
(cherry picked from commit d7a1ae27346a983f658fb88cb525cf8b785b3bb3)
2025-03-02 13:14:23 -05:00
rhjdvsgsgks
98af745e08 Add build tool version to android config (#1803)
(cherry picked from commit 7566918ee749e76c701aeda7e99d81003676a51c)
2025-03-02 13:14:05 -05:00
AntsyLich
56433a624e Add option to mark new duplicate read chapters as read (#1785)
(cherry picked from commit cd0481592c09dc9cfb331805e90e6e5c3752a08c)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt
2025-03-02 13:13:33 -05:00
Mend Renovate
c59cb620dd Update dependency com.android.tools.build:gradle to v8.8.2 (#1784)
(cherry picked from commit b93746b01e78d4e75dbd1c6e9dda1b7b1baa6831)
2025-03-02 13:10:30 -05:00
AntsyLich
f60cb9bb64 Remove alphabetical category sort option (#1781)
(cherry picked from commit 2b0c28938bfd74577d2ff0736b2cc72f4e4705cf)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt
2025-03-02 13:10:23 -05:00
Mend Renovate
949a2a95ad Update dependency androidx.activity:activity-compose to v1.10.1 (#1782)
(cherry picked from commit 4db3817782e73c75abe0b40c93273df90c683a42)
2025-03-02 13:09:29 -05:00
Mend Renovate
0bd700699b Update dependency androidx.constraintlayout:constraintlayout to v2.2.1 (#1783)
(cherry picked from commit ec07843f0cab02d7d1fee9c90eed35441b7b671b)
2025-03-02 13:09:23 -05:00
Cuong-Tran
1d10925829 Add back support for drag-and-drop category reordering (#1427)
(cherry picked from commit 919607cd06ee45ac667a2fd650d85aaf6ebb9762)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt
2025-03-02 13:09:13 -05:00
Cuong-Tran
0e2866260f Add Xiaomi system app to list of invalid browsers (#1776)
(cherry picked from commit d91c7b609359e83fcbb1b93ac16f608f8d45a2f2)

# Conflicts:
#	CHANGELOG.md
2025-03-02 13:02:10 -05:00
Roshan Varughese
02ace23c38 Add option to export minimal library information to a CSV file (#1161)
(cherry picked from commit fab8b17d99c44a08555b1f584c56d62a47737ca0)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt
2025-03-02 13:01:51 -05:00
Jobobby04
3e16adf961 SpotlessApply 2025-03-02 12:59:36 -05:00
AntsyLich
fb1a3da0ea Use .toUri() extension function
(cherry picked from commit 0dda64b9d80a47a96fb52d13b5e0ece6d5fca2b1)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
2025-03-02 12:55:41 -05:00
AntsyLich
5f2e979bb5 Remove F-droid warnings
(cherry picked from commit 181dbbb638686a284fa24c4e43d7c022a4f8e4bb)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt
#	app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
#	domain/src/test/java/tachiyomi/domain/release/interactor/GetApplicationReleaseTest.kt
2025-03-02 12:54:48 -05:00
MajorTanya
5d4d15aa9c Add private tracking support for Kitsu (#1774)
(cherry picked from commit 1dd81ef1e1b383f379f4e8e53d27a47cf7f0278f)

# Conflicts:
#	CHANGELOG.md
2025-03-02 12:45:02 -05:00
Mend Renovate
fb71d0cd68 Update dependency gradle to v8.13 (#1773)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 2d0be5b0c93c9e3991ca593304d81d4d22dd72de)
2025-03-02 12:44:44 -05:00
Mend Renovate
a189a7eaec Update dependency com.android.tools:desugar_jdk_libs to v2.1.5 (#1772)
(cherry picked from commit 4d7350e3184f13cbcfda357f75859dad0d679154)
2025-03-02 12:44:36 -05:00
NarwhalHorns
59a6bd700b Support for private tracking with AniList and Bangumi (#1736)
Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 49b2b346b65c2631a8369c8f6643e945720770de)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt
#	app/src/main/java/eu/kanade/test/DummyTracker.kt
2025-03-02 12:44:26 -05:00
MajorTanya
278224676b Fix Bangumi login regression (#1770)
Caused by #1748.

Two different issues actually.

Firstly, the getUsername API call uses the authClient, which uses the
BangumiInterceptor to get the current OAuth data and attach the
Authorization header. However, on login, #1748 did not try to set the
new auth details until after attempting to call getUsername.
This would cause Mihon to think the user was not authenticated with
Bangumi and cancel the process.

This is fixed by having Mihon store the OAuth credentials in the
interceptor first before attempting to call getUsername.

The second issue is a simple trailing dollar sign in the API URL for
the getUsername method. This was removed.

(cherry picked from commit badc229a2312c0c750c34631f303ac4ca970dc71)
2025-03-02 12:30:27 -05:00
MajorTanya
66f2877a3f Add back explicit update(track) call to Bangumi (#1771)
Most if not all other trackers do this too. Technically this causes
some request duplication (since things like the BaseTracker's
setRemoteLastChapterRead fire anyway due to the tracker sheet being
open. But considering the reduced number of requests in other places,
I think this is still acceptable.

This change will allow #1736 to proceed, hopefully.

(cherry picked from commit 277d8bad8e8d21cd74dc1681da09a4b980f455e0)
2025-03-02 12:30:16 -05:00
MajorTanya
a97deb0036 Add "Monochrome" theme (#1752)
This theme is mainly geared towards e-Ink displays with limited/no
colour capabilities. Previous themes like Yin & Yang would make heavy
use of greyscale colours which could look off on some devices.

This theme is probably not conformant to Material Design 3 colour
scheme guidelines, but it does boast some amazing WebAIM contrast
ratios (#FFFFFF text on #000000 background gets a ratio of 21:1, vice
versa too).

Initially, this was intended as a purely black and white theme but
some contrast issues arose, such as the download badges (tertiary
background, onTertiary text colour) having the same colour as unread
badges (primary/onPrimary), or the step indicators (stops) not being
visible on sliders (since they use the colours of the opposite state
track (active region stops are the colour of the inactive region track
and vice versa).

To mitigate this, each variant (dark/light) of the theme has one
additional grey mixed in for their tertiary and secondaryContainer
colours each. For the dark variant, this is a #A0A0A0 background for
#000000 text (8.03:1 contrast ratio) and for the light variant, it is
a #505050 background for #FFFFFF text (8.06:1 contrast ratio).
This results in distinct unread vs download badges and visible steps
in the sliders.

---------

Co-authored-by: Sunspark-007 <73711243+Sunspark-007@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 8b48d1016b851b425e4f66d44bca098220585c37)

# Conflicts:
#	CHANGELOG.md
2025-03-02 12:30:09 -05:00
MajorTanya
ab976d8b07 Migrate to Bangumi's newer v0 API (#1748)
This comes with many benefits:
- Starting dates are now available and shown to users
- Lays groundwork to add private tracking for Bangumi, e.g. in #1736
- Mihon makes approximately 2-4 times fewer calls to Bangumi's API
- Simplified interceptor for the access token addition
  - v0 does not allow access tokens in the query string
- There is actively maintained documentation for it

Also shrunk the DTOs for Bangumi by removing attributes we have no
use for either now or in the foreseeable future. Volume data remains
in case Mihon wants to ever support volumes. But attributes such as
user avatars, nicknames, data relating to Bangumi's tag & meta-tag
systems, etc. have been removed or just not added to the DTOs.

(cherry picked from commit a96fbba3dc354e363b85923c52feceb88dc34447)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt
2025-03-02 12:29:47 -05:00
Mend Renovate
cb2cfa7e94 Update dependency androidx.compose:compose-bom to v2025 (#1651)
(cherry picked from commit d8a530266ffd7774df1af6c0dc5fc7e66fe2b20c)
2025-03-02 12:24:56 -05:00
Cuong-Tran
2c2f84bb29 Fix backup/restore of category related preferences (#1726)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit e1724d1aa0e3340e1404cfd80bd264831d86a879)

# Conflicts:
#	CHANGELOG.md
2025-03-02 12:24:48 -05:00
Cuong-Tran
7156b0dcce Reuse AppBar in manga screen (#1367)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 2cd52d5a1ff48b0f9cf17245c1bfa66f99b8c187)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt
2025-03-02 12:24:07 -05:00
Jobobby04
f62671742c Fix build 2025-03-02 12:21:13 -05:00
AntsyLich
58be872bef Cleanup and tweak preference widgets (#1769)
(cherry picked from commit ebfbbf0741c04dc450a943d2cf77f48eed5c6dfa)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
2025-03-02 12:21:03 -05:00
Cuong-Tran
ce6b847c8b Fix App's preferences referencing deleted categories (#1734)
(cherry picked from commit eeb683069a3a0be7e769ac9273b5accc582e03ec)

# Conflicts:
#	CHANGELOG.md
#	app/build.gradle.kts
2025-03-02 11:57:47 -05:00
Roshan Varughese
9c22e7fcb7 Add button to favorite manga from history screen (#1733)
(cherry picked from commit 7e71a34256e79b03a8a8ea50334b1ccece4b7154)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt
2025-03-02 11:56:46 -05:00
NGB-Was-Taken
452f36939a Apply "Downloaded only" filter to all entries regardless of favourite status (#1603)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 29ee53f4612b6ec9b399da9d29f18cfd0b1a2768)

# Conflicts:
#	CHANGELOG.md
2025-03-02 11:56:09 -05:00
Mend Renovate
f0b821e2df Update aboutlib.version to v11.6.3 (#1737)
(cherry picked from commit 6a223f34a0430dba2917e2fe2b737540658e01e2)
2025-03-02 11:55:45 -05:00
BrutuZ
cda87a5c07 Ignore hidden files/folders for Local Source chapter list (#1763)
(cherry picked from commit c97fe71e290604849299f1ebb9dfe1295188ca60)

# Conflicts:
#	CHANGELOG.md
2025-03-02 11:55:35 -05:00
Mend Renovate
10844339b8 Update aboutlib.version to v11.6.0 (#1728)
(cherry picked from commit 8e81a5e68b61a7db36cd3ef39ac3f319c4d6e0a1)
2025-03-02 11:53:48 -05:00
Mend Renovate
042785e188 Update plugin firebase-crashlytics to v3.0.3 (#1702)
(cherry picked from commit b08270d52310d30670bb3b81dfebb594759e2dd8)
2025-03-02 11:53:42 -05:00
AntsyLich
07740ae83c Add more editor configs and move ktlint config to it (#1731)
(cherry picked from commit 34d1e6fa278846dd8eb6ea82c936818d4610d3c2)

# Conflicts:
#	.editorconfig
#	buildSrc/src/main/kotlin/mihon.code.lint.gradle.kts
2025-03-02 11:53:21 -05:00
Mend Renovate
9d08fe05c1 Update dependency com.android.tools.build:gradle to v8.8.1 (#1723)
(cherry picked from commit a80965f7f18e51a8cd0b5029b34fe4fe9c04b494)
2025-03-02 11:51:17 -05:00
Mend Renovate
516114011f Update paging.version to v3.3.6 (#1717)
(cherry picked from commit 59ee61039b0e221ee6c00c052f89f32413eb502f)
2025-03-02 11:51:10 -05:00
Mend Renovate
bb08522a32 Update dependency io.coil-kt.coil3:coil-bom to v3.1.0 (#1701)
(cherry picked from commit b7a96e69465e3fd63fbe901591e6fca6f9557334)
2025-03-02 11:51:04 -05:00
Mend Renovate
25949c3296 Update moko to v0.24.5 (#1694)
(cherry picked from commit 31a3f9e051f211af38c4a62b5a3bcfc711c93ee3)
2025-03-02 11:50:58 -05:00
AntsyLich
5720774bbf Rework slider UI
Fixes #1474

(cherry picked from commit e8c9cb2c2e4c24443368f0d653c5283f9671ffec)

# Conflicts:
#	presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt
2025-03-02 11:50:51 -05:00
Mend Renovate
74c8b20a85 Update aboutlib.version to v11.5.0 (#1663)
(cherry picked from commit d592ab2e8712d13169942a7e7f53ef0c29a77a7b)
2025-03-02 11:50:17 -05:00
Mend Renovate
744b714c25 Update dependency gradle to v8.12.1 (#1662)
(cherry picked from commit 9d6ed93daaa91217fc82fb856e6d3d4eedd0092a)
2025-03-02 11:50:10 -05:00
Mend Renovate
73d57239f7 Update kotlin monorepo to v2.1.10 (#1671)
(cherry picked from commit 34efa8d9017f58001a93db4e53b4ca03a0ab2660)
2025-03-02 11:50:03 -05:00
MajorTanya
325a706840 Add Infinix system app to list of invalid browsers (#1684)
* Add Infinix system app to list of invalid browsers

`com.transsion.resolver` being picked by the system as a suitable
browser caused a Mihon user with an Infinix device to be unable to
open any links in browsers, including tracker login and opening a
WebView page in a real browser.

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

* Add docstring to DeviceUtil.invalidDefaultBrowsers

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit bfc8320aa4720dc3b908aab1b8a1ffb99d43193b)

# Conflicts:
#	CHANGELOG.md
2025-03-02 11:42:37 -05:00
MajorTanya
c179b1812c Fix MAL tracker losing track of login expiration (#1682)
* Add missing @EncodeDefault annotation to MALOAuth

Similar to the situation with Bangumi, the missing annotation means
kotlinx.serialization would _provide_ the default value upon
instantiation but not serialise it to disk. This means the isExpired()
calculation would effectively rarely/never do its job correctly,
leading to Mihon sending expired tokens to MAL and causing problems
for everyone involved.

Overall, this change _could_ (should) lead to a drastic reduction in
MAL requests failing, leading to users having to relink their MAL
accounts.

Also switched createdAt to be in seconds instead of milliseconds as
all other trackers use seconds for timestamps (except for AniList,
which uses milliseconds but doesn't use a createdAt timestamp anyway).

* Add CHANGELOG.md entry

(cherry picked from commit 29ec7c125a3f1a1f39a90f8eba2d3e39b5af9797)

# Conflicts:
#	CHANGELOG.md
2025-03-02 11:42:08 -05:00
MajorTanya
1fa05703fa Fix Bangumi tracker losing track of login expiration (#1681)
* Fix Bangumi tracking losing track of login state

kotlinx.serialization does NOT serialize default values (like
createdAt in BGMOAuth.kt), so every time the Bangumi tracker
deserialized the tracker OAuth, createdAt was set to the time of the
read, not the time of issuance.

Separately, BangumiInterceptor did correctly fetch new OAuth
credentials upon detected expiry of the stored credentials and saved
them, but did not use them for the current request (the new
credentials were used for all subsequent requests only). This led to
401 errors from Bangumi because the expired access_token was provided.
 A subsequent request using the newly acquired access_token would end
 up being successful.

* Add CHANGELOG.md entry

(cherry picked from commit dce6aacf02d07f3f123b19b1b74cbbe18c28852b)

# Conflicts:
#	CHANGELOG.md
2025-03-02 11:40:49 -05:00
MajorTanya
b34f807d33 Add zoned "Current time" to debug info and include year & timezone in logcat output (#1672)
* Add zoned date & time to debug info & logs

This should help distinguish log entries that happened recently and
may be related to crashes from older entries that occurred before now.

* Change logcat date and time output format

After some discussion, it was decided to adjust the logcat date and
time display to include the year and the timezone in the logcat
output. This results in a line start like this:

`2025-01-27 18:37:46.662 +0100`

which follows the following DateTimeFormatter pattern:

`yyyy-MM-dd HH:mm:ss.SSS Z`

* Add CHANGELOG.md entry

(cherry picked from commit 503d0be66772c37e08e69e5d022475245b706fd1)

# Conflicts:
#	CHANGELOG.md
2025-03-02 11:40:32 -05:00
renovate[bot]
fda27e6eba
Update dependency com.google.oauth-client:google-oauth-client to v1.38.0 (#1402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-02 11:38:14 -05:00
Tim Schneeberger
217503eab0
feat(MangaDex): use tracker links to associate mangas automatically with trackers (#1387)
* feat: add searchById support to trackers (MAL, AniList, MangaUpdates only)

* feat: add new preference to toggle auto selection of tracker items using source metadata if available

* feat: add new preference to toggle auto selection of tracker items using source metadata if available

* feat: add automatic title selection using source metadata to TrackInfoDialog.kt

* style: apply spotless

* refactor: remove hardcoded MangaDexSearchMetadata cast and introduce common interface
2025-03-02 11:37:50 -05:00
lord-ne
8d062cecfd
Use COMPLETE category when sync finishes (#1385) 2025-03-02 11:37:07 -05:00
BrutuZ
614839c023
Fix CDN subdomain for delegated source covers (#1384) 2025-03-02 11:36:41 -05:00
Tim Schneeberger
254980695b
feat: batch processing for recommendations & sort by relevancy (#1383)
* refactor: use NoResultsException

* refactor: cleanup RecommendationPagingSources

* refactor: turn wake/wifi lock functions into reusable extensions

* feat: implement batch recommendation (initial version)

* fix: serialization issues

* fix: wrong source id

* refactor: increase performance using virtual paging

* refactor: update string

* refactor: handle 404 of MD source correctly

* style: add newline

* refactor: create universal throttle manager

* refactor: throttle requests

* chore: remove unused strings

* feat: rank recommendations by match count

* feat: add badges indicating match count to batch recommendations

* fix: handle rec search with no results

* fix: validate flags in pre-search bottom sheet

* feat: implement 'hide library entries' for recommendation search using custom SmartSearchEngine for library items

* style: run spotless

* fix: cancel button

* fix: racing condition causing loss of state
2025-03-02 11:36:07 -05:00
Weblate (bot)
28cca49635
Translations update from Hosted Weblate (#1379)
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/as/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hant/
Translation: Mihon/TachiyomiSY

Co-authored-by: Corrado Belmonte <corrado.spam@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Eji-san <ejierubani@gmail.com>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Nam Pai <namhg911@gmail.com>
Co-authored-by: Shiratori <kuromaruhatake@gmail.com>
Co-authored-by: Tim Schneeberger <tim.schneeberger@outlook.de>
Co-authored-by: quangpao <ddquangbao@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
2025-03-02 11:32:25 -05:00
415 changed files with 10742 additions and 3636 deletions

View File

@ -1,12 +1,32 @@
[*.{kt,kts}]
max_line_length = 120
indent_size = 4
root = true
[*]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{xml,sq,sqm}]
indent_size = 4
# noinspection EditorConfigKeyCorrectness
[*.{kt,kts}]
indent_size = 4
max_line_length = 120
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_class-signature = disabled
ktlint_standard_discouraged-comment-location = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-signature = disabled
# SY
ktlint_standard_filename = disabled
ktlint_standard_argument-list-wrapping = disabled
ktlint_standard_function-naming = disabled
@ -14,3 +34,7 @@ ktlint_standard_property-naming = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_comment-wrapping = disabled
ktlint_standard_max-line-length = disabled
ktlint_standard_type-argument-comment = disabled
ktlint_standard_value-argument-comment = disabled
ktlint_standard_value-parameter-comment = disabled

View File

@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: ❌ Help with Extensions
url: https://mihon.app/docs/faq/browse/extensions
about: For extension-related questions/issues
- name: 🖥️ Mihon website
url: https://mihon.app/
about: Guides, troubleshooting, and answers to common questions

View File

@ -43,9 +43,9 @@ body:
attributes:
label: Crash logs
description: |
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
If you're experiencing crashes, if possible, go to the app's **More → Settings → Advanced** page, press **Dump crash logs** and share the crash logs here.
placeholder: |
You can paste the crash logs in plain text or upload it as an attachment.
You can upload the crash log file as an attachment, or paste the crash logs in plain text if needed.
- type: input
id: tachiyomisy-version
@ -53,7 +53,7 @@ body:
label: TachiyomiSY version
description: You can find your TachiyomiSY version in **More → About**.
placeholder: |
Example: "1.11.0"
Example: "1.12.0"
validations:
required: true
@ -96,9 +96,9 @@ body:
required: true
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true
- label: I have updated the app to version **[1.11.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
- label: I have updated the app to version **[1.12.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
required: true
- label: I have updated all installed extensions.
- label: I have filled out all of the requested information in this form, including specific version numbers.
required: true
- label: I will fill out all of the requested information in this form.
- label: I understand that **Mihon does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions.
required: true

View File

@ -31,7 +31,7 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to version **[1.11.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
- label: I have updated the app to version **[1.12.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@ -63,6 +63,8 @@ jobs:
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: '35.0.1'
- name: Clean up build artifacts
run: |

View File

@ -1,10 +1,10 @@
name: Remote Dispatch Action Initiator
on:
push:
branches:
branches:
- 'preview'
jobs:
trigger_preview_build:
name: Trigger preview build
@ -14,8 +14,14 @@ jobs:
- name: Clone repo
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
- name: Set up gradle
uses: gradle/actions/setup-gradle@v4
- name: Create Tag
run: |
@ -28,3 +34,6 @@ jobs:
-H 'Accept: application/vnd.github.everest-preview+json' \
-u ${{ secrets.ACCESS_TOKEN }} \
--data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}'
- name: Run unit tests
run: ./gradlew testDebugUnitTest testDevDebugUnitTest

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Moderate issues
uses: tachiyomiorg/issue-moderator-action@v2.6.0
uses: tachiyomiorg/issue-moderator-action@v2.6.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
duplicate-label: Duplicate

View File

@ -1,19 +0,0 @@
name: Lock threads
on:
# Daily
schedule:
- cron: '0 0 * * *'
# Manual trigger
workflow_dispatch:
inputs:
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: '2'
pr-inactive-days: '2'

5
.gitignore vendored
View File

@ -18,4 +18,7 @@ local.properties
# SY ignores
google-services.json
/app/src/main/assets/client_secrets.json
*.apk
*.apk
# Custom ignores
/keys

View File

@ -31,12 +31,12 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi.sy"
versionCode = 71
versionName = "1.11.0"
versionCode = 75
versionName = "1.12.0"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
ndk {
@ -71,6 +71,8 @@ android {
isMinifyEnabled = true
isShrinkResources = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = true)}\"")
}
create("benchmark") {
initWith(getByName("release"))
@ -237,7 +239,7 @@ dependencies {
implementation(libs.preferencektx)
// Dependency injection
implementation(libs.injekt.core)
implementation(libs.injekt)
// Image loading
implementation(platform(libs.coil.bom))
@ -255,13 +257,15 @@ dependencies {
exclude(group = "androidx.viewpager", module = "viewpager")
}
implementation(libs.insetter)
implementation(libs.bundles.richtext)
implementation(libs.richeditor.compose)
implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager)
implementation(libs.compose.materialmotion)
implementation(libs.swipe)
implementation(libs.compose.webview)
implementation(libs.compose.grid)
implementation(libs.reorderable)
implementation(libs.bundles.markdown)
// Logging
implementation(libs.logcat)
@ -306,17 +310,12 @@ dependencies {
// Koin
implementation(sylibs.koin.core)
implementation(sylibs.koin.android)
// ZXing Android Embedded
implementation(sylibs.zxing.android.embedded)
}
androidComponents {
beforeVariants { variantBuilder ->
// Disables standardBenchmark
if (variantBuilder.buildType == "benchmark") {
variantBuilder.enable = variantBuilder.productFlavors.containsAll(
listOf("default" to "dev"),
)
}
}
onVariants(selector().withFlavor("default" to "standard")) {
// Only excluding in standard flavor because this breaks
// Layout Inspector's Compose tree

View File

@ -359,7 +359,7 @@
<data android:scheme="https" />
<data android:scheme="http" />
<data android:host="pururin.io" />
<data android:host="pururin.me" />
<data android:pathPattern="/gallery/..*" />
</intent-filter>
@ -413,6 +413,10 @@
android:scheme="tachiyomisy" />
</intent-filter>
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
tools:remove="screenOrientation" />
</application>
<uses-sdk tools:overrideLibrary="rikka.shizuku.api"

View File

@ -82,6 +82,7 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.service.ReleaseService
@ -110,7 +111,7 @@ class DomainModule : InjektModule {
addFactory { RenameCategory(get()) }
addFactory { ReorderCategory(get()) }
addFactory { UpdateCategory(get()) }
addFactory { DeleteCategory(get()) }
addFactory { DeleteCategory(get(), get(), get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetDuplicateLibraryManga(get()) }
@ -128,6 +129,7 @@ class DomainModule : InjektModule {
addFactory { SetMangaViewerFlags(get()) }
addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get(), get()) }
addFactory { UpdateMangaNotes(get()) }
addFactory { SetMangaCategories(get()) }
addFactory { GetExcludedScanlators(get()) }
addFactory { SetExcludedScanlators(get()) }
@ -152,7 +154,7 @@ class DomainModule : InjektModule {
addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { GetAvailableScanlators(get()) }
addFactory { FilterChaptersForDownload(get(), get(), get(), get()) }

View File

@ -28,7 +28,6 @@ import tachiyomi.data.source.SavedSearchRepositoryImpl
import tachiyomi.domain.chapter.interactor.DeleteChapters
import tachiyomi.domain.chapter.interactor.GetChapterByUrl
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
import tachiyomi.domain.history.interactor.GetHistoryByMangaId
import tachiyomi.domain.manga.interactor.DeleteByMergeId
import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries
import tachiyomi.domain.manga.interactor.DeleteMangaById
@ -88,7 +87,6 @@ class SYDomainModule : InjektModule {
addFactory { DeleteChapters(get()) }
addFactory { DeleteMangaById(get()) }
addFactory { FilterSerializer() }
addFactory { GetHistoryByMangaId(get()) }
addFactory { GetChapterByUrl(get()) }
addFactory { GetSourceCategories(get()) }
addFactory { CreateSourceCategory(get()) }

View File

@ -20,6 +20,7 @@ import tachiyomi.domain.chapter.model.NoChaptersException
import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.chapter.service.ChapterRecognition
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.source.local.isLocal
import java.lang.Long.max
@ -35,6 +36,7 @@ class SyncChaptersWithSource(
private val updateChapter: UpdateChapter,
private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators,
private val libraryPreferences: LibraryPreferences,
) {
/**
@ -150,12 +152,18 @@ class SyncChaptersWithSource(
return emptyList()
}
val reAdded = mutableListOf<Chapter>()
val changedOrDuplicateReadUrls = mutableSetOf<String>()
val deletedChapterNumbers = TreeSet<Double>()
val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
val readChapterNumbers = dbChapters
.asSequence()
.filter { it.read && it.isRecognizedNumber }
.map { it.chapterNumber }
.toSet()
removedChapters.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
@ -165,12 +173,20 @@ class SyncChaptersWithSource(
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to it.dateFetch }
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get()
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_NEW)
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
// Sources MUST return the chapters from most to less recent, which is common.
var itemCount = newChapters.size
var updatedToAdd = newChapters.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
if (chapter.chapterNumber in readChapterNumbers && markDuplicateAsRead) {
changedOrDuplicateReadUrls.add(chapter.url)
chapter = chapter.copy(read = true)
}
if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
chapter = chapter.copy(
@ -183,19 +199,19 @@ class SyncChaptersWithSource(
chapter = chapter.copy(dateFetch = it)
}
reAdded.add(chapter)
changedOrDuplicateReadUrls.add(chapter.url)
chapter
}
// --> EXH (carry over reading progress)
if (manga.isEhBasedManga()) {
val finalAdded = updatedToAdd.subtract(reAdded)
val finalAdded = updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls }
if (finalAdded.isNotEmpty()) {
val max = dbChapters.maxOfOrNull { it.lastPageRead }
if (max != null && max > 0) {
updatedToAdd = updatedToAdd.map {
if (it !in reAdded) {
if (it.url !in changedOrDuplicateReadUrls) {
it.copy(lastPageRead = max)
} else {
it
@ -225,12 +241,8 @@ class SyncChaptersWithSource(
// Note that last_update actually represents last time the chapter list changed at all
updateManga.awaitUpdateLastUpdate(manga.id)
val reAddedUrls = reAdded.map { it.url }.toHashSet()
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
return updatedToAdd.filterNot {
it.url in reAddedUrls || it.scanlator in excludedScanlators
}
return updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls || it.scanlator in excludedScanlators }
}
}

View File

@ -23,7 +23,7 @@ class GetPagePreviews(
return try {
val pagePreviews = try {
pagePreviewCache.getPageListFromCache(manga, chapterIds, page)
} catch (e: Exception) {
} catch (_: Exception) {
source.getPagePreviewList(manga.toSManga(), chapters.map { it.toSChapter() }, page).also {
pagePreviewCache.putPageListToCache(manga, chapterIds, it)
}

View File

@ -4,6 +4,7 @@ import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
@ -32,9 +33,8 @@ class UpdateManga(
remoteManga: SManga,
manualFetch: Boolean,
coverCache: CoverCache = Injekt.get(),
// SY -->
libraryPreferences: LibraryPreferences = Injekt.get(),
downloadManager: DownloadManager = Injekt.get(),
// SY <--
): Boolean {
val remoteTitle = try {
remoteManga.title
@ -42,14 +42,13 @@ class UpdateManga(
""
}
// SY -->
val title = if (remoteTitle.isNotBlank() && localManga.ogTitle != remoteTitle) {
downloadManager.renameMangaDir(localManga.ogTitle, remoteTitle, localManga.source)
remoteTitle
} else {
null
}
// SY <--
// if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db
val title =
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) {
remoteTitle
} else {
null
}
val coverLastModified =
when {
@ -69,7 +68,7 @@ class UpdateManga(
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
return mangaRepository.update(
val success = mangaRepository.update(
MangaUpdate(
id = localManga.id,
title = title,
@ -84,6 +83,10 @@ class UpdateManga(
initialized = true,
),
)
if (success && title != null) {
downloadManager.renameManga(localManga, title)
}
return success
}
suspend fun awaitUpdateFetchInterval(

View File

@ -23,7 +23,7 @@ val Manga.readerOrientation: Long
val Manga.downloadedFilter: TriState
get() {
if (forceDownloaded()) return TriState.ENABLED_IS
if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) {
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
@ -35,18 +35,17 @@ fun Manga.chaptersFiltered(): Boolean {
downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriState.DISABLED
}
fun Manga.forceDownloaded(): Boolean {
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
}
fun Manga.toSManga(): SManga = SManga.create().also {
it.url = url
it.title = title
it.artist = artist
it.author = author
it.description = description
it.genre = genre.orEmpty().joinToString()
it.status = status.toInt()
// SY -->
it.title = ogTitle
it.artist = ogArtist
it.author = ogAuthor
it.description = ogDescription
it.genre = ogGenre.orEmpty().joinToString()
it.status = ogStatus.toInt()
// SY <--
it.thumbnail_url = thumbnailUrl
it.initialized = initialized
}
@ -79,24 +78,6 @@ fun Manga.copyFrom(other: SManga): Manga {
)
}
fun SManga.toDomainManga(sourceId: Long): Manga {
return Manga.create().copy(
url = url,
// SY -->
ogTitle = title,
ogArtist = artist,
ogAuthor = author,
ogThumbnailUrl = thumbnail_url,
ogDescription = description,
ogGenre = getGenres(),
ogStatus = status.toLong(),
// SY <--
updateStrategy = update_strategy,
initialized = initialized,
source = sourceId,
)
}
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
return coverCache.getCustomCoverFile(id).exists()
}

View File

@ -88,5 +88,32 @@ class SourcePreferences(
BANDWIDTH_HERO,
WSRV_NL,
}
fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
fun defaultMangaOrder() = preferenceStore.getString("default_manga_order", "")
fun migrationSources() = preferenceStore.getString("migrate_sources", "")
fun smartMigration() = preferenceStore.getBoolean("smart_migrate", false)
fun useSourceWithMost() = preferenceStore.getBoolean("use_source_with_most", false)
fun skipPreMigration() = preferenceStore.getBoolean(Preference.appStateKey("skip_pre_migration"), false)
fun hideNotFoundMigration() = preferenceStore.getBoolean("hide_not_found_migration", false)
fun showOnlyUpdatesMigration() = preferenceStore.getBoolean("show_only_updates_migration", false)
fun allowLocalSourceHiddenFolders() = preferenceStore.getBoolean("allow_local_source_hidden_folders", false)
fun preferredMangaDexId() = preferenceStore.getString("preferred_mangaDex_id", "0")
fun mangadexSyncToLibraryIndexes() = preferenceStore.getStringSet(
"pref_mangadex_sync_to_library_indexes",
emptySet(),
)
fun recommendationSearchFlags() = preferenceStore.getInt("rec_search_flags", Int.MAX_VALUE)
// SY <--
}

View File

@ -10,6 +10,7 @@ fun Track.copyPersonalFrom(other: Track): Track {
status = other.status,
startDate = other.startDate,
finishDate = other.finishDate,
private = other.private,
)
}
@ -26,6 +27,7 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
it.tracking_url = remoteUrl
it.started_reading_date = startDate
it.finished_reading_date = finishDate
it.private = private
}
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
@ -44,5 +46,6 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
remoteUrl = tracking_url,
startDate = started_reading_date,
finishDate = finished_reading_date,
private = private,
)
}

View File

@ -42,4 +42,11 @@ class TrackPreferences(
"pref_auto_update_manga_on_mark_read",
AutoTrackState.ALWAYS,
)
// SY -->
fun resolveUsingSourceMetadata() = preferenceStore.getBoolean(
"pref_resolve_using_source_metadata_key",
true,
)
// SY <--
}

View File

@ -1,8 +1,6 @@
package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import tachiyomi.i18n.MR
enum class AppTheme(val titleRes: StringResource?) {
@ -11,15 +9,14 @@ enum class AppTheme(val titleRes: StringResource?) {
GREEN_APPLE(MR.strings.theme_greenapple),
LAVENDER(MR.strings.theme_lavender),
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
// TODO: re-enable for preview
NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }),
NORD(MR.strings.theme_nord),
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
TAKO(MR.strings.theme_tako),
TEALTURQUOISE(MR.strings.theme_tealturquoise),
TIDAL_WAVE(MR.strings.theme_tidalwave),
YINYANG(MR.strings.theme_yinyang),
YOTSUBA(MR.strings.theme_yotsuba),
MONOCHROME(MR.strings.theme_monochrome),
// Deprecated
DARK_BLUE(null),

View File

@ -82,10 +82,18 @@ fun BrowseSourceContent(
}
}
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
LoadingScreen(Modifier.padding(contentPadding))
return
}
if (mangaList.itemCount == 0) {
EmptyScreen(
modifier = Modifier.padding(contentPadding),
message = getErrorMessage(errorState),
message = when (errorState) {
is LoadState.Error -> getErrorMessage(errorState)
else -> stringResource(MR.strings.no_results_found)
},
actions = if (source is LocalSource /* SY --> */ && onLocalSourceHelpClick != null /* SY <-- */) {
persistentListOf(
EmptyScreenAction(
@ -104,7 +112,7 @@ fun BrowseSourceContent(
// SY -->
if (onWebViewClick != null) {
EmptyScreenAction(
MR.strings.action_open_in_web_view,
stringRes = MR.strings.action_open_in_web_view,
icon = Icons.Outlined.Public,
onClick = onWebViewClick,
)
@ -113,7 +121,7 @@ fun BrowseSourceContent(
},
if (onHelpClick != null) {
EmptyScreenAction(
MR.strings.label_help,
stringRes = MR.strings.label_help,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onHelpClick,
)
@ -128,13 +136,6 @@ fun BrowseSourceContent(
return
}
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
LoadingScreen(
modifier = Modifier.padding(contentPadding),
)
return
}
// SY -->
if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) {
BrowseSourceEHentaiList(

View File

@ -19,6 +19,7 @@ import eu.kanade.presentation.library.components.MangaComfortableGridItem
import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.RaisedSearchMetadata
import exh.metadata.metadata.RankedSearchMetadata
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover
@ -119,6 +120,14 @@ private fun BrowseSourceComfortableGridItem(
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
} else if (metadata is RankedSearchMetadata) {
metadata.rank?.let {
Badge(
text = "+$it",
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
}
},
// SY <--

View File

@ -19,6 +19,7 @@ import eu.kanade.presentation.library.components.MangaCompactGridItem
import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.RaisedSearchMetadata
import exh.metadata.metadata.RankedSearchMetadata
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover
@ -119,6 +120,14 @@ private fun BrowseSourceCompactGridItem(
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
} else if (metadata is RankedSearchMetadata) {
metadata.rank?.let {
Badge(
text = "+$it",
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
}
},
// SY <--

View File

@ -16,6 +16,7 @@ import eu.kanade.presentation.library.components.MangaListItem
import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.RaisedSearchMetadata
import exh.metadata.metadata.RankedSearchMetadata
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover
@ -110,6 +111,14 @@ private fun BrowseSourceListItem(
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
} else if (metadata is RankedSearchMetadata) {
metadata.rank?.let {
Badge(
text = "+$it",
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
}
// SY <--
},

View File

@ -2,23 +2,25 @@ package eu.kanade.presentation.category
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.CategoryListItem
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
@ -32,11 +34,9 @@ import tachiyomi.presentation.core.util.plus
fun CategoryScreen(
state: CategoryScreenState.Success,
onClickCreate: () -> Unit,
onClickSortAlphabetically: () -> Unit,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
onClickMoveUp: (Category) -> Unit,
onClickMoveDown: (Category) -> Unit,
onChangeOrder: (Category, Int) -> Unit,
navigateUp: () -> Unit,
) {
val lazyListState = rememberLazyListState()
@ -45,17 +45,6 @@ fun CategoryScreen(
AppBar(
title = stringResource(MR.strings.action_edit_categories),
navigateUp = navigateUp,
actions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_sort),
icon = Icons.Outlined.SortByAlpha,
onClick = onClickSortAlphabetically,
),
),
)
},
scrollBehavior = scrollBehavior,
)
},
@ -77,12 +66,10 @@ fun CategoryScreen(
CategoryContent(
categories = state.categories,
lazyListState = lazyListState,
paddingValues = paddingValues + topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
paddingValues = paddingValues,
onClickRename = onClickRename,
onClickDelete = onClickDelete,
onMoveUp = onClickMoveUp,
onMoveDown = onClickMoveDown,
onChangeOrder = onChangeOrder,
)
}
}
@ -94,28 +81,44 @@ private fun CategoryContent(
paddingValues: PaddingValues,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
onMoveUp: (Category) -> Unit,
onMoveDown: (Category) -> Unit,
onChangeOrder: (Category, Int) -> Unit,
) {
val categoriesState = remember { categories.toMutableStateList() }
val reorderableState = rememberReorderableLazyListState(lazyListState, paddingValues) { from, to ->
val item = categoriesState.removeAt(from.index)
categoriesState.add(to.index, item)
onChangeOrder(item, to.index)
}
LaunchedEffect(categories) {
if (!reorderableState.isAnyItemDragging) {
categoriesState.clear()
categoriesState.addAll(categories)
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState,
contentPadding = paddingValues,
contentPadding = paddingValues +
topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
itemsIndexed(
items = categories,
key = { _, category -> "category-${category.id}" },
) { index, category ->
CategoryListItem(
modifier = Modifier.animateItem(),
category = category,
canMoveUp = index != 0,
canMoveDown = index != categories.lastIndex,
onMoveUp = onMoveUp,
onMoveDown = onMoveDown,
onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) },
)
items(
items = categoriesState,
key = { category -> category.key },
) { category ->
ReorderableItem(reorderableState, category.key) {
CategoryListItem(
modifier = Modifier.animateItem(),
category = category,
onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) },
)
}
}
}
}
private val Category.key inline get() = "category-$id"

View File

@ -219,35 +219,6 @@ fun CategoryDeleteDialog(
)
}
@Composable
fun CategorySortAlphabeticallyDialog(
onDismissRequest: () -> Unit,
onSort: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = {
onSort()
onDismissRequest()
}) {
Text(text = stringResource(MR.strings.action_ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
title = {
Text(text = stringResource(MR.strings.action_sort_category))
},
text = {
Text(text = stringResource(MR.strings.sort_category_confirmation))
},
)
}
@Composable
fun ChangeCategoryDialog(
initialSelection: ImmutableList<CheckboxState<Category>>,

View File

@ -2,14 +2,11 @@ package eu.kanade.presentation.category.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.ArrowDropUp
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DragHandle
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
@ -19,57 +16,42 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import sh.calvin.reorderable.ReorderableCollectionItemScope
import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun CategoryListItem(
fun ReorderableCollectionItemScope.CategoryListItem(
category: Category,
canMoveUp: Boolean,
canMoveDown: Boolean,
onMoveUp: (Category) -> Unit,
onMoveDown: (Category) -> Unit,
onRename: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
ElevatedCard(
modifier = modifier,
) {
ElevatedCard(modifier = modifier) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onRename() }
.clickable(onClick = onRename)
.padding(vertical = MaterialTheme.padding.small)
.padding(
start = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium,
start = MaterialTheme.padding.small,
end = MaterialTheme.padding.medium,
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Icon(
imageVector = Icons.Outlined.DragHandle,
contentDescription = null,
modifier = Modifier
.padding(MaterialTheme.padding.medium)
.draggableHandle(),
)
Text(
text = category.name,
modifier = Modifier
.padding(start = MaterialTheme.padding.medium),
modifier = Modifier.weight(1f),
)
}
Row {
IconButton(
onClick = { onMoveUp(category) },
enabled = canMoveUp,
) {
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
}
IconButton(
onClick = { onMoveDown(category) },
enabled = canMoveDown,
) {
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) {
Icon(
imageVector = Icons.Outlined.Edit,
@ -77,7 +59,10 @@ fun CategoryListItem(
)
}
IconButton(onClick = onDelete) {
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete))
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(MR.strings.action_delete),
)
}
}
}

View File

@ -1,10 +1,9 @@
package eu.kanade.presentation.components
import androidx.activity.compose.BackHandler
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Dialog
@ -28,20 +27,14 @@ fun NavigatorAdaptiveSheet(
screen = screen,
content = { sheetNavigator ->
AdaptiveSheet(
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
onDismissRequest = onDismissRequest,
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
) {
ScreenTransition(
navigator = sheetNavigator,
transition = {
fadeIn(animationSpec = tween(220, delayMillis = 90)) togetherWith
fadeOut(animationSpec = tween(90))
},
)
BackHandler(
enabled = sheetNavigator.size > 1,
onBack = sheetNavigator::pop,
enterTransition = { fadeIn(animationSpec = tween(220, delayMillis = 90)) },
exitTransition = { fadeOut(animationSpec = tween(90)) },
sizeTransform = { SizeTransform() },
)
}
@ -79,10 +72,10 @@ fun AdaptiveSheet(
properties = dialogProperties,
) {
AdaptiveSheetImpl(
modifier = modifier,
isTabletUi = isTabletUi,
enableSwipeDismiss = enableSwipeDismiss,
onDismissRequest = onDismissRequest,
modifier = modifier,
) {
content()
}

View File

@ -36,6 +36,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
@ -201,6 +202,7 @@ fun AppBarActions(
}
},
state = rememberTooltipState(),
focusable = false,
) {
IconButton(
onClick = it.onClick,
@ -225,6 +227,7 @@ fun AppBarActions(
}
},
state = rememberTooltipState(),
focusable = false,
) {
IconButton(
onClick = { showMenu = !showMenu },
@ -289,6 +292,7 @@ fun SearchToolbar(
onSearch(searchQuery)
focusManager.clearFocus()
keyboardController?.hide()
focusManager.moveFocus(FocusDirection.Next)
}
BasicTextField(
@ -352,6 +356,7 @@ fun SearchToolbar(
}
},
state = rememberTooltipState(),
focusable = false,
) {
IconButton(
onClick = onClick,
@ -371,6 +376,7 @@ fun SearchToolbar(
}
},
state = rememberTooltipState(),
focusable = false,
) {
IconButton(
onClick = {

View File

@ -39,6 +39,7 @@ fun HistoryScreen(
onSearchQueryChange: (String?) -> Unit,
onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onClickFavorite: (mangaId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
) {
Scaffold(
@ -85,6 +86,7 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
)
}
}
@ -98,6 +100,7 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit,
onClickFavorite: (HistoryWithRelations) -> Unit,
) {
FastScrollLazyColumn(
contentPadding = contentPadding,
@ -127,6 +130,7 @@ private fun HistoryScreenContent(
onClickCover = { onClickCover(value) },
onClickResume = { onClickResume(value) },
onClickDelete = { onClickDelete(value) },
onClickFavorite = { onClickFavorite(value) },
)
}
}
@ -153,6 +157,7 @@ internal fun HistoryScreenPreviews(
onClickCover = {},
onClickResume = { _, _ -> run {} },
onDialogChange = {},
onClickFavorite = {},
)
}
}

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -39,6 +40,7 @@ fun HistoryItem(
onClickCover: () -> Unit,
onClickResume: () -> Unit,
onClickDelete: () -> Unit,
onClickFavorite: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
@ -82,6 +84,16 @@ fun HistoryItem(
)
}
if (!history.coverData.isMangaFavorite) {
IconButton(onClick = onClickFavorite) {
Icon(
imageVector = Icons.Outlined.FavoriteBorder,
contentDescription = stringResource(MR.strings.add_to_library),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
IconButton(onClick = onClickDelete) {
Icon(
imageVector = Icons.Outlined.Delete,
@ -105,6 +117,7 @@ private fun HistoryItemPreviews(
onClickCover = {},
onClickResume = {},
onClickDelete = {},
onClickFavorite = {},
)
}
}

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -309,15 +310,16 @@ private fun ColumnScope.DisplayPage(
val columns by columnPreference.collectAsState()
SliderItem(
label = stringResource(MR.strings.pref_library_columns),
max = 10,
value = columns,
valueRange = 0..10,
label = stringResource(MR.strings.pref_library_columns),
valueText = if (columns > 0) {
stringResource(MR.strings.pref_library_columns_per_row, columns)
columns.toString()
} else {
stringResource(MR.strings.label_default)
stringResource(MR.strings.label_auto)
},
onChange = columnPreference::set,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
}
@ -326,6 +328,10 @@ private fun ColumnScope.DisplayPage(
label = stringResource(MR.strings.action_display_download_badge),
pref = screenModel.libraryPreferences.downloadBadge(),
)
CheckboxItem(
label = stringResource(MR.strings.action_display_unread_badge),
pref = screenModel.libraryPreferences.unreadBadge(),
)
CheckboxItem(
label = stringResource(MR.strings.action_display_local_badge),
pref = screenModel.libraryPreferences.localBadge(),

View File

@ -21,13 +21,14 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.domain.manga.model.forceDownloaded
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import kotlinx.collections.immutable.persistentListOf
@ -40,6 +41,8 @@ import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.theme.active
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
fun ChapterSettingsDialog(
@ -63,6 +66,8 @@ fun ChapterSettingsDialog(
)
}
val downloadedOnly = remember { Injekt.get<BasePreferences>().downloadedOnly().get() }
TabbedDialog(
onDismissRequest = onDismissRequest,
tabTitles = persistentListOf(
@ -97,7 +102,7 @@ fun ChapterSettingsDialog(
FilterPage(
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
onDownloadFilterChanged = onDownloadFilterChanged
.takeUnless { manga?.forceDownloaded() == true },
.takeUnless { downloadedOnly },
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
onUnreadFilterChanged = onUnreadFilterChanged,
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,

View File

@ -1,44 +1,95 @@
package eu.kanade.presentation.manga
import androidx.compose.foundation.clickable
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Brush
import androidx.compose.material.icons.filled.PersonOutline
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Book
import androidx.compose.material.icons.outlined.SwapVert
import androidx.compose.material.icons.outlined.AttachMoney
import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Pause
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastMaxOfOrNull
import coil3.request.ImageRequest
import coil3.request.crossfade
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.components.BadgeGroup
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
fun DuplicateMangaDialog(
duplicates: List<MangaWithChapterCount>,
onDismissRequest: () -> Unit,
onConfirm: () -> Unit,
onOpenManga: () -> Unit,
onMigrate: () -> Unit,
onOpenManga: (manga: Manga) -> Unit,
onMigrate: (manga: Manga) -> Unit,
modifier: Modifier = Modifier,
) {
val sourceManager = remember { Injekt.get<SourceManager>() }
val minHeight = LocalPreferenceMinHeight.current
val horizontalPadding = PaddingValues(horizontal = TabbedDialogPaddings.Horizontal)
val horizontalPaddingModifier = Modifier.padding(horizontalPadding)
AdaptiveSheet(
modifier = modifier,
@ -46,81 +97,310 @@ fun DuplicateMangaDialog(
) {
Column(
modifier = Modifier
.padding(
vertical = TabbedDialogPaddings.Vertical,
horizontal = TabbedDialogPaddings.Horizontal,
)
.padding(vertical = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState())
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
Text(
modifier = Modifier.padding(TitlePadding),
text = stringResource(MR.strings.are_you_sure),
text = stringResource(MR.strings.possible_duplicates_title),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.then(horizontalPaddingModifier)
.padding(top = MaterialTheme.padding.small),
)
Text(
text = stringResource(MR.strings.confirm_add_duplicate_manga),
text = stringResource(MR.strings.possible_duplicates_summary),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.then(horizontalPaddingModifier),
)
Spacer(Modifier.height(PaddingSize))
TextPreferenceWidget(
title = stringResource(MR.strings.action_show_manga),
icon = Icons.Outlined.Book,
onPreferenceClick = {
onDismissRequest()
onOpenManga()
},
)
HorizontalDivider()
TextPreferenceWidget(
title = stringResource(MR.strings.action_migrate_duplicate),
icon = Icons.Outlined.SwapVert,
onPreferenceClick = {
onDismissRequest()
onMigrate()
},
)
HorizontalDivider()
TextPreferenceWidget(
title = stringResource(MR.strings.action_add_anyway),
icon = Icons.Outlined.Add,
onPreferenceClick = {
onDismissRequest()
onConfirm()
},
)
Row(
modifier = Modifier
.sizeIn(minHeight = minHeight)
.clickable { onDismissRequest.invoke() }
.padding(ButtonPadding)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
LazyRow(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
modifier = Modifier.height(getMaximumMangaCardHeight(duplicates)),
contentPadding = horizontalPadding,
) {
OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
Text(
modifier = Modifier
.padding(vertical = 8.dp),
text = stringResource(MR.strings.action_cancel),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.titleLarge,
fontSize = 16.sp,
items(
items = duplicates,
key = { it.manga.id },
) {
DuplicateMangaListItem(
duplicate = it,
getSource = { sourceManager.getOrStub(it.manga.source) },
onMigrate = { onMigrate(it.manga) },
onDismissRequest = onDismissRequest,
onOpenManga = { onOpenManga(it.manga) },
)
}
}
Column(modifier = horizontalPaddingModifier) {
HorizontalDivider()
TextPreferenceWidget(
title = stringResource(MR.strings.action_add_anyway),
icon = Icons.Outlined.Add,
onPreferenceClick = {
onDismissRequest()
onConfirm()
},
modifier = Modifier.clip(CircleShape),
)
}
OutlinedButton(
onClick = onDismissRequest,
modifier = Modifier
.then(horizontalPaddingModifier)
.padding(bottom = MaterialTheme.padding.medium)
.heightIn(min = minHeight)
.fillMaxWidth(),
) {
Text(
modifier = Modifier.padding(vertical = MaterialTheme.padding.extraSmall),
text = stringResource(MR.strings.action_cancel),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyLarge,
)
}
}
}
}
private val PaddingSize = 16.dp
@Composable
private fun DuplicateMangaListItem(
duplicate: MangaWithChapterCount,
getSource: () -> Source,
onDismissRequest: () -> Unit,
onOpenManga: () -> Unit,
onMigrate: () -> Unit,
) {
val source = getSource()
val manga = duplicate.manga
Column(
modifier = Modifier
.width(MangaCardWidth)
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surface)
.combinedClickable(
onLongClick = { onOpenManga() },
onClick = {
onDismissRequest()
onMigrate()
},
)
.padding(MaterialTheme.padding.small),
) {
Box {
MangaCover.Book(
data = ImageRequest.Builder(LocalContext.current)
.data(manga)
.crossfade(true)
.build(),
modifier = Modifier.fillMaxWidth(),
)
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
) {
Badge(
color = MaterialTheme.colorScheme.secondary,
textColor = MaterialTheme.colorScheme.onSecondary,
text = pluralStringResource(
MR.plurals.manga_num_chapters,
duplicate.chapterCount.toInt(),
duplicate.chapterCount,
),
)
}
}
private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall))
Text(
text = manga.title,
style = MaterialTheme.typography.titleSmall,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
)
if (!manga.author.isNullOrBlank()) {
MangaDetailRow(
text = manga.author!!,
iconImageVector = Icons.Filled.PersonOutline,
maxLines = 2,
)
}
if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) {
MangaDetailRow(
text = manga.artist!!,
iconImageVector = Icons.Filled.Brush,
maxLines = 2,
)
}
MangaDetailRow(
text = when (manga.status) {
SManga.ONGOING.toLong() -> stringResource(MR.strings.ongoing)
SManga.COMPLETED.toLong() -> stringResource(MR.strings.completed)
SManga.LICENSED.toLong() -> stringResource(MR.strings.licensed)
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(MR.strings.publishing_finished)
SManga.CANCELLED.toLong() -> stringResource(MR.strings.cancelled)
SManga.ON_HIATUS.toLong() -> stringResource(MR.strings.on_hiatus)
else -> stringResource(MR.strings.unknown)
},
iconImageVector = when (manga.status) {
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
else -> Icons.Outlined.Block
},
)
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
if (source is StubSource) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.error,
)
}
Text(
text = source.name,
style = MaterialTheme.typography.labelSmall,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}
@Composable
private fun MangaDetailRow(
text: String,
iconImageVector: ImageVector,
maxLines: Int = 1,
) {
Row(
modifier = Modifier
.secondaryItemAlpha()
.padding(top = MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = iconImageVector,
contentDescription = null,
modifier = Modifier.size(MangaDetailsIconWidth),
)
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
)
}
}
@Composable
private fun getMaximumMangaCardHeight(duplicates: List<MangaWithChapterCount>): Dp {
val density = LocalDensity.current
val typography = MaterialTheme.typography
val textMeasurer = rememberTextMeasurer()
val smallPadding = with(density) { MaterialTheme.padding.small.roundToPx() }
val extraSmallPadding = with(density) { MaterialTheme.padding.extraSmall.roundToPx() }
val width = with(density) { MangaCardWidth.roundToPx() - (2 * smallPadding) }
val iconWidth = with(density) { MangaDetailsIconWidth.roundToPx() }
val coverHeight = width / MangaCover.Book.ratio
val constraints = Constraints(maxWidth = width)
val detailsConstraints = Constraints(maxWidth = width - iconWidth - extraSmallPadding)
return remember(
duplicates,
density,
typography,
textMeasurer,
smallPadding,
extraSmallPadding,
coverHeight,
constraints,
detailsConstraints,
) {
duplicates.fastMaxOfOrNull {
calculateMangaCardHeight(
manga = it.manga,
density = density,
typography = typography,
textMeasurer = textMeasurer,
smallPadding = smallPadding,
extraSmallPadding = extraSmallPadding,
coverHeight = coverHeight,
constraints = constraints,
detailsConstraints = detailsConstraints,
)
}
?: 0.dp
}
}
private fun calculateMangaCardHeight(
manga: Manga,
density: Density,
typography: Typography,
textMeasurer: TextMeasurer,
smallPadding: Int,
extraSmallPadding: Int,
coverHeight: Float,
constraints: Constraints,
detailsConstraints: Constraints,
): Dp {
val titleHeight = textMeasurer.measureHeight(manga.title, typography.titleSmall, 2, constraints)
val authorHeight = if (!manga.author.isNullOrBlank()) {
textMeasurer.measureHeight(manga.author!!, typography.bodySmall, 2, detailsConstraints)
} else {
0
}
val artistHeight = if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) {
textMeasurer.measureHeight(manga.artist!!, typography.bodySmall, 2, detailsConstraints)
} else {
0
}
val statusHeight = textMeasurer.measureHeight("", typography.bodySmall, 2, detailsConstraints)
val sourceHeight = textMeasurer.measureHeight("", typography.labelSmall, 1, constraints)
val totalHeight = coverHeight + titleHeight + authorHeight + artistHeight + statusHeight + sourceHeight
return with(density) { ((2 * smallPadding) + totalHeight + (5 * extraSmallPadding)).toDp() }
}
private fun TextMeasurer.measureHeight(
text: String,
style: TextStyle,
maxLines: Int,
constraints: Constraints,
): Int = measure(
text = text,
style = style,
overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
constraints = constraints,
)
.size
.height
private val MangaCardWidth = 150.dp
private val MangaDetailsIconWidth = 16.dp

View File

@ -0,0 +1,45 @@
package eu.kanade.presentation.manga
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.manga.components.MangaNotesTextArea
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun MangaNotesScreen(
state: MangaNotesScreen.State,
navigateUp: () -> Unit,
onUpdate: (String) -> Unit,
) {
Scaffold(
topBar = { topBarScrollBehavior ->
AppBar(
titleContent = {
AppBarTitle(
title = stringResource(MR.strings.action_edit_notes),
subtitle = state.manga.title,
)
},
navigateUp = navigateUp,
scrollBehavior = topBarScrollBehavior,
)
},
) { contentPadding ->
MangaNotesTextArea(
state = state,
onUpdate = onUpdate,
modifier = Modifier
.padding(contentPadding)
.consumeWindowInsets(contentPadding)
.imePadding(),
)
}
}

View File

@ -117,7 +117,7 @@ fun MangaScreen(
isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
navigateUp: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
@ -142,6 +142,7 @@ fun MangaScreen(
onEditCategoryClicked: (() -> Unit)?,
onEditFetchIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// SY -->
onMetadataViewerClicked: () -> Unit,
onEditInfoClicked: () -> Unit,
@ -182,7 +183,7 @@ fun MangaScreen(
nextUpdate = nextUpdate,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onBackClicked = onBackClicked,
navigateUp = navigateUp,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked,
@ -201,6 +202,7 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked,
onEditNotesClicked = onEditNotesClicked,
// SY -->
onMetadataViewerClicked = onMetadataViewerClicked,
onEditInfoClicked = onEditInfoClicked,
@ -228,7 +230,7 @@ fun MangaScreen(
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
nextUpdate = nextUpdate,
onBackClicked = onBackClicked,
navigateUp = navigateUp,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked,
@ -247,6 +249,7 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked,
onEditNotesClicked = onEditNotesClicked,
// SY -->
onMetadataViewerClicked = onMetadataViewerClicked,
onEditInfoClicked = onEditInfoClicked,
@ -277,7 +280,7 @@ private fun MangaScreenSmallImpl(
nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
navigateUp: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
@ -303,6 +306,7 @@ private fun MangaScreenSmallImpl(
onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// SY -->
onMetadataViewerClicked: () -> Unit,
onEditInfoClicked: () -> Unit,
@ -345,14 +349,9 @@ private fun MangaScreenSmallImpl(
}
// SY <--
val internalOnBackPressed = {
if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
}
BackHandler(enabled = isAnySelected) {
onAllChapterSelected(false)
}
BackHandler(onBack = internalOnBackPressed)
Scaffold(
topBar = {
@ -365,26 +364,25 @@ private fun MangaScreenSmallImpl(
val isFirstItemScrolled by remember {
derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
}
val animatedTitleAlpha by animateFloatAsState(
val titleAlpha by animateFloatAsState(
if (!isFirstItemVisible) 1f else 0f,
label = "Top Bar Title",
)
val animatedBgAlpha by animateFloatAsState(
val backgroundAlpha by animateFloatAsState(
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
label = "Top Bar Background",
)
MangaToolbar(
title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha },
hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed,
navigateUp = navigateUp,
onClickFilter = onFilterClicked,
onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickEditNotes = onEditNotesClicked,
// SY -->
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
@ -392,8 +390,11 @@ private fun MangaScreenSmallImpl(
onClickMerge = onMergeClicked.takeIf { state.showMergeInOverflow },
// SY <--
actionModeCounter = selectedChapterCount,
onCancelActionMode = { onAllChapterSelected(false) },
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { titleAlpha },
backgroundAlphaProvider = { backgroundAlpha },
)
},
bottomBar = {
@ -519,8 +520,10 @@ private fun MangaScreenSmallImpl(
defaultExpandState = state.isFromSource,
description = state.manga.description,
tagsProvider = { state.manga.genre },
notes = state.manga.notes,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onEditNotes = onEditNotesClicked,
// SY -->
doSearch = onSearch,
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
@ -600,7 +603,7 @@ fun MangaScreenLargeImpl(
nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
navigateUp: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
@ -626,6 +629,7 @@ fun MangaScreenLargeImpl(
onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// SY -->
onMetadataViewerClicked: () -> Unit,
onEditInfoClicked: () -> Unit,
@ -672,14 +676,9 @@ fun MangaScreenLargeImpl(
val chapterListState = rememberLazyListState()
val internalOnBackPressed = {
if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
}
BackHandler(enabled = isAnySelected) {
onAllChapterSelected(false)
}
BackHandler(onBack = internalOnBackPressed)
Scaffold(
topBar = {
@ -689,25 +688,27 @@ fun MangaScreenLargeImpl(
MangaToolbar(
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed,
navigateUp = navigateUp,
onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onClickEditNotes = onEditNotesClicked,
// SY -->
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
onClickMergedSettings = onMergedSettingsClicked.takeIf { state.manga.source == MERGED_SOURCE_ID },
onClickMerge = onMergeClicked.takeIf { state.showMergeInOverflow },
// SY <--
onCancelActionMode = { onAllChapterSelected(false) },
actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { 1f },
backgroundAlphaProvider = { 1f },
)
},
bottomBar = {
@ -814,8 +815,10 @@ fun MangaScreenLargeImpl(
defaultExpandState = true,
description = state.manga.description,
tagsProvider = { state.manga.genre },
notes = state.manga.notes,
onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard,
onEditNotes = onEditNotesClicked,
// SY -->
doSearch = onSearch,
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {

View File

@ -28,7 +28,6 @@ import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.icons.outlined.SwapCalls
@ -237,6 +236,7 @@ fun LibraryBottomActionMenu(
// SY -->
onClickCleanTitles: (() -> Unit)?,
onClickMigrate: (() -> Unit)?,
onClickCollectRecommendations: (() -> Unit)?,
onClickAddToMangaDex: (() -> Unit)?,
onClickResetInfo: (() -> Unit)?,
// SY <--
@ -267,7 +267,10 @@ fun LibraryBottomActionMenu(
}
}
// SY -->
val showOverflow = onClickCleanTitles != null || onClickAddToMangaDex != null || onClickResetInfo != null
val showOverflow = onClickCleanTitles != null ||
onClickAddToMangaDex != null ||
onClickResetInfo != null ||
onClickCollectRecommendations != null
val configuration = LocalConfiguration.current
val moveMarkPrev = remember { !configuration.isTabletUi() }
var overFlowOpen by remember { mutableStateOf(false) }
@ -358,6 +361,12 @@ fun LibraryBottomActionMenu(
onClick = onClickMigrate,
)
}
if (onClickCollectRecommendations != null) {
DropdownMenuItem(
text = { Text(stringResource(SYMR.strings.rec_search_short)) },
onClick = onClickCollectRecommendations,
)
}
if (onClickAddToMangaDex != null) {
DropdownMenuItem(
text = { Text(stringResource(SYMR.strings.mangadex_add_to_follows)) },

View File

@ -3,6 +3,9 @@ package eu.kanade.presentation.manga.components
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
@ -25,18 +28,22 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.updatePadding
import coil3.asDrawable
import coil3.imageLoader
@ -49,11 +56,14 @@ import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.manga.EditCoverAction
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import kotlinx.collections.immutable.persistentListOf
import soup.compose.material.motion.MotionConstants
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.PredictiveBack
import tachiyomi.presentation.core.util.clickableNoIndication
import kotlin.coroutines.cancellation.CancellationException
@Composable
fun MangaCoverDialog(
@ -152,10 +162,32 @@ fun MangaCoverDialog(
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
var scale by remember { mutableFloatStateOf(1f) }
PredictiveBackHandler { progress ->
try {
progress.collect { backEvent ->
scale = lerp(1f, 0.8f, PredictiveBack.transform(backEvent.progress))
}
onDismissRequest()
} catch (e: CancellationException) {
animate(
initialValue = scale,
targetValue = 1f,
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
) { value, _ ->
scale = value
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.clickableNoIndication(onClick = onDismissRequest),
.clickableNoIndication(onClick = onDismissRequest)
.graphicsLayer {
scaleX = scale
scaleY = scale
},
) {
AndroidView(
factory = {
@ -172,20 +204,20 @@ fun MangaCoverDialog(
.memoryCachePolicy(CachePolicy.DISABLED)
.target { image ->
val drawable = image.asDrawable(view.context.resources)
// Copy bitmap in case it came from memory cache
// Because SSIV needs to thoroughly read the image
val copy = (drawable as? BitmapDrawable)?.let {
val config = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Bitmap.Config.HARDWARE
} else {
Bitmap.Config.ARGB_8888
}
BitmapDrawable(
view.context.resources,
it.bitmap.copy(config, false),
val copy = (drawable as? BitmapDrawable)
?.bitmap
?.copy(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Bitmap.Config.HARDWARE
} else {
Bitmap.Config.ARGB_8888
},
false,
)
} ?: drawable
?.toDrawable(view.context.resources)
?: drawable
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))
}
.build()

View File

@ -77,6 +77,8 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.mikepenz.markdown.model.markdownAnnotator
import com.mikepenz.markdown.model.markdownAnnotatorConfig
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga
@ -95,8 +97,6 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@Composable
fun MangaInfoBox(
isTabletUi: Boolean,
@ -250,8 +250,10 @@ fun ExpandableMangaDescription(
defaultExpandState: Boolean,
description: String?,
tagsProvider: () -> List<String>?,
notes: String,
onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit,
onEditNotes: () -> Unit,
// SY -->
searchMetadataChips: SearchMetadataChips?,
doSearch: (query: String, global: Boolean) -> Unit,
@ -264,15 +266,12 @@ fun ExpandableMangaDescription(
}
val desc =
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
val trimmedDescription = remember(desc) {
desc
.replace(whitespaceLineRegex, "\n")
.trimEnd()
}
MangaSummary(
expandedDescription = desc,
shrunkDescription = trimmedDescription,
description = desc,
expanded = expanded,
notes = notes,
onEditNotesClicked = onEditNotes,
modifier = Modifier
.padding(top = 8.dp)
.padding(horizontal = 16.dp)
@ -594,11 +593,26 @@ private fun ColumnScope.MangaContentInfo(
}
}
private val descriptionAnnotator = markdownAnnotator(
annotate = { content, child ->
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
append(content.substring(child.startOffset, child.endOffset))
return@markdownAnnotator true
}
false
},
config = markdownAnnotatorConfig(
eolAsNewLine = true,
),
)
@Composable
private fun MangaSummary(
expandedDescription: String,
shrunkDescription: String,
description: String,
notes: String,
expanded: Boolean,
onEditNotesClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
val animProgress by animateFloatAsState(
@ -610,25 +624,40 @@ private fun MangaSummary(
contents = listOf(
{
Text(
text = "\n\n", // Shows at least 3 lines
// Shows at least 3 lines if no notes
// when there are notes show 6
text = if (notes.isBlank()) "\n\n" else "\n\n\n\n\n",
style = MaterialTheme.typography.bodyMedium,
)
},
{
Text(
text = expandedDescription,
style = MaterialTheme.typography.bodyMedium,
)
},
{
SelectionContainer {
Text(
text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(),
Column {
MangaNotesSection(
content = notes,
expanded = true,
onEditNotes = onEditNotesClicked,
)
MarkdownRender(
content = description,
modifier = Modifier.secondaryItemAlpha(),
annotator = descriptionAnnotator,
)
}
},
{
Column {
MangaNotesSection(
content = notes,
expanded = expanded,
onEditNotes = onEditNotesClicked,
)
SelectionContainer {
MarkdownRender(
content = description,
modifier = Modifier.secondaryItemAlpha(),
annotator = descriptionAnnotator,
)
}
}
},
{

View File

@ -0,0 +1,60 @@
package eu.kanade.presentation.manga.components
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.RichText
private val FADE_TIME = tween<Float>(500)
@Composable
fun MangaNotesDisplay(
content: String,
modifier: Modifier,
) {
val alpha = remember { Animatable(1f) }
var contentUpdatedOnce by remember { mutableStateOf(false) }
val richTextState = rememberRichTextState()
val primaryColor = MaterialTheme.colorScheme.primary
LaunchedEffect(content) {
richTextState.setMarkdown(content)
if (!contentUpdatedOnce) {
contentUpdatedOnce = true
return@LaunchedEffect
}
alpha.snapTo(targetValue = 0f)
alpha.animateTo(targetValue = 1f, animationSpec = FADE_TIME)
}
LaunchedEffect(Unit) {
richTextState.config.unorderedListIndent = 4
richTextState.config.orderedListIndent = 20
}
LaunchedEffect(primaryColor) {
richTextState.config.linkColor = primaryColor
}
SelectionContainer {
RichText(
modifier = modifier
// Only animate size if the notes changes
.then(if (contentUpdatedOnce) Modifier.animateContentSize() else Modifier)
.alpha(alpha.value),
style = MaterialTheme.typography.bodyMedium,
state = richTextState,
)
}
}

View File

@ -0,0 +1,90 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
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.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Button
import tachiyomi.presentation.core.components.material.ButtonDefaults
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun MangaNotesSection(
content: String,
expanded: Boolean,
onEditNotes: () -> Unit,
modifier: Modifier = Modifier,
) {
if (content.isBlank()) return
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
MangaNotesDisplay(
content = content,
modifier = modifier.fillMaxWidth(),
)
if (expanded) {
Button(
onClick = onEditNotes,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.primary,
),
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 4.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Filled.EditNote,
contentDescription = null,
modifier = Modifier
.size(16.dp),
)
Text(
stringResource(MR.strings.action_edit_notes),
)
}
}
}
HorizontalDivider(
modifier = Modifier
.padding(
top = if (expanded) 0.dp else 12.dp,
bottom = if (expanded) 16.dp else 12.dp,
),
)
}
}
@PreviewLightDark
@Composable
private fun MangaNotesSectionPreview() {
MangaNotesSection(
onEditNotes = {},
expanded = true,
content = "# Hello world\ntest1234 hi there!",
)
}

View File

@ -0,0 +1,224 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.background
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
import androidx.compose.material.icons.outlined.FormatBold
import androidx.compose.material.icons.outlined.FormatItalic
import androidx.compose.material.icons.outlined.FormatListNumbered
import androidx.compose.material.icons.outlined.FormatUnderlined
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditorDefaults.richTextEditorColors
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import kotlin.time.Duration.Companion.seconds
private const val MAX_LENGTH = 250
private const val MAX_LENGTH_WARN = MAX_LENGTH * 0.9
@Composable
fun MangaNotesTextArea(
state: MangaNotesScreen.State,
onUpdate: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
val richTextState = rememberRichTextState()
val primaryColor = MaterialTheme.colorScheme.primary
DisposableEffect(scope, richTextState) {
snapshotFlow { richTextState.annotatedString }
.debounce(0.25.seconds)
.distinctUntilChanged()
.map { richTextState.toMarkdown() }
.onEach { onUpdate(it) }
.launchIn(scope)
onDispose {
onUpdate(richTextState.toMarkdown())
}
}
LaunchedEffect(Unit) {
richTextState.setMarkdown(state.notes)
richTextState.config.unorderedListIndent = 4
richTextState.config.orderedListIndent = 20
}
LaunchedEffect(primaryColor) {
richTextState.config.linkColor = primaryColor
}
val focusRequester = remember { FocusRequester() }
LaunchedEffect(focusRequester) {
focusRequester.requestFocus()
}
val textLength = remember(richTextState.annotatedString) { richTextState.toText().length }
Column(
modifier = modifier
.padding(horizontal = MaterialTheme.padding.small)
.fillMaxSize(),
) {
RichTextEditor(
state = richTextState,
textStyle = MaterialTheme.typography.bodyLarge,
maxLength = MAX_LENGTH,
placeholder = {
Text(text = stringResource(MR.strings.notes_placeholder))
},
colors = richTextEditorColors(
containerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
contentPadding = PaddingValues(
horizontal = MaterialTheme.padding.medium,
),
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.focusRequester(focusRequester),
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(vertical = MaterialTheme.padding.small)
.fillMaxWidth(),
) {
LazyRow(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
item {
MangaNotesTextAreaButton(
onClick = { richTextState.toggleSpanStyle(SpanStyle(fontWeight = FontWeight.Bold)) },
isSelected = richTextState.currentSpanStyle.fontWeight == FontWeight.Bold,
icon = Icons.Outlined.FormatBold,
)
}
item {
MangaNotesTextAreaButton(
onClick = { richTextState.toggleSpanStyle(SpanStyle(fontStyle = FontStyle.Italic)) },
isSelected = richTextState.currentSpanStyle.fontStyle == FontStyle.Italic,
icon = Icons.Outlined.FormatItalic,
)
}
item {
MangaNotesTextAreaButton(
onClick = {
richTextState.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.Underline))
},
isSelected = richTextState.currentSpanStyle.textDecoration
?.contains(TextDecoration.Underline)
?: false,
icon = Icons.Outlined.FormatUnderlined,
)
}
item {
VerticalDivider(
modifier = Modifier
.padding(horizontal = MaterialTheme.padding.extraSmall)
.height(MaterialTheme.padding.large),
)
}
item {
MangaNotesTextAreaButton(
onClick = { richTextState.toggleUnorderedList() },
isSelected = richTextState.isUnorderedList,
icon = Icons.AutoMirrored.Outlined.FormatListBulleted,
)
}
item {
MangaNotesTextAreaButton(
onClick = { richTextState.toggleOrderedList() },
isSelected = richTextState.isOrderedList,
icon = Icons.Outlined.FormatListNumbered,
)
}
}
Box(
contentAlignment = Alignment.Center,
) {
Text(
text = (MAX_LENGTH - textLength).toString(),
color = if (textLength > MAX_LENGTH_WARN) {
MaterialTheme.colorScheme.error
} else {
Color.Unspecified
},
modifier = Modifier.padding(MaterialTheme.padding.extraSmall),
)
}
}
}
}
@Composable
fun MangaNotesTextAreaButton(
onClick: () -> Unit,
icon: ImageVector,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.clip(MaterialTheme.shapes.small)
.clickable(
onClick = onClick,
enabled = true,
role = Role.Button,
),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = icon,
contentDescription = icon.name,
tint = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary,
modifier = Modifier
.background(color = if (isSelected) MaterialTheme.colorScheme.onBackground else Color.Transparent)
.padding(MaterialTheme.padding.extraSmall),
)
}
}

View File

@ -1,18 +1,12 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -20,12 +14,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.DownloadDropdownMenu
import eu.kanade.presentation.components.UpIcon
import eu.kanade.presentation.manga.DownloadAction
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
@ -36,15 +30,15 @@ import tachiyomi.presentation.core.theme.active
@Composable
fun MangaToolbar(
title: String,
titleAlphaProvider: () -> Float,
hasFilters: Boolean,
onBackClicked: () -> Unit,
navigateUp: () -> Unit,
onClickFilter: () -> Unit,
onClickShare: (() -> Unit)?,
onClickDownload: ((DownloadAction) -> Unit)?,
onClickEditCategory: (() -> Unit)?,
onClickRefresh: () -> Unit,
onClickMigrate: (() -> Unit)?,
onClickEditNotes: () -> Unit,
// SY -->
onClickEditInfo: (() -> Unit)?,
onClickRecommend: (() -> Unit)?,
@ -54,152 +48,151 @@ fun MangaToolbar(
// For action mode
actionModeCounter: Int,
onCancelActionMode: () -> Unit,
onSelectAll: () -> Unit,
onInvertSelection: () -> Unit,
titleAlphaProvider: () -> Float,
backgroundAlphaProvider: () -> Float,
modifier: Modifier = Modifier,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
) {
Column(
val isActionMode = actionModeCounter > 0
AppBar(
titleContent = {
if (isActionMode) {
AppBarTitle(actionModeCounter.toString())
} else {
AppBarTitle(title, modifier = Modifier.alpha(titleAlphaProvider()))
}
},
modifier = modifier,
) {
val isActionMode = actionModeCounter > 0
TopAppBar(
title = {
Text(
text = if (isActionMode) actionModeCounter.toString() else title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()),
backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
navigateUp = navigateUp,
actions = {
var downloadExpanded by remember { mutableStateOf(false) }
if (onClickDownload != null) {
val onDismissRequest = { downloadExpanded = false }
DownloadDropdownMenu(
expanded = downloadExpanded,
onDismissRequest = onDismissRequest,
onDownloadClicked = onClickDownload,
)
},
navigationIcon = {
IconButton(onClick = onBackClicked) {
UpIcon(navigationIcon = Icons.Outlined.Close.takeIf { isActionMode })
}
},
actions = {
if (isActionMode) {
AppBarActions(
persistentListOf(
}
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder().apply {
if (isActionMode) {
add(
AppBar.Action(
title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = onSelectAll,
),
)
add(
AppBar.Action(
title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack,
onClick = onInvertSelection,
),
),
)
} else {
var downloadExpanded by remember { mutableStateOf(false) }
)
return@apply
}
if (onClickDownload != null) {
val onDismissRequest = { downloadExpanded = false }
DownloadDropdownMenu(
expanded = downloadExpanded,
onDismissRequest = onDismissRequest,
onDownloadClicked = onClickDownload,
add(
AppBar.Action(
title = stringResource(MR.strings.manga_download),
icon = Icons.Outlined.Download,
onClick = { downloadExpanded = !downloadExpanded },
),
)
}
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
if (onClickDownload != null) {
add(
AppBar.Action(
title = stringResource(MR.strings.manga_download),
icon = Icons.Outlined.Download,
onClick = { downloadExpanded = !downloadExpanded },
),
)
}
add(
AppBar.Action(
title = stringResource(MR.strings.action_filter),
icon = Icons.Outlined.FilterList,
iconTint = filterTint,
onClick = onClickFilter,
),
)
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_webview_refresh),
onClick = onClickRefresh,
),
)
if (onClickEditCategory != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_edit_categories),
onClick = onClickEditCategory,
),
)
}
if (onClickMigrate != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_migrate),
onClick = onClickMigrate,
),
)
}
if (onClickShare != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_share),
onClick = onClickShare,
),
)
}
// SY -->
if (onClickMerge != null) {
add(
AppBar.OverflowAction(
title = stringResource(SYMR.strings.merge),
onClick = onClickMerge,
),
)
}
if (onClickEditInfo != null) {
add(
AppBar.OverflowAction(
title = stringResource(SYMR.strings.action_edit_info),
onClick = onClickEditInfo,
),
)
}
if (onClickRecommend != null) {
add(
AppBar.OverflowAction(
title = stringResource(SYMR.strings.az_recommends),
onClick = onClickRecommend,
),
)
}
if (onClickMergedSettings != null) {
add(
AppBar.OverflowAction(
title = stringResource(SYMR.strings.merge_settings),
onClick = onClickMergedSettings,
),
)
}
// SY <--
}
.build(),
add(
AppBar.Action(
title = stringResource(MR.strings.action_filter),
icon = Icons.Outlined.FilterList,
iconTint = filterTint,
onClick = onClickFilter,
),
)
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_webview_refresh),
onClick = onClickRefresh,
),
)
if (onClickEditCategory != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_edit_categories),
onClick = onClickEditCategory,
),
)
}
if (onClickMigrate != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_migrate),
onClick = onClickMigrate,
),
)
}
if (onClickShare != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_share),
onClick = onClickShare,
),
)
}
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_notes),
onClick = onClickEditNotes,
),
)
// SY -->
if (onClickMerge != null) {
add(
AppBar.OverflowAction(
title = stringResource(SYMR.strings.merge),
onClick = onClickMerge,
),
)
}
if (onClickEditInfo != null) {
add(
AppBar.OverflowAction(
title = stringResource(SYMR.strings.action_edit_info),
onClick = onClickEditInfo,
),
)
}
if (onClickRecommend != null) {
add(
AppBar.OverflowAction(
title = stringResource(SYMR.strings.az_recommends),
onClick = onClickRecommend,
),
)
}
if (onClickMergedSettings != null) {
add(
AppBar.OverflowAction(
title = stringResource(SYMR.strings.merge_settings),
onClick = onClickMergedSettings,
),
)
}
// SY <--
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
),
)
}
.build(),
)
},
isActionMode = isActionMode,
onCancelActionMode = onCancelActionMode,
)
}

View File

@ -0,0 +1,253 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
import com.mikepenz.markdown.compose.LocalBulletListHandler
import com.mikepenz.markdown.compose.Markdown
import com.mikepenz.markdown.compose.components.markdownComponents
import com.mikepenz.markdown.compose.elements.MarkdownBulletList
import com.mikepenz.markdown.compose.elements.MarkdownDivider
import com.mikepenz.markdown.compose.elements.MarkdownOrderedList
import com.mikepenz.markdown.compose.elements.MarkdownTable
import com.mikepenz.markdown.compose.elements.MarkdownTableHeader
import com.mikepenz.markdown.compose.elements.MarkdownTableRow
import com.mikepenz.markdown.compose.elements.MarkdownText
import com.mikepenz.markdown.compose.elements.listDepth
import com.mikepenz.markdown.model.DefaultMarkdownColors
import com.mikepenz.markdown.model.DefaultMarkdownTypography
import com.mikepenz.markdown.model.MarkdownAnnotator
import com.mikepenz.markdown.model.MarkdownColors
import com.mikepenz.markdown.model.MarkdownPadding
import com.mikepenz.markdown.model.MarkdownTypography
import com.mikepenz.markdown.model.markdownAnnotator
import com.mikepenz.markdown.model.rememberMarkdownState
import org.intellij.markdown.MarkdownTokenTypes.Companion.HTML_TAG
import org.intellij.markdown.flavours.MarkdownFlavourDescriptor
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
import org.intellij.markdown.flavours.commonmark.CommonMarkMarkerProcessor
import org.intellij.markdown.flavours.gfm.table.GitHubTableMarkerProvider
import org.intellij.markdown.parser.MarkerProcessor
import org.intellij.markdown.parser.MarkerProcessorFactory
import org.intellij.markdown.parser.ProductionHolder
import org.intellij.markdown.parser.constraints.CommonMarkdownConstraints
import org.intellij.markdown.parser.constraints.MarkdownConstraints
import org.intellij.markdown.parser.markerblocks.MarkerBlockProvider
import org.intellij.markdown.parser.markerblocks.providers.AtxHeaderProvider
import org.intellij.markdown.parser.markerblocks.providers.BlockQuoteProvider
import org.intellij.markdown.parser.markerblocks.providers.CodeBlockProvider
import org.intellij.markdown.parser.markerblocks.providers.CodeFenceProvider
import org.intellij.markdown.parser.markerblocks.providers.HorizontalRuleProvider
import org.intellij.markdown.parser.markerblocks.providers.ListMarkerProvider
import org.intellij.markdown.parser.markerblocks.providers.SetextHeaderProvider
import tachiyomi.presentation.core.components.material.padding
@Composable
fun MarkdownRender(
content: String,
modifier: Modifier = Modifier,
flavour: MarkdownFlavourDescriptor = SimpleMarkdownFlavourDescriptor,
annotator: MarkdownAnnotator = remember { markdownAnnotator() },
) {
Markdown(
markdownState = rememberMarkdownState(
content = content,
flavour = flavour,
immediate = true,
),
annotator = annotator,
colors = getMarkdownColors(),
typography = getMarkdownTypography(),
padding = markdownPadding,
components = markdownComponents,
imageTransformer = Coil3ImageTransformerImpl,
modifier = modifier,
)
}
@Composable
@ReadOnlyComposable
private fun getMarkdownColors(): MarkdownColors {
val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
return DefaultMarkdownColors(
text = MaterialTheme.colorScheme.onSurface,
codeText = Color.Unspecified,
inlineCodeText = Color.Unspecified,
linkText = Color.Unspecified,
codeBackground = codeBackground,
inlineCodeBackground = codeBackground,
dividerColor = MaterialTheme.colorScheme.outlineVariant,
tableText = Color.Unspecified,
tableBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f),
)
}
@Composable
@ReadOnlyComposable
private fun getMarkdownTypography(): MarkdownTypography {
val link = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
)
return DefaultMarkdownTypography(
h1 = MaterialTheme.typography.headlineMedium,
h2 = MaterialTheme.typography.headlineSmall,
h3 = MaterialTheme.typography.titleLarge,
h4 = MaterialTheme.typography.titleMedium,
h5 = MaterialTheme.typography.titleSmall,
h6 = MaterialTheme.typography.bodyLarge,
text = MaterialTheme.typography.bodyMedium,
code = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
inlineCode = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
quote = MaterialTheme.typography.bodyMedium.plus(SpanStyle(fontStyle = FontStyle.Italic)),
paragraph = MaterialTheme.typography.bodyMedium,
ordered = MaterialTheme.typography.bodyMedium,
bullet = MaterialTheme.typography.bodyMedium,
list = MaterialTheme.typography.bodyMedium,
link = link,
textLink = TextLinkStyles(style = link.toSpanStyle()),
table = MaterialTheme.typography.bodyMedium,
)
}
private val markdownPadding = object : MarkdownPadding {
override val block: Dp = 2.dp
override val blockQuote: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 0.dp)
override val blockQuoteBar: PaddingValues.Absolute = PaddingValues.Absolute(
left = 4.dp,
top = 2.dp,
right = 4.dp,
bottom = 2.dp,
)
override val blockQuoteText: PaddingValues = PaddingValues(vertical = 4.dp)
override val codeBlock: PaddingValues = PaddingValues(8.dp)
override val list: Dp = 0.dp
override val listIndent: Dp = 8.dp
override val listItemBottom: Dp = 0.dp
override val listItemTop: Dp = 0.dp
}
private val markdownComponents = markdownComponents(
horizontalRule = {
MarkdownDivider(
modifier = Modifier
.padding(vertical = MaterialTheme.padding.extraSmall)
.fillMaxWidth(),
)
},
orderedList = { ol ->
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
MarkdownOrderedList(
content = ol.content,
node = ol.node,
style = ol.typography.ordered,
depth = ol.listDepth,
markerModifier = { Modifier.alignBy(FirstBaseline) },
listModifier = { Modifier.alignBy(FirstBaseline) },
)
}
},
unorderedList = { ul ->
val markers = listOf("", "", "", "")
CompositionLocalProvider(
LocalBulletListHandler provides { _, _, _, _, _ -> "${markers[ul.listDepth % markers.size]} " },
) {
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
MarkdownBulletList(
content = ul.content,
node = ul.node,
style = ul.typography.bullet,
markerModifier = { Modifier.alignBy(FirstBaseline) },
listModifier = { Modifier.alignBy(FirstBaseline) },
)
}
}
},
table = { t ->
MarkdownTable(
content = t.content,
node = t.node,
style = t.typography.text,
headerBlock = { content, header, tableWidth, style ->
MarkdownTableHeader(
content = content,
header = header,
tableWidth = tableWidth,
style = style,
maxLines = Int.MAX_VALUE,
)
},
rowBlock = { content, header, tableWidth, style ->
MarkdownTableRow(
content = content,
header = header,
tableWidth = tableWidth,
style = style,
maxLines = Int.MAX_VALUE,
)
},
)
},
custom = { type, model ->
if (type in DISALLOWED_MARKDOWN_TYPES) {
MarkdownText(
content = model.content.substring(model.node.startOffset, model.node.endOffset),
style = model.typography.text,
)
}
},
)
private object SimpleMarkdownFlavourDescriptor : CommonMarkFlavourDescriptor() {
override val markerProcessorFactory: MarkerProcessorFactory = SimpleMarkdownProcessFactory
}
private object SimpleMarkdownProcessFactory : MarkerProcessorFactory {
override fun createMarkerProcessor(productionHolder: ProductionHolder): MarkerProcessor<*> {
return SimpleMarkdownMarkerProcessor(productionHolder, CommonMarkdownConstraints.BASE)
}
}
/**
* Like `CommonMarkFlavour`, but with html blocks and reference links removed and
* table support added
*/
private class SimpleMarkdownMarkerProcessor(
productionHolder: ProductionHolder,
constraints: MarkdownConstraints,
) : CommonMarkMarkerProcessor(productionHolder, constraints) {
private val markerBlockProviders = listOf(
CodeBlockProvider(),
HorizontalRuleProvider(),
CodeFenceProvider(),
SetextHeaderProvider(),
BlockQuoteProvider(),
ListMarkerProvider(),
AtxHeaderProvider(),
GitHubTableMarkerProvider(),
)
override fun getMarkerBlockProviders(): List<MarkerBlockProvider<StateInfo>> {
return markerBlockProviders
}
}
val DISALLOWED_MARKDOWN_TYPES = arrayOf(HTML_TAG)

View File

@ -1,13 +1,6 @@
package eu.kanade.presentation.more
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label
@ -29,7 +22,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.R
@ -49,7 +41,6 @@ fun MoreScreen(
onDownloadedOnlyChange: (Boolean) -> Unit,
incognitoMode: Boolean,
onIncognitoModeChange: (Boolean) -> Unit,
isFDroid: Boolean,
// SY -->
showNavUpdates: Boolean,
showNavHistory: Boolean,
@ -66,26 +57,7 @@ fun MoreScreen(
) {
val uriHandler = LocalUriHandler.current
Scaffold(
topBar = {
Column(
modifier = Modifier.windowInsetsPadding(
WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
),
) {
if (isFDroid) {
WarningBanner(
textRes = MR.strings.fdroid_warning,
modifier = Modifier.clickable {
uriHandler.openUri(
"https://mihon.app/docs/faq/general#how-do-i-update-from-the-f-droid-builds",
)
},
)
}
}
},
) { contentPadding ->
Scaffold { contentPadding ->
ScrollbarLazyColumn(
modifier = Modifier.padding(contentPadding),
) {

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.more
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -13,13 +14,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.string.RichTextStringStyle
import eu.kanade.presentation.manga.components.MarkdownRender
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@ -42,17 +40,15 @@ fun NewUpdateScreen(
rejectText = stringResource(MR.strings.action_not_now),
onRejectClick = onRejectUpdate,
) {
RichText(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MaterialTheme.padding.large),
style = RichTextStyle(
stringStyle = RichTextStringStyle(
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
),
),
) {
Markdown(content = changelogInfo)
MarkdownRender(
content = changelogInfo,
flavour = GFMFlavourDescriptor(),
)
TextButton(
onClick = onOpenInBrowser,

View File

@ -42,7 +42,9 @@ fun OnboardingScreen(
}
val isLastStep = currentStep == steps.lastIndex
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
BackHandler(enabled = currentStep != 0) {
currentStep--
}
InfoScreen(
icon = Icons.Outlined.RocketLaunch,

View File

@ -4,7 +4,6 @@ import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
@ -32,6 +31,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
@ -111,7 +111,7 @@ internal class PermissionStep : OnboardingStep {
onButtonClick = {
@SuppressLint("BatteryLife")
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}")
data = "package:${context.packageName}".toUri()
}
context.startActivity(intent)
},

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.more.settings
import androidx.annotation.IntRange
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
@ -20,7 +21,7 @@ sealed class Preference {
// SY <--
abstract val icon: ImageVector?
abstract val onValueChanged: suspend (newValue: T) -> Boolean
abstract val onValueChanged: suspend (value: T) -> Boolean
/**
* A basic [PreferenceItem] that only displays texts.
@ -28,57 +29,58 @@ sealed class Preference {
data class TextPreference(
override val title: String,
override val subtitle: CharSequence? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
val onClick: (() -> Unit)? = null,
) : PreferenceItem<String>()
) : PreferenceItem<String>() {
override val icon: ImageVector? = null
override val onValueChanged: suspend (value: String) -> Boolean = { true }
}
/**
* A [PreferenceItem] that provides a two-state toggleable option.
*/
data class SwitchPreference(
val pref: PreferenceData<Boolean>,
val preference: PreferenceData<Boolean>,
override val title: String,
override val subtitle: CharSequence? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Boolean) -> Boolean = { true },
) : PreferenceItem<Boolean>()
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true },
) : PreferenceItem<Boolean>() {
override val icon: ImageVector? = null
}
/**
* A [PreferenceItem] that provides a slider to select an integer number.
*/
data class SliderPreference(
val value: Int,
val min: Int = 0,
val max: Int,
override val title: String = "",
override val title: String,
val valueRange: IntProgression = 0..1,
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
override val subtitle: String? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Int) -> Boolean = { true },
) : PreferenceItem<Int>()
override val onValueChanged: suspend (value: Int) -> Boolean = { true },
) : PreferenceItem<Int>() {
override val icon: ImageVector? = null
}
/**
* A [PreferenceItem] that displays a list of entries as a dialog.
*/
@Suppress("UNCHECKED_CAST")
data class ListPreference<T>(
val pref: PreferenceData<T>,
val preference: PreferenceData<T>,
val entries: ImmutableMap<T, String>,
override val title: String,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
val entries: ImmutableMap<T, String>,
override val onValueChanged: suspend (value: T) -> Boolean = { true },
) : PreferenceItem<T>() {
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
internal fun internalSet(value: Any) = preference.set(value as T)
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T)
@Composable
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
@ -90,15 +92,14 @@ sealed class Preference {
*/
data class BasicListPreference(
val value: String,
val entries: ImmutableMap<String, String>,
override val title: String,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
val entries: ImmutableMap<String, String>,
override val onValueChanged: suspend (value: String) -> Boolean = { true },
) : PreferenceItem<String>()
/**
@ -106,52 +107,51 @@ sealed class Preference {
* Multiple entries can be selected at the same time.
*/
data class MultiSelectListPreference(
val pref: PreferenceData<Set<String>>,
val preference: PreferenceData<Set<String>>,
val entries: ImmutableMap<String, String>,
override val title: String,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (
value: Set<String>,
entries: ImmutableMap<String, String>,
) -> String? = { v, e ->
val combined = remember(v) {
v.map { e[it] }
.takeIf { it.isNotEmpty() }
?.joinToString()
} ?: stringResource(MR.strings.none)
subtitle?.format(combined)
},
val subtitleProvider: @Composable (value: Set<String>, entries: ImmutableMap<String, String>) -> String? =
{ v, e ->
val combined = remember(v, e) {
v.mapNotNull { e[it] }
.joinToString()
.takeUnless { it.isBlank() }
}
?: stringResource(MR.strings.none)
subtitle?.format(combined)
},
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
val entries: ImmutableMap<String, String>,
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
) : PreferenceItem<Set<String>>()
/**
* A [PreferenceItem] that shows a EditText in the dialog.
*/
data class EditTextPreference(
val pref: PreferenceData<String>,
val preference: PreferenceData<String>,
override val title: String,
override val subtitle: String? = "%s",
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
) : PreferenceItem<String>()
override val onValueChanged: suspend (value: String) -> Boolean = { true },
) : PreferenceItem<String>() {
override val icon: ImageVector? = null
}
/**
* A [PreferenceItem] for individual tracker.
*/
data class TrackerPreference(
val tracker: Tracker,
override val title: String,
val login: () -> Unit,
val logout: () -> Unit,
) : PreferenceItem<String>() {
override val title: String = ""
override val enabled: Boolean = true
override val subtitle: String? = null
override val icon: ImageVector? = null
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
override val onValueChanged: suspend (value: String) -> Boolean = { true }
}
data class InfoPreference(
@ -160,7 +160,7 @@ sealed class Preference {
override val enabled: Boolean = true
override val subtitle: String? = null
override val icon: ImageVector? = null
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
override val onValueChanged: suspend (value: String) -> Boolean = { true }
}
data class CustomPreference(
@ -170,7 +170,7 @@ sealed class Preference {
override val enabled: Boolean = true
override val subtitle: String? = null
override val icon: ImageVector? = null
override val onValueChanged: suspend (newValue: Unit) -> Boolean = { true }
override val onValueChanged: suspend (value: Unit) -> Boolean = { true }
}
}

View File

@ -5,6 +5,8 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
@ -12,16 +14,20 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.InfoWidget
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.more.settings.widget.PrefsVerticalPadding
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TitleFontSize
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.components.BaseSliderItem
import tachiyomi.presentation.core.util.collectAsState
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
@ -60,7 +66,7 @@ internal fun PreferenceItem(
) {
when (item) {
is Preference.PreferenceItem.SwitchPreference -> {
val value by item.pref.collectAsState()
val value by item.preference.collectAsState()
SwitchPreferenceWidget(
title = item.title,
subtitle = item.subtitle,
@ -69,29 +75,33 @@ internal fun PreferenceItem(
onCheckedChanged = { newValue ->
scope.launch {
if (item.onValueChanged(newValue)) {
item.pref.set(newValue)
item.preference.set(newValue)
}
}
},
)
}
is Preference.PreferenceItem.SliderPreference -> {
// TODO: use different composable?
SliderItem(
BaseSliderItem(
label = item.title,
min = item.min,
max = item.max,
value = item.value,
valueRange = item.valueRange,
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
steps = item.steps,
labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
onChange = {
scope.launch {
item.onValueChanged(it)
}
},
modifier = Modifier.padding(
horizontal = PrefsHorizontalPadding,
vertical = PrefsVerticalPadding,
),
)
}
is Preference.PreferenceItem.ListPreference<*> -> {
val value by item.pref.collectAsState()
val value by item.preference.collectAsState()
ListPreferenceWidget(
value = value,
title = item.title,
@ -118,14 +128,14 @@ internal fun PreferenceItem(
)
}
is Preference.PreferenceItem.MultiSelectListPreference -> {
val values by item.pref.collectAsState()
val values by item.preference.collectAsState()
MultiSelectListPreferenceWidget(
preference = item,
values = values,
onValuesChange = { newValues ->
scope.launch {
if (item.onValueChanged(newValues)) {
item.pref.set(newValues.toMutableSet())
item.preference.set(newValues.toMutableSet())
}
}
},
@ -140,7 +150,7 @@ internal fun PreferenceItem(
)
}
is Preference.PreferenceItem.EditTextPreference -> {
val values by item.pref.collectAsState()
val values by item.preference.collectAsState()
EditTextPreferenceWidget(
title = item.title,
subtitle = item.subtitle,
@ -148,7 +158,7 @@ internal fun PreferenceItem(
value = values,
onConfirm = {
val accepted = item.onValueChanged(it)
if (accepted) item.pref.set(it)
if (accepted) item.preference.set(it)
accepted
},
)

View File

@ -13,13 +13,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.window.DialogProperties
import eu.kanade.tachiyomi.util.system.toast
import exh.log.xLogE
import exh.source.ExhPreferences
import exh.uconfig.EHConfigurator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.stringResource
@ -29,8 +29,8 @@ import kotlin.time.Duration.Companion.seconds
@Composable
fun ConfigureExhDialog(run: Boolean, onRunning: () -> Unit) {
val unsortedPreferences = remember {
Injekt.get<UnsortedPreferences>()
val exhPreferences = remember {
Injekt.get<ExhPreferences>()
}
var warnDialogOpen by remember { mutableStateOf(false) }
var configureDialogOpen by remember { mutableStateOf(false) }
@ -38,7 +38,7 @@ fun ConfigureExhDialog(run: Boolean, onRunning: () -> Unit) {
LaunchedEffect(run) {
if (run) {
if (unsortedPreferences.exhShowSettingsUploadWarning().get()) {
if (exhPreferences.exhShowSettingsUploadWarning().get()) {
warnDialogOpen = true
} else {
configureDialogOpen = true
@ -57,7 +57,7 @@ fun ConfigureExhDialog(run: Boolean, onRunning: () -> Unit) {
confirmButton = {
TextButton(
onClick = {
unsortedPreferences.exhShowSettingsUploadWarning().set(false)
exhPreferences.exhShowSettingsUploadWarning().set(false)
configureDialogOpen = true
warnDialogOpen = false
},

View File

@ -72,6 +72,7 @@ import exh.pref.DelegateSourcePreferences
import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID
import exh.source.ExhPreferences
import exh.util.toAnnotatedString
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
@ -86,8 +87,8 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetAllManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.source.service.SourceManager
@ -114,6 +115,7 @@ object SettingsAdvancedScreen : SearchableSettings {
val basePreferences = remember { Injekt.get<BasePreferences>() }
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
return listOf(
Preference.PreferenceItem.TextPreference(
@ -126,7 +128,7 @@ object SettingsAdvancedScreen : SearchableSettings {
},
),
/* SY --> Preference.PreferenceItem.SwitchPreference(
pref = networkPreferences.verboseLogging(),
preference = networkPreferences.verboseLogging(),
title = stringResource(MR.strings.pref_verbose_logging),
subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
onValueChanged = {
@ -154,7 +156,7 @@ object SettingsAdvancedScreen : SearchableSettings {
getBackgroundActivityGroup(),
getDataGroup(),
getNetworkGroup(networkPreferences = networkPreferences),
getLibraryGroup(),
getLibraryGroup(libraryPreferences = libraryPreferences),
getReaderGroup(basePreferences = basePreferences),
getExtensionsGroup(basePreferences = basePreferences),
// SY -->
@ -272,8 +274,7 @@ object SettingsAdvancedScreen : SearchableSettings {
},
),
Preference.PreferenceItem.ListPreference(
pref = networkPreferences.dohProvider(),
title = stringResource(MR.strings.pref_dns_over_https),
preference = networkPreferences.dohProvider(),
entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled),
PREF_DOH_CLOUDFLARE to "Cloudflare",
@ -289,13 +290,14 @@ object SettingsAdvancedScreen : SearchableSettings {
PREF_DOH_NJALLA to "Njalla",
PREF_DOH_SHECAN to "Shecan",
),
title = stringResource(MR.strings.pref_dns_over_https),
onValueChanged = {
context.toast(MR.strings.requires_app_restart)
true
},
),
Preference.PreferenceItem.EditTextPreference(
pref = userAgentPref,
preference = userAgentPref,
title = stringResource(MR.strings.pref_user_agent_string),
onValueChanged = {
try {
@ -322,7 +324,9 @@ object SettingsAdvancedScreen : SearchableSettings {
}
@Composable
private fun getLibraryGroup(): Preference.PreferenceGroup {
private fun getLibraryGroup(
libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current
@ -350,6 +354,11 @@ object SettingsAdvancedScreen : SearchableSettings {
}
},
),
Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.updateMangaTitles(),
title = stringResource(MR.strings.pref_update_library_manga_titles),
subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary),
),
),
)
}
@ -372,13 +381,7 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_reader),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = basePreferences.hardwareBitmapThreshold(),
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
subtitleProvider = { value, options ->
stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty())
},
enabled = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED &&
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
preference = basePreferences.hardwareBitmapThreshold(),
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
.mapIndexed { index, option ->
val display = if (index == 0) {
@ -390,10 +393,16 @@ object SettingsAdvancedScreen : SearchableSettings {
}
.toMap()
.toImmutableMap(),
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
subtitleProvider = { value, options ->
stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty())
},
enabled = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED &&
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
),
Preference.PreferenceItem.SwitchPreference(
pref = basePreferences.alwaysDecodeLongStripWithSSIV(),
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv),
preference = basePreferences.alwaysDecodeLongStripWithSSIV(),
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_2),
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
),
Preference.PreferenceItem.TextPreference(
@ -444,8 +453,7 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(MR.strings.label_extensions),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = extensionInstallerPref,
title = stringResource(MR.strings.ext_installer_pref),
preference = extensionInstallerPref,
entries = extensionInstallerPref.entries
.filter {
// TODO: allow private option in stable versions once URL handling is more fleshed out
@ -457,6 +465,7 @@ object SettingsAdvancedScreen : SearchableSettings {
}
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
title = stringResource(MR.strings.ext_installer_pref),
onValueChanged = {
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
!context.isShizukuInstalled
@ -618,7 +627,7 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(SYMR.strings.data_saver),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = sourcePreferences.dataSaver(),
preference = sourcePreferences.dataSaver(),
title = stringResource(SYMR.strings.data_saver),
subtitle = stringResource(SYMR.strings.data_saver_summary),
entries = persistentMapOf(
@ -628,28 +637,28 @@ object SettingsAdvancedScreen : SearchableSettings {
),
),
Preference.PreferenceItem.EditTextPreference(
pref = sourcePreferences.dataSaverServer(),
preference = sourcePreferences.dataSaverServer(),
title = stringResource(SYMR.strings.bandwidth_data_saver_server),
subtitle = stringResource(SYMR.strings.data_saver_server_summary),
enabled = dataSaver == DataSaver.BANDWIDTH_HERO,
),
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.dataSaverDownloader(),
preference = sourcePreferences.dataSaverDownloader(),
title = stringResource(SYMR.strings.data_saver_downloader),
enabled = dataSaver != DataSaver.NONE,
),
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.dataSaverIgnoreJpeg(),
preference = sourcePreferences.dataSaverIgnoreJpeg(),
title = stringResource(SYMR.strings.data_saver_ignore_jpeg),
enabled = dataSaver != DataSaver.NONE,
),
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.dataSaverIgnoreGif(),
preference = sourcePreferences.dataSaverIgnoreGif(),
title = stringResource(SYMR.strings.data_saver_ignore_gif),
enabled = dataSaver != DataSaver.NONE,
),
Preference.PreferenceItem.ListPreference(
pref = sourcePreferences.dataSaverImageQuality(),
preference = sourcePreferences.dataSaverImageQuality(),
title = stringResource(SYMR.strings.data_saver_image_quality),
subtitle = stringResource(SYMR.strings.data_saver_image_quality_summary),
entries = listOf(
@ -668,7 +677,7 @@ object SettingsAdvancedScreen : SearchableSettings {
val dataSaverImageFormatJpeg by sourcePreferences.dataSaverImageFormatJpeg()
.collectAsState()
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.dataSaverImageFormatJpeg(),
preference = sourcePreferences.dataSaverImageFormatJpeg(),
title = stringResource(SYMR.strings.data_saver_image_format),
subtitle = if (dataSaverImageFormatJpeg) {
stringResource(SYMR.strings.data_saver_image_format_summary_on)
@ -679,7 +688,7 @@ object SettingsAdvancedScreen : SearchableSettings {
)
},
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.dataSaverColorBW(),
preference = sourcePreferences.dataSaverColorBW(),
title = stringResource(SYMR.strings.data_saver_color_bw),
enabled = dataSaver == DataSaver.BANDWIDTH_HERO,
),
@ -692,14 +701,14 @@ object SettingsAdvancedScreen : SearchableSettings {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
val unsortedPreferences = remember { Injekt.get<UnsortedPreferences>() }
val exhPreferences = remember { Injekt.get<ExhPreferences>() }
val delegateSourcePreferences = remember { Injekt.get<DelegateSourcePreferences>() }
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
return Preference.PreferenceGroup(
title = stringResource(SYMR.strings.developer_tools),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.isHentaiEnabled(),
preference = exhPreferences.isHentaiEnabled(),
title = stringResource(SYMR.strings.toggle_hentai_features),
subtitle = stringResource(SYMR.strings.toggle_hentai_features_summary),
onValueChanged = {
@ -714,7 +723,7 @@ object SettingsAdvancedScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = delegateSourcePreferences.delegateSources(),
preference = delegateSourcePreferences.delegateSources(),
title = stringResource(SYMR.strings.toggle_delegated_sources),
subtitle = stringResource(
SYMR.strings.toggle_delegated_sources_summary,
@ -724,7 +733,7 @@ object SettingsAdvancedScreen : SearchableSettings {
),
),
Preference.PreferenceItem.ListPreference(
pref = unsortedPreferences.logLevel(),
preference = exhPreferences.logLevel(),
title = stringResource(SYMR.strings.log_level),
subtitle = stringResource(SYMR.strings.log_level_summary),
entries = EHLogLevel.entries.mapIndexed { index, ehLogLevel ->
@ -734,7 +743,7 @@ object SettingsAdvancedScreen : SearchableSettings {
}.toMap().toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.enableSourceBlacklist(),
preference = sourcePreferences.enableSourceBlacklist(),
title = stringResource(SYMR.strings.enable_source_blacklist),
subtitle = stringResource(
SYMR.strings.enable_source_blacklist_summary,
@ -778,7 +787,7 @@ object SettingsAdvancedScreen : SearchableSettings {
}
Preference.PreferenceItem.SwitchPreference(
title = stringResource(SYMR.strings.encrypt_database),
pref = securityPreferences.encryptDatabase(),
preference = securityPreferences.encryptDatabase(),
subtitle = stringResource(SYMR.strings.encrypt_database_subtitle),
onValueChanged = {
if (it) {

View File

@ -88,7 +88,7 @@ object SettingsAppearanceScreen : SearchableSettings {
}
},
Preference.PreferenceItem.SwitchPreference(
pref = amoledPref,
preference = amoledPref,
title = stringResource(MR.strings.pref_dark_theme_pure_black),
enabled = themeMode != ThemeMode.LIGHT,
onValueChanged = {
@ -122,28 +122,28 @@ object SettingsAppearanceScreen : SearchableSettings {
onClick = { navigator.push(AppLanguageScreen()) },
),
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.tabletUiMode(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
preference = uiPreferences.tabletUiMode(),
entries = TabletUiMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
onValueChanged = {
context.toast(MR.strings.requires_app_restart)
true
},
),
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.dateFormat(),
title = stringResource(MR.strings.pref_date_format),
preference = uiPreferences.dateFormat(),
entries = DateFormats
.associateWith {
val formattedDate = UiPreferences.dateFormat(it).format(now)
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
}
.toImmutableMap(),
title = stringResource(MR.strings.pref_date_format),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.relativeTime(),
preference = uiPreferences.relativeTime(),
title = stringResource(MR.strings.pref_relative_format),
subtitle = stringResource(
MR.strings.pref_relative_format_summary,
@ -164,16 +164,16 @@ object SettingsAppearanceScreen : SearchableSettings {
stringResource(SYMR.strings.pref_category_fork),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.expandFilters(),
preference = uiPreferences.expandFilters(),
title = stringResource(SYMR.strings.toggle_expand_search_filters),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.recommendsInOverflow(),
preference = uiPreferences.recommendsInOverflow(),
title = stringResource(SYMR.strings.put_recommends_in_overflow),
subtitle = stringResource(SYMR.strings.put_recommends_in_overflow_summary),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.mergeInOverflow(),
preference = uiPreferences.mergeInOverflow(),
title = stringResource(SYMR.strings.put_merge_in_overflow),
subtitle = stringResource(SYMR.strings.put_merge_in_overflow_summary),
),
@ -189,8 +189,7 @@ object SettingsAppearanceScreen : SearchableSettings {
} else {
stringResource(MR.strings.disabled)
},
min = 0,
max = 10,
valueRange = 0..10,
onValueChanged = {
uiPreferences.previewsRowCount().set(it)
true
@ -206,15 +205,15 @@ object SettingsAppearanceScreen : SearchableSettings {
stringResource(SYMR.strings.pref_category_navbar),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.showNavUpdates(),
preference = uiPreferences.showNavUpdates(),
title = stringResource(SYMR.strings.pref_hide_updates_button),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.showNavHistory(),
preference = uiPreferences.showNavHistory(),
title = stringResource(SYMR.strings.pref_hide_history_button),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.bottomBarLabels(),
preference = uiPreferences.bottomBarLabels(),
title = stringResource(SYMR.strings.pref_show_bottom_bar_labels),
),
),

View File

@ -20,7 +20,6 @@ import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import kotlinx.collections.immutable.persistentListOf
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.pluralStringResource
@ -49,7 +48,6 @@ object SettingsBrowseScreen : SearchableSettings {
val scope = rememberCoroutineScope()
val hideFeedTab by remember { Injekt.get<UiPreferences>().hideFeedTab().asState(scope) }
val uiPreferences = remember { Injekt.get<UiPreferences>() }
val unsortedPreferences = remember { Injekt.get<UnsortedPreferences>() }
// SY <--
return listOf(
// SY -->
@ -67,17 +65,17 @@ object SettingsBrowseScreen : SearchableSettings {
)
},
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.sourcesTabCategoriesFilter(),
preference = sourcePreferences.sourcesTabCategoriesFilter(),
title = stringResource(SYMR.strings.pref_source_source_filtering),
subtitle = stringResource(SYMR.strings.pref_source_source_filtering_summery),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.useNewSourceNavigation(),
preference = uiPreferences.useNewSourceNavigation(),
title = stringResource(SYMR.strings.pref_source_navigation),
subtitle = stringResource(SYMR.strings.pref_source_navigation_summery),
),
Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.allowLocalSourceHiddenFolders(),
preference = sourcePreferences.allowLocalSourceHiddenFolders(),
title = stringResource(SYMR.strings.pref_local_source_hidden_folders),
subtitle = stringResource(SYMR.strings.pref_local_source_hidden_folders_summery),
),
@ -87,11 +85,11 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(SYMR.strings.feed),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.hideFeedTab(),
preference = uiPreferences.hideFeedTab(),
title = stringResource(SYMR.strings.pref_hide_feed),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.feedTabInFront(),
preference = uiPreferences.feedTabInFront(),
title = stringResource(SYMR.strings.pref_feed_position),
subtitle = stringResource(SYMR.strings.pref_feed_position_summery),
enabled = hideFeedTab.not(),
@ -103,7 +101,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.label_sources),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.hideInLibraryItems(),
preference = sourcePreferences.hideInLibraryItems(),
title = stringResource(MR.strings.pref_hide_in_library_items),
),
Preference.PreferenceItem.TextPreference(
@ -119,7 +117,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_nsfw_content),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.showNsfwSource(),
preference = sourcePreferences.showNsfwSource(),
title = stringResource(MR.strings.pref_show_nsfw_source),
subtitle = stringResource(MR.strings.requires_app_restart),
onValueChanged = {
@ -131,6 +129,24 @@ object SettingsBrowseScreen : SearchableSettings {
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.parental_controls_info)),
),
),
getMigrationCategory(sourcePreferences),
)
}
@Composable
fun getMigrationCategory(sourcePreferences: SourcePreferences): Preference.PreferenceGroup {
val skipPreMigration by sourcePreferences.skipPreMigration().collectAsState()
val migrationSources by sourcePreferences.migrationSources().collectAsState()
return Preference.PreferenceGroup(
stringResource(SYMR.strings.migration),
enabled = skipPreMigration || migrationSources.isNotEmpty(),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
preference = sourcePreferences.skipPreMigration(),
title = stringResource(SYMR.strings.skip_pre_migration),
subtitle = stringResource(SYMR.strings.pref_skip_pre_migration_summary),
),
),
)
}
}

View File

@ -7,7 +7,9 @@ import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
@ -15,7 +17,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
@ -24,6 +28,7 @@ import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@ -31,13 +36,17 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.net.toUri
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.zxing.client.android.Intents
import com.hippo.unifile.UniFile
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
@ -46,12 +55,16 @@ import eu.kanade.presentation.more.settings.screen.data.StorageInfo
import eu.kanade.presentation.more.settings.screen.data.SyncSettingsSelector
import eu.kanade.presentation.more.settings.screen.data.SyncTriggerOptionsScreen
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
import eu.kanade.tachiyomi.data.export.LibraryExporter
import eu.kanade.tachiyomi.data.export.LibraryExporter.ExportOptions
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.data.sync.SyncManager
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
@ -60,6 +73,7 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource
@ -69,6 +83,8 @@ import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
@ -111,6 +127,7 @@ object SettingsDataScreen : SearchableSettings {
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(),
getExportGroup(),
) + getSyncPreferences(syncPreferences = syncPreferences, syncService = syncService)
}
@ -255,8 +272,7 @@ object SettingsDataScreen : SearchableSettings {
// Automatic backups
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.backupInterval(),
title = stringResource(MR.strings.pref_backup_interval),
preference = backupPreferences.backupInterval(),
entries = persistentMapOf(
0 to stringResource(MR.strings.off),
6 to stringResource(MR.strings.update_6hour),
@ -265,6 +281,7 @@ object SettingsDataScreen : SearchableSettings {
48 to stringResource(MR.strings.update_48hour),
168 to stringResource(MR.strings.update_weekly),
),
title = stringResource(MR.strings.pref_backup_interval),
onValueChanged = {
BackupCreateJob.setupTask(context, it)
true
@ -348,13 +365,151 @@ object SettingsDataScreen : SearchableSettings {
),
// SY <--
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoClearChapterCache(),
preference = libraryPreferences.autoClearChapterCache(),
title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
),
),
)
}
@Composable
private fun getExportGroup(): Preference.PreferenceGroup {
var showDialog by remember { mutableStateOf(false) }
var exportOptions by remember {
mutableStateOf(
ExportOptions(
includeTitle = true,
includeAuthor = true,
includeArtist = true,
),
)
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
val getFavorites = remember { Injekt.get<GetFavorites>() }
var favorites by remember { mutableStateOf<List<Manga>>(emptyList()) }
LaunchedEffect(Unit) {
favorites = getFavorites.await()
}
val saveFileLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("text/csv"),
) { uri ->
uri?.let {
scope.launch {
LibraryExporter.exportToCsv(
context = context,
uri = it,
favorites = favorites,
options = exportOptions,
onExportComplete = {
scope.launch(Dispatchers.Main) {
context.toast(MR.strings.library_exported)
}
},
)
}
}
}
if (showDialog) {
ColumnSelectionDialog(
options = exportOptions,
onConfirm = { options ->
exportOptions = options
saveFileLauncher.launch("mihon_library.csv")
},
onDismissRequest = { showDialog = false },
)
}
return Preference.PreferenceGroup(
title = stringResource(MR.strings.export),
preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.library_list),
onClick = { showDialog = true },
),
),
)
}
@Composable
private fun ColumnSelectionDialog(
options: ExportOptions,
onConfirm: (ExportOptions) -> Unit,
onDismissRequest: () -> Unit,
) {
var titleSelected by remember { mutableStateOf(options.includeTitle) }
var authorSelected by remember { mutableStateOf(options.includeAuthor) }
var artistSelected by remember { mutableStateOf(options.includeArtist) }
AlertDialog(
onDismissRequest = onDismissRequest,
title = {
Text(text = stringResource(MR.strings.migration_dialog_what_to_include))
},
text = {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = titleSelected,
onCheckedChange = { checked ->
titleSelected = checked
if (!checked) {
authorSelected = false
artistSelected = false
}
},
)
Text(text = stringResource(MR.strings.title))
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = authorSelected,
onCheckedChange = { authorSelected = it },
enabled = titleSelected,
)
Text(text = stringResource(MR.strings.author))
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = artistSelected,
onCheckedChange = { artistSelected = it },
enabled = titleSelected,
)
Text(text = stringResource(MR.strings.artist))
}
}
},
confirmButton = {
TextButton(
onClick = {
onConfirm(
ExportOptions(
includeTitle = titleSelected,
includeAuthor = authorSelected,
includeArtist = artistSelected,
),
)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_save))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
)
}
// SY -->
@Composable
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
return listOf(
@ -362,7 +517,7 @@ object SettingsDataScreen : SearchableSettings {
title = stringResource(SYMR.strings.pref_sync_service_category),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = syncPreferences.syncService(),
preference = syncPreferences.syncService(),
title = stringResource(SYMR.strings.pref_sync_service),
entries = persistentMapOf(
SyncManager.SyncService.NONE.value to stringResource(MR.strings.off),
@ -502,11 +657,27 @@ object SettingsDataScreen : SearchableSettings {
@Composable
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
val scope = rememberCoroutineScope()
val qrScanLauncher = rememberLauncherForActivityResult(ScanContract()) {
if (it.contents != null && it.contents.isNotEmpty()) {
syncPreferences.clientAPIKey().set(it.contents)
}
}
val context = LocalContext.current
val scanOptions = remember {
ScanOptions().apply {
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
setOrientationLocked(false)
setPrompt(SYMR.strings.scan_qr_code.getString(context))
addExtra(Intents.Scan.SCAN_TYPE, Intents.Scan.MIXED_SCAN)
}
}
return listOf(
Preference.PreferenceItem.EditTextPreference(
title = stringResource(SYMR.strings.pref_sync_host),
subtitle = stringResource(SYMR.strings.pref_sync_host_summ),
pref = syncPreferences.clientHost(),
preference = syncPreferences.clientHost(),
onValueChanged = { newValue ->
scope.launch {
// Trim spaces at the beginning and end, then remove trailing slash if present
@ -517,11 +688,32 @@ object SettingsDataScreen : SearchableSettings {
true
},
),
Preference.PreferenceItem.EditTextPreference(
Preference.PreferenceItem.CustomPreference(
title = stringResource(SYMR.strings.pref_sync_api_key),
subtitle = stringResource(SYMR.strings.pref_sync_api_key_summ),
pref = syncPreferences.clientAPIKey(),
),
) {
val values by syncPreferences.clientAPIKey().collectAsState()
EditTextPreferenceWidget(
title = stringResource(SYMR.strings.pref_sync_api_key),
subtitle = stringResource(SYMR.strings.pref_sync_api_key_summ),
onConfirm = {
syncPreferences.clientAPIKey().set(it)
true
},
icon = null,
value = values,
widget = {
IconButton(
onClick = { qrScanLauncher.launch(scanOptions) },
modifier = Modifier.padding(start = TrailingWidgetBuffer),
) {
Icon(
Icons.Filled.QrCodeScanner,
contentDescription = stringResource(SYMR.strings.scan_qr_code),
)
}
},
)
},
)
}
@ -567,7 +759,7 @@ object SettingsDataScreen : SearchableSettings {
title = stringResource(SYMR.strings.pref_sync_automatic_category),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = syncIntervalPref,
preference = syncIntervalPref,
title = stringResource(SYMR.strings.pref_sync_interval),
entries = persistentMapOf(
0 to stringResource(MR.strings.off),
@ -591,4 +783,5 @@ object SettingsDataScreen : SearchableSettings {
),
)
}
// SY <--
}

View File

@ -39,15 +39,15 @@ object SettingsDownloadScreen : SearchableSettings {
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
return listOf(
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.downloadOnlyOverWifi(),
preference = downloadPreferences.downloadOnlyOverWifi(),
title = stringResource(MR.strings.connected_to_wifi),
),
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.saveChaptersAsCBZ(),
preference = downloadPreferences.saveChaptersAsCBZ(),
title = stringResource(MR.strings.save_chapter_as_cbz),
),
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.splitTallImages(),
preference = downloadPreferences.splitTallImages(),
title = stringResource(MR.strings.split_tall_images),
subtitle = stringResource(MR.strings.split_tall_images_summary),
),
@ -72,12 +72,11 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_delete_chapters),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.removeAfterMarkedAsRead(),
preference = downloadPreferences.removeAfterMarkedAsRead(),
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
),
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.removeAfterReadSlots(),
title = stringResource(MR.strings.pref_remove_after_read),
preference = downloadPreferences.removeAfterReadSlots(),
entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled),
0 to stringResource(MR.strings.last_read_chapter),
@ -86,9 +85,10 @@ object SettingsDownloadScreen : SearchableSettings {
3 to stringResource(MR.strings.fourth_to_last),
4 to stringResource(MR.strings.fifth_to_last),
),
title = stringResource(MR.strings.pref_remove_after_read),
),
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.removeBookmarkedChapters(),
preference = downloadPreferences.removeBookmarkedChapters(),
title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
),
getExcludedCategoriesPreference(
@ -105,11 +105,11 @@ object SettingsDownloadScreen : SearchableSettings {
categories: () -> List<Category>,
): Preference.PreferenceItem.MultiSelectListPreference {
return Preference.PreferenceItem.MultiSelectListPreference(
pref = downloadPreferences.removeExcludeCategories(),
title = stringResource(MR.strings.pref_remove_exclude_categories),
preference = downloadPreferences.removeExcludeCategories(),
entries = categories()
.associate { it.id.toString() to it.visualName }
.toImmutableMap(),
title = stringResource(MR.strings.pref_remove_exclude_categories),
)
}
@ -149,11 +149,11 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_auto_download),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = downloadNewChaptersPref,
preference = downloadNewChaptersPref,
title = stringResource(MR.strings.pref_download_new),
),
Preference.PreferenceItem.SwitchPreference(
pref = downloadNewUnreadChaptersOnlyPref,
preference = downloadNewUnreadChaptersOnlyPref,
title = stringResource(MR.strings.pref_download_new_unread_chapters_only),
enabled = downloadNewChapters,
),
@ -164,8 +164,8 @@ object SettingsDownloadScreen : SearchableSettings {
included = included,
excluded = excluded,
),
onClick = { showDialog = true },
enabled = downloadNewChapters,
onClick = { showDialog = true },
),
),
)
@ -179,8 +179,7 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.download_ahead),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.autoDownloadWhileReading(),
title = stringResource(MR.strings.auto_download_while_reading),
preference = downloadPreferences.autoDownloadWhileReading(),
entries = listOf(0, 2, 3, 5, 10)
.associateWith {
if (it == 0) {
@ -190,6 +189,7 @@ object SettingsDownloadScreen : SearchableSettings {
}
}
.toImmutableMap(),
title = stringResource(MR.strings.auto_download_while_reading),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)),
),

View File

@ -43,7 +43,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat.startActivity
import eu.kanade.presentation.library.components.SyncFavoritesWarningDialog
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
@ -52,6 +51,7 @@ import exh.eh.EHentaiUpdateWorker
import exh.eh.EHentaiUpdateWorkerConstants
import exh.eh.EHentaiUpdaterStats
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.source.ExhPreferences
import exh.ui.login.EhLoginActivity
import exh.util.nullIfBlank
import kotlinx.collections.immutable.persistentListOf
@ -64,7 +64,6 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI
import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries
@ -89,22 +88,22 @@ object SettingsEhScreen : SearchableSettings {
@Composable
override fun getTitleRes() = SYMR.strings.pref_category_eh
override fun isEnabled(): Boolean = Injekt.get<UnsortedPreferences>().isHentaiEnabled().get()
override fun isEnabled(): Boolean = Injekt.get<ExhPreferences>().isHentaiEnabled().get()
@Composable
fun Reconfigure(
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
openWarnConfigureDialogController: () -> Unit,
) {
var initialLoadGuard by remember { mutableStateOf(false) }
val useHentaiAtHome by unsortedPreferences.useHentaiAtHome().collectAsState()
val useJapaneseTitle by unsortedPreferences.useJapaneseTitle().collectAsState()
val useOriginalImages by unsortedPreferences.exhUseOriginalImages().collectAsState()
val ehTagFilterValue by unsortedPreferences.ehTagFilterValue().collectAsState()
val ehTagWatchingValue by unsortedPreferences.ehTagWatchingValue().collectAsState()
val settingsLanguages by unsortedPreferences.exhSettingsLanguages().collectAsState()
val enabledCategories by unsortedPreferences.exhEnabledCategories().collectAsState()
val imageQuality by unsortedPreferences.imageQuality().collectAsState()
val useHentaiAtHome by exhPreferences.useHentaiAtHome().collectAsState()
val useJapaneseTitle by exhPreferences.useJapaneseTitle().collectAsState()
val useOriginalImages by exhPreferences.exhUseOriginalImages().collectAsState()
val ehTagFilterValue by exhPreferences.ehTagFilterValue().collectAsState()
val ehTagWatchingValue by exhPreferences.ehTagWatchingValue().collectAsState()
val settingsLanguages by exhPreferences.exhSettingsLanguages().collectAsState()
val enabledCategories by exhPreferences.exhEnabledCategories().collectAsState()
val imageQuality by exhPreferences.imageQuality().collectAsState()
DisposableEffect(
useHentaiAtHome,
useJapaneseTitle,
@ -125,15 +124,15 @@ object SettingsEhScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val unsortedPreferences: UnsortedPreferences = remember { Injekt.get() }
val exhPreferences: ExhPreferences = remember { Injekt.get() }
val getFlatMetadataById: GetFlatMetadataById = remember { Injekt.get() }
val deleteFavoriteEntries: DeleteFavoriteEntries = remember { Injekt.get() }
val getExhFavoriteMangaWithMetadata: GetExhFavoriteMangaWithMetadata = remember { Injekt.get() }
val exhentaiEnabled by unsortedPreferences.enableExhentai().collectAsState()
val exhentaiEnabled by exhPreferences.enableExhentai().collectAsState()
var runConfigureDialog by remember { mutableStateOf(false) }
val openWarnConfigureDialogController = { runConfigureDialog = true }
Reconfigure(unsortedPreferences, openWarnConfigureDialogController)
Reconfigure(exhPreferences, openWarnConfigureDialogController)
ConfigureExhDialog(run = runConfigureDialog, onRunning = { runConfigureDialog = false })
@ -141,36 +140,36 @@ object SettingsEhScreen : SearchableSettings {
Preference.PreferenceGroup(
stringResource(SYMR.strings.ehentai_prefs_account_settings),
preferenceItems = persistentListOf(
getLoginPreference(unsortedPreferences, openWarnConfigureDialogController),
useHentaiAtHome(exhentaiEnabled, unsortedPreferences),
useJapaneseTitle(exhentaiEnabled, unsortedPreferences),
useOriginalImages(exhentaiEnabled, unsortedPreferences),
getLoginPreference(exhPreferences, openWarnConfigureDialogController),
useHentaiAtHome(exhentaiEnabled, exhPreferences),
useJapaneseTitle(exhentaiEnabled, exhPreferences),
useOriginalImages(exhentaiEnabled, exhPreferences),
watchedTags(exhentaiEnabled),
tagFilterThreshold(exhentaiEnabled, unsortedPreferences),
tagWatchingThreshold(exhentaiEnabled, unsortedPreferences),
settingsLanguages(exhentaiEnabled, unsortedPreferences),
enabledCategories(exhentaiEnabled, unsortedPreferences),
watchedListDefaultState(exhentaiEnabled, unsortedPreferences),
imageQuality(exhentaiEnabled, unsortedPreferences),
enhancedEhentaiView(unsortedPreferences),
tagFilterThreshold(exhentaiEnabled, exhPreferences),
tagWatchingThreshold(exhentaiEnabled, exhPreferences),
settingsLanguages(exhentaiEnabled, exhPreferences),
enabledCategories(exhentaiEnabled, exhPreferences),
watchedListDefaultState(exhentaiEnabled, exhPreferences),
imageQuality(exhentaiEnabled, exhPreferences),
enhancedEhentaiView(exhPreferences),
),
),
Preference.PreferenceGroup(
stringResource(SYMR.strings.favorites_sync),
preferenceItems = persistentListOf(
readOnlySync(unsortedPreferences),
readOnlySync(exhPreferences),
syncFavoriteNotes(),
lenientSync(unsortedPreferences),
lenientSync(exhPreferences),
forceSyncReset(deleteFavoriteEntries),
),
),
Preference.PreferenceGroup(
stringResource(SYMR.strings.gallery_update_checker),
preferenceItems = persistentListOf(
updateCheckerFrequency(unsortedPreferences),
autoUpdateRequirements(unsortedPreferences),
updateCheckerFrequency(exhPreferences),
autoUpdateRequirements(exhPreferences),
updaterStatistics(
unsortedPreferences,
exhPreferences,
getExhFavoriteMangaWithMetadata,
getFlatMetadataById,
),
@ -181,7 +180,7 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun getLoginPreference(
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
openWarnConfigureDialogController: () -> Unit,
): Preference.PreferenceItem.SwitchPreference {
val activityResultContract =
@ -192,9 +191,9 @@ object SettingsEhScreen : SearchableSettings {
}
}
val context = LocalContext.current
val value by unsortedPreferences.enableExhentai().collectAsState()
val value by exhPreferences.enableExhentai().collectAsState()
return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.enableExhentai(),
preference = exhPreferences.enableExhentai(),
title = stringResource(SYMR.strings.enable_exhentai),
subtitle = if (!value) {
stringResource(SYMR.strings.requires_login)
@ -203,7 +202,7 @@ object SettingsEhScreen : SearchableSettings {
},
onValueChanged = { newVal ->
if (!newVal) {
unsortedPreferences.enableExhentai().set(false)
exhPreferences.enableExhentai().set(false)
true
} else {
activityResultContract.launch(EhLoginActivity.newIntent(context))
@ -216,10 +215,10 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun useHentaiAtHome(
exhentaiEnabled: Boolean,
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
): Preference.PreferenceItem.ListPreference<Int> {
return Preference.PreferenceItem.ListPreference(
pref = unsortedPreferences.useHentaiAtHome(),
preference = exhPreferences.useHentaiAtHome(),
title = stringResource(SYMR.strings.use_hentai_at_home),
subtitle = stringResource(SYMR.strings.use_hentai_at_home_summary),
entries = persistentMapOf(
@ -233,11 +232,11 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun useJapaneseTitle(
exhentaiEnabled: Boolean,
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
): Preference.PreferenceItem.SwitchPreference {
val value by unsortedPreferences.useJapaneseTitle().collectAsState()
val value by exhPreferences.useJapaneseTitle().collectAsState()
return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.useJapaneseTitle(),
preference = exhPreferences.useJapaneseTitle(),
title = stringResource(SYMR.strings.show_japanese_titles),
subtitle = if (value) {
stringResource(SYMR.strings.show_japanese_titles_option_1)
@ -251,11 +250,11 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun useOriginalImages(
exhentaiEnabled: Boolean,
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
): Preference.PreferenceItem.SwitchPreference {
val value by unsortedPreferences.exhUseOriginalImages().collectAsState()
val value by exhPreferences.exhUseOriginalImages().collectAsState()
return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.exhUseOriginalImages(),
preference = exhPreferences.exhUseOriginalImages(),
title = stringResource(SYMR.strings.use_original_images),
subtitle = if (value) {
stringResource(SYMR.strings.use_original_images_on)
@ -273,8 +272,7 @@ object SettingsEhScreen : SearchableSettings {
title = stringResource(SYMR.strings.watched_tags),
subtitle = stringResource(SYMR.strings.watched_tags_summary),
onClick = {
startActivity(
context,
context.startActivity(
WebViewActivity.newIntent(
context,
url = "https://exhentai.org/mytags",
@ -353,9 +351,9 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun tagFilterThreshold(
exhentaiEnabled: Boolean,
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
): Preference.PreferenceItem.TextPreference {
val value by unsortedPreferences.ehTagFilterValue().collectAsState()
val value by exhPreferences.ehTagFilterValue().collectAsState()
var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) {
TagThresholdDialog(
@ -366,7 +364,7 @@ object SettingsEhScreen : SearchableSettings {
outsideRangeError = stringResource(SYMR.strings.tag_filtering_threshhold_error),
onValueChange = {
dialogOpen = false
unsortedPreferences.ehTagFilterValue().set(it)
exhPreferences.ehTagFilterValue().set(it)
},
)
}
@ -383,9 +381,9 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun tagWatchingThreshold(
exhentaiEnabled: Boolean,
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
): Preference.PreferenceItem.TextPreference {
val value by unsortedPreferences.ehTagWatchingValue().collectAsState()
val value by exhPreferences.ehTagWatchingValue().collectAsState()
var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) {
TagThresholdDialog(
@ -396,7 +394,7 @@ object SettingsEhScreen : SearchableSettings {
outsideRangeError = stringResource(SYMR.strings.tag_watching_threshhold_error),
onValueChange = {
dialogOpen = false
unsortedPreferences.ehTagWatchingValue().set(it)
exhPreferences.ehTagWatchingValue().set(it)
},
)
}
@ -606,9 +604,9 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun settingsLanguages(
exhentaiEnabled: Boolean,
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
): Preference.PreferenceItem.TextPreference {
val value by unsortedPreferences.exhSettingsLanguages().collectAsState()
val value by exhPreferences.exhSettingsLanguages().collectAsState()
var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) {
LanguagesDialog(
@ -616,7 +614,7 @@ object SettingsEhScreen : SearchableSettings {
initialValue = value,
onValueChange = {
dialogOpen = false
unsortedPreferences.exhSettingsLanguages().set(it)
exhPreferences.exhSettingsLanguages().set(it)
},
)
}
@ -772,9 +770,9 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun enabledCategories(
exhentaiEnabled: Boolean,
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
): Preference.PreferenceItem.TextPreference {
val value by unsortedPreferences.exhEnabledCategories().collectAsState()
val value by exhPreferences.exhEnabledCategories().collectAsState()
var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) {
FrontPageCategoriesDialog(
@ -782,7 +780,7 @@ object SettingsEhScreen : SearchableSettings {
initialValue = value,
onValueChange = {
dialogOpen = false
unsortedPreferences.exhEnabledCategories().set(it)
exhPreferences.exhEnabledCategories().set(it)
},
)
}
@ -799,10 +797,10 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun watchedListDefaultState(
exhentaiEnabled: Boolean,
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.exhWatchedListDefaultState(),
preference = exhPreferences.exhWatchedListDefaultState(),
title = stringResource(SYMR.strings.watched_list_default),
subtitle = stringResource(SYMR.strings.watched_list_state_summary),
enabled = exhentaiEnabled,
@ -812,10 +810,10 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun imageQuality(
exhentaiEnabled: Boolean,
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
): Preference.PreferenceItem.ListPreference<String> {
return Preference.PreferenceItem.ListPreference(
pref = unsortedPreferences.imageQuality(),
preference = exhPreferences.imageQuality(),
title = stringResource(SYMR.strings.eh_image_quality_summary),
subtitle = stringResource(SYMR.strings.eh_image_quality),
entries = persistentMapOf(
@ -831,18 +829,18 @@ object SettingsEhScreen : SearchableSettings {
}
@Composable
fun enhancedEhentaiView(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference {
fun enhancedEhentaiView(exhPreferences: ExhPreferences): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.enhancedEHentaiView(),
preference = exhPreferences.enhancedEHentaiView(),
title = stringResource(SYMR.strings.pref_enhanced_e_hentai_view),
subtitle = stringResource(SYMR.strings.pref_enhanced_e_hentai_view_summary),
)
}
@Composable
fun readOnlySync(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference {
fun readOnlySync(exhPreferences: ExhPreferences): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.exhReadOnlySync(),
preference = exhPreferences.exhReadOnlySync(),
title = stringResource(SYMR.strings.disable_favorites_uploading),
subtitle = stringResource(SYMR.strings.disable_favorites_uploading_summary),
)
@ -865,9 +863,9 @@ object SettingsEhScreen : SearchableSettings {
}
@Composable
fun lenientSync(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference {
fun lenientSync(exhPreferences: ExhPreferences): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.exhLenientSync(),
preference = exhPreferences.exhLenientSync(),
title = stringResource(SYMR.strings.ignore_sync_errors),
subtitle = stringResource(SYMR.strings.ignore_sync_errors_summary),
)
@ -937,12 +935,12 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun updateCheckerFrequency(
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
): Preference.PreferenceItem.ListPreference<Int> {
val value by unsortedPreferences.exhAutoUpdateFrequency().collectAsState()
val value by exhPreferences.exhAutoUpdateFrequency().collectAsState()
val context = LocalContext.current
return Preference.PreferenceItem.ListPreference(
pref = unsortedPreferences.exhAutoUpdateFrequency(),
preference = exhPreferences.exhAutoUpdateFrequency(),
title = stringResource(SYMR.strings.time_between_batches),
subtitle = if (value == 0) {
stringResource(SYMR.strings.time_between_batches_summary_1, stringResource(MR.strings.app_name))
@ -973,12 +971,12 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun autoUpdateRequirements(
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
): Preference.PreferenceItem.MultiSelectListPreference {
val value by unsortedPreferences.exhAutoUpdateRequirements().collectAsState()
val value by exhPreferences.exhAutoUpdateRequirements().collectAsState()
val context = LocalContext.current
return Preference.PreferenceItem.MultiSelectListPreference(
pref = unsortedPreferences.exhAutoUpdateRequirements(),
preference = exhPreferences.exhAutoUpdateRequirements(),
title = stringResource(SYMR.strings.auto_update_restrictions),
subtitle = remember(value) {
context.stringResource(
@ -1141,7 +1139,7 @@ object SettingsEhScreen : SearchableSettings {
@Composable
fun updaterStatistics(
unsortedPreferences: UnsortedPreferences,
exhPreferences: ExhPreferences,
getExhFavoriteMangaWithMetadata: GetExhFavoriteMangaWithMetadata,
getFlatMetadataById: GetFlatMetadataById,
): Preference.PreferenceItem.TextPreference {
@ -1152,7 +1150,7 @@ object SettingsEhScreen : SearchableSettings {
value = withIOContext {
try {
val stats =
unsortedPreferences.exhAutoUpdateStats().get().nullIfBlank()?.let {
exhPreferences.exhAutoUpdateStats().get().nullIfBlank()?.let {
Json.decodeFromString<EHentaiUpdaterStats>(it)
}

View File

@ -25,7 +25,6 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.launch
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.ResetCategoryFlags
import tachiyomi.domain.category.model.Category
@ -38,6 +37,8 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_EXISTING
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_NEW
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.pluralStringResource
@ -57,17 +58,13 @@ object SettingsLibraryScreen : SearchableSettings {
val getCategories = remember { Injekt.get<GetCategories>() }
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
// SY -->
val unsortedPreferences = remember { Injekt.get<UnsortedPreferences>() }
// SY <--
return listOf(
getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences),
getGlobalUpdateGroup(allCategories, libraryPreferences),
getChapterSwipeActionsGroup(libraryPreferences),
getBehaviorGroup(libraryPreferences),
// SY -->
getSortingCategory(LocalNavigator.currentOrThrow, libraryPreferences),
getMigrationCategory(unsortedPreferences),
// SY <--
)
}
@ -100,12 +97,12 @@ object SettingsLibraryScreen : SearchableSettings {
onClick = { navigator.push(CategoryScreen()) },
),
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.defaultCategory(),
title = stringResource(MR.strings.default_category),
preference = libraryPreferences.defaultCategory(),
entries = ids.zip(labels).toMap().toImmutableMap(),
title = stringResource(MR.strings.default_category),
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.categorizedDisplaySettings(),
preference = libraryPreferences.categorizedDisplaySettings(),
title = stringResource(MR.strings.categorized_display_settings),
onValueChanged = {
if (!it) {
@ -157,8 +154,7 @@ object SettingsLibraryScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_library_update),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = autoUpdateIntervalPref,
title = stringResource(MR.strings.pref_library_update_interval),
preference = autoUpdateIntervalPref,
entries = persistentMapOf(
0 to stringResource(MR.strings.update_never),
12 to stringResource(MR.strings.update_12hour),
@ -167,21 +163,22 @@ object SettingsLibraryScreen : SearchableSettings {
72 to stringResource(MR.strings.update_72hour),
168 to stringResource(MR.strings.update_weekly),
),
title = stringResource(MR.strings.pref_library_update_interval),
onValueChanged = {
LibraryUpdateJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryPreferences.autoUpdateDeviceRestrictions(),
enabled = autoUpdateInterval > 0,
title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions),
preference = libraryPreferences.autoUpdateDeviceRestrictions(),
entries = persistentMapOf(
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
DEVICE_CHARGING to stringResource(MR.strings.charging),
),
title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions),
enabled = autoUpdateInterval > 0,
onValueChanged = {
// Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
@ -199,7 +196,7 @@ object SettingsLibraryScreen : SearchableSettings {
),
// SY -->
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.groupLibraryUpdateType(),
preference = libraryPreferences.groupLibraryUpdateType(),
title = stringResource(SYMR.strings.library_group_updates),
entries = persistentMapOf(
GroupLibraryMode.GLOBAL to stringResource(SYMR.strings.library_group_updates_global),
@ -210,45 +207,37 @@ object SettingsLibraryScreen : SearchableSettings {
),
// SY <--
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoUpdateMetadata(),
preference = libraryPreferences.autoUpdateMetadata(),
title = stringResource(MR.strings.pref_library_update_refresh_metadata),
subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary),
),
Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryPreferences.autoUpdateMangaRestrictions(),
title = stringResource(MR.strings.pref_library_update_smart_update),
preference = libraryPreferences.autoUpdateMangaRestrictions(),
entries = persistentMapOf(
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read),
MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started),
MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period),
),
title = stringResource(MR.strings.pref_library_update_smart_update),
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.newShowUpdatesCount(),
preference = libraryPreferences.newShowUpdatesCount(),
title = stringResource(MR.strings.pref_library_update_show_tab_badge),
),
// SY -->
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.libraryReadDuplicateChapters(),
title = stringResource(SYMR.strings.pref_library_mark_duplicate_chapters),
subtitle = stringResource(SYMR.strings.pref_library_mark_duplicate_chapters_summary),
),
// SY <--
),
)
}
@Composable
private fun getChapterSwipeActionsGroup(
private fun getBehaviorGroup(
libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_chapter_swipe),
title = stringResource(MR.strings.pref_behavior),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeToStartAction(),
title = stringResource(MR.strings.pref_chapter_swipe_start),
preference = libraryPreferences.swipeToStartAction(),
entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled),
@ -259,10 +248,10 @@ object SettingsLibraryScreen : SearchableSettings {
LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download),
),
title = stringResource(MR.strings.pref_chapter_swipe_start),
),
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeToEndAction(),
title = stringResource(MR.strings.pref_chapter_swipe_end),
preference = libraryPreferences.swipeToEndAction(),
entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled),
@ -273,6 +262,17 @@ object SettingsLibraryScreen : SearchableSettings {
LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download),
),
title = stringResource(MR.strings.pref_chapter_swipe_end),
),
Preference.PreferenceItem.MultiSelectListPreference(
preference = libraryPreferences.markDuplicateReadChapterAsRead(),
entries = persistentMapOf(
MARK_DUPLICATE_CHAPTER_READ_EXISTING to
stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_existing),
MARK_DUPLICATE_CHAPTER_READ_NEW to
stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_new),
),
title = stringResource(MR.strings.pref_mark_duplicate_read_chapter_read),
),
),
)
@ -295,22 +295,5 @@ object SettingsLibraryScreen : SearchableSettings {
),
)
}
@Composable
fun getMigrationCategory(unsortedPreferences: UnsortedPreferences): Preference.PreferenceGroup {
val skipPreMigration by unsortedPreferences.skipPreMigration().collectAsState()
val migrationSources by unsortedPreferences.migrationSources().collectAsState()
return Preference.PreferenceGroup(
stringResource(SYMR.strings.migration),
enabled = skipPreMigration || migrationSources.isNotEmpty(),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.skipPreMigration(),
title = stringResource(SYMR.strings.skip_pre_migration),
subtitle = stringResource(SYMR.strings.pref_skip_pre_migration_summary),
),
),
)
}
// SY <--
}

View File

@ -44,7 +44,6 @@ import logcat.LogPriority
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.padding
@ -65,14 +64,13 @@ object SettingsMangadexScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val sourcePreferences: SourcePreferences = remember { Injekt.get() }
val unsortedPreferences: UnsortedPreferences = remember { Injekt.get() }
val trackPreferences: TrackPreferences = remember { Injekt.get() }
val mdex = remember { MdUtil.getEnabledMangaDex(unsortedPreferences, sourcePreferences) } ?: return emptyList()
val mdex = remember { MdUtil.getEnabledMangaDex(sourcePreferences) } ?: return emptyList()
return listOf(
loginPreference(mdex, trackPreferences),
preferredMangaDexId(unsortedPreferences, sourcePreferences),
syncMangaDexIntoThis(unsortedPreferences),
preferredMangaDexId(sourcePreferences),
syncMangaDexIntoThis(sourcePreferences),
syncLibraryToMangaDex(),
)
}
@ -174,11 +172,10 @@ object SettingsMangadexScreen : SearchableSettings {
@Composable
fun preferredMangaDexId(
unsortedPreferences: UnsortedPreferences,
sourcePreferences: SourcePreferences,
): Preference.PreferenceItem.ListPreference<String> {
return Preference.PreferenceItem.ListPreference(
pref = unsortedPreferences.preferredMangaDexId(),
preference = sourcePreferences.preferredMangaDexId(),
title = stringResource(SYMR.strings.mangadex_preffered_source),
subtitle = stringResource(SYMR.strings.mangadex_preffered_source_summary),
entries = MdUtil.getEnabledMangaDexs(sourcePreferences)
@ -250,7 +247,7 @@ object SettingsMangadexScreen : SearchableSettings {
}
@Composable
fun syncMangaDexIntoThis(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.TextPreference {
fun syncMangaDexIntoThis(sourcePreferences: SourcePreferences): Preference.PreferenceItem.TextPreference {
val context = LocalContext.current
var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) {
@ -258,7 +255,7 @@ object SettingsMangadexScreen : SearchableSettings {
onDismissRequest = { dialogOpen = false },
onSelectionConfirmed = { items ->
dialogOpen = false
unsortedPreferences.mangadexSyncToLibraryIndexes().set(
sourcePreferences.mangadexSyncToLibraryIndexes().set(
List(items.size) { index -> (index + 1).toString() }.toSet(),
)
LibraryUpdateJob.startNow(

View File

@ -39,45 +39,45 @@ object SettingsReaderScreen : SearchableSettings {
return listOf(
Preference.PreferenceItem.ListPreference(
pref = readerPref.defaultReadingMode(),
title = stringResource(MR.strings.pref_viewer_type),
preference = readerPref.defaultReadingMode(),
entries = ReadingMode.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_type),
),
Preference.PreferenceItem.ListPreference(
pref = readerPref.doubleTapAnimSpeed(),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
preference = readerPref.doubleTapAnimSpeed(),
entries = persistentMapOf(
1 to stringResource(MR.strings.double_tap_anim_speed_0),
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPref.showReadingMode(),
preference = readerPref.showReadingMode(),
title = stringResource(MR.strings.pref_show_reading_mode),
subtitle = stringResource(MR.strings.pref_show_reading_mode_summary),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPref.showNavigationOverlayOnStart(),
preference = readerPref.showNavigationOverlayOnStart(),
title = stringResource(MR.strings.pref_show_navigation_mode),
subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary),
),
// SY -->
Preference.PreferenceItem.SwitchPreference(
pref = readerPref.forceHorizontalSeekbar(),
preference = readerPref.forceHorizontalSeekbar(),
title = stringResource(SYMR.strings.pref_force_horz_seekbar),
subtitle = stringResource(SYMR.strings.pref_force_horz_seekbar_summary),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPref.landscapeVerticalSeekbar(),
preference = readerPref.landscapeVerticalSeekbar(),
title = stringResource(SYMR.strings.pref_show_vert_seekbar_landscape),
subtitle = stringResource(SYMR.strings.pref_show_vert_seekbar_landscape_summary),
enabled = !forceHorizontalSeekbar,
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPref.leftVerticalSeekbar(),
preference = readerPref.leftVerticalSeekbar(),
title = stringResource(SYMR.strings.pref_left_handed_vertical_seekbar),
subtitle = stringResource(SYMR.strings.pref_left_handed_vertical_seekbar_summary),
enabled = !forceHorizontalSeekbar,
@ -85,7 +85,7 @@ object SettingsReaderScreen : SearchableSettings {
// SY <--
/* SY -->
Preference.PreferenceItem.SwitchPreference(
pref = readerPref.pageTransitions(),
preference = readerPref.pageTransitions(),
title = stringResource(MR.strings.pref_page_transitions),
),
SY <-- */
@ -114,39 +114,39 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_display),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.defaultOrientationType(),
title = stringResource(MR.strings.pref_rotation_type),
preference = readerPreferences.defaultOrientationType(),
entries = ReaderOrientation.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(),
title = stringResource(MR.strings.pref_rotation_type),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerTheme(),
title = stringResource(MR.strings.pref_reader_theme),
preference = readerPreferences.readerTheme(),
entries = persistentMapOf(
1 to stringResource(MR.strings.black_background),
2 to stringResource(MR.strings.gray_background),
0 to stringResource(MR.strings.white_background),
3 to stringResource(MR.strings.automatic_background),
),
title = stringResource(MR.strings.pref_reader_theme),
),
Preference.PreferenceItem.SwitchPreference(
pref = fullscreenPref,
preference = fullscreenPref,
title = stringResource(MR.strings.pref_fullscreen),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cutoutShort(),
preference = readerPreferences.cutoutShort(),
title = stringResource(MR.strings.pref_cutout_short),
enabled = fullscreen &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.keepScreenOn(),
preference = readerPreferences.keepScreenOn(),
title = stringResource(MR.strings.pref_keep_screen_on),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.showPageNumber(),
preference = readerPreferences.showPageNumber(),
title = stringResource(MR.strings.pref_show_page_number),
),
),
@ -169,43 +169,41 @@ object SettingsReaderScreen : SearchableSettings {
title = "E-Ink",
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.flashOnPageChange(),
preference = readerPreferences.flashOnPageChange(),
title = stringResource(MR.strings.pref_flash_page),
subtitle = stringResource(MR.strings.pref_flash_page_summ),
),
Preference.PreferenceItem.SliderPreference(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
min = 1,
max = 15,
valueRange = 1..15,
title = stringResource(MR.strings.pref_flash_duration),
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
enabled = flashPageState,
onValueChanged = {
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
true
},
enabled = flashPageState,
),
Preference.PreferenceItem.SliderPreference(
value = flashInterval,
min = 1,
max = 10,
valueRange = 1..10,
title = stringResource(MR.strings.pref_flash_page_interval),
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
enabled = flashPageState,
onValueChanged = {
flashIntervalPref.set(it)
true
},
enabled = flashPageState,
),
Preference.PreferenceItem.ListPreference(
pref = flashColorPref,
title = stringResource(MR.strings.pref_flash_with),
preference = flashColorPref,
entries = persistentMapOf(
ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black),
ReaderPreferences.FlashColor.WHITE to stringResource(MR.strings.pref_flash_style_white),
ReaderPreferences.FlashColor.WHITE_BLACK
to stringResource(MR.strings.pref_flash_style_white_black),
),
title = stringResource(MR.strings.pref_flash_with),
enabled = flashPageState,
),
),
@ -218,26 +216,19 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_reading),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipRead(),
preference = readerPreferences.skipRead(),
title = stringResource(MR.strings.pref_skip_read_chapters),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipFiltered(),
preference = readerPreferences.skipFiltered(),
title = stringResource(MR.strings.pref_skip_filtered_chapters),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipDupe(),
preference = readerPreferences.skipDupe(),
title = stringResource(MR.strings.pref_skip_dupe_chapters),
),
// SY -->
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.markReadDupe(),
title = stringResource(SYMR.strings.pref_mark_read_dupe_chapters),
subtitle = stringResource(SYMR.strings.pref_mark_read_dupe_chapters_summary),
),
// SY <--
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.alwaysShowChapterTransition(),
preference = readerPreferences.alwaysShowChapterTransition(),
title = stringResource(MR.strings.pref_always_show_chapter_transition),
),
),
@ -260,16 +251,15 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pager_viewer),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
preference = navModePref,
entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) }
.toMap()
.toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_nav),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.pagerNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
preference = readerPreferences.pagerNavInverted(),
entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL,
@ -278,46 +268,47 @@ object SettingsReaderScreen : SearchableSettings {
)
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
enabled = navMode != 5,
),
Preference.PreferenceItem.ListPreference(
pref = imageScaleTypePref,
title = stringResource(MR.strings.pref_image_scale_type),
preference = imageScaleTypePref,
entries = ReaderPreferences.ImageScaleType
.mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap()
.toImmutableMap(),
title = stringResource(MR.strings.pref_image_scale_type),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.zoomStart(),
title = stringResource(MR.strings.pref_zoom_start),
preference = readerPreferences.zoomStart(),
entries = ReaderPreferences.ZoomStart
.mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap()
.toImmutableMap(),
title = stringResource(MR.strings.pref_zoom_start),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBorders(),
preference = readerPreferences.cropBorders(),
title = stringResource(MR.strings.pref_crop_borders),
),
// SY -->
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.pageTransitionsPager(),
preference = readerPreferences.pageTransitionsPager(),
title = stringResource(MR.strings.pref_page_transitions),
),
// SY <--
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.landscapeZoom(),
preference = readerPreferences.landscapeZoom(),
title = stringResource(MR.strings.pref_landscape_zoom),
enabled = imageScaleType == 1,
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.navigateToPan(),
preference = readerPreferences.navigateToPan(),
title = stringResource(MR.strings.pref_navigate_pan),
enabled = navMode != 5,
),
Preference.PreferenceItem.SwitchPreference(
pref = dualPageSplitPref,
preference = dualPageSplitPref,
title = stringResource(MR.strings.pref_dual_page_split),
onValueChanged = {
rotateToFitPref.set(false)
@ -325,13 +316,13 @@ object SettingsReaderScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageInvertPaged(),
preference = readerPreferences.dualPageInvertPaged(),
title = stringResource(MR.strings.pref_dual_page_invert),
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
enabled = dualPageSplit,
),
Preference.PreferenceItem.SwitchPreference(
pref = rotateToFitPref,
preference = rotateToFitPref,
title = stringResource(MR.strings.pref_page_rotate),
onValueChanged = {
dualPageSplitPref.set(false)
@ -339,7 +330,7 @@ object SettingsReaderScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageRotateToFitInvert(),
preference = readerPreferences.dualPageRotateToFitInvert(),
title = stringResource(MR.strings.pref_page_rotate_invert),
enabled = rotateToFit,
),
@ -365,16 +356,15 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.webtoon_viewer),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
preference = navModePref,
entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) }
.toMap()
.toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_nav),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.webtoonNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
preference = readerPreferences.webtoonNavInverted(),
entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL,
@ -383,35 +373,37 @@ object SettingsReaderScreen : SearchableSettings {
)
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
enabled = navMode != 5,
),
Preference.PreferenceItem.SliderPreference(
value = webtoonSidePadding,
valueRange = ReaderPreferences.let {
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
},
title = stringResource(MR.strings.pref_webtoon_side_padding),
subtitle = numberFormat.format(webtoonSidePadding / 100f),
min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
onValueChanged = {
webtoonSidePaddingPref.set(it)
true
},
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerHideThreshold(),
title = stringResource(MR.strings.pref_hide_threshold),
preference = readerPreferences.readerHideThreshold(),
entries = persistentMapOf(
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
),
title = stringResource(MR.strings.pref_hide_threshold),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBordersWebtoon(),
preference = readerPreferences.cropBordersWebtoon(),
title = stringResource(MR.strings.pref_crop_borders),
),
Preference.PreferenceItem.SwitchPreference(
pref = dualPageSplitPref,
preference = dualPageSplitPref,
title = stringResource(MR.strings.pref_dual_page_split),
onValueChanged = {
rotateToFitPref.set(false)
@ -419,13 +411,13 @@ object SettingsReaderScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageInvertWebtoon(),
preference = readerPreferences.dualPageInvertWebtoon(),
title = stringResource(MR.strings.pref_dual_page_invert),
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
enabled = dualPageSplit,
),
Preference.PreferenceItem.SwitchPreference(
pref = rotateToFitPref,
preference = rotateToFitPref,
title = stringResource(MR.strings.pref_page_rotate),
onValueChanged = {
dualPageSplitPref.set(false)
@ -433,21 +425,21 @@ object SettingsReaderScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageRotateToFitInvertWebtoon(),
preference = readerPreferences.dualPageRotateToFitInvertWebtoon(),
title = stringResource(MR.strings.pref_page_rotate_invert),
enabled = rotateToFit,
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
preference = readerPreferences.webtoonDoubleTapZoomEnabled(),
title = stringResource(MR.strings.pref_double_tap_zoom),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonDisableZoomOut(),
preference = readerPreferences.webtoonDisableZoomOut(),
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
),
// SY -->
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.pageTransitionsWebtoon(),
preference = readerPreferences.pageTransitionsWebtoon(),
title = stringResource(MR.strings.pref_page_transitions),
),
// SY <--
@ -462,12 +454,12 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.vertical_plus_viewer),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.continuousVerticalTappingByPage(),
preference = readerPreferences.continuousVerticalTappingByPage(),
title = stringResource(SYMR.strings.tap_scroll_page),
subtitle = stringResource(SYMR.strings.tap_scroll_page_summary),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBordersContinuousVertical(),
preference = readerPreferences.cropBordersContinuousVertical(),
title = stringResource(MR.strings.pref_crop_borders),
),
),
@ -483,11 +475,11 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_reader_navigation),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readWithVolumeKeysPref,
preference = readWithVolumeKeysPref,
title = stringResource(MR.strings.pref_read_with_volume_keys),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.readWithVolumeKeysInverted(),
preference = readerPreferences.readWithVolumeKeysInverted(),
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
enabled = readWithVolumeKeys,
),
@ -501,11 +493,11 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_reader_actions),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.readWithLongTap(),
preference = readerPreferences.readWithLongTap(),
title = stringResource(MR.strings.pref_read_with_long_tap),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.folderPerManga(),
preference = readerPreferences.folderPerManga(),
title = stringResource(MR.strings.pref_create_folder_per_manga),
subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary),
),
@ -520,7 +512,7 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(SYMR.strings.page_downloading),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.preloadSize(),
preference = readerPreferences.preloadSize(),
title = stringResource(SYMR.strings.reader_preload_amount),
subtitle = stringResource(SYMR.strings.reader_preload_amount_summary),
entries = persistentMapOf(
@ -535,13 +527,13 @@ object SettingsReaderScreen : SearchableSettings {
),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerThreads(),
preference = readerPreferences.readerThreads(),
title = stringResource(SYMR.strings.download_threads),
subtitle = stringResource(SYMR.strings.download_threads_summary),
entries = List(5) { it }.associateWith { it.toString() }.toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.cacheSize(),
preference = readerPreferences.cacheSize(),
title = stringResource(SYMR.strings.reader_cache_size),
subtitle = stringResource(SYMR.strings.reader_cache_size_summary),
entries = persistentMapOf(
@ -564,7 +556,7 @@ object SettingsReaderScreen : SearchableSettings {
),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.aggressivePageLoading(),
preference = readerPreferences.aggressivePageLoading(),
title = stringResource(SYMR.strings.aggressively_load_pages),
subtitle = stringResource(SYMR.strings.aggressively_load_pages_summary),
),
@ -579,21 +571,21 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(SYMR.strings.pref_category_fork),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.readerInstantRetry(),
preference = readerPreferences.readerInstantRetry(),
title = stringResource(SYMR.strings.skip_queue_on_retry),
subtitle = stringResource(SYMR.strings.skip_queue_on_retry_summary),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.preserveReadingPosition(),
preference = readerPreferences.preserveReadingPosition(),
title = stringResource(SYMR.strings.preserve_reading_position),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.useAutoWebtoon(),
preference = readerPreferences.useAutoWebtoon(),
title = stringResource(SYMR.strings.auto_webtoon_mode),
subtitle = stringResource(SYMR.strings.auto_webtoon_mode_summary),
),
Preference.PreferenceItem.MultiSelectListPreference(
pref = readerPreferences.readerBottomButtons(),
preference = readerPreferences.readerBottomButtons(),
title = stringResource(SYMR.strings.reader_bottom_buttons),
subtitle = stringResource(SYMR.strings.reader_bottom_buttons_summary),
entries = ReaderBottomButton.entries
@ -601,7 +593,7 @@ object SettingsReaderScreen : SearchableSettings {
.toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.pageLayout(),
preference = readerPreferences.pageLayout(),
title = stringResource(SYMR.strings.page_layout),
subtitle = stringResource(SYMR.strings.automatic_can_still_switch),
entries = ReaderPreferences.PageLayouts
@ -610,12 +602,12 @@ object SettingsReaderScreen : SearchableSettings {
.toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.invertDoublePages(),
preference = readerPreferences.invertDoublePages(),
title = stringResource(SYMR.strings.invert_double_pages),
enabled = pageLayout != PagerConfig.PageLayout.SINGLE_PAGE,
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.centerMarginType(),
preference = readerPreferences.centerMarginType(),
title = stringResource(SYMR.strings.center_margin),
subtitle = stringResource(SYMR.strings.pref_center_margin_summary),
entries = ReaderPreferences.CenterMarginTypes
@ -624,7 +616,7 @@ object SettingsReaderScreen : SearchableSettings {
.toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.archiveReaderMode(),
preference = readerPreferences.archiveReaderMode(),
title = stringResource(SYMR.strings.pref_archive_reader_mode),
subtitle = stringResource(SYMR.strings.pref_archive_reader_mode_summary),
entries = ReaderPreferences.archiveModeTypes

View File

@ -96,7 +96,7 @@ object SettingsSecurityScreen : SearchableSettings {
title = stringResource(MR.strings.pref_security),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = useAuthPref,
preference = useAuthPref,
title = stringResource(MR.strings.lock_with_biometrics),
enabled = authSupported,
onValueChanged = {
@ -106,9 +106,7 @@ object SettingsSecurityScreen : SearchableSettings {
},
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.lockAppAfter(),
title = stringResource(MR.strings.lock_when_idle),
enabled = authSupported && useAuth,
preference = securityPreferences.lockAppAfter(),
entries = LockAfterValues
.associateWith {
when (it) {
@ -118,6 +116,8 @@ object SettingsSecurityScreen : SearchableSettings {
}
}
.toImmutableMap(),
title = stringResource(MR.strings.lock_when_idle),
enabled = authSupported && useAuth,
onValueChanged = {
(context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.lock_when_idle),
@ -125,25 +125,25 @@ object SettingsSecurityScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.hideNotificationContent(),
preference = securityPreferences.hideNotificationContent(),
title = stringResource(MR.strings.hide_notification_content),
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.secureScreen(),
title = stringResource(MR.strings.secure_screen),
preference = securityPreferences.secureScreen(),
entries = SecurityPreferences.SecureScreenMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
title = stringResource(MR.strings.secure_screen),
),
// SY -->
Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.passwordProtectDownloads(),
preference = securityPreferences.passwordProtectDownloads(),
title = stringResource(SYMR.strings.password_protect_downloads),
subtitle = stringResource(SYMR.strings.password_protect_downloads_summary),
enabled = isCbzPasswordSet,
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.encryptionType(),
preference = securityPreferences.encryptionType(),
title = stringResource(SYMR.strings.encryption_type),
entries = SecurityPreferences.EncryptionType.entries
.associateWith { stringResource(it.titleRes) }
@ -384,12 +384,12 @@ object SettingsSecurityScreen : SearchableSettings {
title = stringResource(MR.strings.pref_firebase),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.crashlytics(),
preference = privacyPreferences.crashlytics(),
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
),
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.analytics(),
preference = privacyPreferences.analytics(),
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
),

View File

@ -30,8 +30,11 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
@ -59,6 +62,7 @@ import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
@ -125,51 +129,52 @@ object SettingsTrackingScreen : SearchableSettings {
return listOf(
Preference.PreferenceItem.SwitchPreference(
pref = trackPreferences.autoUpdateTrack(),
preference = trackPreferences.autoUpdateTrack(),
title = stringResource(MR.strings.pref_auto_update_manga_sync),
),
Preference.PreferenceItem.ListPreference(
pref = trackPreferences.autoUpdateTrackOnMarkRead(),
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
preference = trackPreferences.autoUpdateTrackOnMarkRead(),
entries = AutoTrackState.entries
.associateWith { stringResource(it.titleRes) }
.toPersistentMap(),
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
),
// SY -->
Preference.PreferenceItem.SwitchPreference(
preference = trackPreferences.resolveUsingSourceMetadata(),
title = stringResource(SYMR.strings.pref_tracker_resolve_using_source_metadata),
subtitle = stringResource(SYMR.strings.pref_tracker_resolve_using_source_metadata_summary),
),
// SY <--
Preference.PreferenceGroup(
title = stringResource(MR.strings.services),
preferenceItems = persistentListOf(
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.myAnimeList.name,
tracker = trackerManager.myAnimeList,
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.myAnimeList) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.aniList.name,
tracker = trackerManager.aniList,
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.aniList) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.kitsu.name,
tracker = trackerManager.kitsu,
login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) },
logout = { dialog = LogoutDialog(trackerManager.kitsu) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.mangaUpdates.name,
tracker = trackerManager.mangaUpdates,
login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) },
logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.shikimori.name,
tracker = trackerManager.shikimori,
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.shikimori) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.bangumi.name,
tracker = trackerManager.bangumi,
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.bangumi) },
@ -183,7 +188,6 @@ object SettingsTrackingScreen : SearchableSettings {
enhancedTrackers.first
.map { service ->
Preference.PreferenceItem.TrackerPreference(
title = service.name,
tracker = service,
login = { (service as EnhancedTracker).loginNoop() },
logout = service::logout,
@ -227,7 +231,9 @@ object SettingsTrackingScreen : SearchableSettings {
text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.semantics { contentType = ContentType.Username + ContentType.EmailAddress },
value = username,
onValueChange = { username = it },
label = { Text(text = stringResource(uNameStringRes)) },
@ -238,7 +244,9 @@ object SettingsTrackingScreen : SearchableSettings {
var hidePassword by remember { mutableStateOf(true) }
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.semantics { contentType = ContentType.Password },
value = password,
onValueChange = { password = it },
label = { Text(text = stringResource(MR.strings.password)) },
@ -287,7 +295,7 @@ object SettingsTrackingScreen : SearchableSettings {
}
},
) {
val id = if (processing) MR.strings.loading else MR.strings.login
val id = if (processing) MR.strings.logging_in else MR.strings.login
Text(text = stringResource(id))
}
},

View File

@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.m3.util.htmlReadyLicenseContent
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen
import tachiyomi.i18n.MR

View File

@ -1,8 +1,10 @@
package eu.kanade.presentation.more.settings.screen.advanced
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
@ -12,6 +14,7 @@ import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -42,16 +45,16 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.core.common.util.lang.toLong
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.data.Database
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
import tachiyomi.domain.source.model.Source
import tachiyomi.domain.source.model.SourceWithCount
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.LazyColumnWithAction
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
@ -73,18 +76,45 @@ class ClearDatabaseScreen : Screen() {
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
is ClearDatabaseScreenModel.State.Ready -> {
if (s.showConfirmation) {
// SY -->
var keepReadManga by remember { mutableStateOf(true) }
// SY <--
AlertDialog(
title = {
Text(text = stringResource(MR.strings.are_you_sure))
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Text(text = stringResource(MR.strings.clear_database_text))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(MR.strings.clear_db_exclude_read),
modifier = Modifier.weight(1f),
)
Switch(
checked = keepReadManga,
onCheckedChange = { keepReadManga = it },
)
}
if (!keepReadManga) {
Text(
text = stringResource(MR.strings.clear_database_history_warning),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
}
}
},
onDismissRequest = model::hideConfirmation,
confirmButton = {
TextButton(
onClick = {
scope.launchUI {
// SY -->
model.removeMangaBySourceId(keepReadManga)
// SY <--
model.clearSelection()
model.hideConfirmation()
context.toast(MR.strings.clear_database_completed)
@ -99,20 +129,6 @@ class ClearDatabaseScreen : Screen() {
Text(text = stringResource(MR.strings.action_cancel))
}
},
text = {
// SY -->
Column {
// SY <--
Text(text = stringResource(MR.strings.clear_database_confirmation))
// SY -->
LabeledCheckbox(
label = stringResource(SYMR.strings.clear_db_exclude_read),
checked = keepReadManga,
onCheckedChange = { keepReadManga = it },
)
}
// SY <--
},
)
}
@ -224,15 +240,9 @@ private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenMod
}
}
suspend fun removeMangaBySourceId(/* SY --> */keepReadManga: Boolean /* SY <-- */) = withNonCancellableContext {
suspend fun removeMangaBySourceId(keepReadManga: Boolean) = withNonCancellableContext {
val state = state.value as? State.Ready ?: return@withNonCancellableContext
// SY -->
if (keepReadManga) {
database.mangasQueries.deleteMangasNotInLibraryAndNotReadBySourceIds(state.selection)
} else {
database.mangasQueries.deleteMangasNotInLibraryBySourceIds(state.selection)
}
// SY <--
database.mangasQueries.deleteNonLibraryManga(state.selection, keepReadManga.toLong())
database.historyQueries.removeResettedHistory()
}

View File

@ -25,7 +25,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -86,7 +85,8 @@ internal fun BasePreferenceWidget(
}
}
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = composed {
@Composable
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier {
var highlightFlag by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (highlighted) {
@ -116,7 +116,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
},
label = "highlight",
)
Modifier.background(color = highlight)
return this.background(color = highlight)
}
internal val TrailingWidgetBuffer = 16.dp

View File

@ -31,6 +31,7 @@ fun EditTextPreferenceWidget(
subtitle: String?,
icon: ImageVector?,
value: String,
widget: @Composable (() -> Unit)? = null,
onConfirm: suspend (String) -> Boolean,
) {
var isDialogShown by remember { mutableStateOf(false) }
@ -39,6 +40,7 @@ fun EditTextPreferenceWidget(
title = title,
subtitle = subtitle?.format(value),
icon = icon,
widget = widget,
onPreferenceClick = { isDialogShown = true },
)

View File

@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -36,12 +37,12 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
if (customBrightness) {
val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState()
SliderItem(
label = stringResource(MR.strings.pref_custom_brightness),
min = -75,
max = 100,
value = customBrightnessValue,
valueText = customBrightnessValue.toString(),
valueRange = -75..100,
steps = 0,
label = stringResource(MR.strings.pref_custom_brightness),
onChange = { screenModel.preferences.customBrightnessValue().set(it) },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
}
@ -53,48 +54,52 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
if (colorFilter) {
val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState()
SliderItem(
label = stringResource(MR.strings.color_filter_r_value),
max = 255,
value = colorFilterValue.red,
valueText = colorFilterValue.red.toString(),
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_r_value),
onChange = { newRValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, RED_MASK, 16)
}
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
SliderItem(
label = stringResource(MR.strings.color_filter_g_value),
max = 255,
value = colorFilterValue.green,
valueText = colorFilterValue.green.toString(),
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_g_value),
onChange = { newGValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newGValue, GREEN_MASK, 8)
}
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
SliderItem(
label = stringResource(MR.strings.color_filter_b_value),
max = 255,
value = colorFilterValue.blue,
valueText = colorFilterValue.blue.toString(),
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_b_value),
onChange = { newBValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newBValue, BLUE_MASK, 0)
}
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
SliderItem(
label = stringResource(MR.strings.color_filter_a_value),
max = 255,
value = colorFilterValue.alpha,
valueText = colorFilterValue.alpha.toString(),
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_a_value),
onChange = { newAValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newAValue, ALPHA_MASK, 24)
}
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState()

View File

@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -119,21 +120,21 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
if (flashPageState) {
SliderItem(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
valueRange = 1..15,
label = stringResource(MR.strings.pref_flash_duration),
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
min = 1,
max = 15,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
SliderItem(
value = flashInterval,
valueRange = 1..10,
label = stringResource(MR.strings.pref_flash_page_interval),
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
onChange = {
flashIntervalPref.set(it)
},
min = 1,
max = 10,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
SettingsChipRow(MR.strings.pref_flash_with) {
flashColors.map { (labelRes, value) ->

View File

@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -192,14 +193,14 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
SliderItem(
label = stringResource(MR.strings.pref_webtoon_side_padding),
min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
value = webtoonSidePadding,
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
label = stringResource(MR.strings.pref_webtoon_side_padding),
valueText = numberFormat.format(webtoonSidePadding / 100f),
onChange = {
screenModel.preferences.webtoonSidePadding().set(it)
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
CheckboxItem(

View File

@ -13,6 +13,7 @@ import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
import eu.kanade.presentation.theme.colorscheme.MonetColorScheme
import eu.kanade.presentation.theme.colorscheme.MonochromeColorScheme
import eu.kanade.presentation.theme.colorscheme.NordColorScheme
import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme
import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme
@ -79,6 +80,7 @@ private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
AppTheme.LAVENDER to LavenderColorScheme,
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
AppTheme.MONOCHROME to MonochromeColorScheme,
AppTheme.NORD to NordColorScheme,
AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme,
AppTheme.TAKO to TakoColorScheme,

View File

@ -0,0 +1,84 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
internal object MonochromeColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFFFFFFF),
onPrimary = Color(0xFF000000),
primaryContainer = Color(0xFFFFFFFF),
onPrimaryContainer = Color(0xFF000000),
secondary = Color(0xFFFFFFFF),
onSecondary = Color(0xFF000000),
secondaryContainer = Color(0xFF777777),
onSecondaryContainer = Color(0xFF000000),
tertiary = Color(0xFF777777),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFFFFFFFF),
onTertiaryContainer = Color(0xFF000000),
error = Color(0xFFFFFFFF),
onError = Color(0xFF000000),
errorContainer = Color(0xFFFFFFFF),
onErrorContainer = Color(0xFF000000),
background = Color(0xFF000000),
onBackground = Color(0xFFFFFFFF),
surface = Color(0xFF000000),
onSurface = Color(0xFFFFFFFF),
surfaceVariant = Color(0xFF000000),
onSurfaceVariant = Color(0xFFFFFFFF),
outline = Color(0xFFFFFFFF),
outlineVariant = Color(0xFFFFFFFF),
scrim = Color(0xFF000000),
inverseSurface = Color(0xFFFFFFFF),
inverseOnSurface = Color(0xFF000000),
inversePrimary = Color(0xFF000000),
surfaceDim = Color(0xFF000000),
surfaceBright = Color(0xFFFFFFFF),
surfaceContainerLowest = Color(0xFF000000),
surfaceContainerLow = Color(0xFF000000),
surfaceContainer = Color(0xFF000000),
surfaceContainerHigh = Color(0xFF000000),
surfaceContainerHighest = Color(0xFF000000),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF000000),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFF000000),
onPrimaryContainer = Color(0xFFFFFFFF),
secondary = Color(0xFF000000),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFF888888),
onSecondaryContainer = Color(0xFFFFFFFF),
tertiary = Color(0xFF888888),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFF000000),
onTertiaryContainer = Color(0xFFFFFFFF),
error = Color(0xFF000000),
onError = Color(0xFFFFFFFF),
errorContainer = Color(0xFF000000),
onErrorContainer = Color(0xFFFFFFFF),
background = Color(0xFFFFFFFF),
onBackground = Color(0xFF000000),
surface = Color(0xFFFFFFFF),
onSurface = Color(0xFF000000),
surfaceVariant = Color(0xFFFFFFFF),
onSurfaceVariant = Color(0xFF000000),
outline = Color(0xFF000000),
outlineVariant = Color(0xFF000000),
scrim = Color(0xFF000000),
inverseSurface = Color(0xFF000000),
inverseOnSurface = Color(0xFFFFFFFF),
inversePrimary = Color(0xFFFFFFFF),
surfaceDim = Color(0xFFFFFFFF),
surfaceBright = Color(0xFFFFFFFF),
surfaceContainerLowest = Color(0xFFFFFFFF),
surfaceContainerLow = Color(0xFFFFFFFF),
surfaceContainer = Color(0xFFFFFFFF),
surfaceContainerHigh = Color(0xFFFFFFFF),
surfaceContainerHighest = Color(0xFFFFFFFF),
)
}

View File

@ -10,10 +10,12 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
@ -22,6 +24,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@ -70,6 +75,7 @@ fun TrackInfoDialogHome(
onOpenInBrowser: (TrackItem) -> Unit,
onRemoved: (TrackItem) -> Unit,
onCopyLink: (TrackItem) -> Unit,
onTogglePrivate: (TrackItem) -> Unit,
) {
Column(
modifier = Modifier
@ -84,6 +90,7 @@ fun TrackInfoDialogHome(
if (item.track != null) {
val supportsScoring = item.tracker.getScoreList().isNotEmpty()
val supportsReadingDates = item.tracker.supportsReadingDates
val supportsPrivate = item.tracker.supportsPrivateTracking
TrackInfoItem(
title = item.track.title,
tracker = item.tracker,
@ -115,6 +122,9 @@ fun TrackInfoDialogHome(
onOpenInBrowser = { onOpenInBrowser(item) },
onRemoved = { onRemoved(item) },
onCopyLink = { onCopyLink(item) },
private = item.track.private,
onTogglePrivate = { onTogglePrivate(item) }
.takeIf { supportsPrivate },
)
} else {
TrackInfoItemEmpty(
@ -144,17 +154,37 @@ private fun TrackInfoItem(
onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit,
onCopyLink: () -> Unit,
private: Boolean,
onTogglePrivate: (() -> Unit)?,
) {
val context = LocalContext.current
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
TrackLogoIcon(
tracker = tracker,
onClick = onOpenInBrowser,
onLongClick = onCopyLink,
)
BadgedBox(
badge = {
if (private) {
Badge(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.absoluteOffset(x = (-5).dp),
) {
Icon(
imageVector = Icons.Filled.VisibilityOff,
contentDescription = stringResource(MR.strings.tracked_privately),
modifier = Modifier.size(14.dp),
)
}
}
},
) {
TrackLogoIcon(
tracker = tracker,
onClick = onOpenInBrowser,
onLongClick = onCopyLink,
)
}
Box(
modifier = Modifier
.height(48.dp)
@ -181,6 +211,8 @@ private fun TrackInfoItem(
onOpenInBrowser = onOpenInBrowser,
onRemoved = onRemoved,
onCopyLink = onCopyLink,
private = private,
onTogglePrivate = onTogglePrivate,
)
}
@ -291,6 +323,8 @@ private fun TrackInfoItemMenu(
onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit,
onCopyLink: () -> Unit,
private: Boolean,
onTogglePrivate: (() -> Unit)?,
) {
var expanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
@ -318,6 +352,25 @@ private fun TrackInfoItemMenu(
expanded = false
},
)
if (onTogglePrivate != null) {
DropdownMenuItem(
text = {
Text(
stringResource(
if (private) {
MR.strings.action_toggle_private_off
} else {
MR.strings.action_toggle_private_on
},
),
)
},
onClick = {
onTogglePrivate()
expanded = false
},
)
}
DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_remove)) },
onClick = {

View File

@ -25,7 +25,9 @@ internal class TrackInfoDialogHomePreviewProvider :
remoteUrl = "https://example.com",
startDate = 0L,
finishDate = 0L,
private = false,
)
private val privateTrack = aTrack.copy(private = true)
private val trackItemWithoutTrack = TrackItem(
track = null,
tracker = DummyTracker(
@ -40,6 +42,13 @@ internal class TrackInfoDialogHomePreviewProvider :
name = "Example Tracker 2",
),
)
private val trackItemWithPrivateTrack = TrackItem(
track = privateTrack,
tracker = DummyTracker(
id = 2L,
name = "Example Tracker 2",
),
)
private val trackersWithAndWithoutTrack = @Composable {
TrackInfoDialogHome(
@ -57,6 +66,7 @@ internal class TrackInfoDialogHomePreviewProvider :
onOpenInBrowser = {},
onRemoved = {},
onCopyLink = {},
onTogglePrivate = {},
)
}
@ -73,6 +83,24 @@ internal class TrackInfoDialogHomePreviewProvider :
onOpenInBrowser = {},
onRemoved = {},
onCopyLink = {},
onTogglePrivate = {},
)
}
private val trackerWithPrivateTracking = @Composable {
TrackInfoDialogHome(
trackItems = listOf(trackItemWithPrivateTrack),
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
onStatusClick = {},
onChapterClick = {},
onScoreClick = {},
onStartDateEdit = {},
onEndDateEdit = {},
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
onCopyLink = {},
onTogglePrivate = {},
)
}
@ -80,5 +108,6 @@ internal class TrackInfoDialogHomePreviewProvider :
get() = sequenceOf(
trackersWithAndWithoutTrack,
noTrackers,
trackerWithPrivateTracking,
)
}

View File

@ -33,6 +33,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem
@ -90,8 +91,9 @@ fun TrackerSearch(
queryResult: Result<List<TrackSearch>>?,
selected: TrackSearch?,
onSelectedChange: (TrackSearch) -> Unit,
onConfirmSelection: () -> Unit,
onConfirmSelection: (private: Boolean) -> Unit,
onDismissRequest: () -> Unit,
supportsPrivateTracking: Boolean,
) {
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
@ -164,15 +166,31 @@ fun TrackerSearch(
enter = fadeIn() + slideInVertically { it / 2 },
exit = slideOutVertically { it / 2 } + fadeOut(),
) {
Button(
onClick = { onConfirmSelection() },
Row(
modifier = Modifier
.padding(12.dp)
.padding(MaterialTheme.padding.small)
.windowInsetsPadding(WindowInsets.navigationBars)
.fillMaxWidth(),
elevation = ButtonDefaults.elevatedButtonElevation(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Text(text = stringResource(MR.strings.action_track))
Button(
onClick = { onConfirmSelection(false) },
modifier = Modifier.weight(1f),
elevation = ButtonDefaults.elevatedButtonElevation(),
) {
Text(text = stringResource(MR.strings.action_track))
}
if (supportsPrivateTracking) {
Button(
onClick = { onConfirmSelection(true) },
elevation = ButtonDefaults.elevatedButtonElevation(),
) {
Icon(
imageVector = Icons.Filled.VisibilityOff,
contentDescription = stringResource(MR.strings.action_toggle_private_on),
)
}
}
}
}
},
@ -286,6 +304,15 @@ private fun SearchResultItem(
}
},
)
if (trackSearch.authors.isNotEmpty() || trackSearch.artists.isNotEmpty()) {
Text(
text = (trackSearch.authors + trackSearch.artists).distinct().joinToString(),
modifier = Modifier.secondaryItemAlpha(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
)
}
if (type.isNotBlank()) {
SearchResultItemDetails(
title = stringResource(MR.strings.track_type),

View File

@ -5,8 +5,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.Locale
import kotlin.random.Random
internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
@ -20,6 +23,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
supportsPrivateTracking = false,
)
}
private val fullPageWithoutSelected = @Composable {
@ -31,6 +35,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
supportsPrivateTracking = false,
)
}
private val loading = @Composable {
@ -42,12 +47,27 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
supportsPrivateTracking = false,
)
}
private val fullPageWithPrivateTracking = @Composable {
val items = someTrackSearches().take(30).toList()
TrackerSearch(
state = TextFieldState(initialText = "search text"),
onDispatchQuery = {},
queryResult = Result.success(items),
selected = items[1],
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
supportsPrivateTracking = true,
)
}
override val values: Sequence<@Composable () -> Unit> = sequenceOf(
fullPageWithSecondSelected,
fullPageWithoutSelected,
loading,
fullPageWithPrivateTracking,
)
private fun someTrackSearches(): Sequence<TrackSearch> = sequence {
@ -56,6 +76,8 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
}
}
private val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
private fun randTrackSearch() = TrackSearch().let {
it.id = Random.nextLong()
it.manga_id = Random.nextLong()
@ -71,11 +93,17 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
it.finished_reading_date = 0L
it.tracking_url = "https://example.com/tracker-example"
it.cover_url = "https://example.com/cover.png"
it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString()
it.start_date = formatter.format(Date.from(Instant.now().minus((1L..365).random(), ChronoUnit.DAYS)))
it.summary = lorem((0..40).random()).joinToString()
it.publishing_status = if (Random.nextBoolean()) "Finished" else ""
it.publishing_type = if (Random.nextBoolean()) "Oneshot" else ""
it.artists = randomNames()
it.authors = randomNames()
it
}
private fun randomNames(): List<String> = (0..(0..3).random()).map { lorem((3..5).random()).joinToString() }
private fun lorem(words: Int): Sequence<String> =
LoremIpsum(words).values
}

View File

@ -60,7 +60,9 @@ fun UpdateScreen(
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
onOpenChapter: (UpdatesItem) -> Unit,
) {
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
BackHandler(enabled = state.selectionMode) {
onSelectAll(false)
}
Scaffold(
topBar = { scrollBehavior ->

View File

@ -1,12 +1,46 @@
package eu.kanade.presentation.util
import android.annotation.SuppressLint
import androidx.activity.BackEventCompat
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.SeekableTransitionState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.rememberTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.ScreenModelStore
import cafe.adriel.voyager.core.screen.Screen
@ -15,18 +49,28 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.ScreenTransitionContent
import eu.kanade.tachiyomi.util.view.getWindowRadius
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import soup.compose.material.motion.animation.materialSharedAxisX
import soup.compose.material.motion.animation.materialSharedAxisXIn
import soup.compose.material.motion.animation.materialSharedAxisXOut
import soup.compose.material.motion.animation.rememberSlideDistance
import tachiyomi.presentation.core.util.PredictiveBack
import kotlin.coroutines.cancellation.CancellationException
import kotlin.math.absoluteValue
/**
* For invoking back press to the parent activity
*/
@SuppressLint("ComposeCompositionLocalUsage")
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
@ -59,39 +103,278 @@ interface AssistContentScreen {
fun onProvideAssistUrl(): String?
}
@OptIn(InternalVoyagerApi::class)
@Composable
fun DefaultNavigatorScreenTransition(
navigator: Navigator,
modifier: Modifier = Modifier,
) {
val slideDistance = rememberSlideDistance()
val screenCandidatesToDispose = rememberSaveable(saver = screenCandidatesToDisposeSaver()) {
mutableStateOf(emptySet())
}
val currentScreens = navigator.items
DisposableEffect(currentScreens) {
onDispose {
val newScreenKeys = navigator.items.map { it.key }
screenCandidatesToDispose.value += currentScreens.filter { it.key !in newScreenKeys }
}
}
val slideDistance = rememberSlideDistance(slideDistance = 30.dp)
ScreenTransition(
navigator = navigator,
transition = {
materialSharedAxisX(
forward = navigator.lastEvent != StackEvent.Pop,
slideDistance = slideDistance,
)
},
modifier = modifier,
enterTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
} else {
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
}
},
exitTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
} else {
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
}
},
popEnterTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
} else {
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
}
},
popExitTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
} else {
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
}
},
content = { screen ->
if (this.transition.targetState == this.transition.currentState) {
LaunchedEffect(Unit) {
val newScreens = navigator.items.map { it.key }
val screensToDispose = screenCandidatesToDispose.value.filterNot { it.key in newScreens }
if (screensToDispose.isNotEmpty()) {
screensToDispose.forEach { navigator.dispose(it) }
navigator.clearEvent()
}
screenCandidatesToDispose.value = emptySet()
}
}
screen.Content()
},
)
}
enum class SwipeEdge {
Unknown,
Left,
Right,
}
private enum class AnimationType {
Pop,
Cancel,
}
@Composable
fun ScreenTransition(
navigator: Navigator,
transition: AnimatedContentTransitionScope<Screen>.() -> ContentTransform,
modifier: Modifier = Modifier,
enterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = { fadeIn() },
exitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = { fadeOut() },
popEnterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = enterTransition,
popExitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = exitTransition,
sizeTransform: (AnimatedContentTransitionScope<Screen>.() -> SizeTransform?)? = null,
flingAnimationSpec: () -> AnimationSpec<Float> = { spring(stiffness = Spring.StiffnessLow) },
content: ScreenTransitionContent = { it.Content() },
) {
AnimatedContent(
targetState = navigator.lastItem,
transitionSpec = transition,
val view = LocalView.current
val viewConfig = LocalViewConfiguration.current
val scope = rememberCoroutineScope()
val state = remember {
ScreenTransitionState(
navigator = navigator,
scope = scope,
flingAnimationSpec = flingAnimationSpec(),
windowCornerRadius = view.getWindowRadius().toFloat(),
)
}
val transitionState = remember { SeekableTransitionState(navigator.lastItem) }
val transition = rememberTransition(transitionState = transitionState)
if (state.isPredictiveBack || state.isAnimating) {
LaunchedEffect(state.progress) {
if (!state.isPredictiveBack) return@LaunchedEffect
val previousEntry = navigator.items.getOrNull(navigator.size - 2)
if (previousEntry != null) {
transitionState.seekTo(fraction = state.progress, targetState = previousEntry)
}
}
} else {
LaunchedEffect(navigator) {
snapshotFlow { navigator.lastItem }
.collect {
state.cancelCancelAnimation()
if (it != transitionState.currentState) {
transitionState.animateTo(it)
} else {
transitionState.snapTo(it)
}
}
}
}
PredictiveBackHandler(enabled = navigator.canPop) { backEvent ->
state.cancelCancelAnimation()
var startOffset: Offset? = null
backEvent
.dropWhile {
if (startOffset == null) startOffset = Offset(it.touchX, it.touchY)
if (state.isAnimating) return@dropWhile true
// Touch slop check
val diff = Offset(it.touchX, it.touchY) - startOffset!!
diff.x.absoluteValue < viewConfig.touchSlop && diff.y.absoluteValue < viewConfig.touchSlop
}
.onCompletion {
if (it == null) {
state.finish()
} else {
state.cancel()
}
}
.collect {
state.setPredictiveBackProgress(
progress = it.progress,
swipeEdge = when (it.swipeEdge) {
BackEventCompat.EDGE_LEFT -> SwipeEdge.Left
BackEventCompat.EDGE_RIGHT -> SwipeEdge.Right
else -> SwipeEdge.Unknown
},
)
}
}
transition.AnimatedContent(
modifier = modifier,
label = "transition",
) { screen ->
navigator.saveableState("transition", screen) {
content(screen)
transitionSpec = {
val pop = navigator.lastEvent == StackEvent.Pop || state.isPredictiveBack
ContentTransform(
targetContentEnter = if (pop) {
popEnterTransition(state.swipeEdge)
} else {
enterTransition(state.swipeEdge)
},
initialContentExit = if (pop) {
popExitTransition(state.swipeEdge)
} else {
exitTransition(state.swipeEdge)
},
targetContentZIndex = if (pop) 0f else 1f,
sizeTransform = sizeTransform?.invoke(this),
)
},
contentKey = { it.key },
) {
navigator.saveableState("transition", it) {
content(it)
}
}
}
@Stable
private class ScreenTransitionState(
private val navigator: Navigator,
private val scope: CoroutineScope,
private val flingAnimationSpec: AnimationSpec<Float>,
windowCornerRadius: Float,
) {
var isPredictiveBack: Boolean by mutableStateOf(false)
private set
var progress: Float by mutableFloatStateOf(0f)
private set
var swipeEdge: SwipeEdge by mutableStateOf(SwipeEdge.Unknown)
private set
private var animationJob: Pair<Job, AnimationType>? by mutableStateOf(null)
val isAnimating: Boolean
get() = animationJob?.first?.isActive == true
val windowCornerShape = RoundedCornerShape(windowCornerRadius)
private fun reset() {
this.isPredictiveBack = false
this.swipeEdge = SwipeEdge.Unknown
this.animationJob = null
}
fun setPredictiveBackProgress(progress: Float, swipeEdge: SwipeEdge) {
this.progress = lerp(0f, 0.65f, PredictiveBack.transform(progress))
this.swipeEdge = swipeEdge
this.isPredictiveBack = true
}
fun finish() {
if (!isPredictiveBack) {
navigator.pop()
return
}
animationJob = scope.launch {
try {
animate(
initialValue = progress,
targetValue = 1f,
animationSpec = flingAnimationSpec,
block = { i, _ -> progress = i },
)
navigator.pop()
} catch (e: CancellationException) {
// Cancelled
progress = 0f
} finally {
reset()
}
} to AnimationType.Pop
}
fun cancel() {
if (!isPredictiveBack) {
return
}
animationJob = scope.launch {
try {
animate(
initialValue = progress,
targetValue = 0f,
animationSpec = flingAnimationSpec,
block = { i, _ -> progress = i },
)
} catch (e: CancellationException) {
// Cancelled
progress = 1f
} finally {
reset()
}
} to AnimationType.Cancel
}
fun cancelCancelAnimation() {
if (animationJob?.second == AnimationType.Cancel) {
animationJob?.first?.cancel()
animationJob = null
}
}
}
private fun screenCandidatesToDisposeSaver(): Saver<MutableState<Set<Screen>>, List<Screen>> {
return Saver(
save = { it.value.toList() },
restore = { mutableStateOf(it.toSet()) },
)
}

View File

@ -44,7 +44,6 @@ import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun WebViewScreenContent(
onNavigateUp: () -> Unit,

View File

@ -277,18 +277,16 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
try {
// Override the value passed as X-Requested-With in WebView requests
val stackTrace = Looper.getMainLooper().thread.stackTrace
val chromiumElement = stackTrace.find {
it.className.equals(
"org.chromium.base.BuildInfo",
ignoreCase = true,
)
}
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME
val isChromiumCall = stackTrace.any { trace ->
trace.className.equals("org.chromium.base.BuildInfo", ignoreCase = true) &&
setOf("getAll", "getPackageName", "<init>").any { trace.methodName.equals(it, ignoreCase = true) }
}
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
} catch (_: Exception) {
}
}
return super.getPackageName()
}
@ -330,7 +328,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
return super.generateFileName(
logLevel,
timestamp,
) + "-${BuildConfig.BUILD_TYPE}.log"
) + "-${BuildConfig.BUILD_TYPE}.txt"
}
}
flattener { timeMillis, level, tag, message ->

View File

@ -80,7 +80,7 @@ class BackupNotifier(private val context: Context) {
addAction(
R.drawable.ic_share_24dp,
context.stringResource(MR.strings.action_share),
NotificationReceiver.shareBackupPendingBroadcast(context, file.uri),
NotificationReceiver.shareBackupPendingActivity(context, file.uri),
)
show(Notifications.ID_BACKUP_COMPLETE)

View File

@ -118,13 +118,15 @@ class MangaBackupCreator(
private fun Manga.toBackupManga(/* SY --> */customMangaInfo: CustomMangaInfo?/* SY <-- */) =
BackupManga(
url = this.url,
title = this.title,
artist = this.artist,
author = this.author,
description = this.description,
genre = this.genre.orEmpty(),
status = this.status.toInt(),
thumbnailUrl = this.thumbnailUrl,
// SY -->
title = this.ogTitle,
artist = this.ogArtist,
author = this.ogAuthor,
description = this.ogDescription,
genre = this.ogGenre.orEmpty(),
status = this.ogStatus.toInt(),
thumbnailUrl = this.ogThumbnailUrl,
// SY <--
favorite = this.favorite,
source = this.source,
dateAdded = this.dateAdded,
@ -135,6 +137,7 @@ private fun Manga.toBackupManga(/* SY --> */customMangaInfo: CustomMangaInfo?/*
lastModifiedAt = this.lastModifiedAt,
favoriteModifiedAt = this.favoriteModifiedAt,
version = this.version,
notes = this.notes,
// SY -->
).also { backupManga ->
customMangaInfo?.let {

View File

@ -8,6 +8,7 @@ import tachiyomi.domain.category.model.Category
class BackupCategory(
@ProtoNumber(1) var name: String,
@ProtoNumber(2) var order: Long = 0,
@ProtoNumber(3) var id: Long = 0,
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
@ProtoNumber(100) var flags: Long = 0,
// SY specific values
@ -24,6 +25,7 @@ class BackupCategory(
val backupCategoryMapper = { category: Category ->
BackupCategory(
id = category.id,
name = category.name,
order = category.order,
flags = category.flags,

View File

@ -38,8 +38,10 @@ data class BackupManga(
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
@ProtoNumber(106) var lastModifiedAt: Long = 0,
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
// Mihon values start here
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
@ProtoNumber(109) var version: Long = 0,
@ProtoNumber(110) var notes: String = "",
// SY specific values
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
@ -77,6 +79,7 @@ data class BackupManga(
lastModifiedAt = this@BackupManga.lastModifiedAt,
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
version = this@BackupManga.version,
notes = this@BackupManga.notes,
)
}
}

View File

@ -25,6 +25,7 @@ data class BackupTracking(
@ProtoNumber(10) var startedReadingDate: Long = 0,
// finishedReadingDate is called endReadTime in 1.x
@ProtoNumber(11) var finishedReadingDate: Long = 0,
@ProtoNumber(12) var private: Boolean = false,
@ProtoNumber(100) var mediaId: Long = 0,
) {
@ -48,6 +49,7 @@ data class BackupTracking(
startDate = this@BackupTracking.startedReadingDate,
finishDate = this@BackupTracking.finishedReadingDate,
remoteUrl = this@BackupTracking.trackingUrl,
private = this@BackupTracking.private,
)
}
}
@ -66,6 +68,7 @@ val backupTrackMapper = {
remoteUrl: String,
startDate: Long,
finishDate: Long,
private: Boolean,
->
BackupTracking(
syncId = syncId.toInt(),
@ -80,5 +83,6 @@ val backupTrackMapper = {
startedReadingDate = startDate,
finishedReadingDate = finishDate,
trackingUrl = remoteUrl,
private = private,
)
}

View File

@ -108,7 +108,7 @@ class BackupRestorer(
}
// SY <--
if (options.appSettings) {
restoreAppPreferences(backup.backupPreferences)
restoreAppPreferences(backup.backupPreferences, backup.backupCategories.takeIf { options.categories })
}
if (options.sourceSettings) {
restoreSourcePreferences(backup.backupSourcePreferences)
@ -173,9 +173,15 @@ class BackupRestorer(
}
}
private fun CoroutineScope.restoreAppPreferences(preferences: List<BackupPreference>) = launch {
private fun CoroutineScope.restoreAppPreferences(
preferences: List<BackupPreference>,
categories: List<BackupCategory>?,
) = launch {
ensureActive()
preferenceRestorer.restoreApp(preferences)
preferenceRestorer.restoreApp(
preferences,
categories,
)
restoreProgress += 1
notifier.showRestoreProgress(

View File

@ -139,13 +139,15 @@ class MangaRestorer(
mangasQueries.update(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre?.joinToString(separator = ", "),
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
// SY -->
artist = manga.ogArtist,
author = manga.ogAuthor,
description = manga.ogDescription,
genre = manga.ogGenre?.joinToString(separator = ", "),
title = manga.ogTitle,
status = manga.ogStatus,
thumbnailUrl = manga.ogThumbnailUrl,
// SY <--
favorite = manga.favorite,
lastUpdate = manga.lastUpdate,
nextUpdate = null,
@ -159,6 +161,7 @@ class MangaRestorer(
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
version = manga.version,
isSyncing = 1,
notes = manga.notes,
)
}
return manga
@ -274,13 +277,15 @@ class MangaRestorer(
mangasQueries.insert(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre,
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
// SY -->
artist = manga.ogArtist,
author = manga.ogAuthor,
description = manga.ogDescription,
genre = manga.ogGenre,
title = manga.ogTitle,
status = manga.ogStatus,
thumbnailUrl = manga.ogThumbnailUrl,
// SY <--
favorite = manga.favorite,
lastUpdate = manga.lastUpdate,
nextUpdate = 0L,
@ -292,6 +297,7 @@ class MangaRestorer(
dateAdded = manga.dateAdded,
updateStrategy = manga.updateStrategy,
version = manga.version,
notes = manga.notes,
)
mangasQueries.selectLastInsertedRowId()
}
@ -447,6 +453,7 @@ class MangaRestorer(
track.remoteUrl,
track.startDate,
track.finishDate,
track.private,
track.id,
)
}
@ -455,6 +462,7 @@ class MangaRestorer(
}
// SY -->
/**
* Restore the categories from Json
*

View File

@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers
import android.content.Context
import android.util.Log
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
@ -14,66 +16,122 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.source.sourcePreferences
import tachiyomi.core.common.preference.AndroidPreferenceStore
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.plusAssign
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class PreferenceRestorer(
private val context: Context,
private val getCategories: GetCategories = Injekt.get(),
private val preferenceStore: PreferenceStore = Injekt.get(),
) {
fun restoreApp(preferences: List<BackupPreference>) {
restorePreferences(preferences, preferenceStore)
suspend fun restoreApp(
preferences: List<BackupPreference>,
backupCategories: List<BackupCategory>?,
) {
restorePreferences(
preferences,
preferenceStore,
backupCategories,
)
LibraryUpdateJob.setupTask(context)
BackupCreateJob.setupTask(context)
}
fun restoreSource(preferences: List<BackupSourcePreferences>) {
suspend fun restoreSource(preferences: List<BackupSourcePreferences>) {
preferences.forEach {
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
restorePreferences(it.prefs, sourcePrefs)
}
}
private fun restorePreferences(
private suspend fun restorePreferences(
toRestore: List<BackupPreference>,
preferenceStore: PreferenceStore,
backupCategories: List<BackupCategory>? = null,
) {
val allCategories = if (backupCategories != null) getCategories.await() else emptyList()
val categoriesByName = allCategories.associateBy { it.name }
val backupCategoriesById = backupCategories?.associateBy { it.id.toString() }.orEmpty()
val prefs = preferenceStore.getAll()
toRestore.forEach { (key, value) ->
when (value) {
is IntPreferenceValue -> {
if (prefs[key] is Int?) {
preferenceStore.getInt(key).set(value.value)
}
}
is LongPreferenceValue -> {
if (prefs[key] is Long?) {
preferenceStore.getLong(key).set(value.value)
}
}
is FloatPreferenceValue -> {
if (prefs[key] is Float?) {
preferenceStore.getFloat(key).set(value.value)
}
}
is StringPreferenceValue -> {
if (prefs[key] is String?) {
preferenceStore.getString(key).set(value.value)
}
}
is BooleanPreferenceValue -> {
if (prefs[key] is Boolean?) {
preferenceStore.getBoolean(key).set(value.value)
}
}
is StringSetPreferenceValue -> {
if (prefs[key] is Set<*>?) {
preferenceStore.getStringSet(key).set(value.value)
try {
when (value) {
is IntPreferenceValue -> {
if (prefs[key] is Int?) {
val newValue = if (key == LibraryPreferences.DEFAULT_CATEGORY_PREF_KEY) {
backupCategoriesById[value.value.toString()]
?.let { categoriesByName[it.name]?.id?.toInt() }
} else {
value.value
}
newValue?.let { preferenceStore.getInt(key).set(it) }
}
}
is LongPreferenceValue -> {
if (prefs[key] is Long?) {
preferenceStore.getLong(key).set(value.value)
}
}
is FloatPreferenceValue -> {
if (prefs[key] is Float?) {
preferenceStore.getFloat(key).set(value.value)
}
}
is StringPreferenceValue -> {
if (prefs[key] is String?) {
preferenceStore.getString(key).set(value.value)
}
}
is BooleanPreferenceValue -> {
if (prefs[key] is Boolean?) {
preferenceStore.getBoolean(key).set(value.value)
}
}
is StringSetPreferenceValue -> {
if (prefs[key] is Set<*>?) {
val restored = restoreCategoriesPreference(
key,
value.value,
preferenceStore,
backupCategoriesById,
categoriesByName,
)
if (!restored) preferenceStore.getStringSet(key).set(value.value)
}
}
}
} catch (e: Exception) {
Log.e("PreferenceRestorer", "Failed to restore preference <$key>", e)
}
}
}
private fun restoreCategoriesPreference(
key: String,
value: Set<String>,
preferenceStore: PreferenceStore,
backupCategoriesById: Map<String, BackupCategory>,
categoriesByName: Map<String, Category>,
): Boolean {
val categoryPreferences = LibraryPreferences.categoryPreferenceKeys + DownloadPreferences.categoryPreferenceKeys
if (key !in categoryPreferences) return false
val ids = value.mapNotNull {
backupCategoriesById[it]?.name?.let { name ->
categoriesByName[name]?.id?.toString()
}
}
if (ids.isNotEmpty()) {
preferenceStore.getStringSet(key) += ids
}
return true
}
}

View File

@ -27,6 +27,9 @@ interface Chapter : SChapter, Serializable {
var version: Long
}
val Chapter.isRecognizedNumber: Boolean
get() = chapter_number >= 0f
fun Chapter.toDomainChapter(): DomainChapter? {
if (id == null || manga_id == null) return null
return DomainChapter(

View File

@ -32,12 +32,15 @@ interface Track : Serializable {
var tracking_url: String
fun copyPersonalFrom(other: Track) {
var private: Boolean
fun copyPersonalFrom(other: Track, copyRemotePrivate: Boolean = true) {
last_chapter_read = other.last_chapter_read
score = other.score
status = other.status
started_reading_date = other.started_reading_date
finished_reading_date = other.finished_reading_date
if (copyRemotePrivate) private = other.private
}
companion object {

View File

@ -29,4 +29,6 @@ class TrackImpl : Track {
override var finished_reading_date: Long = 0
override var tracking_url: String = ""
override var private: Boolean = false
}

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.download
import android.app.Application
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
@ -307,6 +307,41 @@ class DownloadCache(
notifyChanges()
}
/**
* Renames a manga in this cache.
*
* @param manga the manga being renamed.
* @param mangaUniFile the manga's new directory.
* @param newTitle the manga's new title.
*/
suspend fun renameManga(manga: Manga, mangaUniFile: UniFile, newTitle: String) {
rootDownloadsDirMutex.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val oldMangaDirName = provider.getMangaDirName(/* SY --> */ manga.ogTitle /* SY <-- */)
var oldChapterDirs: MutableSet<String>? = null
// Save the old name's cached chapter dirs
if (sourceDir.mangaDirs.containsKey(oldMangaDirName)) {
oldChapterDirs = sourceDir.mangaDirs[oldMangaDirName]?.chapterDirs
sourceDir.mangaDirs -= oldMangaDirName
}
// Retrieve/create the cached manga directory for new name
val newMangaDirName = provider.getMangaDirName(newTitle)
var mangaDir = sourceDir.mangaDirs[newMangaDirName]
if (mangaDir == null) {
mangaDir = MangaDirectory(mangaUniFile)
sourceDir.mangaDirs += newMangaDirName to mangaDir
}
// Add the old chapters to new name's cache
if (!oldChapterDirs.isNullOrEmpty()) {
mangaDir.chapterDirs += oldChapterDirs
}
}
notifyChanges()
}
suspend fun removeSource(source: Source) {
rootDownloadsDirMutex.withLock {
rootDownloadsDir.sourceDirs -= source.id
@ -483,7 +518,7 @@ private object UniFileAsStringSerializer : KSerializer<UniFile?> {
override fun deserialize(decoder: Decoder): UniFile? {
return if (decoder.decodeNotNullMark()) {
UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString()))
UniFile.fromUri(Injekt.get<Application>(), decoder.decodeString().toUri())
} else {
decoder.decodeNull()
}

View File

@ -179,7 +179,7 @@ class DownloadManager(
return files.sortedBy { it.name }
.mapIndexed { i, file ->
Page(i, uri = file.uri).apply { status = Page.State.READY }
Page(i, uri = file.uri).apply { status = Page.State.Ready }
}
}
@ -291,6 +291,7 @@ class DownloadManager(
}
// SY -->
/**
* return the list of all manga folders
*/
@ -316,10 +317,13 @@ class DownloadManager(
if (removeNonFavorite && !manga.favorite) {
val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source)
cleaned += 1 + mangaFolder.listFiles().orEmpty().size
mangaFolder.delete()
cache.removeManga(manga)
return cleaned
.getOrNull()
if (mangaFolder != null) {
cleaned += 1 + mangaFolder.listFiles().orEmpty().size
mangaFolder.delete()
cache.removeManga(manga)
return cleaned
}
}
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
@ -336,8 +340,8 @@ class DownloadManager(
}
if (cache.getDownloadCount(manga) == 0) {
val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source)
if (!mangaFolder.listFiles().isNullOrEmpty()) {
val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source).getOrNull()
if (mangaFolder != null && !mangaFolder.listFiles().isNullOrEmpty()) {
mangaFolder.delete()
cache.removeManga(manga)
} else {
@ -395,6 +399,38 @@ class DownloadManager(
}
}
/**
* Renames manga download folder
*
* @param manga the manga
* @param newTitle the new manga title.
*/
suspend fun renameManga(manga: Manga, newTitle: String) {
val source = sourceManager.getOrStub(manga.source)
val oldFolder = provider.findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return
val newName = provider.getMangaDirName(newTitle)
if (oldFolder.name == newName) return
// just to be safe, don't allow downloads for this manga while renaming it
downloader.removeFromQueue(manga)
val capitalizationChanged = oldFolder.name.equals(newName, ignoreCase = true)
if (capitalizationChanged) {
val tempName = newName + Downloader.TMP_DIR_SUFFIX
if (!oldFolder.renameTo(tempName)) {
logcat(LogPriority.ERROR) { "Failed to rename manga download folder: ${oldFolder.name}" }
return
}
}
if (oldFolder.renameTo(newName)) {
cache.renameManga(manga, oldFolder, newTitle)
} else {
logcat(LogPriority.ERROR) { "Failed to rename manga download folder: ${oldFolder.name}" }
}
}
/**
* Renames an already downloaded chapter
*
@ -405,7 +441,10 @@ class DownloadManager(
*/
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator)
val mangaDir = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source)
val mangaDir = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source).getOrElse { e ->
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
return
}
// Assume there's only 1 version of the chapter name formats present
val oldDownload = oldNames.asSequence()

View File

@ -14,6 +14,7 @@ import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
/**
* This class is used to provide the directories where the downloads should be saved.
@ -35,20 +36,36 @@ class DownloadProvider(
* @param mangaTitle the title of the manga to query.
* @param source the source of the manga.
*/
internal fun getMangaDir(mangaTitle: String, source: Source): UniFile {
try {
return downloadsDir!!
.createDirectory(getSourceDirName(source))!!
.createDirectory(getMangaDirName(mangaTitle))!!
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Invalid download directory" }
throw Exception(
context.stringResource(
MR.strings.invalid_location,
downloadsDir?.displayablePath ?: "",
),
internal fun getMangaDir(mangaTitle: String, source: Source): Result<UniFile> {
val downloadsDir = downloadsDir
if (downloadsDir == null) {
logcat(LogPriority.ERROR) { "Failed to create download directory" }
return Result.failure(
IOException(context.stringResource(MR.strings.storage_failed_to_create_download_directory)),
)
}
val sourceDirName = getSourceDirName(source)
val sourceDir = downloadsDir.createDirectory(sourceDirName)
if (sourceDir == null) {
val displayablePath = downloadsDir.displayablePath + "/$sourceDirName"
logcat(LogPriority.ERROR) { "Failed to create source download directory: $displayablePath" }
return Result.failure(
IOException(context.stringResource(MR.strings.storage_failed_to_create_directory, displayablePath)),
)
}
val mangaDirName = getMangaDirName(mangaTitle)
val mangaDir = sourceDir.createDirectory(mangaDirName)
if (mangaDir == null) {
val displayablePath = sourceDir.displayablePath + "/$mangaDirName"
logcat(LogPriority.ERROR) { "Failed to create manga download directory: $displayablePath" }
return Result.failure(
IOException(context.stringResource(MR.strings.storage_failed_to_create_directory, displayablePath)),
)
}
return Result.success(mangaDir)
}
/**
@ -103,6 +120,7 @@ class DownloadProvider(
}
// SY -->
/**
* Returns a list of all files in manga directory
*

View File

@ -327,7 +327,11 @@ class Downloader(
* @param download the chapter to be downloaded.
*/
private suspend fun downloadChapter(download: Download) {
val mangaDir = provider.getMangaDir(/* SY --> */ download.manga.ogTitle /* SY <-- */, download.source)
val mangaDir = provider.getMangaDir(/* SY --> */ download.manga.ogTitle /* SY <-- */, download.source).getOrElse { e ->
download.status = Download.State.ERROR
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
return
}
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
@ -379,11 +383,11 @@ class Downloader(
flow {
// Fetch image URL if necessary
if (page.imageUrl.isNullOrEmpty()) {
page.status = Page.State.LOAD_PAGE
page.status = Page.State.LoadPage
try {
page.imageUrl = download.source.getImageUrl(page)
} catch (e: Throwable) {
page.status = Page.State.ERROR
page.status = Page.State.Error(e)
}
}
@ -469,12 +473,12 @@ class Downloader(
page.uri = file.uri
page.progress = 100
page.status = Page.State.READY
page.status = Page.State.Ready
} catch (e: Throwable) {
if (e is CancellationException) throw e
// Mark this page as error and allow to download the remaining
page.progress = 0
page.status = Page.State.ERROR
page.status = Page.State.Error(e)
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
}
}
@ -494,7 +498,7 @@ class Downloader(
filename: String,
dataSaver: DataSaver,
): UniFile {
page.status = Page.State.DOWNLOAD_IMAGE
page.status = Page.State.DownloadImage
page.progress = 0
return flow {
val response = source.getImage(page, dataSaver)

View File

@ -29,7 +29,7 @@ data class Download(
get() = pages?.sumOf(Page::progress) ?: 0
val downloadedImages: Int
get() = pages?.count { it.status == Page.State.READY } ?: 0
get() = pages?.count { it.status == Page.State.Ready } ?: 0
@Transient
private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED)

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.data.export
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import tachiyomi.domain.manga.model.Manga
object LibraryExporter {
data class ExportOptions(
val includeTitle: Boolean,
val includeAuthor: Boolean,
val includeArtist: Boolean,
)
suspend fun exportToCsv(
context: Context,
uri: Uri,
favorites: List<Manga>,
options: ExportOptions,
onExportComplete: () -> Unit,
) {
withContext(Dispatchers.IO) {
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
val csvData = generateCsvData(favorites, options)
outputStream.write(csvData.toByteArray())
}
onExportComplete()
}
}
private val escapeRequired = listOf("\r", "\n", "\"", ",")
private fun generateCsvData(favorites: List<Manga>, options: ExportOptions): String {
val columnSize = listOf(
options.includeTitle,
options.includeAuthor,
options.includeArtist,
)
.count { it }
val rows = buildList(favorites.size) {
favorites.forEach { manga ->
buildList(columnSize) {
if (options.includeTitle) add(manga.title)
if (options.includeAuthor) add(manga.author)
if (options.includeArtist) add(manga.artist)
}
.let(::add)
}
}
return rows.joinToString("\r\n") { columns ->
columns.joinToString(",") columns@{ column ->
if (column.isNullOrBlank()) return@columns ""
if (escapeRequired.any { column.contains(it) }) {
column.replace("\"", "\"\"").let { "\"$it\"" }
} else {
column
}
}
}
}
}

View File

@ -23,6 +23,7 @@ import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.copyFrom
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
@ -64,7 +65,6 @@ import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.model.Chapter
@ -101,9 +101,12 @@ import java.time.Instant
import java.time.ZonedDateTime
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.incrementAndFetch
@OptIn(ExperimentalAtomicApi::class)
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
@ -343,7 +346,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
*/
private suspend fun updateChapterList() {
val semaphore = Semaphore(5)
val progressCount = AtomicInteger(0)
val progressCount = AtomicInt(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
@ -401,36 +404,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
) {
try {
val newChapters = updateManga(manga, fetchWindow)
// SY -->
.sortedByDescending { it.sourceOrder }.run {
if (libraryPreferences.libraryReadDuplicateChapters().get()) {
val readChapters = getChaptersByMangaId.await(manga.id).filter {
it.read
}
val newReadChapters = this.filter { chapter ->
chapter.chapterNumber >= 0 &&
readChapters.any {
it.chapterNumber == chapter.chapterNumber
}
}
if (newReadChapters.isNotEmpty()) {
setReadStatus.await(true, *newReadChapters.toTypedArray())
}
this.filterNot { newReadChapters.contains(it) }
} else {
this
}
}
// SY <--
.sortedByDescending { it.sourceOrder }
if (newChapters.isNotEmpty()) {
val chaptersToDownload = filterChaptersForDownload.await(manga, newChapters)
if (chaptersToDownload.isNotEmpty()) {
downloadChapters(manga, chaptersToDownload)
hasDownloads.set(true)
hasDownloads.store(true)
}
libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size }
@ -463,7 +444,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (newUpdates.isNotEmpty()) {
notifier.showUpdateNotifications(newUpdates)
if (hasDownloads.get()) {
if (hasDownloads.load()) {
downloadManager.startDownloads()
}
}
@ -529,7 +510,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateCovers() {
val semaphore = Semaphore(5)
val progressCount = AtomicInteger(0)
val progressCount = AtomicInt(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
coroutineScope {
@ -577,11 +558,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
// SY -->
/**
* filter all follows from Mangadex and only add reading or rereading manga to library
*/
private suspend fun syncFollows() = coroutineScope {
val preferences = Injekt.get<UnsortedPreferences>()
val preferences = Injekt.get<SourcePreferences>()
var count = 0
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager = sourceManager)
?: return@coroutineScope
@ -604,7 +586,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
var dbManga = getManga.await(networkManga.url, mangaDex.id)
if (dbManga == null) {
dbManga = networkToLocalManga.await(
dbManga = networkToLocalManga(
Manga.create().copy(
url = networkManga.url,
ogTitle = networkManga.title,
@ -663,7 +645,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger,
completed: AtomicInt,
manga: Manga,
block: suspend () -> Unit,
) = coroutineScope {
@ -672,7 +654,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
updatingManga.add(manga)
notifier.showProgressNotification(
updatingManga,
completed.get(),
completed.load(),
mangaToUpdate.size,
)
@ -681,10 +663,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
ensureActive()
updatingManga.remove(manga)
completed.getAndIncrement()
completed.incrementAndFetch()
notifier.showProgressNotification(
updatingManga,
completed.get(),
completed.load(),
mangaToUpdate.size,
)
}
@ -753,6 +735,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private const val KEY_TARGET = "target"
// SY -->
/**
* Key for group to update.
*/

View File

@ -37,8 +37,11 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.fetchAndIncrement
@OptIn(ExperimentalAtomicApi::class)
class MetadataUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
@ -97,7 +100,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
private suspend fun updateMetadata() {
val semaphore = Semaphore(5)
val progressCount = AtomicInteger(0)
val progressCount = AtomicInt(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
coroutineScope {
@ -142,7 +145,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger,
completed: AtomicInt,
manga: Manga,
block: suspend () -> Unit,
) = coroutineScope {
@ -151,7 +154,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
updatingManga.add(manga)
notifier.showProgressNotification(
updatingManga,
completed.get(),
completed.load(),
mangaToUpdate.size,
)
@ -160,10 +163,10 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
ensureActive()
updatingManga.remove(manga)
completed.getAndIncrement()
completed.fetchAndIncrement()
notifier.showProgressNotification(
updatingManga,
completed.get(),
completed.load(),
mangaToUpdate.size,
)
}

View File

@ -602,18 +602,17 @@ class NotificationReceiver : BroadcastReceiver() {
}
/**
* Returns [PendingIntent] that starts a share activity for a backup file.
* Returns [PendingIntent] that directly launches a share activity for a backup file.
*
* @param context context of application
* @param uri uri of backup file
* @return [PendingIntent]
*/
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_SHARE_BACKUP
putExtra(EXTRA_URI, uri)
internal fun shareBackupPendingActivity(context: Context, uri: Uri): PendingIntent {
val intent = uri.toShareIntent(context, "application/x-protobuf+gzip").apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
return PendingIntent.getBroadcast(
return PendingIntent.getActivity(
context,
0,
intent,

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