Compare commits

...

88 Commits

Author SHA1 Message Date
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
Jobobby04 c95d7fe30f Re-add Android SDK 2025-01-22 13:31:24 -05:00
213 changed files with 5330 additions and 1614 deletions
+27 -3
View File
@@ -1,12 +1,32 @@
[*.{kt,kts}] root = true
max_line_length = 120
indent_size = 4 [*]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true
[*.xml]
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 = true
ij_kotlin_allow_trailing_comma_on_call_site = 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 = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 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_filename = disabled
ktlint_standard_argument-list-wrapping = disabled ktlint_standard_argument-list-wrapping = disabled
ktlint_standard_function-naming = disabled ktlint_standard_function-naming = disabled
@@ -14,3 +34,7 @@ ktlint_standard_property-naming = disabled
ktlint_standard_multiline-expression-wrapping = disabled ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_string-template-indent = disabled ktlint_standard_string-template-indent = disabled
ktlint_standard_comment-wrapping = 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
+3
View File
@@ -1,5 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: ❌ Help with Extensions
url: https://mihon.app/docs/faq/browse/extensions
about: For extension-related questions/issues
- name: 🖥️ Mihon website - name: 🖥️ Mihon website
url: https://mihon.app/ url: https://mihon.app/
about: Guides, troubleshooting, and answers to common questions about: Guides, troubleshooting, and answers to common questions
+6 -6
View File
@@ -43,9 +43,9 @@ body:
attributes: attributes:
label: Crash logs label: Crash logs
description: | 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: | 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 - type: input
id: tachiyomisy-version id: tachiyomisy-version
@@ -53,7 +53,7 @@ body:
label: TachiyomiSY version label: TachiyomiSY version
description: You can find your TachiyomiSY version in **More → About**. description: You can find your TachiyomiSY version in **More → About**.
placeholder: | placeholder: |
Example: "1.11.0" Example: "1.12.0"
validations: validations:
required: true required: true
@@ -96,9 +96,9 @@ body:
required: true required: true
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/). - label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true 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 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 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 required: true
+1 -1
View File
@@ -31,7 +31,7 @@ body:
required: true required: true
- label: I have written a short but informative title. - label: I have written a short but informative title.
required: true 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 required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true
+6
View File
@@ -17,6 +17,10 @@ jobs:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Android SDK
run: |
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
@@ -59,6 +63,8 @@ jobs:
alias: ${{ secrets.ALIAS }} alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: '35.0.1'
- name: Clean up build artifacts - name: Clean up build artifacts
run: | run: |
+14 -5
View File
@@ -1,10 +1,10 @@
name: Remote Dispatch Action Initiator name: Remote Dispatch Action Initiator
on: on:
push: push:
branches: branches:
- 'preview' - 'preview'
jobs: jobs:
trigger_preview_build: trigger_preview_build:
name: Trigger preview build name: Trigger preview build
@@ -14,8 +14,14 @@ jobs:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Validate Gradle Wrapper - name: Set up JDK
uses: gradle/actions/wrapper-validation@v4 uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
- name: Set up gradle
uses: gradle/actions/setup-gradle@v4
- name: Create Tag - name: Create Tag
run: | run: |
@@ -28,3 +34,6 @@ jobs:
-H 'Accept: application/vnd.github.everest-preview+json' \ -H 'Accept: application/vnd.github.everest-preview+json' \
-u ${{ secrets.ACCESS_TOKEN }} \ -u ${{ secrets.ACCESS_TOKEN }} \
--data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}' --data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}'
- name: Run unit tests
run: ./gradlew testDebugUnitTest testDevDebugUnitTest
-19
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'
+6 -2
View File
@@ -31,8 +31,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi.sy" applicationId = "eu.kanade.tachiyomi.sy"
versionCode = 71 versionCode = 73
versionName = "1.11.0" versionName = "1.12.0"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -262,6 +262,7 @@ dependencies {
implementation(libs.swipe) implementation(libs.swipe)
implementation(libs.compose.webview) implementation(libs.compose.webview)
implementation(libs.compose.grid) implementation(libs.compose.grid)
implementation(libs.reorderable)
// Logging // Logging
implementation(libs.logcat) implementation(libs.logcat)
@@ -306,6 +307,9 @@ dependencies {
// Koin // Koin
implementation(sylibs.koin.core) implementation(sylibs.koin.core)
implementation(sylibs.koin.android) implementation(sylibs.koin.android)
// ZXing Android Embedded
implementation(sylibs.zxing.android.embedded)
} }
androidComponents { androidComponents {
+5 -1
View File
@@ -359,7 +359,7 @@
<data android:scheme="https" /> <data android:scheme="https" />
<data android:scheme="http" /> <data android:scheme="http" />
<data android:host="pururin.io" /> <data android:host="pururin.me" />
<data android:pathPattern="/gallery/..*" /> <data android:pathPattern="/gallery/..*" />
</intent-filter> </intent-filter>
@@ -413,6 +413,10 @@
android:scheme="tachiyomisy" /> android:scheme="tachiyomisy" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
tools:remove="screenOrientation" />
</application> </application>
<uses-sdk tools:overrideLibrary="rikka.shizuku.api" <uses-sdk tools:overrideLibrary="rikka.shizuku.api"
@@ -110,7 +110,7 @@ class DomainModule : InjektModule {
addFactory { RenameCategory(get()) } addFactory { RenameCategory(get()) }
addFactory { ReorderCategory(get()) } addFactory { ReorderCategory(get()) }
addFactory { UpdateCategory(get()) } addFactory { UpdateCategory(get()) }
addFactory { DeleteCategory(get()) } addFactory { DeleteCategory(get(), get(), get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) } addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetDuplicateLibraryManga(get()) } addFactory { GetDuplicateLibraryManga(get()) }
@@ -152,7 +152,7 @@ class DomainModule : InjektModule {
addFactory { UpdateChapter(get()) } addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get(), get()) } addFactory { SetReadStatus(get(), get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() } 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 { GetAvailableScanlators(get()) }
addFactory { FilterChaptersForDownload(get(), get(), get(), get()) } addFactory { FilterChaptersForDownload(get(), get(), get(), get()) }
@@ -20,6 +20,7 @@ import tachiyomi.domain.chapter.model.NoChaptersException
import tachiyomi.domain.chapter.model.toChapterUpdate import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.chapter.repository.ChapterRepository import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.chapter.service.ChapterRecognition import tachiyomi.domain.chapter.service.ChapterRecognition
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import java.lang.Long.max import java.lang.Long.max
@@ -35,6 +36,7 @@ class SyncChaptersWithSource(
private val updateChapter: UpdateChapter, private val updateChapter: UpdateChapter,
private val getChaptersByMangaId: GetChaptersByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators, private val getExcludedScanlators: GetExcludedScanlators,
private val libraryPreferences: LibraryPreferences,
) { ) {
/** /**
@@ -150,12 +152,18 @@ class SyncChaptersWithSource(
return emptyList() return emptyList()
} }
val reAdded = mutableListOf<Chapter>() val changedOrDuplicateReadUrls = mutableSetOf<String>()
val deletedChapterNumbers = TreeSet<Double>() val deletedChapterNumbers = TreeSet<Double>()
val deletedReadChapterNumbers = TreeSet<Double>() val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>() val deletedBookmarkedChapterNumbers = TreeSet<Double>()
val readChapterNumbers = dbChapters
.asSequence()
.filter { it.read && it.isRecognizedNumber }
.map { it.chapterNumber }
.toSet()
removedChapters.forEach { chapter -> removedChapters.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber) if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber) if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
@@ -165,12 +173,20 @@ class SyncChaptersWithSource(
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch } val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to 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 // 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. // Sources MUST return the chapters from most to less recent, which is common.
var itemCount = newChapters.size var itemCount = newChapters.size
var updatedToAdd = newChapters.map { toAddItem -> var updatedToAdd = newChapters.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--) 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 if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
chapter = chapter.copy( chapter = chapter.copy(
@@ -183,19 +199,19 @@ class SyncChaptersWithSource(
chapter = chapter.copy(dateFetch = it) chapter = chapter.copy(dateFetch = it)
} }
reAdded.add(chapter) changedOrDuplicateReadUrls.add(chapter.url)
chapter chapter
} }
// --> EXH (carry over reading progress) // --> EXH (carry over reading progress)
if (manga.isEhBasedManga()) { if (manga.isEhBasedManga()) {
val finalAdded = updatedToAdd.subtract(reAdded) val finalAdded = updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls }
if (finalAdded.isNotEmpty()) { if (finalAdded.isNotEmpty()) {
val max = dbChapters.maxOfOrNull { it.lastPageRead } val max = dbChapters.maxOfOrNull { it.lastPageRead }
if (max != null && max > 0) { if (max != null && max > 0) {
updatedToAdd = updatedToAdd.map { updatedToAdd = updatedToAdd.map {
if (it !in reAdded) { if (it.url !in changedOrDuplicateReadUrls) {
it.copy(lastPageRead = max) it.copy(lastPageRead = max)
} else { } else {
it it
@@ -225,12 +241,8 @@ class SyncChaptersWithSource(
// Note that last_update actually represents last time the chapter list changed at all // Note that last_update actually represents last time the chapter list changed at all
updateManga.awaitUpdateLastUpdate(manga.id) updateManga.awaitUpdateLastUpdate(manga.id)
val reAddedUrls = reAdded.map { it.url }.toHashSet()
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet() val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
return updatedToAdd.filterNot { return updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls || it.scanlator in excludedScanlators }
it.url in reAddedUrls || it.scanlator in excludedScanlators
}
} }
} }
@@ -23,7 +23,7 @@ val Manga.readerOrientation: Long
val Manga.downloadedFilter: TriState val Manga.downloadedFilter: TriState
get() { get() {
if (forceDownloaded()) return TriState.ENABLED_IS if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) { return when (downloadedFilterRaw) {
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
@@ -35,9 +35,6 @@ fun Manga.chaptersFiltered(): Boolean {
downloadedFilter != TriState.DISABLED || downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriState.DISABLED bookmarkedFilter != TriState.DISABLED
} }
fun Manga.forceDownloaded(): Boolean {
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
}
fun Manga.toSManga(): SManga = SManga.create().also { fun Manga.toSManga(): SManga = SManga.create().also {
it.url = url it.url = url
@@ -10,6 +10,7 @@ fun Track.copyPersonalFrom(other: Track): Track {
status = other.status, status = other.status,
startDate = other.startDate, startDate = other.startDate,
finishDate = other.finishDate, finishDate = other.finishDate,
private = other.private,
) )
} }
@@ -26,6 +27,7 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
it.tracking_url = remoteUrl it.tracking_url = remoteUrl
it.started_reading_date = startDate it.started_reading_date = startDate
it.finished_reading_date = finishDate it.finished_reading_date = finishDate
it.private = private
} }
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? { fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
@@ -44,5 +46,6 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
remoteUrl = tracking_url, remoteUrl = tracking_url,
startDate = started_reading_date, startDate = started_reading_date,
finishDate = finished_reading_date, finishDate = finished_reading_date,
private = private,
) )
} }
@@ -42,4 +42,11 @@ class TrackPreferences(
"pref_auto_update_manga_on_mark_read", "pref_auto_update_manga_on_mark_read",
AutoTrackState.ALWAYS, AutoTrackState.ALWAYS,
) )
// SY -->
fun resolveUsingSourceMetadata() = preferenceStore.getBoolean(
"pref_resolve_using_source_metadata_key",
true,
)
// SY <--
} }
@@ -20,6 +20,7 @@ enum class AppTheme(val titleRes: StringResource?) {
TIDAL_WAVE(MR.strings.theme_tidalwave), TIDAL_WAVE(MR.strings.theme_tidalwave),
YINYANG(MR.strings.theme_yinyang), YINYANG(MR.strings.theme_yinyang),
YOTSUBA(MR.strings.theme_yotsuba), YOTSUBA(MR.strings.theme_yotsuba),
MONOCHROME(MR.strings.theme_monochrome),
// Deprecated // Deprecated
DARK_BLUE(null), DARK_BLUE(null),
@@ -19,6 +19,7 @@ import eu.kanade.presentation.library.components.MangaComfortableGridItem
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.RaisedSearchMetadata import exh.metadata.metadata.RaisedSearchMetadata
import exh.metadata.metadata.RankedSearchMetadata
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
@@ -119,6 +120,14 @@ private fun BrowseSourceComfortableGridItem(
textColor = MaterialTheme.colorScheme.onTertiary, textColor = MaterialTheme.colorScheme.onTertiary,
) )
} }
} else if (metadata is RankedSearchMetadata) {
metadata.rank?.let {
Badge(
text = "+$it",
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
} }
}, },
// SY <-- // SY <--
@@ -19,6 +19,7 @@ import eu.kanade.presentation.library.components.MangaCompactGridItem
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.RaisedSearchMetadata import exh.metadata.metadata.RaisedSearchMetadata
import exh.metadata.metadata.RankedSearchMetadata
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
@@ -119,6 +120,14 @@ private fun BrowseSourceCompactGridItem(
textColor = MaterialTheme.colorScheme.onTertiary, textColor = MaterialTheme.colorScheme.onTertiary,
) )
} }
} else if (metadata is RankedSearchMetadata) {
metadata.rank?.let {
Badge(
text = "+$it",
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
} }
}, },
// SY <-- // SY <--
@@ -16,6 +16,7 @@ import eu.kanade.presentation.library.components.MangaListItem
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.RaisedSearchMetadata import exh.metadata.metadata.RaisedSearchMetadata
import exh.metadata.metadata.RankedSearchMetadata
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
@@ -110,6 +111,14 @@ private fun BrowseSourceListItem(
textColor = MaterialTheme.colorScheme.onTertiary, textColor = MaterialTheme.colorScheme.onTertiary,
) )
} }
} else if (metadata is RankedSearchMetadata) {
metadata.rank?.let {
Badge(
text = "+$it",
color = MaterialTheme.colorScheme.tertiary,
textColor = MaterialTheme.colorScheme.onTertiary,
)
}
} }
// SY <-- // SY <--
}, },
@@ -2,23 +2,25 @@ package eu.kanade.presentation.category
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState 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.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable 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 androidx.compose.ui.Modifier
import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.CategoryListItem import eu.kanade.presentation.category.components.CategoryListItem
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.tachiyomi.ui.category.CategoryScreenState import eu.kanade.tachiyomi.ui.category.CategoryScreenState
import kotlinx.collections.immutable.ImmutableList 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.domain.category.model.Category
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@@ -32,11 +34,9 @@ import tachiyomi.presentation.core.util.plus
fun CategoryScreen( fun CategoryScreen(
state: CategoryScreenState.Success, state: CategoryScreenState.Success,
onClickCreate: () -> Unit, onClickCreate: () -> Unit,
onClickSortAlphabetically: () -> Unit,
onClickRename: (Category) -> Unit, onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit, onClickDelete: (Category) -> Unit,
onClickMoveUp: (Category) -> Unit, onChangeOrder: (Category, Int) -> Unit,
onClickMoveDown: (Category) -> Unit,
navigateUp: () -> Unit, navigateUp: () -> Unit,
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
@@ -45,17 +45,6 @@ fun CategoryScreen(
AppBar( AppBar(
title = stringResource(MR.strings.action_edit_categories), title = stringResource(MR.strings.action_edit_categories),
navigateUp = navigateUp, navigateUp = navigateUp,
actions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_sort),
icon = Icons.Outlined.SortByAlpha,
onClick = onClickSortAlphabetically,
),
),
)
},
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
}, },
@@ -77,12 +66,10 @@ fun CategoryScreen(
CategoryContent( CategoryContent(
categories = state.categories, categories = state.categories,
lazyListState = lazyListState, lazyListState = lazyListState,
paddingValues = paddingValues + topSmallPaddingValues + paddingValues = paddingValues,
PaddingValues(horizontal = MaterialTheme.padding.medium),
onClickRename = onClickRename, onClickRename = onClickRename,
onClickDelete = onClickDelete, onClickDelete = onClickDelete,
onMoveUp = onClickMoveUp, onChangeOrder = onChangeOrder,
onMoveDown = onClickMoveDown,
) )
} }
} }
@@ -94,28 +81,44 @@ private fun CategoryContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
onClickRename: (Category) -> Unit, onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit, onClickDelete: (Category) -> Unit,
onMoveUp: (Category) -> Unit, onChangeOrder: (Category, Int) -> Unit,
onMoveDown: (Category) -> 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( LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState, state = lazyListState,
contentPadding = paddingValues, contentPadding = paddingValues +
topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
itemsIndexed( items(
items = categories, items = categoriesState,
key = { _, category -> "category-${category.id}" }, key = { category -> category.key },
) { index, category -> ) { category ->
CategoryListItem( ReorderableItem(reorderableState, category.key) {
modifier = Modifier.animateItem(), CategoryListItem(
category = category, modifier = Modifier.animateItem(),
canMoveUp = index != 0, category = category,
canMoveDown = index != categories.lastIndex, onRename = { onClickRename(category) },
onMoveUp = onMoveUp, onDelete = { onClickDelete(category) },
onMoveDown = onMoveDown, )
onRename = { onClickRename(category) }, }
onDelete = { onClickDelete(category) },
)
} }
} }
} }
private val Category.key inline get() = "category-$id"
@@ -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 @Composable
fun ChangeCategoryDialog( fun ChangeCategoryDialog(
initialSelection: ImmutableList<CheckboxState<Category>>, initialSelection: ImmutableList<CheckboxState<Category>>,
@@ -2,14 +2,11 @@ package eu.kanade.presentation.category.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.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.Delete
import androidx.compose.material.icons.outlined.DragHandle
import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -19,57 +16,42 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import sh.calvin.reorderable.ReorderableCollectionItemScope
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun CategoryListItem( fun ReorderableCollectionItemScope.CategoryListItem(
category: Category, category: Category,
canMoveUp: Boolean,
canMoveDown: Boolean,
onMoveUp: (Category) -> Unit,
onMoveDown: (Category) -> Unit,
onRename: () -> Unit, onRename: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
ElevatedCard( ElevatedCard(modifier = modifier) {
modifier = modifier,
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onRename() } .clickable(onClick = onRename)
.padding(vertical = MaterialTheme.padding.small)
.padding( .padding(
start = MaterialTheme.padding.medium, start = MaterialTheme.padding.small,
top = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium, end = MaterialTheme.padding.medium,
), ),
verticalAlignment = Alignment.CenterVertically, 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(
text = category.name, text = category.name,
modifier = Modifier modifier = Modifier.weight(1f),
.padding(start = MaterialTheme.padding.medium),
) )
}
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) { IconButton(onClick = onRename) {
Icon( Icon(
imageVector = Icons.Outlined.Edit, imageVector = Icons.Outlined.Edit,
@@ -77,7 +59,10 @@ fun CategoryListItem(
) )
} }
IconButton(onClick = onDelete) { 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),
)
} }
} }
} }
@@ -39,6 +39,7 @@ fun HistoryScreen(
onSearchQueryChange: (String?) -> Unit, onSearchQueryChange: (String?) -> Unit,
onClickCover: (mangaId: Long) -> Unit, onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit, onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onClickFavorite: (mangaId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
) { ) {
Scaffold( Scaffold(
@@ -85,6 +86,7 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) }, onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
) )
} }
} }
@@ -98,6 +100,7 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit, onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit, onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit, onClickDelete: (HistoryWithRelations) -> Unit,
onClickFavorite: (HistoryWithRelations) -> Unit,
) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
@@ -127,6 +130,7 @@ private fun HistoryScreenContent(
onClickCover = { onClickCover(value) }, onClickCover = { onClickCover(value) },
onClickResume = { onClickResume(value) }, onClickResume = { onClickResume(value) },
onClickDelete = { onClickDelete(value) }, onClickDelete = { onClickDelete(value) },
onClickFavorite = { onClickFavorite(value) },
) )
} }
} }
@@ -153,6 +157,7 @@ internal fun HistoryScreenPreviews(
onClickCover = {}, onClickCover = {},
onClickResume = { _, _ -> run {} }, onClickResume = { _, _ -> run {} },
onDialogChange = {}, onDialogChange = {},
onClickFavorite = {},
) )
} }
} }
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -39,6 +40,7 @@ fun HistoryItem(
onClickCover: () -> Unit, onClickCover: () -> Unit,
onClickResume: () -> Unit, onClickResume: () -> Unit,
onClickDelete: () -> Unit, onClickDelete: () -> Unit,
onClickFavorite: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( 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) { IconButton(onClick = onClickDelete) {
Icon( Icon(
imageVector = Icons.Outlined.Delete, imageVector = Icons.Outlined.Delete,
@@ -105,6 +117,7 @@ private fun HistoryItemPreviews(
onClickCover = {}, onClickCover = {},
onClickResume = {}, onClickResume = {},
onClickDelete = {}, onClickDelete = {},
onClickFavorite = {},
) )
} }
} }
@@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -309,15 +310,16 @@ private fun ColumnScope.DisplayPage(
val columns by columnPreference.collectAsState() val columns by columnPreference.collectAsState()
SliderItem( SliderItem(
label = stringResource(MR.strings.pref_library_columns),
max = 10,
value = columns, value = columns,
valueRange = 0..10,
label = stringResource(MR.strings.pref_library_columns),
valueText = if (columns > 0) { valueText = if (columns > 0) {
stringResource(MR.strings.pref_library_columns_per_row, columns) columns.toString()
} else { } else {
stringResource(MR.strings.label_default) stringResource(MR.strings.label_auto)
}, },
onChange = columnPreference::set, onChange = columnPreference::set,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
} }
@@ -326,6 +328,10 @@ private fun ColumnScope.DisplayPage(
label = stringResource(MR.strings.action_display_download_badge), label = stringResource(MR.strings.action_display_download_badge),
pref = screenModel.libraryPreferences.downloadBadge(), pref = screenModel.libraryPreferences.downloadBadge(),
) )
CheckboxItem(
label = stringResource(MR.strings.action_display_unread_badge),
pref = screenModel.libraryPreferences.unreadBadge(),
)
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_local_badge), label = stringResource(MR.strings.action_display_local_badge),
pref = screenModel.libraryPreferences.localBadge(), pref = screenModel.libraryPreferences.localBadge(),
@@ -21,13 +21,14 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.downloadedFilter
import eu.kanade.domain.manga.model.forceDownloaded
import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import kotlinx.collections.immutable.persistentListOf 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.components.TriStateItem
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.theme.active import tachiyomi.presentation.core.theme.active
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable @Composable
fun ChapterSettingsDialog( fun ChapterSettingsDialog(
@@ -63,6 +66,8 @@ fun ChapterSettingsDialog(
) )
} }
val downloadedOnly = remember { Injekt.get<BasePreferences>().downloadedOnly().get() }
TabbedDialog( TabbedDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
tabTitles = persistentListOf( tabTitles = persistentListOf(
@@ -97,7 +102,7 @@ fun ChapterSettingsDialog(
FilterPage( FilterPage(
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED, downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
onDownloadFilterChanged = onDownloadFilterChanged onDownloadFilterChanged = onDownloadFilterChanged
.takeUnless { manga?.forceDownloaded() == true }, .takeUnless { downloadedOnly },
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED, unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
onUnreadFilterChanged = onUnreadFilterChanged, onUnreadFilterChanged = onUnreadFilterChanged,
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED, bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,
@@ -117,7 +117,7 @@ fun MangaScreen(
isTabletUi: Boolean, isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, navigateUp: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
@@ -182,7 +182,7 @@ fun MangaScreen(
nextUpdate = nextUpdate, nextUpdate = nextUpdate,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
onBackClicked = onBackClicked, navigateUp = navigateUp,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
@@ -228,7 +228,7 @@ fun MangaScreen(
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
nextUpdate = nextUpdate, nextUpdate = nextUpdate,
onBackClicked = onBackClicked, navigateUp = navigateUp,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
@@ -277,7 +277,7 @@ private fun MangaScreenSmallImpl(
nextUpdate: Instant?, nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, navigateUp: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
@@ -345,14 +345,13 @@ private fun MangaScreenSmallImpl(
} }
// SY <-- // SY <--
val internalOnBackPressed = { BackHandler(onBack = {
if (isAnySelected) { if (isAnySelected) {
onAllChapterSelected(false) onAllChapterSelected(false)
} else { } else {
onBackClicked() navigateUp()
} }
} })
BackHandler(onBack = internalOnBackPressed)
Scaffold( Scaffold(
topBar = { topBar = {
@@ -365,20 +364,18 @@ private fun MangaScreenSmallImpl(
val isFirstItemScrolled by remember { val isFirstItemScrolled by remember {
derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 } derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
} }
val animatedTitleAlpha by animateFloatAsState( val titleAlpha by animateFloatAsState(
if (!isFirstItemVisible) 1f else 0f, if (!isFirstItemVisible) 1f else 0f,
label = "Top Bar Title", label = "Top Bar Title",
) )
val animatedBgAlpha by animateFloatAsState( val backgroundAlpha by animateFloatAsState(
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f, if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
label = "Top Bar Background", label = "Top Bar Background",
) )
MangaToolbar( MangaToolbar(
title = state.manga.title, title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha },
hasFilters = state.filterActive, hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed, navigateUp = navigateUp,
onClickFilter = onFilterClicked, onClickFilter = onFilterClicked,
onClickShare = onShareClicked, onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked, onClickDownload = onDownloadActionClicked,
@@ -392,8 +389,11 @@ private fun MangaScreenSmallImpl(
onClickMerge = onMergeClicked.takeIf { state.showMergeInOverflow }, onClickMerge = onMergeClicked.takeIf { state.showMergeInOverflow },
// SY <-- // SY <--
actionModeCounter = selectedChapterCount, actionModeCounter = selectedChapterCount,
onCancelActionMode = { onAllChapterSelected(false) },
onSelectAll = { onAllChapterSelected(true) }, onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() }, onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { titleAlpha },
backgroundAlphaProvider = { backgroundAlpha },
) )
}, },
bottomBar = { bottomBar = {
@@ -600,7 +600,7 @@ fun MangaScreenLargeImpl(
nextUpdate: Instant?, nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, navigateUp: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
@@ -672,14 +672,13 @@ fun MangaScreenLargeImpl(
val chapterListState = rememberLazyListState() val chapterListState = rememberLazyListState()
val internalOnBackPressed = { BackHandler(onBack = {
if (isAnySelected) { if (isAnySelected) {
onAllChapterSelected(false) onAllChapterSelected(false)
} else { } else {
onBackClicked() navigateUp()
} }
} })
BackHandler(onBack = internalOnBackPressed)
Scaffold( Scaffold(
topBar = { topBar = {
@@ -689,10 +688,8 @@ fun MangaScreenLargeImpl(
MangaToolbar( MangaToolbar(
modifier = Modifier.onSizeChanged { topBarHeight = it.height }, modifier = Modifier.onSizeChanged { topBarHeight = it.height },
title = state.manga.title, title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.filterActive, hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed, navigateUp = navigateUp,
onClickFilter = onFilterButtonClicked, onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked, onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked, onClickDownload = onDownloadActionClicked,
@@ -705,9 +702,12 @@ fun MangaScreenLargeImpl(
onClickMergedSettings = onMergedSettingsClicked.takeIf { state.manga.source == MERGED_SOURCE_ID }, onClickMergedSettings = onMergedSettingsClicked.takeIf { state.manga.source == MERGED_SOURCE_ID },
onClickMerge = onMergeClicked.takeIf { state.showMergeInOverflow }, onClickMerge = onMergeClicked.takeIf { state.showMergeInOverflow },
// SY <-- // SY <--
onCancelActionMode = { onAllChapterSelected(false) },
actionModeCounter = selectedChapterCount, actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) }, onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() }, onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { 1f },
backgroundAlphaProvider = { 1f },
) )
}, },
bottomBar = { bottomBar = {
@@ -28,7 +28,6 @@ import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DoneAll import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Label
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.RemoveDone import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.icons.outlined.SwapCalls import androidx.compose.material.icons.outlined.SwapCalls
@@ -237,6 +236,7 @@ fun LibraryBottomActionMenu(
// SY --> // SY -->
onClickCleanTitles: (() -> Unit)?, onClickCleanTitles: (() -> Unit)?,
onClickMigrate: (() -> Unit)?, onClickMigrate: (() -> Unit)?,
onClickCollectRecommendations: (() -> Unit)?,
onClickAddToMangaDex: (() -> Unit)?, onClickAddToMangaDex: (() -> Unit)?,
onClickResetInfo: (() -> Unit)?, onClickResetInfo: (() -> Unit)?,
// SY <-- // SY <--
@@ -267,7 +267,10 @@ fun LibraryBottomActionMenu(
} }
} }
// SY --> // SY -->
val showOverflow = onClickCleanTitles != null || onClickAddToMangaDex != null || onClickResetInfo != null val showOverflow = onClickCleanTitles != null ||
onClickAddToMangaDex != null ||
onClickResetInfo != null ||
onClickCollectRecommendations != null
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val moveMarkPrev = remember { !configuration.isTabletUi() } val moveMarkPrev = remember { !configuration.isTabletUi() }
var overFlowOpen by remember { mutableStateOf(false) } var overFlowOpen by remember { mutableStateOf(false) }
@@ -358,6 +361,12 @@ fun LibraryBottomActionMenu(
onClick = onClickMigrate, onClick = onClickMigrate,
) )
} }
if (onClickCollectRecommendations != null) {
DropdownMenuItem(
text = { Text(stringResource(SYMR.strings.rec_search_short)) },
onClick = onClickCollectRecommendations,
)
}
if (onClickAddToMangaDex != null) { if (onClickAddToMangaDex != null) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(SYMR.strings.mangadex_add_to_follows)) }, text = { Text(stringResource(SYMR.strings.mangadex_add_to_follows)) },
@@ -1,18 +1,12 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons 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.Download
import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -20,12 +14,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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 androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.DownloadDropdownMenu import eu.kanade.presentation.components.DownloadDropdownMenu
import eu.kanade.presentation.components.UpIcon
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -36,9 +30,8 @@ import tachiyomi.presentation.core.theme.active
@Composable @Composable
fun MangaToolbar( fun MangaToolbar(
title: String, title: String,
titleAlphaProvider: () -> Float,
hasFilters: Boolean, hasFilters: Boolean,
onBackClicked: () -> Unit, navigateUp: () -> Unit,
onClickFilter: () -> Unit, onClickFilter: () -> Unit,
onClickShare: (() -> Unit)?, onClickShare: (() -> Unit)?,
onClickDownload: ((DownloadAction) -> Unit)?, onClickDownload: ((DownloadAction) -> Unit)?,
@@ -54,152 +47,145 @@ fun MangaToolbar(
// For action mode // For action mode
actionModeCounter: Int, actionModeCounter: Int,
onCancelActionMode: () -> Unit,
onSelectAll: () -> Unit, onSelectAll: () -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
titleAlphaProvider: () -> Float,
backgroundAlphaProvider: () -> Float,
modifier: Modifier = Modifier, 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, modifier = modifier,
) { backgroundColor = MaterialTheme.colorScheme
val isActionMode = actionModeCounter > 0 .surfaceColorAtElevation(3.dp)
TopAppBar( .copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
title = { navigateUp = navigateUp,
Text( actions = {
text = if (isActionMode) actionModeCounter.toString() else title, var downloadExpanded by remember { mutableStateOf(false) }
maxLines = 1, if (onClickDownload != null) {
overflow = TextOverflow.Ellipsis, val onDismissRequest = { downloadExpanded = false }
color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()), DownloadDropdownMenu(
expanded = downloadExpanded,
onDismissRequest = onDismissRequest,
onDownloadClicked = onClickDownload,
) )
}, }
navigationIcon = {
IconButton(onClick = onBackClicked) { val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
UpIcon(navigationIcon = Icons.Outlined.Close.takeIf { isActionMode }) AppBarActions(
} actions = persistentListOf<AppBar.AppBarAction>().builder().apply {
}, if (isActionMode) {
actions = { add(
if (isActionMode) {
AppBarActions(
persistentListOf(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_select_all), title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll, icon = Icons.Outlined.SelectAll,
onClick = onSelectAll, onClick = onSelectAll,
), ),
)
add(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_select_inverse), title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack, icon = Icons.Outlined.FlipToBack,
onClick = onInvertSelection, onClick = onInvertSelection,
), ),
), )
) return@apply
} else { }
var downloadExpanded by remember { mutableStateOf(false) }
if (onClickDownload != null) { if (onClickDownload != null) {
val onDismissRequest = { downloadExpanded = false } add(
DownloadDropdownMenu( AppBar.Action(
expanded = downloadExpanded, title = stringResource(MR.strings.manga_download),
onDismissRequest = onDismissRequest, icon = Icons.Outlined.Download,
onDownloadClicked = onClickDownload, onClick = { downloadExpanded = !downloadExpanded },
),
) )
} }
add(
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current AppBar.Action(
AppBarActions( title = stringResource(MR.strings.action_filter),
actions = persistentListOf<AppBar.AppBarAction>().builder() icon = Icons.Outlined.FilterList,
.apply { iconTint = filterTint,
if (onClickDownload != null) { onClick = onClickFilter,
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.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(),
colors = TopAppBarDefaults.topAppBarColors( )
containerColor = MaterialTheme.colorScheme },
.surfaceColorAtElevation(3.dp) isActionMode = isActionMode,
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()), onCancelActionMode = onCancelActionMode,
), )
)
}
} }
@@ -1,13 +1,6 @@
package eu.kanade.presentation.more 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.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label 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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource 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.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -49,7 +41,6 @@ fun MoreScreen(
onDownloadedOnlyChange: (Boolean) -> Unit, onDownloadedOnlyChange: (Boolean) -> Unit,
incognitoMode: Boolean, incognitoMode: Boolean,
onIncognitoModeChange: (Boolean) -> Unit, onIncognitoModeChange: (Boolean) -> Unit,
isFDroid: Boolean,
// SY --> // SY -->
showNavUpdates: Boolean, showNavUpdates: Boolean,
showNavHistory: Boolean, showNavHistory: Boolean,
@@ -66,26 +57,7 @@ fun MoreScreen(
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
Scaffold( Scaffold { contentPadding ->
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 ->
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) { ) {
@@ -4,7 +4,6 @@ import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
@@ -32,6 +31,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
@@ -111,7 +111,7 @@ internal class PermissionStep : OnboardingStep {
onButtonClick = { onButtonClick = {
@SuppressLint("BatteryLife") @SuppressLint("BatteryLife")
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}") data = "package:${context.packageName}".toUri()
} }
context.startActivity(intent) context.startActivity(intent)
}, },
@@ -1,5 +1,6 @@
package eu.kanade.presentation.more.settings package eu.kanade.presentation.more.settings
import androidx.annotation.IntRange
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@@ -20,7 +21,7 @@ sealed class Preference {
// SY <-- // SY <--
abstract val icon: ImageVector? 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. * A basic [PreferenceItem] that only displays texts.
@@ -28,57 +29,58 @@ sealed class Preference {
data class TextPreference( data class TextPreference(
override val title: String, override val title: String,
override val subtitle: CharSequence? = null, override val subtitle: CharSequence? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
val onClick: (() -> Unit)? = null, 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. * A [PreferenceItem] that provides a two-state toggleable option.
*/ */
data class SwitchPreference( data class SwitchPreference(
val pref: PreferenceData<Boolean>, val preference: PreferenceData<Boolean>,
override val title: String, override val title: String,
override val subtitle: CharSequence? = null, override val subtitle: CharSequence? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Boolean) -> Boolean = { true }, override val onValueChanged: suspend (value: Boolean) -> Boolean = { true },
) : PreferenceItem<Boolean>() ) : PreferenceItem<Boolean>() {
override val icon: ImageVector? = null
}
/** /**
* A [PreferenceItem] that provides a slider to select an integer number. * A [PreferenceItem] that provides a slider to select an integer number.
*/ */
data class SliderPreference( data class SliderPreference(
val value: Int, val value: Int,
val min: Int = 0, override val title: String,
val max: Int, val valueRange: IntProgression = 0..1,
override val title: String = "", @IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
override val subtitle: String? = null, override val subtitle: String? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Int) -> Boolean = { true }, override val onValueChanged: suspend (value: Int) -> Boolean = { true },
) : PreferenceItem<Int>() ) : PreferenceItem<Int>() {
override val icon: ImageVector? = null
}
/** /**
* A [PreferenceItem] that displays a list of entries as a dialog. * A [PreferenceItem] that displays a list of entries as a dialog.
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
data class ListPreference<T>( data class ListPreference<T>(
val pref: PreferenceData<T>, val preference: PreferenceData<T>,
val entries: ImmutableMap<T, String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? = val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) }, { v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null, override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: T) -> Boolean = { true }, override val onValueChanged: suspend (value: T) -> Boolean = { true },
val entries: ImmutableMap<T, String>,
) : PreferenceItem<T>() { ) : PreferenceItem<T>() {
internal fun internalSet(newValue: Any) = pref.set(newValue as T) internal fun internalSet(value: Any) = preference.set(value as T)
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T) internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T)
@Composable @Composable
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) = internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
@@ -90,15 +92,14 @@ sealed class Preference {
*/ */
data class BasicListPreference( data class BasicListPreference(
val value: String, val value: String,
val entries: ImmutableMap<String, String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? = val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) }, { v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null, override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }, override val onValueChanged: suspend (value: String) -> Boolean = { true },
val entries: ImmutableMap<String, String>,
) : PreferenceItem<String>() ) : PreferenceItem<String>()
/** /**
@@ -106,52 +107,51 @@ sealed class Preference {
* Multiple entries can be selected at the same time. * Multiple entries can be selected at the same time.
*/ */
data class MultiSelectListPreference( data class MultiSelectListPreference(
val pref: PreferenceData<Set<String>>, val preference: PreferenceData<Set<String>>,
val entries: ImmutableMap<String, String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
val subtitleProvider: @Composable ( val subtitleProvider: @Composable (value: Set<String>, entries: ImmutableMap<String, String>) -> String? =
value: Set<String>, { v, e ->
entries: ImmutableMap<String, String>, val combined = remember(v, e) {
) -> String? = { v, e -> v.mapNotNull { e[it] }
val combined = remember(v) { .joinToString()
v.map { e[it] } .takeUnless { it.isBlank() }
.takeIf { it.isNotEmpty() } }
?.joinToString() ?: stringResource(MR.strings.none)
} ?: stringResource(MR.strings.none) subtitle?.format(combined)
subtitle?.format(combined) },
},
override val icon: ImageVector? = null, override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true }, override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
val entries: ImmutableMap<String, String>,
) : PreferenceItem<Set<String>>() ) : PreferenceItem<Set<String>>()
/** /**
* A [PreferenceItem] that shows a EditText in the dialog. * A [PreferenceItem] that shows a EditText in the dialog.
*/ */
data class EditTextPreference( data class EditTextPreference(
val pref: PreferenceData<String>, val preference: PreferenceData<String>,
override val title: String, override val title: String,
override val subtitle: String? = "%s", override val subtitle: String? = "%s",
override val icon: ImageVector? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }, override val onValueChanged: suspend (value: String) -> Boolean = { true },
) : PreferenceItem<String>() ) : PreferenceItem<String>() {
override val icon: ImageVector? = null
}
/** /**
* A [PreferenceItem] for individual tracker. * A [PreferenceItem] for individual tracker.
*/ */
data class TrackerPreference( data class TrackerPreference(
val tracker: Tracker, val tracker: Tracker,
override val title: String,
val login: () -> Unit, val login: () -> Unit,
val logout: () -> Unit, val logout: () -> Unit,
) : PreferenceItem<String>() { ) : PreferenceItem<String>() {
override val title: String = ""
override val enabled: Boolean = true override val enabled: Boolean = true
override val subtitle: String? = null override val subtitle: String? = null
override val icon: ImageVector? = 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( data class InfoPreference(
@@ -160,7 +160,7 @@ sealed class Preference {
override val enabled: Boolean = true override val enabled: Boolean = true
override val subtitle: String? = null override val subtitle: String? = null
override val icon: ImageVector? = 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( data class CustomPreference(
@@ -170,7 +170,7 @@ sealed class Preference {
override val enabled: Boolean = true override val enabled: Boolean = true
override val subtitle: String? = null override val subtitle: String? = null
override val icon: ImageVector? = null override val icon: ImageVector? = null
override val onValueChanged: suspend (newValue: Unit) -> Boolean = { true } override val onValueChanged: suspend (value: Unit) -> Boolean = { true }
} }
} }
@@ -5,6 +5,8 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically 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.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -12,16 +14,20 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.InfoWidget import eu.kanade.presentation.more.settings.widget.InfoWidget
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget 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.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget 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 eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.BaseSliderItem
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false } val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
@@ -60,7 +66,7 @@ internal fun PreferenceItem(
) { ) {
when (item) { when (item) {
is Preference.PreferenceItem.SwitchPreference -> { is Preference.PreferenceItem.SwitchPreference -> {
val value by item.pref.collectAsState() val value by item.preference.collectAsState()
SwitchPreferenceWidget( SwitchPreferenceWidget(
title = item.title, title = item.title,
subtitle = item.subtitle, subtitle = item.subtitle,
@@ -69,29 +75,33 @@ internal fun PreferenceItem(
onCheckedChanged = { newValue -> onCheckedChanged = { newValue ->
scope.launch { scope.launch {
if (item.onValueChanged(newValue)) { if (item.onValueChanged(newValue)) {
item.pref.set(newValue) item.preference.set(newValue)
} }
} }
}, },
) )
} }
is Preference.PreferenceItem.SliderPreference -> { is Preference.PreferenceItem.SliderPreference -> {
// TODO: use different composable? BaseSliderItem(
SliderItem(
label = item.title, label = item.title,
min = item.min,
max = item.max,
value = item.value, value = item.value,
valueRange = item.valueRange,
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(), valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
steps = item.steps,
labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
onChange = { onChange = {
scope.launch { scope.launch {
item.onValueChanged(it) item.onValueChanged(it)
} }
}, },
modifier = Modifier.padding(
horizontal = PrefsHorizontalPadding,
vertical = PrefsVerticalPadding,
),
) )
} }
is Preference.PreferenceItem.ListPreference<*> -> { is Preference.PreferenceItem.ListPreference<*> -> {
val value by item.pref.collectAsState() val value by item.preference.collectAsState()
ListPreferenceWidget( ListPreferenceWidget(
value = value, value = value,
title = item.title, title = item.title,
@@ -118,14 +128,14 @@ internal fun PreferenceItem(
) )
} }
is Preference.PreferenceItem.MultiSelectListPreference -> { is Preference.PreferenceItem.MultiSelectListPreference -> {
val values by item.pref.collectAsState() val values by item.preference.collectAsState()
MultiSelectListPreferenceWidget( MultiSelectListPreferenceWidget(
preference = item, preference = item,
values = values, values = values,
onValuesChange = { newValues -> onValuesChange = { newValues ->
scope.launch { scope.launch {
if (item.onValueChanged(newValues)) { 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 -> { is Preference.PreferenceItem.EditTextPreference -> {
val values by item.pref.collectAsState() val values by item.preference.collectAsState()
EditTextPreferenceWidget( EditTextPreferenceWidget(
title = item.title, title = item.title,
subtitle = item.subtitle, subtitle = item.subtitle,
@@ -148,7 +158,7 @@ internal fun PreferenceItem(
value = values, value = values,
onConfirm = { onConfirm = {
val accepted = item.onValueChanged(it) val accepted = item.onValueChanged(it)
if (accepted) item.pref.set(it) if (accepted) item.preference.set(it)
accepted accepted
}, },
) )
@@ -126,7 +126,7 @@ object SettingsAdvancedScreen : SearchableSettings {
}, },
), ),
/* SY --> Preference.PreferenceItem.SwitchPreference( /* SY --> Preference.PreferenceItem.SwitchPreference(
pref = networkPreferences.verboseLogging(), preference = networkPreferences.verboseLogging(),
title = stringResource(MR.strings.pref_verbose_logging), title = stringResource(MR.strings.pref_verbose_logging),
subtitle = stringResource(MR.strings.pref_verbose_logging_summary), subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
onValueChanged = { onValueChanged = {
@@ -272,8 +272,7 @@ object SettingsAdvancedScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = networkPreferences.dohProvider(), preference = networkPreferences.dohProvider(),
title = stringResource(MR.strings.pref_dns_over_https),
entries = persistentMapOf( entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled), -1 to stringResource(MR.strings.disabled),
PREF_DOH_CLOUDFLARE to "Cloudflare", PREF_DOH_CLOUDFLARE to "Cloudflare",
@@ -289,13 +288,14 @@ object SettingsAdvancedScreen : SearchableSettings {
PREF_DOH_NJALLA to "Njalla", PREF_DOH_NJALLA to "Njalla",
PREF_DOH_SHECAN to "Shecan", PREF_DOH_SHECAN to "Shecan",
), ),
title = stringResource(MR.strings.pref_dns_over_https),
onValueChanged = { onValueChanged = {
context.toast(MR.strings.requires_app_restart) context.toast(MR.strings.requires_app_restart)
true true
}, },
), ),
Preference.PreferenceItem.EditTextPreference( Preference.PreferenceItem.EditTextPreference(
pref = userAgentPref, preference = userAgentPref,
title = stringResource(MR.strings.pref_user_agent_string), title = stringResource(MR.strings.pref_user_agent_string),
onValueChanged = { onValueChanged = {
try { try {
@@ -372,13 +372,7 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_reader), title = stringResource(MR.strings.pref_category_reader),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = basePreferences.hardwareBitmapThreshold(), preference = 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,
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
.mapIndexed { index, option -> .mapIndexed { index, option ->
val display = if (index == 0) { val display = if (index == 0) {
@@ -390,10 +384,16 @@ object SettingsAdvancedScreen : SearchableSettings {
} }
.toMap() .toMap()
.toImmutableMap(), .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( Preference.PreferenceItem.SwitchPreference(
pref = basePreferences.alwaysDecodeLongStripWithSSIV(), preference = basePreferences.alwaysDecodeLongStripWithSSIV(),
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv), title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_2),
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary), subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
), ),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
@@ -444,8 +444,7 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(MR.strings.label_extensions), title = stringResource(MR.strings.label_extensions),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = extensionInstallerPref, preference = extensionInstallerPref,
title = stringResource(MR.strings.ext_installer_pref),
entries = extensionInstallerPref.entries entries = extensionInstallerPref.entries
.filter { .filter {
// TODO: allow private option in stable versions once URL handling is more fleshed out // TODO: allow private option in stable versions once URL handling is more fleshed out
@@ -457,6 +456,7 @@ object SettingsAdvancedScreen : SearchableSettings {
} }
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.ext_installer_pref),
onValueChanged = { onValueChanged = {
if (it == BasePreferences.ExtensionInstaller.SHIZUKU && if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
!context.isShizukuInstalled !context.isShizukuInstalled
@@ -618,7 +618,7 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(SYMR.strings.data_saver), title = stringResource(SYMR.strings.data_saver),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = sourcePreferences.dataSaver(), preference = sourcePreferences.dataSaver(),
title = stringResource(SYMR.strings.data_saver), title = stringResource(SYMR.strings.data_saver),
subtitle = stringResource(SYMR.strings.data_saver_summary), subtitle = stringResource(SYMR.strings.data_saver_summary),
entries = persistentMapOf( entries = persistentMapOf(
@@ -628,28 +628,28 @@ object SettingsAdvancedScreen : SearchableSettings {
), ),
), ),
Preference.PreferenceItem.EditTextPreference( Preference.PreferenceItem.EditTextPreference(
pref = sourcePreferences.dataSaverServer(), preference = sourcePreferences.dataSaverServer(),
title = stringResource(SYMR.strings.bandwidth_data_saver_server), title = stringResource(SYMR.strings.bandwidth_data_saver_server),
subtitle = stringResource(SYMR.strings.data_saver_server_summary), subtitle = stringResource(SYMR.strings.data_saver_server_summary),
enabled = dataSaver == DataSaver.BANDWIDTH_HERO, enabled = dataSaver == DataSaver.BANDWIDTH_HERO,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.dataSaverDownloader(), preference = sourcePreferences.dataSaverDownloader(),
title = stringResource(SYMR.strings.data_saver_downloader), title = stringResource(SYMR.strings.data_saver_downloader),
enabled = dataSaver != DataSaver.NONE, enabled = dataSaver != DataSaver.NONE,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.dataSaverIgnoreJpeg(), preference = sourcePreferences.dataSaverIgnoreJpeg(),
title = stringResource(SYMR.strings.data_saver_ignore_jpeg), title = stringResource(SYMR.strings.data_saver_ignore_jpeg),
enabled = dataSaver != DataSaver.NONE, enabled = dataSaver != DataSaver.NONE,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.dataSaverIgnoreGif(), preference = sourcePreferences.dataSaverIgnoreGif(),
title = stringResource(SYMR.strings.data_saver_ignore_gif), title = stringResource(SYMR.strings.data_saver_ignore_gif),
enabled = dataSaver != DataSaver.NONE, enabled = dataSaver != DataSaver.NONE,
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = sourcePreferences.dataSaverImageQuality(), preference = sourcePreferences.dataSaverImageQuality(),
title = stringResource(SYMR.strings.data_saver_image_quality), title = stringResource(SYMR.strings.data_saver_image_quality),
subtitle = stringResource(SYMR.strings.data_saver_image_quality_summary), subtitle = stringResource(SYMR.strings.data_saver_image_quality_summary),
entries = listOf( entries = listOf(
@@ -668,7 +668,7 @@ object SettingsAdvancedScreen : SearchableSettings {
val dataSaverImageFormatJpeg by sourcePreferences.dataSaverImageFormatJpeg() val dataSaverImageFormatJpeg by sourcePreferences.dataSaverImageFormatJpeg()
.collectAsState() .collectAsState()
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.dataSaverImageFormatJpeg(), preference = sourcePreferences.dataSaverImageFormatJpeg(),
title = stringResource(SYMR.strings.data_saver_image_format), title = stringResource(SYMR.strings.data_saver_image_format),
subtitle = if (dataSaverImageFormatJpeg) { subtitle = if (dataSaverImageFormatJpeg) {
stringResource(SYMR.strings.data_saver_image_format_summary_on) stringResource(SYMR.strings.data_saver_image_format_summary_on)
@@ -679,7 +679,7 @@ object SettingsAdvancedScreen : SearchableSettings {
) )
}, },
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.dataSaverColorBW(), preference = sourcePreferences.dataSaverColorBW(),
title = stringResource(SYMR.strings.data_saver_color_bw), title = stringResource(SYMR.strings.data_saver_color_bw),
enabled = dataSaver == DataSaver.BANDWIDTH_HERO, enabled = dataSaver == DataSaver.BANDWIDTH_HERO,
), ),
@@ -699,7 +699,7 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(SYMR.strings.developer_tools), title = stringResource(SYMR.strings.developer_tools),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.isHentaiEnabled(), preference = unsortedPreferences.isHentaiEnabled(),
title = stringResource(SYMR.strings.toggle_hentai_features), title = stringResource(SYMR.strings.toggle_hentai_features),
subtitle = stringResource(SYMR.strings.toggle_hentai_features_summary), subtitle = stringResource(SYMR.strings.toggle_hentai_features_summary),
onValueChanged = { onValueChanged = {
@@ -714,7 +714,7 @@ object SettingsAdvancedScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = delegateSourcePreferences.delegateSources(), preference = delegateSourcePreferences.delegateSources(),
title = stringResource(SYMR.strings.toggle_delegated_sources), title = stringResource(SYMR.strings.toggle_delegated_sources),
subtitle = stringResource( subtitle = stringResource(
SYMR.strings.toggle_delegated_sources_summary, SYMR.strings.toggle_delegated_sources_summary,
@@ -724,7 +724,7 @@ object SettingsAdvancedScreen : SearchableSettings {
), ),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = unsortedPreferences.logLevel(), preference = unsortedPreferences.logLevel(),
title = stringResource(SYMR.strings.log_level), title = stringResource(SYMR.strings.log_level),
subtitle = stringResource(SYMR.strings.log_level_summary), subtitle = stringResource(SYMR.strings.log_level_summary),
entries = EHLogLevel.entries.mapIndexed { index, ehLogLevel -> entries = EHLogLevel.entries.mapIndexed { index, ehLogLevel ->
@@ -734,7 +734,7 @@ object SettingsAdvancedScreen : SearchableSettings {
}.toMap().toImmutableMap(), }.toMap().toImmutableMap(),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.enableSourceBlacklist(), preference = sourcePreferences.enableSourceBlacklist(),
title = stringResource(SYMR.strings.enable_source_blacklist), title = stringResource(SYMR.strings.enable_source_blacklist),
subtitle = stringResource( subtitle = stringResource(
SYMR.strings.enable_source_blacklist_summary, SYMR.strings.enable_source_blacklist_summary,
@@ -778,7 +778,7 @@ object SettingsAdvancedScreen : SearchableSettings {
} }
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
title = stringResource(SYMR.strings.encrypt_database), title = stringResource(SYMR.strings.encrypt_database),
pref = securityPreferences.encryptDatabase(), preference = securityPreferences.encryptDatabase(),
subtitle = stringResource(SYMR.strings.encrypt_database_subtitle), subtitle = stringResource(SYMR.strings.encrypt_database_subtitle),
onValueChanged = { onValueChanged = {
if (it) { if (it) {
@@ -88,7 +88,7 @@ object SettingsAppearanceScreen : SearchableSettings {
} }
}, },
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = amoledPref, preference = amoledPref,
title = stringResource(MR.strings.pref_dark_theme_pure_black), title = stringResource(MR.strings.pref_dark_theme_pure_black),
enabled = themeMode != ThemeMode.LIGHT, enabled = themeMode != ThemeMode.LIGHT,
onValueChanged = { onValueChanged = {
@@ -122,28 +122,28 @@ object SettingsAppearanceScreen : SearchableSettings {
onClick = { navigator.push(AppLanguageScreen()) }, onClick = { navigator.push(AppLanguageScreen()) },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = uiPreferences.tabletUiMode(), preference = uiPreferences.tabletUiMode(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
entries = TabletUiMode.entries entries = TabletUiMode.entries
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
onValueChanged = { onValueChanged = {
context.toast(MR.strings.requires_app_restart) context.toast(MR.strings.requires_app_restart)
true true
}, },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = uiPreferences.dateFormat(), preference = uiPreferences.dateFormat(),
title = stringResource(MR.strings.pref_date_format),
entries = DateFormats entries = DateFormats
.associateWith { .associateWith {
val formattedDate = UiPreferences.dateFormat(it).format(now) val formattedDate = UiPreferences.dateFormat(it).format(now)
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)" "${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
} }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_date_format),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.relativeTime(), preference = uiPreferences.relativeTime(),
title = stringResource(MR.strings.pref_relative_format), title = stringResource(MR.strings.pref_relative_format),
subtitle = stringResource( subtitle = stringResource(
MR.strings.pref_relative_format_summary, MR.strings.pref_relative_format_summary,
@@ -164,16 +164,16 @@ object SettingsAppearanceScreen : SearchableSettings {
stringResource(SYMR.strings.pref_category_fork), stringResource(SYMR.strings.pref_category_fork),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.expandFilters(), preference = uiPreferences.expandFilters(),
title = stringResource(SYMR.strings.toggle_expand_search_filters), title = stringResource(SYMR.strings.toggle_expand_search_filters),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.recommendsInOverflow(), preference = uiPreferences.recommendsInOverflow(),
title = stringResource(SYMR.strings.put_recommends_in_overflow), title = stringResource(SYMR.strings.put_recommends_in_overflow),
subtitle = stringResource(SYMR.strings.put_recommends_in_overflow_summary), subtitle = stringResource(SYMR.strings.put_recommends_in_overflow_summary),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.mergeInOverflow(), preference = uiPreferences.mergeInOverflow(),
title = stringResource(SYMR.strings.put_merge_in_overflow), title = stringResource(SYMR.strings.put_merge_in_overflow),
subtitle = stringResource(SYMR.strings.put_merge_in_overflow_summary), subtitle = stringResource(SYMR.strings.put_merge_in_overflow_summary),
), ),
@@ -189,8 +189,7 @@ object SettingsAppearanceScreen : SearchableSettings {
} else { } else {
stringResource(MR.strings.disabled) stringResource(MR.strings.disabled)
}, },
min = 0, valueRange = 0..10,
max = 10,
onValueChanged = { onValueChanged = {
uiPreferences.previewsRowCount().set(it) uiPreferences.previewsRowCount().set(it)
true true
@@ -206,15 +205,15 @@ object SettingsAppearanceScreen : SearchableSettings {
stringResource(SYMR.strings.pref_category_navbar), stringResource(SYMR.strings.pref_category_navbar),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.showNavUpdates(), preference = uiPreferences.showNavUpdates(),
title = stringResource(SYMR.strings.pref_hide_updates_button), title = stringResource(SYMR.strings.pref_hide_updates_button),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.showNavHistory(), preference = uiPreferences.showNavHistory(),
title = stringResource(SYMR.strings.pref_hide_history_button), title = stringResource(SYMR.strings.pref_hide_history_button),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.bottomBarLabels(), preference = uiPreferences.bottomBarLabels(),
title = stringResource(SYMR.strings.pref_show_bottom_bar_labels), title = stringResource(SYMR.strings.pref_show_bottom_bar_labels),
), ),
), ),
@@ -67,17 +67,17 @@ object SettingsBrowseScreen : SearchableSettings {
) )
}, },
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.sourcesTabCategoriesFilter(), preference = sourcePreferences.sourcesTabCategoriesFilter(),
title = stringResource(SYMR.strings.pref_source_source_filtering), title = stringResource(SYMR.strings.pref_source_source_filtering),
subtitle = stringResource(SYMR.strings.pref_source_source_filtering_summery), subtitle = stringResource(SYMR.strings.pref_source_source_filtering_summery),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.useNewSourceNavigation(), preference = uiPreferences.useNewSourceNavigation(),
title = stringResource(SYMR.strings.pref_source_navigation), title = stringResource(SYMR.strings.pref_source_navigation),
subtitle = stringResource(SYMR.strings.pref_source_navigation_summery), subtitle = stringResource(SYMR.strings.pref_source_navigation_summery),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.allowLocalSourceHiddenFolders(), preference = unsortedPreferences.allowLocalSourceHiddenFolders(),
title = stringResource(SYMR.strings.pref_local_source_hidden_folders), title = stringResource(SYMR.strings.pref_local_source_hidden_folders),
subtitle = stringResource(SYMR.strings.pref_local_source_hidden_folders_summery), subtitle = stringResource(SYMR.strings.pref_local_source_hidden_folders_summery),
), ),
@@ -87,11 +87,11 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(SYMR.strings.feed), title = stringResource(SYMR.strings.feed),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.hideFeedTab(), preference = uiPreferences.hideFeedTab(),
title = stringResource(SYMR.strings.pref_hide_feed), title = stringResource(SYMR.strings.pref_hide_feed),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.feedTabInFront(), preference = uiPreferences.feedTabInFront(),
title = stringResource(SYMR.strings.pref_feed_position), title = stringResource(SYMR.strings.pref_feed_position),
subtitle = stringResource(SYMR.strings.pref_feed_position_summery), subtitle = stringResource(SYMR.strings.pref_feed_position_summery),
enabled = hideFeedTab.not(), enabled = hideFeedTab.not(),
@@ -103,7 +103,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.label_sources), title = stringResource(MR.strings.label_sources),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.hideInLibraryItems(), preference = sourcePreferences.hideInLibraryItems(),
title = stringResource(MR.strings.pref_hide_in_library_items), title = stringResource(MR.strings.pref_hide_in_library_items),
), ),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
@@ -119,7 +119,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_nsfw_content), title = stringResource(MR.strings.pref_category_nsfw_content),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.showNsfwSource(), preference = sourcePreferences.showNsfwSource(),
title = stringResource(MR.strings.pref_show_nsfw_source), title = stringResource(MR.strings.pref_show_nsfw_source),
subtitle = stringResource(MR.strings.requires_app_restart), subtitle = stringResource(MR.strings.requires_app_restart),
onValueChanged = { onValueChanged = {
@@ -7,7 +7,9 @@ import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth 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.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MultiChoiceSegmentedButtonRow import androidx.compose.material3.MultiChoiceSegmentedButtonRow
@@ -24,6 +28,7 @@ import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@@ -31,13 +36,17 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.net.toUri import androidx.core.net.toUri
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.zxing.client.android.Intents
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import eu.kanade.domain.sync.SyncPreferences import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen 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.SyncSettingsSelector
import eu.kanade.presentation.more.settings.screen.data.SyncTriggerOptionsScreen import eu.kanade.presentation.more.settings.screen.data.SyncTriggerOptionsScreen
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget 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.PrefsHorizontalPadding
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.PagePreviewCache 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.SyncDataJob
import eu.kanade.tachiyomi.data.sync.SyncManager import eu.kanade.tachiyomi.data.sync.SyncManager
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService 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 eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource 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.core.common.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences 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.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
@@ -111,6 +127,7 @@ object SettingsDataScreen : SearchableSettings {
getBackupAndRestoreGroup(backupPreferences = backupPreferences), getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(), getDataGroup(),
getExportGroup(),
) + getSyncPreferences(syncPreferences = syncPreferences, syncService = syncService) ) + getSyncPreferences(syncPreferences = syncPreferences, syncService = syncService)
} }
@@ -255,8 +272,7 @@ object SettingsDataScreen : SearchableSettings {
// Automatic backups // Automatic backups
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = backupPreferences.backupInterval(), preference = backupPreferences.backupInterval(),
title = stringResource(MR.strings.pref_backup_interval),
entries = persistentMapOf( entries = persistentMapOf(
0 to stringResource(MR.strings.off), 0 to stringResource(MR.strings.off),
6 to stringResource(MR.strings.update_6hour), 6 to stringResource(MR.strings.update_6hour),
@@ -265,6 +281,7 @@ object SettingsDataScreen : SearchableSettings {
48 to stringResource(MR.strings.update_48hour), 48 to stringResource(MR.strings.update_48hour),
168 to stringResource(MR.strings.update_weekly), 168 to stringResource(MR.strings.update_weekly),
), ),
title = stringResource(MR.strings.pref_backup_interval),
onValueChanged = { onValueChanged = {
BackupCreateJob.setupTask(context, it) BackupCreateJob.setupTask(context, it)
true true
@@ -348,13 +365,151 @@ object SettingsDataScreen : SearchableSettings {
), ),
// SY <-- // SY <--
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoClearChapterCache(), preference = libraryPreferences.autoClearChapterCache(),
title = stringResource(MR.strings.pref_auto_clear_chapter_cache), 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 @Composable
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> { private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
return listOf( return listOf(
@@ -362,7 +517,7 @@ object SettingsDataScreen : SearchableSettings {
title = stringResource(SYMR.strings.pref_sync_service_category), title = stringResource(SYMR.strings.pref_sync_service_category),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = syncPreferences.syncService(), preference = syncPreferences.syncService(),
title = stringResource(SYMR.strings.pref_sync_service), title = stringResource(SYMR.strings.pref_sync_service),
entries = persistentMapOf( entries = persistentMapOf(
SyncManager.SyncService.NONE.value to stringResource(MR.strings.off), SyncManager.SyncService.NONE.value to stringResource(MR.strings.off),
@@ -502,11 +657,27 @@ object SettingsDataScreen : SearchableSettings {
@Composable @Composable
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> { private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
val scope = rememberCoroutineScope() 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( return listOf(
Preference.PreferenceItem.EditTextPreference( Preference.PreferenceItem.EditTextPreference(
title = stringResource(SYMR.strings.pref_sync_host), title = stringResource(SYMR.strings.pref_sync_host),
subtitle = stringResource(SYMR.strings.pref_sync_host_summ), subtitle = stringResource(SYMR.strings.pref_sync_host_summ),
pref = syncPreferences.clientHost(), preference = syncPreferences.clientHost(),
onValueChanged = { newValue -> onValueChanged = { newValue ->
scope.launch { scope.launch {
// Trim spaces at the beginning and end, then remove trailing slash if present // Trim spaces at the beginning and end, then remove trailing slash if present
@@ -517,11 +688,32 @@ object SettingsDataScreen : SearchableSettings {
true true
}, },
), ),
Preference.PreferenceItem.EditTextPreference( Preference.PreferenceItem.CustomPreference(
title = stringResource(SYMR.strings.pref_sync_api_key), 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), title = stringResource(SYMR.strings.pref_sync_automatic_category),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = syncIntervalPref, preference = syncIntervalPref,
title = stringResource(SYMR.strings.pref_sync_interval), title = stringResource(SYMR.strings.pref_sync_interval),
entries = persistentMapOf( entries = persistentMapOf(
0 to stringResource(MR.strings.off), 0 to stringResource(MR.strings.off),
@@ -591,4 +783,5 @@ object SettingsDataScreen : SearchableSettings {
), ),
) )
} }
// SY <--
} }
@@ -39,15 +39,15 @@ object SettingsDownloadScreen : SearchableSettings {
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() } val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
return listOf( return listOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.downloadOnlyOverWifi(), preference = downloadPreferences.downloadOnlyOverWifi(),
title = stringResource(MR.strings.connected_to_wifi), title = stringResource(MR.strings.connected_to_wifi),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.saveChaptersAsCBZ(), preference = downloadPreferences.saveChaptersAsCBZ(),
title = stringResource(MR.strings.save_chapter_as_cbz), title = stringResource(MR.strings.save_chapter_as_cbz),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.splitTallImages(), preference = downloadPreferences.splitTallImages(),
title = stringResource(MR.strings.split_tall_images), title = stringResource(MR.strings.split_tall_images),
subtitle = stringResource(MR.strings.split_tall_images_summary), subtitle = stringResource(MR.strings.split_tall_images_summary),
), ),
@@ -72,12 +72,11 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_delete_chapters), title = stringResource(MR.strings.pref_category_delete_chapters),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.removeAfterMarkedAsRead(), preference = downloadPreferences.removeAfterMarkedAsRead(),
title = stringResource(MR.strings.pref_remove_after_marked_as_read), title = stringResource(MR.strings.pref_remove_after_marked_as_read),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.removeAfterReadSlots(), preference = downloadPreferences.removeAfterReadSlots(),
title = stringResource(MR.strings.pref_remove_after_read),
entries = persistentMapOf( entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled), -1 to stringResource(MR.strings.disabled),
0 to stringResource(MR.strings.last_read_chapter), 0 to stringResource(MR.strings.last_read_chapter),
@@ -86,9 +85,10 @@ object SettingsDownloadScreen : SearchableSettings {
3 to stringResource(MR.strings.fourth_to_last), 3 to stringResource(MR.strings.fourth_to_last),
4 to stringResource(MR.strings.fifth_to_last), 4 to stringResource(MR.strings.fifth_to_last),
), ),
title = stringResource(MR.strings.pref_remove_after_read),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.removeBookmarkedChapters(), preference = downloadPreferences.removeBookmarkedChapters(),
title = stringResource(MR.strings.pref_remove_bookmarked_chapters), title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
), ),
getExcludedCategoriesPreference( getExcludedCategoriesPreference(
@@ -105,11 +105,11 @@ object SettingsDownloadScreen : SearchableSettings {
categories: () -> List<Category>, categories: () -> List<Category>,
): Preference.PreferenceItem.MultiSelectListPreference { ): Preference.PreferenceItem.MultiSelectListPreference {
return Preference.PreferenceItem.MultiSelectListPreference( return Preference.PreferenceItem.MultiSelectListPreference(
pref = downloadPreferences.removeExcludeCategories(), preference = downloadPreferences.removeExcludeCategories(),
title = stringResource(MR.strings.pref_remove_exclude_categories),
entries = categories() entries = categories()
.associate { it.id.toString() to it.visualName } .associate { it.id.toString() to it.visualName }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_remove_exclude_categories),
) )
} }
@@ -149,11 +149,11 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_auto_download), title = stringResource(MR.strings.pref_category_auto_download),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = downloadNewChaptersPref, preference = downloadNewChaptersPref,
title = stringResource(MR.strings.pref_download_new), title = stringResource(MR.strings.pref_download_new),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = downloadNewUnreadChaptersOnlyPref, preference = downloadNewUnreadChaptersOnlyPref,
title = stringResource(MR.strings.pref_download_new_unread_chapters_only), title = stringResource(MR.strings.pref_download_new_unread_chapters_only),
enabled = downloadNewChapters, enabled = downloadNewChapters,
), ),
@@ -164,8 +164,8 @@ object SettingsDownloadScreen : SearchableSettings {
included = included, included = included,
excluded = excluded, excluded = excluded,
), ),
onClick = { showDialog = true },
enabled = downloadNewChapters, enabled = downloadNewChapters,
onClick = { showDialog = true },
), ),
), ),
) )
@@ -179,8 +179,7 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.download_ahead), title = stringResource(MR.strings.download_ahead),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.autoDownloadWhileReading(), preference = downloadPreferences.autoDownloadWhileReading(),
title = stringResource(MR.strings.auto_download_while_reading),
entries = listOf(0, 2, 3, 5, 10) entries = listOf(0, 2, 3, 5, 10)
.associateWith { .associateWith {
if (it == 0) { if (it == 0) {
@@ -190,6 +189,7 @@ object SettingsDownloadScreen : SearchableSettings {
} }
} }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.auto_download_while_reading),
), ),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)),
), ),
@@ -43,7 +43,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat.startActivity
import eu.kanade.presentation.library.components.SyncFavoritesWarningDialog import eu.kanade.presentation.library.components.SyncFavoritesWarningDialog
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
@@ -194,7 +193,7 @@ object SettingsEhScreen : SearchableSettings {
val context = LocalContext.current val context = LocalContext.current
val value by unsortedPreferences.enableExhentai().collectAsState() val value by unsortedPreferences.enableExhentai().collectAsState()
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.enableExhentai(), preference = unsortedPreferences.enableExhentai(),
title = stringResource(SYMR.strings.enable_exhentai), title = stringResource(SYMR.strings.enable_exhentai),
subtitle = if (!value) { subtitle = if (!value) {
stringResource(SYMR.strings.requires_login) stringResource(SYMR.strings.requires_login)
@@ -219,7 +218,7 @@ object SettingsEhScreen : SearchableSettings {
unsortedPreferences: UnsortedPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.ListPreference<Int> { ): Preference.PreferenceItem.ListPreference<Int> {
return Preference.PreferenceItem.ListPreference( return Preference.PreferenceItem.ListPreference(
pref = unsortedPreferences.useHentaiAtHome(), preference = unsortedPreferences.useHentaiAtHome(),
title = stringResource(SYMR.strings.use_hentai_at_home), title = stringResource(SYMR.strings.use_hentai_at_home),
subtitle = stringResource(SYMR.strings.use_hentai_at_home_summary), subtitle = stringResource(SYMR.strings.use_hentai_at_home_summary),
entries = persistentMapOf( entries = persistentMapOf(
@@ -237,7 +236,7 @@ object SettingsEhScreen : SearchableSettings {
): Preference.PreferenceItem.SwitchPreference { ): Preference.PreferenceItem.SwitchPreference {
val value by unsortedPreferences.useJapaneseTitle().collectAsState() val value by unsortedPreferences.useJapaneseTitle().collectAsState()
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.useJapaneseTitle(), preference = unsortedPreferences.useJapaneseTitle(),
title = stringResource(SYMR.strings.show_japanese_titles), title = stringResource(SYMR.strings.show_japanese_titles),
subtitle = if (value) { subtitle = if (value) {
stringResource(SYMR.strings.show_japanese_titles_option_1) stringResource(SYMR.strings.show_japanese_titles_option_1)
@@ -255,7 +254,7 @@ object SettingsEhScreen : SearchableSettings {
): Preference.PreferenceItem.SwitchPreference { ): Preference.PreferenceItem.SwitchPreference {
val value by unsortedPreferences.exhUseOriginalImages().collectAsState() val value by unsortedPreferences.exhUseOriginalImages().collectAsState()
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.exhUseOriginalImages(), preference = unsortedPreferences.exhUseOriginalImages(),
title = stringResource(SYMR.strings.use_original_images), title = stringResource(SYMR.strings.use_original_images),
subtitle = if (value) { subtitle = if (value) {
stringResource(SYMR.strings.use_original_images_on) stringResource(SYMR.strings.use_original_images_on)
@@ -273,8 +272,7 @@ object SettingsEhScreen : SearchableSettings {
title = stringResource(SYMR.strings.watched_tags), title = stringResource(SYMR.strings.watched_tags),
subtitle = stringResource(SYMR.strings.watched_tags_summary), subtitle = stringResource(SYMR.strings.watched_tags_summary),
onClick = { onClick = {
startActivity( context.startActivity(
context,
WebViewActivity.newIntent( WebViewActivity.newIntent(
context, context,
url = "https://exhentai.org/mytags", url = "https://exhentai.org/mytags",
@@ -802,7 +800,7 @@ object SettingsEhScreen : SearchableSettings {
unsortedPreferences: UnsortedPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.SwitchPreference { ): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.exhWatchedListDefaultState(), preference = unsortedPreferences.exhWatchedListDefaultState(),
title = stringResource(SYMR.strings.watched_list_default), title = stringResource(SYMR.strings.watched_list_default),
subtitle = stringResource(SYMR.strings.watched_list_state_summary), subtitle = stringResource(SYMR.strings.watched_list_state_summary),
enabled = exhentaiEnabled, enabled = exhentaiEnabled,
@@ -815,7 +813,7 @@ object SettingsEhScreen : SearchableSettings {
unsortedPreferences: UnsortedPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.ListPreference<String> { ): Preference.PreferenceItem.ListPreference<String> {
return Preference.PreferenceItem.ListPreference( return Preference.PreferenceItem.ListPreference(
pref = unsortedPreferences.imageQuality(), preference = unsortedPreferences.imageQuality(),
title = stringResource(SYMR.strings.eh_image_quality_summary), title = stringResource(SYMR.strings.eh_image_quality_summary),
subtitle = stringResource(SYMR.strings.eh_image_quality), subtitle = stringResource(SYMR.strings.eh_image_quality),
entries = persistentMapOf( entries = persistentMapOf(
@@ -833,7 +831,7 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun enhancedEhentaiView(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference { fun enhancedEhentaiView(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.enhancedEHentaiView(), preference = unsortedPreferences.enhancedEHentaiView(),
title = stringResource(SYMR.strings.pref_enhanced_e_hentai_view), title = stringResource(SYMR.strings.pref_enhanced_e_hentai_view),
subtitle = stringResource(SYMR.strings.pref_enhanced_e_hentai_view_summary), subtitle = stringResource(SYMR.strings.pref_enhanced_e_hentai_view_summary),
) )
@@ -842,7 +840,7 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun readOnlySync(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference { fun readOnlySync(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.exhReadOnlySync(), preference = unsortedPreferences.exhReadOnlySync(),
title = stringResource(SYMR.strings.disable_favorites_uploading), title = stringResource(SYMR.strings.disable_favorites_uploading),
subtitle = stringResource(SYMR.strings.disable_favorites_uploading_summary), subtitle = stringResource(SYMR.strings.disable_favorites_uploading_summary),
) )
@@ -867,7 +865,7 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun lenientSync(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference { fun lenientSync(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.exhLenientSync(), preference = unsortedPreferences.exhLenientSync(),
title = stringResource(SYMR.strings.ignore_sync_errors), title = stringResource(SYMR.strings.ignore_sync_errors),
subtitle = stringResource(SYMR.strings.ignore_sync_errors_summary), subtitle = stringResource(SYMR.strings.ignore_sync_errors_summary),
) )
@@ -942,7 +940,7 @@ object SettingsEhScreen : SearchableSettings {
val value by unsortedPreferences.exhAutoUpdateFrequency().collectAsState() val value by unsortedPreferences.exhAutoUpdateFrequency().collectAsState()
val context = LocalContext.current val context = LocalContext.current
return Preference.PreferenceItem.ListPreference( return Preference.PreferenceItem.ListPreference(
pref = unsortedPreferences.exhAutoUpdateFrequency(), preference = unsortedPreferences.exhAutoUpdateFrequency(),
title = stringResource(SYMR.strings.time_between_batches), title = stringResource(SYMR.strings.time_between_batches),
subtitle = if (value == 0) { subtitle = if (value == 0) {
stringResource(SYMR.strings.time_between_batches_summary_1, stringResource(MR.strings.app_name)) stringResource(SYMR.strings.time_between_batches_summary_1, stringResource(MR.strings.app_name))
@@ -978,7 +976,7 @@ object SettingsEhScreen : SearchableSettings {
val value by unsortedPreferences.exhAutoUpdateRequirements().collectAsState() val value by unsortedPreferences.exhAutoUpdateRequirements().collectAsState()
val context = LocalContext.current val context = LocalContext.current
return Preference.PreferenceItem.MultiSelectListPreference( return Preference.PreferenceItem.MultiSelectListPreference(
pref = unsortedPreferences.exhAutoUpdateRequirements(), preference = unsortedPreferences.exhAutoUpdateRequirements(),
title = stringResource(SYMR.strings.auto_update_restrictions), title = stringResource(SYMR.strings.auto_update_restrictions),
subtitle = remember(value) { subtitle = remember(value) {
context.stringResource( context.stringResource(
@@ -38,6 +38,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_COMPLETED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ 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.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.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
@@ -64,7 +66,7 @@ object SettingsLibraryScreen : SearchableSettings {
return listOf( return listOf(
getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences), getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences),
getGlobalUpdateGroup(allCategories, libraryPreferences), getGlobalUpdateGroup(allCategories, libraryPreferences),
getChapterSwipeActionsGroup(libraryPreferences), getBehaviorGroup(libraryPreferences),
// SY --> // SY -->
getSortingCategory(LocalNavigator.currentOrThrow, libraryPreferences), getSortingCategory(LocalNavigator.currentOrThrow, libraryPreferences),
getMigrationCategory(unsortedPreferences), getMigrationCategory(unsortedPreferences),
@@ -100,12 +102,12 @@ object SettingsLibraryScreen : SearchableSettings {
onClick = { navigator.push(CategoryScreen()) }, onClick = { navigator.push(CategoryScreen()) },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.defaultCategory(), preference = libraryPreferences.defaultCategory(),
title = stringResource(MR.strings.default_category),
entries = ids.zip(labels).toMap().toImmutableMap(), entries = ids.zip(labels).toMap().toImmutableMap(),
title = stringResource(MR.strings.default_category),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.categorizedDisplaySettings(), preference = libraryPreferences.categorizedDisplaySettings(),
title = stringResource(MR.strings.categorized_display_settings), title = stringResource(MR.strings.categorized_display_settings),
onValueChanged = { onValueChanged = {
if (!it) { if (!it) {
@@ -157,8 +159,7 @@ object SettingsLibraryScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_library_update), title = stringResource(MR.strings.pref_category_library_update),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = autoUpdateIntervalPref, preference = autoUpdateIntervalPref,
title = stringResource(MR.strings.pref_library_update_interval),
entries = persistentMapOf( entries = persistentMapOf(
0 to stringResource(MR.strings.update_never), 0 to stringResource(MR.strings.update_never),
12 to stringResource(MR.strings.update_12hour), 12 to stringResource(MR.strings.update_12hour),
@@ -167,21 +168,22 @@ object SettingsLibraryScreen : SearchableSettings {
72 to stringResource(MR.strings.update_72hour), 72 to stringResource(MR.strings.update_72hour),
168 to stringResource(MR.strings.update_weekly), 168 to stringResource(MR.strings.update_weekly),
), ),
title = stringResource(MR.strings.pref_library_update_interval),
onValueChanged = { onValueChanged = {
LibraryUpdateJob.setupTask(context, it) LibraryUpdateJob.setupTask(context, it)
true true
}, },
), ),
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryPreferences.autoUpdateDeviceRestrictions(), preference = libraryPreferences.autoUpdateDeviceRestrictions(),
enabled = autoUpdateInterval > 0,
title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions),
entries = persistentMapOf( entries = persistentMapOf(
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi), DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered), DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
DEVICE_CHARGING to stringResource(MR.strings.charging), DEVICE_CHARGING to stringResource(MR.strings.charging),
), ),
title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions),
enabled = autoUpdateInterval > 0,
onValueChanged = { onValueChanged = {
// Post to event looper to allow the preference to be updated. // Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) } ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
@@ -199,7 +201,7 @@ object SettingsLibraryScreen : SearchableSettings {
), ),
// SY --> // SY -->
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.groupLibraryUpdateType(), preference = libraryPreferences.groupLibraryUpdateType(),
title = stringResource(SYMR.strings.library_group_updates), title = stringResource(SYMR.strings.library_group_updates),
entries = persistentMapOf( entries = persistentMapOf(
GroupLibraryMode.GLOBAL to stringResource(SYMR.strings.library_group_updates_global), GroupLibraryMode.GLOBAL to stringResource(SYMR.strings.library_group_updates_global),
@@ -210,45 +212,37 @@ object SettingsLibraryScreen : SearchableSettings {
), ),
// SY <-- // SY <--
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoUpdateMetadata(), preference = libraryPreferences.autoUpdateMetadata(),
title = stringResource(MR.strings.pref_library_update_refresh_metadata), title = stringResource(MR.strings.pref_library_update_refresh_metadata),
subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary), subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary),
), ),
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryPreferences.autoUpdateMangaRestrictions(), preference = libraryPreferences.autoUpdateMangaRestrictions(),
title = stringResource(MR.strings.pref_library_update_smart_update),
entries = persistentMapOf( entries = persistentMapOf(
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read), 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_READ to stringResource(MR.strings.pref_update_only_started),
MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed), 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), 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( Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.newShowUpdatesCount(), preference = libraryPreferences.newShowUpdatesCount(),
title = stringResource(MR.strings.pref_library_update_show_tab_badge), 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 @Composable
private fun getChapterSwipeActionsGroup( private fun getBehaviorGroup(
libraryPreferences: LibraryPreferences, libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_chapter_swipe), title = stringResource(MR.strings.pref_behavior),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeToStartAction(), preference = libraryPreferences.swipeToStartAction(),
title = stringResource(MR.strings.pref_chapter_swipe_start),
entries = persistentMapOf( entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled), stringResource(MR.strings.disabled),
@@ -259,10 +253,10 @@ object SettingsLibraryScreen : SearchableSettings {
LibraryPreferences.ChapterSwipeAction.Download to LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download), stringResource(MR.strings.action_download),
), ),
title = stringResource(MR.strings.pref_chapter_swipe_start),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeToEndAction(), preference = libraryPreferences.swipeToEndAction(),
title = stringResource(MR.strings.pref_chapter_swipe_end),
entries = persistentMapOf( entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled), stringResource(MR.strings.disabled),
@@ -273,6 +267,17 @@ object SettingsLibraryScreen : SearchableSettings {
LibraryPreferences.ChapterSwipeAction.Download to LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download), 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),
), ),
), ),
) )
@@ -305,7 +310,7 @@ object SettingsLibraryScreen : SearchableSettings {
enabled = skipPreMigration || migrationSources.isNotEmpty(), enabled = skipPreMigration || migrationSources.isNotEmpty(),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = unsortedPreferences.skipPreMigration(), preference = unsortedPreferences.skipPreMigration(),
title = stringResource(SYMR.strings.skip_pre_migration), title = stringResource(SYMR.strings.skip_pre_migration),
subtitle = stringResource(SYMR.strings.pref_skip_pre_migration_summary), subtitle = stringResource(SYMR.strings.pref_skip_pre_migration_summary),
), ),
@@ -178,7 +178,7 @@ object SettingsMangadexScreen : SearchableSettings {
sourcePreferences: SourcePreferences, sourcePreferences: SourcePreferences,
): Preference.PreferenceItem.ListPreference<String> { ): Preference.PreferenceItem.ListPreference<String> {
return Preference.PreferenceItem.ListPreference( return Preference.PreferenceItem.ListPreference(
pref = unsortedPreferences.preferredMangaDexId(), preference = unsortedPreferences.preferredMangaDexId(),
title = stringResource(SYMR.strings.mangadex_preffered_source), title = stringResource(SYMR.strings.mangadex_preffered_source),
subtitle = stringResource(SYMR.strings.mangadex_preffered_source_summary), subtitle = stringResource(SYMR.strings.mangadex_preffered_source_summary),
entries = MdUtil.getEnabledMangaDexs(sourcePreferences) entries = MdUtil.getEnabledMangaDexs(sourcePreferences)
@@ -39,45 +39,45 @@ object SettingsReaderScreen : SearchableSettings {
return listOf( return listOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPref.defaultReadingMode(), preference = readerPref.defaultReadingMode(),
title = stringResource(MR.strings.pref_viewer_type),
entries = ReadingMode.entries.drop(1) entries = ReadingMode.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) } .associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_type),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPref.doubleTapAnimSpeed(), preference = readerPref.doubleTapAnimSpeed(),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
entries = persistentMapOf( entries = persistentMapOf(
1 to stringResource(MR.strings.double_tap_anim_speed_0), 1 to stringResource(MR.strings.double_tap_anim_speed_0),
500 to stringResource(MR.strings.double_tap_anim_speed_normal), 500 to stringResource(MR.strings.double_tap_anim_speed_normal),
250 to stringResource(MR.strings.double_tap_anim_speed_fast), 250 to stringResource(MR.strings.double_tap_anim_speed_fast),
), ),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPref.showReadingMode(), preference = readerPref.showReadingMode(),
title = stringResource(MR.strings.pref_show_reading_mode), title = stringResource(MR.strings.pref_show_reading_mode),
subtitle = stringResource(MR.strings.pref_show_reading_mode_summary), subtitle = stringResource(MR.strings.pref_show_reading_mode_summary),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPref.showNavigationOverlayOnStart(), preference = readerPref.showNavigationOverlayOnStart(),
title = stringResource(MR.strings.pref_show_navigation_mode), title = stringResource(MR.strings.pref_show_navigation_mode),
subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary), subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary),
), ),
// SY --> // SY -->
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPref.forceHorizontalSeekbar(), preference = readerPref.forceHorizontalSeekbar(),
title = stringResource(SYMR.strings.pref_force_horz_seekbar), title = stringResource(SYMR.strings.pref_force_horz_seekbar),
subtitle = stringResource(SYMR.strings.pref_force_horz_seekbar_summary), subtitle = stringResource(SYMR.strings.pref_force_horz_seekbar_summary),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPref.landscapeVerticalSeekbar(), preference = readerPref.landscapeVerticalSeekbar(),
title = stringResource(SYMR.strings.pref_show_vert_seekbar_landscape), title = stringResource(SYMR.strings.pref_show_vert_seekbar_landscape),
subtitle = stringResource(SYMR.strings.pref_show_vert_seekbar_landscape_summary), subtitle = stringResource(SYMR.strings.pref_show_vert_seekbar_landscape_summary),
enabled = !forceHorizontalSeekbar, enabled = !forceHorizontalSeekbar,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPref.leftVerticalSeekbar(), preference = readerPref.leftVerticalSeekbar(),
title = stringResource(SYMR.strings.pref_left_handed_vertical_seekbar), title = stringResource(SYMR.strings.pref_left_handed_vertical_seekbar),
subtitle = stringResource(SYMR.strings.pref_left_handed_vertical_seekbar_summary), subtitle = stringResource(SYMR.strings.pref_left_handed_vertical_seekbar_summary),
enabled = !forceHorizontalSeekbar, enabled = !forceHorizontalSeekbar,
@@ -85,7 +85,7 @@ object SettingsReaderScreen : SearchableSettings {
// SY <-- // SY <--
/* SY --> /* SY -->
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPref.pageTransitions(), preference = readerPref.pageTransitions(),
title = stringResource(MR.strings.pref_page_transitions), title = stringResource(MR.strings.pref_page_transitions),
), ),
SY <-- */ SY <-- */
@@ -114,39 +114,39 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_display), title = stringResource(MR.strings.pref_category_display),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.defaultOrientationType(), preference = readerPreferences.defaultOrientationType(),
title = stringResource(MR.strings.pref_rotation_type),
entries = ReaderOrientation.entries.drop(1) entries = ReaderOrientation.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) } .associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_rotation_type),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerTheme(), preference = readerPreferences.readerTheme(),
title = stringResource(MR.strings.pref_reader_theme),
entries = persistentMapOf( entries = persistentMapOf(
1 to stringResource(MR.strings.black_background), 1 to stringResource(MR.strings.black_background),
2 to stringResource(MR.strings.gray_background), 2 to stringResource(MR.strings.gray_background),
0 to stringResource(MR.strings.white_background), 0 to stringResource(MR.strings.white_background),
3 to stringResource(MR.strings.automatic_background), 3 to stringResource(MR.strings.automatic_background),
), ),
title = stringResource(MR.strings.pref_reader_theme),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = fullscreenPref, preference = fullscreenPref,
title = stringResource(MR.strings.pref_fullscreen), title = stringResource(MR.strings.pref_fullscreen),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cutoutShort(), preference = readerPreferences.cutoutShort(),
title = stringResource(MR.strings.pref_cutout_short), title = stringResource(MR.strings.pref_cutout_short),
enabled = fullscreen && enabled = fullscreen &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.keepScreenOn(), preference = readerPreferences.keepScreenOn(),
title = stringResource(MR.strings.pref_keep_screen_on), title = stringResource(MR.strings.pref_keep_screen_on),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.showPageNumber(), preference = readerPreferences.showPageNumber(),
title = stringResource(MR.strings.pref_show_page_number), title = stringResource(MR.strings.pref_show_page_number),
), ),
), ),
@@ -169,43 +169,41 @@ object SettingsReaderScreen : SearchableSettings {
title = "E-Ink", title = "E-Ink",
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.flashOnPageChange(), preference = readerPreferences.flashOnPageChange(),
title = stringResource(MR.strings.pref_flash_page), title = stringResource(MR.strings.pref_flash_page),
subtitle = stringResource(MR.strings.pref_flash_page_summ), subtitle = stringResource(MR.strings.pref_flash_page_summ),
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION, value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
min = 1, valueRange = 1..15,
max = 15,
title = stringResource(MR.strings.pref_flash_duration), title = stringResource(MR.strings.pref_flash_duration),
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
enabled = flashPageState,
onValueChanged = { onValueChanged = {
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
true true
}, },
enabled = flashPageState,
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = flashInterval, value = flashInterval,
min = 1, valueRange = 1..10,
max = 10,
title = stringResource(MR.strings.pref_flash_page_interval), title = stringResource(MR.strings.pref_flash_page_interval),
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
enabled = flashPageState,
onValueChanged = { onValueChanged = {
flashIntervalPref.set(it) flashIntervalPref.set(it)
true true
}, },
enabled = flashPageState,
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = flashColorPref, preference = flashColorPref,
title = stringResource(MR.strings.pref_flash_with),
entries = persistentMapOf( entries = persistentMapOf(
ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black), 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 to stringResource(MR.strings.pref_flash_style_white),
ReaderPreferences.FlashColor.WHITE_BLACK ReaderPreferences.FlashColor.WHITE_BLACK
to stringResource(MR.strings.pref_flash_style_white_black), to stringResource(MR.strings.pref_flash_style_white_black),
), ),
title = stringResource(MR.strings.pref_flash_with),
enabled = flashPageState, enabled = flashPageState,
), ),
), ),
@@ -218,26 +216,26 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_reading), title = stringResource(MR.strings.pref_category_reading),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipRead(), preference = readerPreferences.skipRead(),
title = stringResource(MR.strings.pref_skip_read_chapters), title = stringResource(MR.strings.pref_skip_read_chapters),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipFiltered(), preference = readerPreferences.skipFiltered(),
title = stringResource(MR.strings.pref_skip_filtered_chapters), title = stringResource(MR.strings.pref_skip_filtered_chapters),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipDupe(), preference = readerPreferences.skipDupe(),
title = stringResource(MR.strings.pref_skip_dupe_chapters), title = stringResource(MR.strings.pref_skip_dupe_chapters),
), ),
// SY --> // SY -->
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.markReadDupe(), preference = readerPreferences.markReadDupe(),
title = stringResource(SYMR.strings.pref_mark_read_dupe_chapters), title = stringResource(SYMR.strings.pref_mark_read_dupe_chapters),
subtitle = stringResource(SYMR.strings.pref_mark_read_dupe_chapters_summary), subtitle = stringResource(SYMR.strings.pref_mark_read_dupe_chapters_summary),
), ),
// SY <-- // SY <--
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.alwaysShowChapterTransition(), preference = readerPreferences.alwaysShowChapterTransition(),
title = stringResource(MR.strings.pref_always_show_chapter_transition), title = stringResource(MR.strings.pref_always_show_chapter_transition),
), ),
), ),
@@ -260,16 +258,15 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pager_viewer), title = stringResource(MR.strings.pager_viewer),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = navModePref, preference = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) } .mapIndexed { index, it -> index to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_nav),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.pagerNavInverted(), preference = readerPreferences.pagerNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
entries = persistentListOf( entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE, ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL, ReaderPreferences.TappingInvertMode.HORIZONTAL,
@@ -278,46 +275,47 @@ object SettingsReaderScreen : SearchableSettings {
) )
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = imageScaleTypePref, preference = imageScaleTypePref,
title = stringResource(MR.strings.pref_image_scale_type),
entries = ReaderPreferences.ImageScaleType entries = ReaderPreferences.ImageScaleType
.mapIndexed { index, it -> index + 1 to stringResource(it) } .mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_image_scale_type),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.zoomStart(), preference = readerPreferences.zoomStart(),
title = stringResource(MR.strings.pref_zoom_start),
entries = ReaderPreferences.ZoomStart entries = ReaderPreferences.ZoomStart
.mapIndexed { index, it -> index + 1 to stringResource(it) } .mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_zoom_start),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBorders(), preference = readerPreferences.cropBorders(),
title = stringResource(MR.strings.pref_crop_borders), title = stringResource(MR.strings.pref_crop_borders),
), ),
// SY --> // SY -->
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.pageTransitionsPager(), preference = readerPreferences.pageTransitionsPager(),
title = stringResource(MR.strings.pref_page_transitions), title = stringResource(MR.strings.pref_page_transitions),
), ),
// SY <-- // SY <--
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.landscapeZoom(), preference = readerPreferences.landscapeZoom(),
title = stringResource(MR.strings.pref_landscape_zoom), title = stringResource(MR.strings.pref_landscape_zoom),
enabled = imageScaleType == 1, enabled = imageScaleType == 1,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.navigateToPan(), preference = readerPreferences.navigateToPan(),
title = stringResource(MR.strings.pref_navigate_pan), title = stringResource(MR.strings.pref_navigate_pan),
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = dualPageSplitPref, preference = dualPageSplitPref,
title = stringResource(MR.strings.pref_dual_page_split), title = stringResource(MR.strings.pref_dual_page_split),
onValueChanged = { onValueChanged = {
rotateToFitPref.set(false) rotateToFitPref.set(false)
@@ -325,13 +323,13 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageInvertPaged(), preference = readerPreferences.dualPageInvertPaged(),
title = stringResource(MR.strings.pref_dual_page_invert), title = stringResource(MR.strings.pref_dual_page_invert),
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
enabled = dualPageSplit, enabled = dualPageSplit,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = rotateToFitPref, preference = rotateToFitPref,
title = stringResource(MR.strings.pref_page_rotate), title = stringResource(MR.strings.pref_page_rotate),
onValueChanged = { onValueChanged = {
dualPageSplitPref.set(false) dualPageSplitPref.set(false)
@@ -339,7 +337,7 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageRotateToFitInvert(), preference = readerPreferences.dualPageRotateToFitInvert(),
title = stringResource(MR.strings.pref_page_rotate_invert), title = stringResource(MR.strings.pref_page_rotate_invert),
enabled = rotateToFit, enabled = rotateToFit,
), ),
@@ -365,16 +363,15 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.webtoon_viewer), title = stringResource(MR.strings.webtoon_viewer),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = navModePref, preference = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) } .mapIndexed { index, it -> index to stringResource(it) }
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_nav),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.webtoonNavInverted(), preference = readerPreferences.webtoonNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
entries = persistentListOf( entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE, ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL, ReaderPreferences.TappingInvertMode.HORIZONTAL,
@@ -383,35 +380,37 @@ object SettingsReaderScreen : SearchableSettings {
) )
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
enabled = navMode != 5, enabled = navMode != 5,
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = webtoonSidePadding, value = webtoonSidePadding,
valueRange = ReaderPreferences.let {
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
},
title = stringResource(MR.strings.pref_webtoon_side_padding), title = stringResource(MR.strings.pref_webtoon_side_padding),
subtitle = numberFormat.format(webtoonSidePadding / 100f), subtitle = numberFormat.format(webtoonSidePadding / 100f),
min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
onValueChanged = { onValueChanged = {
webtoonSidePaddingPref.set(it) webtoonSidePaddingPref.set(it)
true true
}, },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerHideThreshold(), preference = readerPreferences.readerHideThreshold(),
title = stringResource(MR.strings.pref_hide_threshold),
entries = persistentMapOf( entries = persistentMapOf(
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest), ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high), ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low), ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest), ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
), ),
title = stringResource(MR.strings.pref_hide_threshold),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBordersWebtoon(), preference = readerPreferences.cropBordersWebtoon(),
title = stringResource(MR.strings.pref_crop_borders), title = stringResource(MR.strings.pref_crop_borders),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = dualPageSplitPref, preference = dualPageSplitPref,
title = stringResource(MR.strings.pref_dual_page_split), title = stringResource(MR.strings.pref_dual_page_split),
onValueChanged = { onValueChanged = {
rotateToFitPref.set(false) rotateToFitPref.set(false)
@@ -419,13 +418,13 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageInvertWebtoon(), preference = readerPreferences.dualPageInvertWebtoon(),
title = stringResource(MR.strings.pref_dual_page_invert), title = stringResource(MR.strings.pref_dual_page_invert),
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary), subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
enabled = dualPageSplit, enabled = dualPageSplit,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = rotateToFitPref, preference = rotateToFitPref,
title = stringResource(MR.strings.pref_page_rotate), title = stringResource(MR.strings.pref_page_rotate),
onValueChanged = { onValueChanged = {
dualPageSplitPref.set(false) dualPageSplitPref.set(false)
@@ -433,21 +432,21 @@ object SettingsReaderScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageRotateToFitInvertWebtoon(), preference = readerPreferences.dualPageRotateToFitInvertWebtoon(),
title = stringResource(MR.strings.pref_page_rotate_invert), title = stringResource(MR.strings.pref_page_rotate_invert),
enabled = rotateToFit, enabled = rotateToFit,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonDoubleTapZoomEnabled(), preference = readerPreferences.webtoonDoubleTapZoomEnabled(),
title = stringResource(MR.strings.pref_double_tap_zoom), title = stringResource(MR.strings.pref_double_tap_zoom),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonDisableZoomOut(), preference = readerPreferences.webtoonDisableZoomOut(),
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out), title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
), ),
// SY --> // SY -->
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.pageTransitionsWebtoon(), preference = readerPreferences.pageTransitionsWebtoon(),
title = stringResource(MR.strings.pref_page_transitions), title = stringResource(MR.strings.pref_page_transitions),
), ),
// SY <-- // SY <--
@@ -462,12 +461,12 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.vertical_plus_viewer), title = stringResource(MR.strings.vertical_plus_viewer),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.continuousVerticalTappingByPage(), preference = readerPreferences.continuousVerticalTappingByPage(),
title = stringResource(SYMR.strings.tap_scroll_page), title = stringResource(SYMR.strings.tap_scroll_page),
subtitle = stringResource(SYMR.strings.tap_scroll_page_summary), subtitle = stringResource(SYMR.strings.tap_scroll_page_summary),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBordersContinuousVertical(), preference = readerPreferences.cropBordersContinuousVertical(),
title = stringResource(MR.strings.pref_crop_borders), title = stringResource(MR.strings.pref_crop_borders),
), ),
), ),
@@ -483,11 +482,11 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_reader_navigation), title = stringResource(MR.strings.pref_reader_navigation),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readWithVolumeKeysPref, preference = readWithVolumeKeysPref,
title = stringResource(MR.strings.pref_read_with_volume_keys), title = stringResource(MR.strings.pref_read_with_volume_keys),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.readWithVolumeKeysInverted(), preference = readerPreferences.readWithVolumeKeysInverted(),
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted), title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
enabled = readWithVolumeKeys, enabled = readWithVolumeKeys,
), ),
@@ -501,11 +500,11 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_reader_actions), title = stringResource(MR.strings.pref_reader_actions),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.readWithLongTap(), preference = readerPreferences.readWithLongTap(),
title = stringResource(MR.strings.pref_read_with_long_tap), title = stringResource(MR.strings.pref_read_with_long_tap),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.folderPerManga(), preference = readerPreferences.folderPerManga(),
title = stringResource(MR.strings.pref_create_folder_per_manga), title = stringResource(MR.strings.pref_create_folder_per_manga),
subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary), subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary),
), ),
@@ -520,7 +519,7 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(SYMR.strings.page_downloading), title = stringResource(SYMR.strings.page_downloading),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.preloadSize(), preference = readerPreferences.preloadSize(),
title = stringResource(SYMR.strings.reader_preload_amount), title = stringResource(SYMR.strings.reader_preload_amount),
subtitle = stringResource(SYMR.strings.reader_preload_amount_summary), subtitle = stringResource(SYMR.strings.reader_preload_amount_summary),
entries = persistentMapOf( entries = persistentMapOf(
@@ -535,13 +534,13 @@ object SettingsReaderScreen : SearchableSettings {
), ),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerThreads(), preference = readerPreferences.readerThreads(),
title = stringResource(SYMR.strings.download_threads), title = stringResource(SYMR.strings.download_threads),
subtitle = stringResource(SYMR.strings.download_threads_summary), subtitle = stringResource(SYMR.strings.download_threads_summary),
entries = List(5) { it }.associateWith { it.toString() }.toImmutableMap(), entries = List(5) { it }.associateWith { it.toString() }.toImmutableMap(),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.cacheSize(), preference = readerPreferences.cacheSize(),
title = stringResource(SYMR.strings.reader_cache_size), title = stringResource(SYMR.strings.reader_cache_size),
subtitle = stringResource(SYMR.strings.reader_cache_size_summary), subtitle = stringResource(SYMR.strings.reader_cache_size_summary),
entries = persistentMapOf( entries = persistentMapOf(
@@ -564,7 +563,7 @@ object SettingsReaderScreen : SearchableSettings {
), ),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.aggressivePageLoading(), preference = readerPreferences.aggressivePageLoading(),
title = stringResource(SYMR.strings.aggressively_load_pages), title = stringResource(SYMR.strings.aggressively_load_pages),
subtitle = stringResource(SYMR.strings.aggressively_load_pages_summary), subtitle = stringResource(SYMR.strings.aggressively_load_pages_summary),
), ),
@@ -579,21 +578,21 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(SYMR.strings.pref_category_fork), title = stringResource(SYMR.strings.pref_category_fork),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.readerInstantRetry(), preference = readerPreferences.readerInstantRetry(),
title = stringResource(SYMR.strings.skip_queue_on_retry), title = stringResource(SYMR.strings.skip_queue_on_retry),
subtitle = stringResource(SYMR.strings.skip_queue_on_retry_summary), subtitle = stringResource(SYMR.strings.skip_queue_on_retry_summary),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.preserveReadingPosition(), preference = readerPreferences.preserveReadingPosition(),
title = stringResource(SYMR.strings.preserve_reading_position), title = stringResource(SYMR.strings.preserve_reading_position),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.useAutoWebtoon(), preference = readerPreferences.useAutoWebtoon(),
title = stringResource(SYMR.strings.auto_webtoon_mode), title = stringResource(SYMR.strings.auto_webtoon_mode),
subtitle = stringResource(SYMR.strings.auto_webtoon_mode_summary), subtitle = stringResource(SYMR.strings.auto_webtoon_mode_summary),
), ),
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
pref = readerPreferences.readerBottomButtons(), preference = readerPreferences.readerBottomButtons(),
title = stringResource(SYMR.strings.reader_bottom_buttons), title = stringResource(SYMR.strings.reader_bottom_buttons),
subtitle = stringResource(SYMR.strings.reader_bottom_buttons_summary), subtitle = stringResource(SYMR.strings.reader_bottom_buttons_summary),
entries = ReaderBottomButton.entries entries = ReaderBottomButton.entries
@@ -601,7 +600,7 @@ object SettingsReaderScreen : SearchableSettings {
.toImmutableMap(), .toImmutableMap(),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.pageLayout(), preference = readerPreferences.pageLayout(),
title = stringResource(SYMR.strings.page_layout), title = stringResource(SYMR.strings.page_layout),
subtitle = stringResource(SYMR.strings.automatic_can_still_switch), subtitle = stringResource(SYMR.strings.automatic_can_still_switch),
entries = ReaderPreferences.PageLayouts entries = ReaderPreferences.PageLayouts
@@ -610,12 +609,12 @@ object SettingsReaderScreen : SearchableSettings {
.toImmutableMap(), .toImmutableMap(),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.invertDoublePages(), preference = readerPreferences.invertDoublePages(),
title = stringResource(SYMR.strings.invert_double_pages), title = stringResource(SYMR.strings.invert_double_pages),
enabled = pageLayout != PagerConfig.PageLayout.SINGLE_PAGE, enabled = pageLayout != PagerConfig.PageLayout.SINGLE_PAGE,
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.centerMarginType(), preference = readerPreferences.centerMarginType(),
title = stringResource(SYMR.strings.center_margin), title = stringResource(SYMR.strings.center_margin),
subtitle = stringResource(SYMR.strings.pref_center_margin_summary), subtitle = stringResource(SYMR.strings.pref_center_margin_summary),
entries = ReaderPreferences.CenterMarginTypes entries = ReaderPreferences.CenterMarginTypes
@@ -624,7 +623,7 @@ object SettingsReaderScreen : SearchableSettings {
.toImmutableMap(), .toImmutableMap(),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.archiveReaderMode(), preference = readerPreferences.archiveReaderMode(),
title = stringResource(SYMR.strings.pref_archive_reader_mode), title = stringResource(SYMR.strings.pref_archive_reader_mode),
subtitle = stringResource(SYMR.strings.pref_archive_reader_mode_summary), subtitle = stringResource(SYMR.strings.pref_archive_reader_mode_summary),
entries = ReaderPreferences.archiveModeTypes entries = ReaderPreferences.archiveModeTypes
@@ -96,7 +96,7 @@ object SettingsSecurityScreen : SearchableSettings {
title = stringResource(MR.strings.pref_security), title = stringResource(MR.strings.pref_security),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = useAuthPref, preference = useAuthPref,
title = stringResource(MR.strings.lock_with_biometrics), title = stringResource(MR.strings.lock_with_biometrics),
enabled = authSupported, enabled = authSupported,
onValueChanged = { onValueChanged = {
@@ -106,9 +106,7 @@ object SettingsSecurityScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = securityPreferences.lockAppAfter(), preference = securityPreferences.lockAppAfter(),
title = stringResource(MR.strings.lock_when_idle),
enabled = authSupported && useAuth,
entries = LockAfterValues entries = LockAfterValues
.associateWith { .associateWith {
when (it) { when (it) {
@@ -118,6 +116,8 @@ object SettingsSecurityScreen : SearchableSettings {
} }
} }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.lock_when_idle),
enabled = authSupported && useAuth,
onValueChanged = { onValueChanged = {
(context as FragmentActivity).authenticate( (context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.lock_when_idle), title = context.stringResource(MR.strings.lock_when_idle),
@@ -125,25 +125,25 @@ object SettingsSecurityScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.hideNotificationContent(), preference = securityPreferences.hideNotificationContent(),
title = stringResource(MR.strings.hide_notification_content), title = stringResource(MR.strings.hide_notification_content),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = securityPreferences.secureScreen(), preference = securityPreferences.secureScreen(),
title = stringResource(MR.strings.secure_screen),
entries = SecurityPreferences.SecureScreenMode.entries entries = SecurityPreferences.SecureScreenMode.entries
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toImmutableMap(), .toImmutableMap(),
title = stringResource(MR.strings.secure_screen),
), ),
// SY --> // SY -->
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.passwordProtectDownloads(), preference = securityPreferences.passwordProtectDownloads(),
title = stringResource(SYMR.strings.password_protect_downloads), title = stringResource(SYMR.strings.password_protect_downloads),
subtitle = stringResource(SYMR.strings.password_protect_downloads_summary), subtitle = stringResource(SYMR.strings.password_protect_downloads_summary),
enabled = isCbzPasswordSet, enabled = isCbzPasswordSet,
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = securityPreferences.encryptionType(), preference = securityPreferences.encryptionType(),
title = stringResource(SYMR.strings.encryption_type), title = stringResource(SYMR.strings.encryption_type),
entries = SecurityPreferences.EncryptionType.entries entries = SecurityPreferences.EncryptionType.entries
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
@@ -384,12 +384,12 @@ object SettingsSecurityScreen : SearchableSettings {
title = stringResource(MR.strings.pref_firebase), title = stringResource(MR.strings.pref_firebase),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.crashlytics(), preference = privacyPreferences.crashlytics(),
title = stringResource(MR.strings.onboarding_permission_crashlytics), title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description), subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.analytics(), preference = privacyPreferences.analytics(),
title = stringResource(MR.strings.onboarding_permission_analytics), title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description), subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
), ),
@@ -59,6 +59,7 @@ import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -125,51 +126,52 @@ object SettingsTrackingScreen : SearchableSettings {
return listOf( return listOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = trackPreferences.autoUpdateTrack(), preference = trackPreferences.autoUpdateTrack(),
title = stringResource(MR.strings.pref_auto_update_manga_sync), title = stringResource(MR.strings.pref_auto_update_manga_sync),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = trackPreferences.autoUpdateTrackOnMarkRead(), preference = trackPreferences.autoUpdateTrackOnMarkRead(),
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
entries = AutoTrackState.entries entries = AutoTrackState.entries
.associateWith { stringResource(it.titleRes) } .associateWith { stringResource(it.titleRes) }
.toPersistentMap(), .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( Preference.PreferenceGroup(
title = stringResource(MR.strings.services), title = stringResource(MR.strings.services),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.myAnimeList.name,
tracker = trackerManager.myAnimeList, tracker = trackerManager.myAnimeList,
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.myAnimeList) }, logout = { dialog = LogoutDialog(trackerManager.myAnimeList) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.aniList.name,
tracker = trackerManager.aniList, tracker = trackerManager.aniList,
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.aniList) }, logout = { dialog = LogoutDialog(trackerManager.aniList) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.kitsu.name,
tracker = trackerManager.kitsu, tracker = trackerManager.kitsu,
login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) }, login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) },
logout = { dialog = LogoutDialog(trackerManager.kitsu) }, logout = { dialog = LogoutDialog(trackerManager.kitsu) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.mangaUpdates.name,
tracker = trackerManager.mangaUpdates, tracker = trackerManager.mangaUpdates,
login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) }, login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) },
logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) }, logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.shikimori.name,
tracker = trackerManager.shikimori, tracker = trackerManager.shikimori,
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.shikimori) }, logout = { dialog = LogoutDialog(trackerManager.shikimori) },
), ),
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = trackerManager.bangumi.name,
tracker = trackerManager.bangumi, tracker = trackerManager.bangumi,
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.bangumi) }, logout = { dialog = LogoutDialog(trackerManager.bangumi) },
@@ -183,7 +185,6 @@ object SettingsTrackingScreen : SearchableSettings {
enhancedTrackers.first enhancedTrackers.first
.map { service -> .map { service ->
Preference.PreferenceItem.TrackerPreference( Preference.PreferenceItem.TrackerPreference(
title = service.name,
tracker = service, tracker = service,
login = { (service as EnhancedTracker).loginNoop() }, login = { (service as EnhancedTracker).loginNoop() },
logout = service::logout, logout = service::logout,
@@ -31,6 +31,7 @@ fun EditTextPreferenceWidget(
subtitle: String?, subtitle: String?,
icon: ImageVector?, icon: ImageVector?,
value: String, value: String,
widget: @Composable (() -> Unit)? = null,
onConfirm: suspend (String) -> Boolean, onConfirm: suspend (String) -> Boolean,
) { ) {
var isDialogShown by remember { mutableStateOf(false) } var isDialogShown by remember { mutableStateOf(false) }
@@ -39,6 +40,7 @@ fun EditTextPreferenceWidget(
title = title, title = title,
subtitle = subtitle?.format(value), subtitle = subtitle?.format(value),
icon = icon, icon = icon,
widget = widget,
onPreferenceClick = { isDialogShown = true }, onPreferenceClick = { isDialogShown = true },
) )
@@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -36,12 +37,12 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
if (customBrightness) { if (customBrightness) {
val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState() val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState()
SliderItem( SliderItem(
label = stringResource(MR.strings.pref_custom_brightness),
min = -75,
max = 100,
value = customBrightnessValue, value = customBrightnessValue,
valueText = customBrightnessValue.toString(), valueRange = -75..100,
steps = 0,
label = stringResource(MR.strings.pref_custom_brightness),
onChange = { screenModel.preferences.customBrightnessValue().set(it) }, onChange = { screenModel.preferences.customBrightnessValue().set(it) },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
} }
@@ -53,48 +54,52 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
if (colorFilter) { if (colorFilter) {
val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState() val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState()
SliderItem( SliderItem(
label = stringResource(MR.strings.color_filter_r_value),
max = 255,
value = colorFilterValue.red, value = colorFilterValue.red,
valueText = colorFilterValue.red.toString(), valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_r_value),
onChange = { newRValue -> onChange = { newRValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, RED_MASK, 16) getColorValue(it, newRValue, RED_MASK, 16)
} }
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
SliderItem( SliderItem(
label = stringResource(MR.strings.color_filter_g_value),
max = 255,
value = colorFilterValue.green, value = colorFilterValue.green,
valueText = colorFilterValue.green.toString(), valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_g_value),
onChange = { newGValue -> onChange = { newGValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newGValue, GREEN_MASK, 8) getColorValue(it, newGValue, GREEN_MASK, 8)
} }
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
SliderItem( SliderItem(
label = stringResource(MR.strings.color_filter_b_value),
max = 255,
value = colorFilterValue.blue, value = colorFilterValue.blue,
valueText = colorFilterValue.blue.toString(), valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_b_value),
onChange = { newBValue -> onChange = { newBValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newBValue, BLUE_MASK, 0) getColorValue(it, newBValue, BLUE_MASK, 0)
} }
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
SliderItem( SliderItem(
label = stringResource(MR.strings.color_filter_a_value),
max = 255,
value = colorFilterValue.alpha, value = colorFilterValue.alpha,
valueText = colorFilterValue.alpha.toString(), valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_a_value),
onChange = { newAValue -> onChange = { newAValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newAValue, ALPHA_MASK, 24) getColorValue(it, newAValue, ALPHA_MASK, 24)
} }
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState() val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState()
@@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -119,21 +120,21 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
if (flashPageState) { if (flashPageState) {
SliderItem( SliderItem(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION, value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
valueRange = 1..15,
label = stringResource(MR.strings.pref_flash_duration), label = stringResource(MR.strings.pref_flash_duration),
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) }, onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
min = 1, pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
max = 15,
) )
SliderItem( SliderItem(
value = flashInterval, value = flashInterval,
valueRange = 1..10,
label = stringResource(MR.strings.pref_flash_page_interval), label = stringResource(MR.strings.pref_flash_page_interval),
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
onChange = { onChange = {
flashIntervalPref.set(it) flashIntervalPref.set(it)
}, },
min = 1, pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
max = 10,
) )
SettingsChipRow(MR.strings.pref_flash_with) { SettingsChipRow(MR.strings.pref_flash_with) {
flashColors.map { (labelRes, value) -> flashColors.map { (labelRes, value) ->
@@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -192,14 +193,14 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState() val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
SliderItem( SliderItem(
label = stringResource(MR.strings.pref_webtoon_side_padding),
min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
value = webtoonSidePadding, 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), valueText = numberFormat.format(webtoonSidePadding / 100f),
onChange = { onChange = {
screenModel.preferences.webtoonSidePadding().set(it) screenModel.preferences.webtoonSidePadding().set(it)
}, },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
CheckboxItem( CheckboxItem(
@@ -13,6 +13,7 @@ import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
import eu.kanade.presentation.theme.colorscheme.MonetColorScheme 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.NordColorScheme
import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme
import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme
@@ -79,6 +80,7 @@ private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
AppTheme.GREEN_APPLE to GreenAppleColorScheme, AppTheme.GREEN_APPLE to GreenAppleColorScheme,
AppTheme.LAVENDER to LavenderColorScheme, AppTheme.LAVENDER to LavenderColorScheme,
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme, AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
AppTheme.MONOCHROME to MonochromeColorScheme,
AppTheme.NORD to NordColorScheme, AppTheme.NORD to NordColorScheme,
AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme, AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme,
AppTheme.TAKO to TakoColorScheme, AppTheme.TAKO to TakoColorScheme,
@@ -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),
)
}
@@ -10,10 +10,12 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
@@ -22,6 +24,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert 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.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -70,6 +75,7 @@ fun TrackInfoDialogHome(
onOpenInBrowser: (TrackItem) -> Unit, onOpenInBrowser: (TrackItem) -> Unit,
onRemoved: (TrackItem) -> Unit, onRemoved: (TrackItem) -> Unit,
onCopyLink: (TrackItem) -> Unit, onCopyLink: (TrackItem) -> Unit,
onTogglePrivate: (TrackItem) -> Unit,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -84,6 +90,7 @@ fun TrackInfoDialogHome(
if (item.track != null) { if (item.track != null) {
val supportsScoring = item.tracker.getScoreList().isNotEmpty() val supportsScoring = item.tracker.getScoreList().isNotEmpty()
val supportsReadingDates = item.tracker.supportsReadingDates val supportsReadingDates = item.tracker.supportsReadingDates
val supportsPrivate = item.tracker.supportsPrivateTracking
TrackInfoItem( TrackInfoItem(
title = item.track.title, title = item.track.title,
tracker = item.tracker, tracker = item.tracker,
@@ -115,6 +122,9 @@ fun TrackInfoDialogHome(
onOpenInBrowser = { onOpenInBrowser(item) }, onOpenInBrowser = { onOpenInBrowser(item) },
onRemoved = { onRemoved(item) }, onRemoved = { onRemoved(item) },
onCopyLink = { onCopyLink(item) }, onCopyLink = { onCopyLink(item) },
private = item.track.private,
onTogglePrivate = { onTogglePrivate(item) }
.takeIf { supportsPrivate },
) )
} else { } else {
TrackInfoItemEmpty( TrackInfoItemEmpty(
@@ -144,17 +154,37 @@ private fun TrackInfoItem(
onOpenInBrowser: () -> Unit, onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit, onRemoved: () -> Unit,
onCopyLink: () -> Unit, onCopyLink: () -> Unit,
private: Boolean,
onTogglePrivate: (() -> Unit)?,
) { ) {
val context = LocalContext.current val context = LocalContext.current
Column { Column {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
TrackLogoIcon( BadgedBox(
tracker = tracker, badge = {
onClick = onOpenInBrowser, if (private) {
onLongClick = onCopyLink, 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( Box(
modifier = Modifier modifier = Modifier
.height(48.dp) .height(48.dp)
@@ -181,6 +211,8 @@ private fun TrackInfoItem(
onOpenInBrowser = onOpenInBrowser, onOpenInBrowser = onOpenInBrowser,
onRemoved = onRemoved, onRemoved = onRemoved,
onCopyLink = onCopyLink, onCopyLink = onCopyLink,
private = private,
onTogglePrivate = onTogglePrivate,
) )
} }
@@ -291,6 +323,8 @@ private fun TrackInfoItemMenu(
onOpenInBrowser: () -> Unit, onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit, onRemoved: () -> Unit,
onCopyLink: () -> Unit, onCopyLink: () -> Unit,
private: Boolean,
onTogglePrivate: (() -> Unit)?,
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) { Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
@@ -318,6 +352,25 @@ private fun TrackInfoItemMenu(
expanded = false 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( DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_remove)) }, text = { Text(stringResource(MR.strings.action_remove)) },
onClick = { onClick = {
@@ -25,7 +25,9 @@ internal class TrackInfoDialogHomePreviewProvider :
remoteUrl = "https://example.com", remoteUrl = "https://example.com",
startDate = 0L, startDate = 0L,
finishDate = 0L, finishDate = 0L,
private = false,
) )
private val privateTrack = aTrack.copy(private = true)
private val trackItemWithoutTrack = TrackItem( private val trackItemWithoutTrack = TrackItem(
track = null, track = null,
tracker = DummyTracker( tracker = DummyTracker(
@@ -40,6 +42,13 @@ internal class TrackInfoDialogHomePreviewProvider :
name = "Example Tracker 2", name = "Example Tracker 2",
), ),
) )
private val trackItemWithPrivateTrack = TrackItem(
track = privateTrack,
tracker = DummyTracker(
id = 2L,
name = "Example Tracker 2",
),
)
private val trackersWithAndWithoutTrack = @Composable { private val trackersWithAndWithoutTrack = @Composable {
TrackInfoDialogHome( TrackInfoDialogHome(
@@ -57,6 +66,7 @@ internal class TrackInfoDialogHomePreviewProvider :
onOpenInBrowser = {}, onOpenInBrowser = {},
onRemoved = {}, onRemoved = {},
onCopyLink = {}, onCopyLink = {},
onTogglePrivate = {},
) )
} }
@@ -73,6 +83,24 @@ internal class TrackInfoDialogHomePreviewProvider :
onOpenInBrowser = {}, onOpenInBrowser = {},
onRemoved = {}, onRemoved = {},
onCopyLink = {}, 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( get() = sequenceOf(
trackersWithAndWithoutTrack, trackersWithAndWithoutTrack,
noTrackers, noTrackers,
trackerWithPrivateTracking,
) )
} }
@@ -33,6 +33,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
@@ -90,8 +91,9 @@ fun TrackerSearch(
queryResult: Result<List<TrackSearch>>?, queryResult: Result<List<TrackSearch>>?,
selected: TrackSearch?, selected: TrackSearch?,
onSelectedChange: (TrackSearch) -> Unit, onSelectedChange: (TrackSearch) -> Unit,
onConfirmSelection: () -> Unit, onConfirmSelection: (private: Boolean) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
supportsPrivateTracking: Boolean,
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@@ -164,15 +166,31 @@ fun TrackerSearch(
enter = fadeIn() + slideInVertically { it / 2 }, enter = fadeIn() + slideInVertically { it / 2 },
exit = slideOutVertically { it / 2 } + fadeOut(), exit = slideOutVertically { it / 2 } + fadeOut(),
) { ) {
Button( Row(
onClick = { onConfirmSelection() },
modifier = Modifier modifier = Modifier
.padding(12.dp) .padding(MaterialTheme.padding.small)
.windowInsetsPadding(WindowInsets.navigationBars) .windowInsetsPadding(WindowInsets.navigationBars)
.fillMaxWidth(), .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()) { if (type.isNotBlank()) {
SearchResultItemDetails( SearchResultItemDetails(
title = stringResource(MR.strings.track_type), title = stringResource(MR.strings.track_type),
@@ -5,8 +5,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import java.text.SimpleDateFormat
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.Locale
import kotlin.random.Random import kotlin.random.Random
internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> { internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
@@ -20,6 +23,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {}, onSelectedChange = {},
onConfirmSelection = {}, onConfirmSelection = {},
onDismissRequest = {}, onDismissRequest = {},
supportsPrivateTracking = false,
) )
} }
private val fullPageWithoutSelected = @Composable { private val fullPageWithoutSelected = @Composable {
@@ -31,6 +35,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {}, onSelectedChange = {},
onConfirmSelection = {}, onConfirmSelection = {},
onDismissRequest = {}, onDismissRequest = {},
supportsPrivateTracking = false,
) )
} }
private val loading = @Composable { private val loading = @Composable {
@@ -42,12 +47,27 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {}, onSelectedChange = {},
onConfirmSelection = {}, onConfirmSelection = {},
onDismissRequest = {}, 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( override val values: Sequence<@Composable () -> Unit> = sequenceOf(
fullPageWithSecondSelected, fullPageWithSecondSelected,
fullPageWithoutSelected, fullPageWithoutSelected,
loading, loading,
fullPageWithPrivateTracking,
) )
private fun someTrackSearches(): Sequence<TrackSearch> = sequence { 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 { private fun randTrackSearch() = TrackSearch().let {
it.id = Random.nextLong() it.id = Random.nextLong()
it.manga_id = Random.nextLong() it.manga_id = Random.nextLong()
@@ -71,11 +93,17 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
it.finished_reading_date = 0L it.finished_reading_date = 0L
it.tracking_url = "https://example.com/tracker-example" it.tracking_url = "https://example.com/tracker-example"
it.cover_url = "https://example.com/cover.png" 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.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 it
} }
private fun randomNames(): List<String> = (0..(0..3).random()).map { lorem((3..5).random()).joinToString() }
private fun lorem(words: Int): Sequence<String> = private fun lorem(words: Int): Sequence<String> =
LoremIpsum(words).values LoremIpsum(words).values
} }
@@ -3,6 +3,7 @@ package eu.kanade.presentation.webview
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -26,6 +27,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.kevinnzou.web.AccompanistWebViewClient import com.kevinnzou.web.AccompanistWebViewClient
@@ -37,13 +39,18 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.components.WarningBanner
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.getHtml import eu.kanade.tachiyomi.util.system.getHtml
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Request
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable @Composable
fun WebViewScreenContent( fun WebViewScreenContent(
@@ -58,8 +65,11 @@ fun WebViewScreenContent(
) { ) {
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers) val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
val navigator = rememberWebViewNavigator() val navigator = rememberWebViewNavigator()
val context = LocalContext.current
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val network = remember { Injekt.get<NetworkHelper>() }
val spoofedPackageName = remember { WebViewUtil.spoofedPackageName(context) }
var currentUrl by remember { mutableStateOf(url) } var currentUrl by remember { mutableStateOf(url) }
var showCloudflareHelp by remember { mutableStateOf(false) } var showCloudflareHelp by remember { mutableStateOf(false) }
@@ -114,6 +124,40 @@ fun WebViewScreenContent(
} }
return super.shouldOverrideUrlLoading(view, request) return super.shouldOverrideUrlLoading(view, request)
} }
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?,
): WebResourceResponse? {
return try {
val internalRequest = Request.Builder().apply {
url(request!!.url.toString())
request.requestHeaders.forEach { (key, value) ->
if (key == "X-Requested-With" && value in setOf(context.packageName, spoofedPackageName)) {
return@forEach
}
addHeader(key, value)
}
method(request.method, null)
}.build()
val response = network.nonCloudflareClient.newCall(internalRequest).execute()
val contentType = response.body.contentType()?.let { "${it.type}/${it.subtype}" } ?: "text/html"
val contentEncoding = response.body.contentType()?.charset()?.name() ?: "utf-8"
WebResourceResponse(
contentType,
contentEncoding,
response.code,
response.message,
response.headers.associate { it.first to it.second },
response.body.byteStream(),
)
} catch (e: Throwable) {
super.shouldInterceptRequest(view, request)
}
}
} }
} }
+6 -8
View File
@@ -277,18 +277,16 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
try { try {
// Override the value passed as X-Requested-With in WebView requests // Override the value passed as X-Requested-With in WebView requests
val stackTrace = Looper.getMainLooper().thread.stackTrace val stackTrace = Looper.getMainLooper().thread.stackTrace
val chromiumElement = stackTrace.find { val isChromiumCall = stackTrace.any { trace ->
it.className.equals( trace.className.equals("org.chromium.base.BuildInfo", ignoreCase = true) &&
"org.chromium.base.BuildInfo", setOf("getAll", "getPackageName", "<init>").any { trace.methodName.equals(it, ignoreCase = true) }
ignoreCase = true,
)
}
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME
} }
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
} catch (_: Exception) { } catch (_: Exception) {
} }
} }
return super.getPackageName() return super.getPackageName()
} }
@@ -8,6 +8,7 @@ import tachiyomi.domain.category.model.Category
class BackupCategory( class BackupCategory(
@ProtoNumber(1) var name: String, @ProtoNumber(1) var name: String,
@ProtoNumber(2) var order: Long = 0, @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(3) val updateInterval: Int = 0, 1.x value not used in 0.x
@ProtoNumber(100) var flags: Long = 0, @ProtoNumber(100) var flags: Long = 0,
// SY specific values // SY specific values
@@ -24,6 +25,7 @@ class BackupCategory(
val backupCategoryMapper = { category: Category -> val backupCategoryMapper = { category: Category ->
BackupCategory( BackupCategory(
id = category.id,
name = category.name, name = category.name,
order = category.order, order = category.order,
flags = category.flags, flags = category.flags,
@@ -25,6 +25,7 @@ data class BackupTracking(
@ProtoNumber(10) var startedReadingDate: Long = 0, @ProtoNumber(10) var startedReadingDate: Long = 0,
// finishedReadingDate is called endReadTime in 1.x // finishedReadingDate is called endReadTime in 1.x
@ProtoNumber(11) var finishedReadingDate: Long = 0, @ProtoNumber(11) var finishedReadingDate: Long = 0,
@ProtoNumber(12) var private: Boolean = false,
@ProtoNumber(100) var mediaId: Long = 0, @ProtoNumber(100) var mediaId: Long = 0,
) { ) {
@@ -48,6 +49,7 @@ data class BackupTracking(
startDate = this@BackupTracking.startedReadingDate, startDate = this@BackupTracking.startedReadingDate,
finishDate = this@BackupTracking.finishedReadingDate, finishDate = this@BackupTracking.finishedReadingDate,
remoteUrl = this@BackupTracking.trackingUrl, remoteUrl = this@BackupTracking.trackingUrl,
private = this@BackupTracking.private,
) )
} }
} }
@@ -66,6 +68,7 @@ val backupTrackMapper = {
remoteUrl: String, remoteUrl: String,
startDate: Long, startDate: Long,
finishDate: Long, finishDate: Long,
private: Boolean,
-> ->
BackupTracking( BackupTracking(
syncId = syncId.toInt(), syncId = syncId.toInt(),
@@ -80,5 +83,6 @@ val backupTrackMapper = {
startedReadingDate = startDate, startedReadingDate = startDate,
finishedReadingDate = finishDate, finishedReadingDate = finishDate,
trackingUrl = remoteUrl, trackingUrl = remoteUrl,
private = private,
) )
} }
@@ -108,7 +108,7 @@ class BackupRestorer(
} }
// SY <-- // SY <--
if (options.appSettings) { if (options.appSettings) {
restoreAppPreferences(backup.backupPreferences) restoreAppPreferences(backup.backupPreferences, backup.backupCategories.takeIf { options.categories })
} }
if (options.sourceSettings) { if (options.sourceSettings) {
restoreSourcePreferences(backup.backupSourcePreferences) 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() ensureActive()
preferenceRestorer.restoreApp(preferences) preferenceRestorer.restoreApp(
preferences,
categories,
)
restoreProgress += 1 restoreProgress += 1
notifier.showRestoreProgress( notifier.showRestoreProgress(
@@ -447,6 +447,7 @@ class MangaRestorer(
track.remoteUrl, track.remoteUrl,
track.startDate, track.startDate,
track.finishDate, track.finishDate,
track.private,
track.id, track.id,
) )
} }
@@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers package eu.kanade.tachiyomi.data.backup.restore.restorers
import android.content.Context import android.content.Context
import android.util.Log
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob 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.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue 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 eu.kanade.tachiyomi.source.sourcePreferences
import tachiyomi.core.common.preference.AndroidPreferenceStore import tachiyomi.core.common.preference.AndroidPreferenceStore
import tachiyomi.core.common.preference.PreferenceStore 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class PreferenceRestorer( class PreferenceRestorer(
private val context: Context, private val context: Context,
private val getCategories: GetCategories = Injekt.get(),
private val preferenceStore: PreferenceStore = Injekt.get(), private val preferenceStore: PreferenceStore = Injekt.get(),
) { ) {
suspend fun restoreApp(
fun restoreApp(preferences: List<BackupPreference>) { preferences: List<BackupPreference>,
restorePreferences(preferences, preferenceStore) backupCategories: List<BackupCategory>?,
) {
restorePreferences(
preferences,
preferenceStore,
backupCategories,
)
LibraryUpdateJob.setupTask(context) LibraryUpdateJob.setupTask(context)
BackupCreateJob.setupTask(context) BackupCreateJob.setupTask(context)
} }
fun restoreSource(preferences: List<BackupSourcePreferences>) { suspend fun restoreSource(preferences: List<BackupSourcePreferences>) {
preferences.forEach { preferences.forEach {
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey)) val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
restorePreferences(it.prefs, sourcePrefs) restorePreferences(it.prefs, sourcePrefs)
} }
} }
private fun restorePreferences( private suspend fun restorePreferences(
toRestore: List<BackupPreference>, toRestore: List<BackupPreference>,
preferenceStore: PreferenceStore, 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() val prefs = preferenceStore.getAll()
toRestore.forEach { (key, value) -> toRestore.forEach { (key, value) ->
when (value) { try {
is IntPreferenceValue -> { when (value) {
if (prefs[key] is Int?) { is IntPreferenceValue -> {
preferenceStore.getInt(key).set(value.value) if (prefs[key] is Int?) {
} val newValue = if (key == LibraryPreferences.DEFAULT_CATEGORY_PREF_KEY) {
} backupCategoriesById[value.value.toString()]
is LongPreferenceValue -> { ?.let { categoriesByName[it.name]?.id?.toInt() }
if (prefs[key] is Long?) { } else {
preferenceStore.getLong(key).set(value.value) value.value
} }
}
is FloatPreferenceValue -> { newValue?.let { preferenceStore.getInt(key).set(it) }
if (prefs[key] is Float?) { }
preferenceStore.getFloat(key).set(value.value) }
} is LongPreferenceValue -> {
} if (prefs[key] is Long?) {
is StringPreferenceValue -> { preferenceStore.getLong(key).set(value.value)
if (prefs[key] is String?) { }
preferenceStore.getString(key).set(value.value) }
} is FloatPreferenceValue -> {
} if (prefs[key] is Float?) {
is BooleanPreferenceValue -> { preferenceStore.getFloat(key).set(value.value)
if (prefs[key] is Boolean?) { }
preferenceStore.getBoolean(key).set(value.value) }
} is StringPreferenceValue -> {
} if (prefs[key] is String?) {
is StringSetPreferenceValue -> { preferenceStore.getString(key).set(value.value)
if (prefs[key] is Set<*>?) { }
preferenceStore.getStringSet(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
}
} }
@@ -27,6 +27,9 @@ interface Chapter : SChapter, Serializable {
var version: Long var version: Long
} }
val Chapter.isRecognizedNumber: Boolean
get() = chapter_number >= 0f
fun Chapter.toDomainChapter(): DomainChapter? { fun Chapter.toDomainChapter(): DomainChapter? {
if (id == null || manga_id == null) return null if (id == null || manga_id == null) return null
return DomainChapter( return DomainChapter(
@@ -32,12 +32,15 @@ interface Track : Serializable {
var tracking_url: String 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 last_chapter_read = other.last_chapter_read
score = other.score score = other.score
status = other.status status = other.status
started_reading_date = other.started_reading_date started_reading_date = other.started_reading_date
finished_reading_date = other.finished_reading_date finished_reading_date = other.finished_reading_date
if (copyRemotePrivate) private = other.private
} }
companion object { companion object {
@@ -29,4 +29,6 @@ class TrackImpl : Track {
override var finished_reading_date: Long = 0 override var finished_reading_date: Long = 0
override var tracking_url: String = "" override var tracking_url: String = ""
override var private: Boolean = false
} }
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.download
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.net.Uri import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
@@ -483,7 +483,7 @@ private object UniFileAsStringSerializer : KSerializer<UniFile?> {
override fun deserialize(decoder: Decoder): UniFile? { override fun deserialize(decoder: Decoder): UniFile? {
return if (decoder.decodeNotNullMark()) { return if (decoder.decodeNotNullMark()) {
UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString())) UniFile.fromUri(Injekt.get<Application>(), decoder.decodeString().toUri())
} else { } else {
decoder.decodeNull() decoder.decodeNull()
} }
@@ -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
}
}
}
}
}
@@ -401,29 +401,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
) { ) {
try { try {
val newChapters = updateManga(manga, fetchWindow) val newChapters = updateManga(manga, fetchWindow)
// SY --> .sortedByDescending { it.sourceOrder }
.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 <--
if (newChapters.isNotEmpty()) { if (newChapters.isNotEmpty()) {
val chaptersToDownload = filterChaptersForDownload.await(manga, newChapters) val chaptersToDownload = filterChaptersForDownload.await(manga, newChapters)
@@ -27,7 +27,7 @@ class SyncNotifier(private val context: Context) {
} }
private val completeNotificationBuilder = context.notificationBuilder( private val completeNotificationBuilder = context.notificationBuilder(
Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS, Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE,
) { ) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_tachi)
@@ -7,6 +7,7 @@ import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -38,6 +39,8 @@ abstract class BaseTracker(
// Application and remote support for reading dates // Application and remote support for reading dates
override val supportsReadingDates: Boolean = false override val supportsReadingDates: Boolean = false
override val supportsPrivateTracking: Boolean = false
// TODO: Store all scores as 10 point in the future maybe? // TODO: Store all scores as 10 point in the future maybe?
override fun get10PointScore(track: DomainTrack): Double { override fun get10PointScore(track: DomainTrack): Double {
return track.score return track.score
@@ -121,10 +124,21 @@ abstract class BaseTracker(
updateRemote(track) updateRemote(track)
} }
override suspend fun setRemotePrivate(track: Track, private: Boolean) {
track.private = private
updateRemote(track)
}
// SY -->
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? { override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
throw NotImplementedError("Not implemented.") throw NotImplementedError("Not implemented.")
} }
override suspend fun searchById(id: String): TrackSearch? {
throw NotImplementedError("Not implemented.")
}
// SY <--
private suspend fun updateRemote(track: Track): Unit = withIOContext { private suspend fun updateRemote(track: Track): Unit = withIOContext {
try { try {
update(track) update(track)
@@ -23,6 +23,8 @@ interface Tracker {
// Application and remote support for reading dates // Application and remote support for reading dates
val supportsReadingDates: Boolean val supportsReadingDates: Boolean
val supportsPrivateTracking: Boolean
@ColorInt @ColorInt
fun getLogoColor(): Int fun getLogoColor(): Int
@@ -84,5 +86,11 @@ interface Tracker {
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long) suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
suspend fun setRemotePrivate(track: Track, private: Boolean)
// SY -->
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata?
suspend fun searchById(id: String): TrackSearch?
// SY <--
} }
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -44,6 +43,8 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
override val supportsReadingDates: Boolean = true override val supportsReadingDates: Boolean = true
override val supportsPrivateTracking: Boolean = true
private val scorePreference = trackPreferences.anilistScoreType() private val scorePreference = trackPreferences.anilistScoreType()
init { init {
@@ -184,7 +185,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val remoteTrack = api.findLibManga(track, getUsername().toInt()) val remoteTrack = api.findLibManga(track, getUsername().toInt())
return if (remoteTrack != null) { return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
track.library_id = remoteTrack.library_id track.library_id = remoteTrack.library_id
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
@@ -237,6 +238,12 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
return api.getMangaMetadata(track) return api.getMangaMetadata(track)
} }
// SY -->
override suspend fun searchById(id: String): TrackSearch {
return api.searchById(id)
}
// SY <--
fun saveOAuth(alOAuth: ALOAuth?) { fun saveOAuth(alOAuth: ALOAuth?) {
trackPreferences.trackToken(this).set(json.encodeToString(alOAuth)) trackPreferences.trackToken(this).set(json.encodeToString(alOAuth))
} }
@@ -5,6 +5,7 @@ import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALIdSearchResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult
@@ -45,8 +46,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun addLibManga(track: Track): Track { suspend fun addLibManga(track: Track): Track {
return withIOContext { return withIOContext {
val query = """ val query = """
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
| id | id
| status | status
|} |}
@@ -59,6 +60,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("mangaId", track.remote_id) put("mangaId", track.remote_id)
put("progress", track.last_chapter_read.toInt()) put("progress", track.last_chapter_read.toInt())
put("status", track.toApiStatus()) put("status", track.toApiStatus())
put("private", track.private)
} }
} }
with(json) { with(json) {
@@ -82,11 +84,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
return withIOContext { return withIOContext {
val query = """ val query = """
|mutation UpdateManga( |mutation UpdateManga(
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, |${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput |${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|) { |) {
|SaveMediaListEntry( |SaveMediaListEntry(
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, |id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt |scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|) { |) {
|id |id
@@ -105,6 +107,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("score", track.score.toInt()) put("score", track.score.toInt())
put("startedAt", createDate(track.started_reading_date)) put("startedAt", createDate(track.started_reading_date))
put("completedAt", createDate(track.finished_reading_date)) put("completedAt", createDate(track.finished_reading_date))
put("private", track.private)
} }
} }
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
@@ -141,6 +144,19 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|Page (perPage: 50) { |Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|id |id
|staff {
|edges {
|role
|id
|node {
|name {
|full
|userPreferred
|native
|}
|}
|}
|}
|title { |title {
|userPreferred |userPreferred
|} |}
@@ -193,6 +209,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|status |status
|scoreRaw: score(format: POINT_100) |scoreRaw: score(format: POINT_100)
|progress |progress
|private
|startedAt { |startedAt {
|year |year
|month |month
@@ -220,6 +237,19 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|month |month
|day |day
|} |}
|staff {
|edges {
|role
|id
|node {
|name {
|full
|userPreferred
|native
|}
|}
|}
|}
|} |}
|} |}
|} |}
@@ -356,6 +386,56 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
} }
// SY -->
suspend fun searchById(id: String): TrackSearch {
return withIOContext {
val query = """
|query (${'$'}mangaId: Int!) {
|Media (id: ${'$'}mangaId) {
|id
|title {
|userPreferred
|}
|coverImage {
|large
|}
|format
|status
|chapters
|description
|startDate {
|year
|month
|day
|}
|averageScore
|}
|}
|
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("mangaId", id)
}
}
with(json) {
authClient.newCall(
POST(
API_URL,
body = payload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
.parseAs<ALIdSearchResult>()
.data.media
.toALManga()
.toTrack()
}
}
}
// SY <--
private fun createDate(dateValue: Long): JsonObject { private fun createDate(dateValue: Long): JsonObject {
if (dateValue == 0L) { if (dateValue == 0L) {
return buildJsonObject { return buildJsonObject {
@@ -19,6 +19,7 @@ data class ALManga(
val startDateFuzzy: Long, val startDateFuzzy: Long,
val totalChapters: Long, val totalChapters: Long,
val averageScore: Int, val averageScore: Int,
val staff: ALStaff,
) { ) {
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply { fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
remote_id = remoteId remote_id = remoteId
@@ -38,6 +39,11 @@ data class ALManga(
"" ""
} }
} }
staff.edges.forEach {
val name = it.node.name() ?: return@forEach
if ("Story" in it.role) authors += name
if ("Art" in it.role) artists += name
}
} }
} }
@@ -49,6 +55,7 @@ data class ALUserManga(
val startDateFuzzy: Long, val startDateFuzzy: Long,
val completedDateFuzzy: Long, val completedDateFuzzy: Long,
val manga: ALManga, val manga: ALManga,
val private: Boolean,
) { ) {
fun toTrack() = Track.create(TrackerManager.ANILIST).apply { fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
remote_id = manga.remoteId remote_id = manga.remoteId
@@ -60,6 +67,7 @@ data class ALUserManga(
last_chapter_read = chaptersRead.toDouble() last_chapter_read = chaptersRead.toDouble()
library_id = libraryId library_id = libraryId
total_chapters = manga.totalChapters total_chapters = manga.totalChapters
private = this@ALUserManga.private
} }
private fun toTrackStatus() = when (listStatus) { private fun toTrackStatus() = when (listStatus) {
@@ -17,24 +17,8 @@ data class ALMangaMetadataData(
@Serializable @Serializable
data class ALMangaMetadataMedia( data class ALMangaMetadataMedia(
val id: Long, val id: Long,
val title: ALItemTitle, val title: ALStaffName,
val coverImage: ItemCover, val coverImage: ItemCover,
val description: String?, val description: String?,
val staff: ALStaff, val staff: ALStaff,
) )
@Serializable
data class ALStaff(
val edges: List<ALStaffEdge>,
)
@Serializable
data class ALStaffEdge(
val role: String,
val node: ALStaffNode,
)
@Serializable
data class ALStaffNode(
val name: ALItemTitle,
)
@@ -18,3 +18,16 @@ data class ALSearchPage(
data class ALSearchMedia( data class ALSearchMedia(
val media: List<ALSearchItem>, val media: List<ALSearchItem>,
) )
// SY -->
@Serializable
data class ALIdSearchResult(
val data: ALIdSearchMedia,
)
@Serializable
data class ALIdSearchMedia(
@SerialName("Media")
val media: ALSearchItem,
)
// SY <--
@@ -13,6 +13,7 @@ data class ALSearchItem(
val startDate: ALFuzzyDate, val startDate: ALFuzzyDate,
val chapters: Long?, val chapters: Long?,
val averageScore: Int?, val averageScore: Int?,
val staff: ALStaff,
) { ) {
fun toALManga(): ALManga = ALManga( fun toALManga(): ALManga = ALManga(
remoteId = id, remoteId = id,
@@ -24,6 +25,7 @@ data class ALSearchItem(
startDateFuzzy = startDate.toEpochMilli(), startDateFuzzy = startDate.toEpochMilli(),
totalChapters = chapters ?: 0, totalChapters = chapters ?: 0,
averageScore = averageScore ?: -1, averageScore = averageScore ?: -1,
staff = staff,
) )
} }
@@ -36,3 +38,31 @@ data class ALItemTitle(
data class ItemCover( data class ItemCover(
val large: String, val large: String,
) )
@Serializable
data class ALStaff(
val edges: List<ALEdge>,
)
@Serializable
data class ALEdge(
val role: String,
val id: Int,
val node: ALStaffNode,
)
@Serializable
data class ALStaffNode(
val name: ALStaffName,
)
@Serializable
data class ALStaffName(
val userPreferred: String? = null,
val native: String? = null,
val full: String? = null,
) {
operator fun invoke(): String? {
return userPreferred ?: full ?: native
}
}
@@ -28,6 +28,7 @@ data class ALUserListItem(
val startedAt: ALFuzzyDate, val startedAt: ALFuzzyDate,
val completedAt: ALFuzzyDate, val completedAt: ALFuzzyDate,
val media: ALSearchItem, val media: ALSearchItem,
val private: Boolean,
) { ) {
fun toALUserManga(): ALUserManga { fun toALUserManga(): ALUserManga {
return ALUserManga( return ALUserManga(
@@ -38,6 +39,7 @@ data class ALUserListItem(
startDateFuzzy = startedAt.toEpochMilli(), startDateFuzzy = startedAt.toEpochMilli(),
completedDateFuzzy = completedAt.toEpochMilli(), completedDateFuzzy = completedAt.toEpochMilli(),
manga = media.toALManga(), manga = media.toALManga(),
private = private,
) )
} }
} }
@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -24,6 +23,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
private val api by lazy { BangumiApi(id, client, interceptor) } private val api by lazy { BangumiApi(id, client, interceptor) }
override val supportsPrivateTracking: Boolean = true
override fun getScoreList(): ImmutableList<String> = SCORE_LIST override fun getScoreList(): ImmutableList<String> = SCORE_LIST
override fun displayScore(track: DomainTrack): String { override fun displayScore(track: DomainTrack): String {
@@ -49,26 +50,23 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
} }
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val statusTrack = api.statusLibManga(track) val statusTrack = api.statusLibManga(track, getUsername())
val remoteTrack = api.findLibManga(track) return if (statusTrack != null) {
return if (remoteTrack != null && statusTrack != null) { track.copyPersonalFrom(statusTrack, copyRemotePrivate = false)
track.copyPersonalFrom(remoteTrack) track.library_id = statusTrack.library_id
track.library_id = remoteTrack.library_id track.score = statusTrack.score
track.last_chapter_read = statusTrack.last_chapter_read
track.total_chapters = statusTrack.total_chapters
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
track.status = if (hasReadChapters) READING else statusTrack.status track.status = if (hasReadChapters) READING else statusTrack.status
} }
track.score = statusTrack.score update(track)
track.last_chapter_read = statusTrack.last_chapter_read
track.total_chapters = remoteTrack.total_chapters
refresh(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.status = if (hasReadChapters) READING else PLAN_TO_READ track.status = if (hasReadChapters) READING else PLAN_TO_READ
track.score = 0.0 track.score = 0.0
add(track) add(track)
update(track)
} }
} }
@@ -81,11 +79,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
} }
override suspend fun refresh(track: Track): Track { override suspend fun refresh(track: Track): Track {
val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga") val remoteStatusTrack = api.statusLibManga(track, getUsername()) ?: throw Exception("Could not find manga")
track.copyPersonalFrom(remoteStatusTrack) track.copyPersonalFrom(remoteStatusTrack)
api.findLibManga(track)?.let { remoteTrack ->
track.total_chapters = remoteTrack.total_chapters
}
return track return track
} }
@@ -118,8 +113,12 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
try { try {
val oauth = api.accessToken(code) val oauth = api.accessToken(code)
interceptor.newAuth(oauth) interceptor.newAuth(oauth)
saveCredentials(oauth.userId.toString(), oauth.accessToken) // Users can set a 'username' (not nickname) once which effectively
} catch (e: Throwable) { // replaces the stringified ID in certain queries.
// If no username is set, the API returns the user ID as a strings
var username = api.getUsername()
saveCredentials(username, oauth.accessToken)
} catch (_: Throwable) {
logout() logout()
} }
} }
@@ -131,7 +130,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
fun restoreToken(): BGMOAuth? { fun restoreToken(): BGMOAuth? {
return try { return try {
json.decodeFromString<BGMOAuth>(trackPreferences.trackToken(this).get()) json.decodeFromString<BGMOAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) { } catch (_: Exception) {
null null
} }
} }
@@ -143,11 +142,11 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
} }
companion object { companion object {
const val READING = 3L const val PLAN_TO_READ = 1L
const val COMPLETED = 2L const val COMPLETED = 2L
const val READING = 3L
const val ON_HOLD = 4L const val ON_HOLD = 4L
const val DROPPED = 5L const val DROPPED = 5L
const val PLAN_TO_READ = 1L
private val SCORE_LIST = IntRange(0, 10) private val SCORE_LIST = IntRange(0, 10)
.map(Int::toString) .map(Int::toString)
@@ -5,25 +5,31 @@ import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchItem
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSubject import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSubject
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMUser
import eu.kanade.tachiyomi.data.track.bangumi.dto.Infobox import eu.kanade.tachiyomi.data.track.bangumi.dto.Infobox
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers.Companion.headersOf
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
class BangumiApi( class BangumiApi(
@@ -38,11 +44,17 @@ class BangumiApi(
suspend fun addLibManga(track: Track): Track { suspend fun addLibManga(track: Track): Track {
return withIOContext { return withIOContext {
val body = FormBody.Builder() val url = "$API_URL/v0/users/-/collections/${track.remote_id}"
.add("rating", track.score.toInt().toString()) val body = buildJsonObject {
.add("status", track.toApiStatus()) put("type", track.toApiStatus())
.build() put("rate", track.score.toInt().coerceIn(0, 10))
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body)) put("ep_status", track.last_chapter_read.toInt())
put("private", track.private)
}
.toString()
.toRequestBody()
// Returns with 202 Accepted on success with no body
authClient.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON)))
.awaitSuccess() .awaitSuccess()
track track
} }
@@ -50,83 +62,79 @@ class BangumiApi(
suspend fun updateLibManga(track: Track): Track { suspend fun updateLibManga(track: Track): Track {
return withIOContext { return withIOContext {
// read status update val url = "$API_URL/v0/users/-/collections/${track.remote_id}"
val sbody = FormBody.Builder() val body = buildJsonObject {
.add("rating", track.score.toInt().toString()) put("type", track.toApiStatus())
.add("status", track.toApiStatus()) put("rate", track.score.toInt().coerceIn(0, 10))
.build() put("ep_status", track.last_chapter_read.toInt())
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody)) put("private", track.private)
.awaitSuccess() }
.toString()
.toRequestBody()
// chapter update val request = Request.Builder()
val body = FormBody.Builder() .url(url)
.add("watched_eps", track.last_chapter_read.toInt().toString()) .patch(body)
.headers(headersOf("Content-Type", APP_JSON))
.build() .build()
authClient.newCall( // Returns with 204 No Content
POST("$API_URL/subject/${track.remote_id}/update/watched_eps", body = body), authClient.newCall(request)
).awaitSuccess() .awaitSuccess()
track track
} }
} }
suspend fun search(search: String): List<TrackSearch> { suspend fun search(search: String): List<TrackSearch> {
// This API is marked as experimental in the documentation
// but that has been the case since 2022 with few significant
// changes to the schema for this endpoint since
// "实验性 API 本 schema 和实际的 API 行为都可能随时发生改动"
return withIOContext { return withIOContext {
val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}" val url = "$API_URL/v0/search/subjects?limit=20"
.toUri() val body = buildJsonObject {
.buildUpon() put("keyword", search)
.appendQueryParameter("type", "1") put("sort", "match")
.appendQueryParameter("responseGroup", "large") putJsonObject("filter") {
.appendQueryParameter("max_results", "20") putJsonArray("type") {
.build() add(1) // "Book" (书籍) type
}
}
}
.toString()
.toRequestBody()
with(json) { with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON)))
.awaitSuccess() .awaitSuccess()
.parseAs<BGMSearchResult>() .parseAs<BGMSearchResult>()
.let { result -> .data
if (result.code == 404) emptyList<TrackSearch>() .map { it.toTrackSearch(trackId) }
result.list
?.map { it.toTrackSearch(trackId) }
.orEmpty()
}
} }
} }
} }
suspend fun findLibManga(track: Track): Track? { suspend fun statusLibManga(track: Track, username: String): Track? {
return withIOContext { return withIOContext {
val url = "$API_URL/v0/users/$username/collections/${track.remote_id}"
with(json) { with(json) {
authClient.newCall(GET("$API_URL/subject/${track.remote_id}")) try {
.awaitSuccess() authClient.newCall(GET(url, cache = CacheControl.FORCE_NETWORK))
.parseAs<BGMSearchItem>() .awaitSuccess()
.toTrackSearch(trackId) .parseAs<BGMCollectionResponse>()
} .let {
} track.status = it.getStatus()
} track.last_chapter_read = it.epStatus?.toDouble() ?: 0.0
track.score = it.rate?.toDouble() ?: 0.0
suspend fun statusLibManga(track: Track): Track? { track.total_chapters = it.subject?.eps?.toLong() ?: 0L
return withIOContext { track
val urlUserRead = "$API_URL/collection/${track.remote_id}" }
val requestUserRead = Request.Builder() } catch (e: HttpException) {
.url(urlUserRead) if (e.code == 404) { // "subject is not collected by user"
.cacheControl(CacheControl.FORCE_NETWORK) null
.get() } else {
.build() throw e
// TODO: get user readed chapter here
with(json) {
authClient.newCall(requestUserRead)
.awaitSuccess()
.parseAs<BGMCollectionResponse>()
.let {
if (it.code == 400) return@let null
track.status = it.status?.id!!
track.last_chapter_read = it.epStatus!!.toDouble()
track.score = it.rating!!
track
} }
}
} }
} }
} }
@@ -161,24 +169,31 @@ class BangumiApi(
suspend fun accessToken(code: String): BGMOAuth { suspend fun accessToken(code: String): BGMOAuth {
return withIOContext { return withIOContext {
val body = FormBody.Builder()
.add("grant_type", "authorization_code")
.add("client_id", CLIENT_ID)
.add("client_secret", CLIENT_SECRET)
.add("code", code)
.add("redirect_uri", REDIRECT_URL)
.build()
with(json) { with(json) {
client.newCall(accessTokenRequest(code)) client.newCall(POST(OAUTH_URL, body = body))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs<BGMOAuth>()
} }
} }
} }
private fun accessTokenRequest(code: String) = POST( suspend fun getUsername(): String {
OAUTH_URL, return withIOContext {
body = FormBody.Builder() with(json) {
.add("grant_type", "authorization_code") authClient.newCall(GET("$API_URL/v0/me"))
.add("client_id", CLIENT_ID) .awaitSuccess()
.add("client_secret", CLIENT_SECRET) .parseAs<BGMUser>()
.add("code", code) .username
.add("redirect_uri", REDIRECT_URL) }
.build(), }
) }
companion object { companion object {
private const val CLIENT_ID = "bgm291665acbd06a4c28" private const val CLIENT_ID = "bgm291665acbd06a4c28"
@@ -190,6 +205,8 @@ class BangumiApi(
private const val REDIRECT_URL = "mihon://bangumi-auth" private const val REDIRECT_URL = "mihon://bangumi-auth"
private const val APP_JSON = "application/json"
fun authUrl(): Uri = fun authUrl(): Uri =
LOGIN_URL.toUri().buildUpon() LOGIN_URL.toUri().buildUpon()
.appendQueryParameter("client_id", CLIENT_ID) .appendQueryParameter("client_id", CLIENT_ID)
@@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -21,12 +20,13 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
val currAuth = oauth ?: throw Exception("Not authenticated with Bangumi") var currAuth: BGMOAuth = oauth ?: throw Exception("Not authenticated with Bangumi")
if (currAuth.isExpired()) { if (currAuth.isExpired()) {
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!)) val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!))
if (response.isSuccessful) { if (response.isSuccessful) {
newAuth(json.decodeFromString<BGMOAuth>(response.body.string())) currAuth = json.decodeFromString<BGMOAuth>(response.body.string())
newAuth(currAuth)
} else { } else {
response.close() response.close()
} }
@@ -38,14 +38,7 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
"jobobby04/TachiyomiSY/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/jobobby04/tachiyomisy)", "jobobby04/TachiyomiSY/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/jobobby04/tachiyomisy)",
) )
.apply { .apply {
if (originalRequest.method == "GET") { addHeader("Authorization", "Bearer ${currAuth.accessToken}")
val newUrl = originalRequest.url.newBuilder()
.addQueryParameter("access_token", currAuth.accessToken)
.build()
url(newUrl)
} else {
post(addToken(currAuth.accessToken, originalRequest.body as FormBody))
}
} }
.build() .build()
.let(chain::proceed) .let(chain::proceed)
@@ -67,13 +60,4 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
bangumi.saveToken(oauth) bangumi.saveToken(oauth)
} }
private fun addToken(token: String, oidFormBody: FormBody): FormBody {
val newFormBody = FormBody.Builder()
for (i in 0..<oidFormBody.size) {
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
}
newFormBody.add("access_token", token)
return newFormBody.build()
}
} }
@@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.data.track.bangumi
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
fun Track.toApiStatus() = when (status) { fun Track.toApiStatus() = when (status) {
Bangumi.READING -> "do" Bangumi.PLAN_TO_READ -> 1
Bangumi.COMPLETED -> "collect" Bangumi.COMPLETED -> 2
Bangumi.ON_HOLD -> "on_hold" Bangumi.READING -> 3
Bangumi.DROPPED -> "dropped" Bangumi.ON_HOLD -> 4
Bangumi.PLAN_TO_READ -> "wish" Bangumi.DROPPED -> 5
else -> throw NotImplementedError("Unknown status: $status") else -> throw NotImplementedError("Unknown status: $status")
} }
@@ -1,28 +1,34 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto package eu.kanade.tachiyomi.data.track.bangumi.dto
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
// Incomplete DTO with only our needed attributes
data class BGMCollectionResponse( data class BGMCollectionResponse(
val code: Int?, val rate: Int?,
val `private`: Int? = 0, val type: Int?,
val comment: String? = "",
@SerialName("ep_status") @SerialName("ep_status")
val epStatus: Int? = 0, val epStatus: Int? = 0,
@SerialName("lasttouch")
val lastTouch: Int? = 0,
val rating: Double? = 0.0,
val status: Status? = Status(),
val tag: List<String?>? = emptyList(),
val user: User? = User(),
@SerialName("vol_status") @SerialName("vol_status")
val volStatus: Int? = 0, val volStatus: Int? = 0,
) val private: Boolean = false,
val subject: BGMSlimSubject? = null,
) {
fun getStatus(): Long = when (type) {
1 -> Bangumi.PLAN_TO_READ
2 -> Bangumi.COMPLETED
3 -> Bangumi.READING
4 -> Bangumi.ON_HOLD
5 -> Bangumi.DROPPED
else -> throw NotImplementedError("Unknown status: $type")
}
}
@Serializable @Serializable
data class Status( // Incomplete DTO with only our needed attributes
val id: Long? = 0, data class BGMSlimSubject(
val name: String? = "", val volumes: Int?,
val type: String? = "", val eps: Int?,
) )
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto package eu.kanade.tachiyomi.data.track.bangumi.dto
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -10,6 +11,7 @@ data class BGMOAuth(
@SerialName("token_type") @SerialName("token_type")
val tokenType: String, val tokenType: String,
@SerialName("created_at") @SerialName("created_at")
@EncodeDefault
val createdAt: Long = System.currentTimeMillis() / 1000, val createdAt: Long = System.currentTimeMillis() / 1000,
@SerialName("expires_in") @SerialName("expires_in")
val expiresIn: Long, val expiresIn: Long,
@@ -6,45 +6,53 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class BGMSearchResult( data class BGMSearchResult(
val list: List<BGMSearchItem>?, val total: Int,
val code: Int?, val limit: Int,
val offset: Int,
val data: List<BGMSubject> = emptyList(),
) )
@Serializable @Serializable
data class BGMSearchItem( // Incomplete DTO with only our needed attributes
data class BGMSubject(
val id: Long, val id: Long,
@SerialName("name_cn") @SerialName("name_cn")
val nameCn: String, val nameCn: String,
val name: String, val name: String,
val type: Int,
val summary: String?, val summary: String?,
val images: BGMSearchItemCovers?, val date: String?, // YYYY-MM-DD
@SerialName("eps_count") val images: BGMSubjectImages?,
val epsCount: Long?, val volumes: Long = 0,
val rating: BGMSearchItemRating?, val eps: Long = 0,
val url: String, val rating: BGMSubjectRating?,
// SY -->
val infobox: List<Infobox> = emptyList(),
// SY <--
) { ) {
fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply { fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply {
remote_id = this@BGMSearchItem.id remote_id = this@BGMSubject.id
title = nameCn.ifBlank { name } title = nameCn.ifBlank { name }
cover_url = images?.common.orEmpty() cover_url = images?.common.orEmpty()
summary = if (nameCn.isNotBlank()) { summary = if (nameCn.isNotBlank()) {
"作品原名:$name" + this@BGMSearchItem.summary?.let { "\n$it" }.orEmpty() "作品原名:$name" + this@BGMSubject.summary?.let { "\n${it.trim()}" }.orEmpty()
} else { } else {
this@BGMSearchItem.summary.orEmpty() this@BGMSubject.summary?.trim().orEmpty()
} }
score = rating?.score ?: -1.0 score = rating?.score ?: -1.0
tracking_url = url tracking_url = "https://bangumi.tv/subject/${this@BGMSubject.id}"
total_chapters = epsCount ?: 0 total_chapters = eps
start_date = date ?: ""
} }
} }
@Serializable @Serializable
data class BGMSearchItemCovers( // Incomplete DTO with only our needed attributes
data class BGMSubjectImages(
val common: String?, val common: String?,
) )
@Serializable @Serializable
data class BGMSearchItemRating( // Incomplete DTO with only our needed attributes
data class BGMSubjectRating(
val score: Double?, val score: Double?,
) )
@@ -1,23 +1,9 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto package eu.kanade.tachiyomi.data.track.bangumi.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Avatar( // Incomplete DTO with only our needed attributes
val large: String? = "", data class BGMUser(
val medium: String? = "", val username: String,
val small: String? = "",
)
@Serializable
data class User(
val avatar: Avatar? = Avatar(),
val id: Int? = 0,
val nickname: String? = "",
val sign: String? = "",
val url: String? = "",
@SerialName("usergroup")
val userGroup: Int? = 0,
val username: String? = "",
) )
@@ -10,17 +10,6 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
@Serializable
data class BGMSubject(
val images: BGMSearchItemCovers?,
val summary: String,
val name: String,
@SerialName("name_cn")
val nameCn: String,
val infobox: List<Infobox>,
val id: Long,
)
// infobox deserializer and related classes courtesy of // infobox deserializer and related classes courtesy of
// https://github.com/Snd-R/komf/blob/4c260a3dcd326a5e1d74ac9662eec8124ab7e461/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/model/BangumiSubject.kt#L53-L89 // https://github.com/Snd-R/komf/blob/4c260a3dcd326a5e1d74ac9662eec8124ab7e461/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/model/BangumiSubject.kt#L53-L89
object InfoBoxSerializer : JsonContentPolymorphicSerializer<Infobox>(Infobox::class) { object InfoBoxSerializer : JsonContentPolymorphicSerializer<Infobox>(Infobox::class) {
@@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -30,6 +29,8 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
override val supportsReadingDates: Boolean = true override val supportsReadingDates: Boolean = true
override val supportsPrivateTracking: Boolean = true
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val interceptor by lazy { KitsuInterceptor(this) } private val interceptor by lazy { KitsuInterceptor(this) }
@@ -102,7 +103,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val remoteTrack = api.findLibManga(track, getUserId()) val remoteTrack = api.findLibManga(track, getUserId())
return if (remoteTrack != null) { return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
track.remote_id = remoteTrack.remote_id track.remote_id = remoteTrack.remote_id
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
@@ -155,7 +156,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
fun restoreToken(): KitsuOAuth? { fun restoreToken(): KitsuOAuth? {
return try { return try {
json.decodeFromString<KitsuOAuth>(trackPreferences.trackToken(this).get()) json.decodeFromString<KitsuOAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) { } catch (_: Exception) {
null null
} }
} }
@@ -49,6 +49,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
putJsonObject("attributes") { putJsonObject("attributes") {
put("status", track.toApiStatus()) put("status", track.toApiStatus())
put("progress", track.last_chapter_read.toInt()) put("progress", track.last_chapter_read.toInt())
put("private", track.private)
} }
putJsonObject("relationships") { putJsonObject("relationships") {
putJsonObject("user") { putJsonObject("user") {
@@ -97,6 +98,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
put("ratingTwenty", track.toApiScore()) put("ratingTwenty", track.toApiScore())
put("startedAt", KitsuDateHelper.convert(track.started_reading_date)) put("startedAt", KitsuDateHelper.convert(track.started_reading_date))
put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date)) put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date))
put("private", track.private)
} }
} }
} }
@@ -42,6 +42,7 @@ data class KitsuListSearchResult(
} }
score = userDataAttrs.ratingTwenty?.let { it / 2.0 } ?: 0.0 score = userDataAttrs.ratingTwenty?.let { it / 2.0 } ?: 0.0
last_chapter_read = userDataAttrs.progress.toDouble() last_chapter_read = userDataAttrs.progress.toDouble()
private = userDataAttrs.private
} }
} }
} }
@@ -59,6 +60,7 @@ data class KitsuListSearchItemDataAttributes(
val finishedAt: String?, val finishedAt: String?,
val ratingTwenty: Int?, val ratingTwenty: Int?,
val progress: Int, val progress: Int,
val private: Boolean,
) )
@Serializable @Serializable
@@ -133,6 +133,29 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
} }
} }
// SY -->
override suspend fun searchById(id: String): TrackSearch? {
/*
* MangaUpdates uses newer base36 IDs (in URLs displayed as an encoded string, internally as a long)
* as well as older sequential numeric IDs, which were phased out to prevent heavy load caused by
* database scraping. Unfortunately, sites like MD sometimes still provides links with the old IDs,
* so we need to convert them.
* Because the API only accepts the newer IDs, we are forced to access the legacy non-API website
* (ex. https://www.mangaupdates.com/series.html?id=15), which is a permanent redirect (HTTP 308) to the new one.
*/
val base36Id = if (id.matches(Regex("""^\d+$"""))) {
api.convertToNewId(id.toInt()) ?: return null
} else {
id
}
return base36Id.toLong(36).let { longId ->
api.getSeries(longId).toTrackSearch(this.id)
}
}
// SY <--
fun restoreSession(): String? { fun restoreSession(): String? {
return trackPreferences.trackPassword(this).get().ifBlank { null } return trackPreferences.trackPassword(this).get().ifBlank { null }
} }
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.PUT
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -25,6 +26,7 @@ import kotlinx.serialization.json.putJsonObject
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
@@ -190,14 +192,35 @@ class MangaUpdatesApi(
} }
} }
suspend fun getSeries(track: DomainTrack): MURecord { suspend fun getSeries(track: DomainTrack): MURecord =
getSeries(track.remoteId)
// SY -->
suspend fun getSeries(remoteId: Long): MURecord {
return with(json) { return with(json) {
client.newCall(GET("$BASE_URL/v1/series/${track.remoteId}")) client.newCall(GET("$BASE_URL/v1/series/$remoteId"))
.awaitSuccess() .awaitSuccess()
.parseAs<MURecord>() .parseAs<MURecord>()
} }
} }
suspend fun convertToNewId(legacyId: Int): String? =
client.newBuilder()
.followRedirects(false)
.build()
.newCall(GET("https://www.mangaupdates.com/series.html?id=$legacyId"))
.await()
.takeIf(Response::isRedirect)
?.header("Location")
?.let {
// Extract the new id from the redirected URL
Regex("""/series/(\w+)(/([\w-]+)?)?/?${'$'}""")
.find(it)
?.groups?.get(1)
?.value
}
// SY <--
companion object { companion object {
private const val BASE_URL = "https://api.mangaupdates.com" private const val BASE_URL = "https://api.mangaupdates.com"
@@ -30,8 +30,14 @@ class TrackSearch : Track {
override var finished_reading_date: Long = 0 override var finished_reading_date: Long = 0
override var private: Boolean = false
override lateinit var tracking_url: String override lateinit var tracking_url: String
var authors: List<String> = emptyList()
var artists: List<String> = emptyList()
var cover_url: String = "" var cover_url: String = ""
var summary: String = "" var summary: String = ""
@@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -161,6 +160,12 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
return api.getMangaMetadata(track) return api.getMangaMetadata(track)
} }
// SY -->
override suspend fun searchById(id: String): TrackSearch {
return api.getMangaDetails(id.toInt())
}
// SY <--
fun getIfAuthExpired(): Boolean { fun getIfAuthExpired(): Boolean {
return trackPreferences.trackAuthExpired(this).get() return trackPreferences.trackAuthExpired(this).get()
} }
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.myanimelist.dto package eu.kanade.tachiyomi.data.track.myanimelist.dto
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -14,10 +15,11 @@ data class MALOAuth(
@SerialName("expires_in") @SerialName("expires_in")
val expiresIn: Long, val expiresIn: Long,
@SerialName("created_at") @SerialName("created_at")
val createdAt: Long = System.currentTimeMillis(), @EncodeDefault
val createdAt: Long = System.currentTimeMillis() / 1000,
) { ) {
// Assumes expired a minute earlier // Assumes expired a minute earlier
private val adjustedExpiresIn: Long = (expiresIn - 60) * 1000 private val adjustedExpiresIn: Long = (expiresIn - 60)
fun isExpired() = createdAt + adjustedExpiresIn < System.currentTimeMillis() fun isExpired() = createdAt + adjustedExpiresIn < System.currentTimeMillis() / 1000
} }
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.updater
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
import eu.kanade.tachiyomi.util.system.isPreviewBuildType import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import exh.syDebugVersion import exh.syDebugVersion
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@@ -25,7 +24,6 @@ class AppUpdateChecker {
// SY --> // SY -->
isPreviewBuildType, isPreviewBuildType,
// SY <-- // SY <--
context.isInstalledFromFDroid(),
BuildConfig.COMMIT_COUNT.toInt(), BuildConfig.COMMIT_COUNT.toInt(),
BuildConfig.VERSION_NAME, BuildConfig.VERSION_NAME,
GITHUB_REPO, GITHUB_REPO,
@@ -38,9 +36,6 @@ class AppUpdateChecker {
when (result) { when (result) {
is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release) is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier(
context,
).promptFdroidUpdate()
else -> {} else -> {}
} }
@@ -139,27 +139,6 @@ internal class AppUpdateNotifier(private val context: Context) {
notificationBuilder.show(Notifications.ID_APP_UPDATE_PROMPT) notificationBuilder.show(Notifications.ID_APP_UPDATE_PROMPT)
} }
/**
* Some people are still installing the app from F-Droid, so we avoid prompting GitHub-based
* updates.
*
* We can prompt them to migrate to the GitHub version though.
*/
fun promptFdroidUpdate() {
with(notificationBuilder) {
setContentTitle(context.stringResource(MR.strings.update_check_notification_update_available))
setContentText(context.stringResource(MR.strings.update_check_fdroid_migration_info))
setSmallIcon(R.drawable.ic_tachi)
setContentIntent(
NotificationHandler.openUrl(
context,
"https://mihon.app/docs/faq/general#how-do-i-update-from-the-f-droid-builds",
),
)
}
notificationBuilder.show(Notifications.ID_APP_UPDATE_PROMPT)
}
/** /**
* Call when apk download throws a error * Call when apk download throws a error
* *
@@ -4,8 +4,8 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.Uri
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
@@ -138,7 +138,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Broadc
private fun notify(context: Context, pkgName: String, action: String) { private fun notify(context: Context, pkgName: String, action: String) {
Intent(action).apply { Intent(action).apply {
data = Uri.parse("package:$pkgName") data = "package:$pkgName".toUri()
`package` = context.packageName `package` = context.packageName
context.sendBroadcast(this) context.sendBroadcast(this)
} }
@@ -199,7 +199,7 @@ class NHentai(delegate: HttpSource, val context: Context) :
private fun thumbnailUrlFromType(mediaId: String, page: Int, t: String) = private fun thumbnailUrlFromType(mediaId: String, page: Int, t: String) =
NHentaiSearchMetadata.typeToExtension(t)?.let { NHentaiSearchMetadata.typeToExtension(t)?.let {
"https://t3.nhentai.net/galleries/$mediaId/${page}t.$it" "https://t1.nhentai.net/galleries/$mediaId/${page}t.$it"
} }
override suspend fun fetchPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl?): Response { override suspend fun fetchPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl?): Response {
@@ -126,8 +126,7 @@ class Pururin(delegate: HttpSource, val context: Context) :
} }
override val matchingHosts = listOf( override val matchingHosts = listOf(
"pururin.io", "pururin.me",
"www.pururin.io",
) )
override suspend fun mapUrlToMangaUrl(uri: Uri): String { override suspend fun mapUrlToMangaUrl(uri: Uri): String {
@@ -33,6 +33,7 @@ private val themeResources: Map<AppTheme, Int> = mapOf(
AppTheme.GREEN_APPLE to R.style.Theme_Tachiyomi_GreenApple, AppTheme.GREEN_APPLE to R.style.Theme_Tachiyomi_GreenApple,
AppTheme.LAVENDER to R.style.Theme_Tachiyomi_Lavender, AppTheme.LAVENDER to R.style.Theme_Tachiyomi_Lavender,
AppTheme.MIDNIGHT_DUSK to R.style.Theme_Tachiyomi_MidnightDusk, AppTheme.MIDNIGHT_DUSK to R.style.Theme_Tachiyomi_MidnightDusk,
AppTheme.MONOCHROME to R.style.Theme_Tachiyomi_Monochrome,
AppTheme.NORD to R.style.Theme_Tachiyomi_Nord, AppTheme.NORD to R.style.Theme_Tachiyomi_Nord,
AppTheme.STRAWBERRY_DAIQUIRI to R.style.Theme_Tachiyomi_StrawberryDaiquiri, AppTheme.STRAWBERRY_DAIQUIRI to R.style.Theme_Tachiyomi_StrawberryDaiquiri,
AppTheme.TAKO to R.style.Theme_Tachiyomi_Tako, AppTheme.TAKO to R.style.Theme_Tachiyomi_Tako,
@@ -58,12 +58,14 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
binding.migTracking.isChecked = MigrationFlags.hasTracks(flags) binding.migTracking.isChecked = MigrationFlags.hasTracks(flags)
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags) binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags) binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
binding.migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteChapters(flags)
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migTracking.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migTracking.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migDeleteDownloaded.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.useSmartSearch.bindToPreference(preferences.smartMigration()) binding.useSmartSearch.bindToPreference(preferences.smartMigration())
binding.extraSearchParamText.isVisible = false binding.extraSearchParamText.isVisible = false

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