Compare commits

..

59 Commits

Author SHA1 Message Date
Jobobby04 dcd44c42ed Merge branch 'release' 2024-03-02 15:39:33 -05:00
Jobobby04 6c563d7619 Fix for duplicate read 2024-03-02 15:38:32 -05:00
Jobobby04 97f22c500b 1.10.5 2024-03-02 15:38:05 -05:00
Jobobby04 9cbeccfa15 Fix for duplicate read 2024-03-02 15:34:28 -05:00
MajorTanya 86722a31d0 Fix DelayedTrackingUpdateJob spam on update errors (#411)
* Fix DelayedTrackingUpdateJob spam on update errors

DelayedTrackingUpdateJob would start spamming when it encountered an
error (e.g. a tracker has an issue) and never stop.
This seems to stem from a circular dependency between the Job's
`doWork` and TrackChapter's `await`.

TrackChapter sets up a completely new instance of the
DelayedTrackingUpdateJob if any Exception was thrown during the track
update.

This causes the Job to get replaced (as per the WorkManager's set
ExistingWorkPolicy).

Because of this, the guard clause at the start of doWork would never
trigger, as all instances of the Job would report being the 0th try
(because they were completely new instances).

This simple fix introduces a boolean `isRetry` parameter to
TrackChapter's await method, which is set to `false` by default.
DelayedTrackingUpdateJob however sets this parameter to `true`, which
means TrackChapter won't try to set up the Job again.

* Rename isRetry parameter to setupJobOnFailure

This also inverts the logic, so true & false were swapped.

(cherry picked from commit 617bf491eee1d1010dc23c3f6df5d148700feb44)
2024-03-02 15:32:56 -05:00
AntsyLich 589b33a673 Fix detekt issue
(cherry picked from commit 9254079957e383e4aa5c914ccd9705612e0892d0)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt
2024-03-02 15:32:31 -05:00
AntsyLich dae0348710 Don't add custom User Agent for MAL
Closes #298

(cherry picked from commit 7974a1fc0c2b0e237d5704a033a60ec0fd60cdbc)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt
2024-03-02 15:32:29 -05:00
Jobobby04 a7f6155627 Ignore chapters with 0 or under chapter numbers
(cherry picked from commit d8082de1db)
2024-03-02 15:31:38 -05:00
Jobobby04 7f5bc4a3e5 Use new gradle workflow 2024-03-02 15:28:40 -05:00
Jobobby04 ec32278f1a Throw IOException 2024-03-02 14:25:31 -05:00
AntsyLich c02c5aa915 Revert changes to gradle.properties
(cherry picked from commit d6ba3c824972c8a2196516edd42e9e8be6385f36)
2024-03-02 14:24:34 -05:00
AntsyLich f267f2ad5b detekt my beloved
(cherry picked from commit c56f4665ef0276c54f5abebd9ab93e2e283739a6)
2024-03-02 14:24:25 -05:00
renovate[bot] 03bc09c1aa Update dependency me.saket.swipe:swipe to v1.3.0 (#343)
* Update dependency me.saket.swipe:swipe to v1.3.0

* Update MangaChapterListItem.kt

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit b51a0a38bd9849b573bc2de0d936d6ab2becb83c)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt
2024-03-02 14:24:16 -05:00
AntsyLich c4df418081 Switch to Coil3
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit f72b6e4d7c1f2f93d705402e4d80c94160bef54d)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt
2024-03-02 14:22:54 -05:00
AntsyLich a9c79d5fb3 Remove custom Pager
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit 84984ef7e1d7242924120cd2f171cb9dd75bc916)
2024-03-02 14:02:34 -05:00
AntsyLich 529100a947 Enable experimental Compose compiler optimization
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit 9f48def1e2718abd5b4aad3cb6ee8af6b39e76cc)
2024-03-02 14:02:12 -05:00
AntsyLich 062f6d5aa0 ChapterDownloadIndicator: Remove composed modifier usage
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit e83bfb0d3511f3c049d1dfe6ca13e74467655e08)
2024-03-02 14:01:43 -05:00
AntsyLich deb0a95985 Upgrade Compose
(cherry picked from commit 0301362430af6f74678dcae801b70d6aeb371a56)
2024-03-02 14:01:34 -05:00
Shamicen ca70f80900 Made some changes to ComicInfo metadata (#459)
* Made some changes to ComicInfo metadata

The web field now contains a " " separated list of source and tracker urls.
The translator field will now use the source name if the scanlator field is empty.

* lint

* use already existing source instance

* made translator not nullable

* implemented requested changes

created new Mihon exclusive ComicInfo source field  and populated it with SourceName

reverted previous changes to translator field

* Update core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt

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

* Update app/src/main/java/eu/kanade/domain/manga/model/Manga.kt

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

* Update app/src/main/java/eu/kanade/domain/manga/model/Manga.kt

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

* Update app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt

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

* Update app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt

---------

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

# Conflicts:
#	app/src/main/java/eu/kanade/domain/manga/model/Manga.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
#	core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt
2024-03-02 14:01:12 -05:00
renovate[bot] 23e3ec20b6 Update Kotlin (#422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 802a2c5c1ea73c79967fb5b535b6249506df1870)
2024-03-02 13:59:43 -05:00
renovate[bot] 32d97ed194 Update dependency io.coil-kt:coil-bom to v2.6.0 (#447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit c1c174698525e982107cc955d8dff711c4498989)
2024-03-02 13:59:34 -05:00
renovate[bot] 167a4e9820 Update dependency org.junit.jupiter:junit-jupiter to v5.10.2 (#419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 4fcbd80a8ec72f4113d0ee9d457c000dcd8e7440)
2024-03-02 13:59:25 -05:00
renovate[bot] aef0b50663 Update dependency com.google.firebase:firebase-analytics-ktx to v21.5.1 (#417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 16969193c718cc8e6cabb473ecb1d0d7bc02f33e)

# Conflicts:
#	gradle/libs.versions.toml
2024-03-02 13:59:16 -05:00
renovate[bot] 5b5e6c8f44 Update dependency androidx.test.uiautomator:uiautomator to v2.3.0 (#416)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 55637ddfe154c2ed60efb5d9d1466d2106c97f9f)
2024-03-02 13:58:48 -05:00
renovate[bot] 5dc96384bd Update detekt to v1.23.5 (#267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit e50358dc4be7cbd866a79d052edc62689f0e4ca5)
2024-03-02 13:58:39 -05:00
AntsyLich affdea3ec2 Fix detekt issue
(cherry picked from commit 9254079957e383e4aa5c914ccd9705612e0892d0)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt
2024-03-02 13:57:49 -05:00
AntsyLich 1436d86c7e Don't add custom User Agent for MAL
Closes #298

(cherry picked from commit 7974a1fc0c2b0e237d5704a033a60ec0fd60cdbc)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt
2024-03-02 13:56:42 -05:00
renovate[bot] b7c9eaa981 Update dependency com.squareup.okio:okio to v3.8.0 (#423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 1521c359412518731ef7338255cdd280321faa70)
2024-03-02 13:56:12 -05:00
Splintor db99ab526a Allow disabling reader's zoom out (#302)
* Allow disabling reader's zoom out (#62)

* Renamed disable zoom out pref and string

* Zoom to default rate if the scale is inferior

* Fixed null value check and formatting

* Fixed detekt

(cherry picked from commit c15f3f2fd5b11cc9c2088ae2fa444f4fe35ea740)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt
2024-03-02 13:55:43 -05:00
renovate[bot] 133c34dee2 Update dependency com.google.gms:google-services to v4.4.1 (#418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 21020e1797ae9c0373e5132e1dbcd9a64455c17f)
2024-03-02 13:48:11 -05:00
Weblate (bot) 775cf258ba Translations update from Hosted Weblate (#301)
* Translated using Weblate (Spanish)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/

* Translated using Weblate (Chuvash)

Currently translated at 75.7% (601 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cv/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/

* Translated using Weblate (Chuvash)

Currently translated at 100.0% (17 of 17 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/cv/

* Translated using Weblate (Esperanto)

Currently translated at 100.0% (17 of 17 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/eo/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (17 of 17 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/tr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/

* Translated using Weblate (Polish)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pl/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/

* Translated using Weblate (Hungarian)

Currently translated at 97.3% (772 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hu/

* Translated using Weblate (Hungarian)

Currently translated at 94.1% (16 of 17 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hu/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/

* Translated using Weblate (Romanian)

Currently translated at 99.3% (788 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ro/

* Translated using Weblate (Romanian)

Currently translated at 100.0% (17 of 17 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ro/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (17 of 17 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/uk/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/

* Translated using Weblate (Nepali)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (793 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/

* Translated using Weblate (French)

Currently translated at 100.0% (17 of 17 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/fr/

* Translated using Weblate (Esperanto)

Currently translated at 62.0% (492 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/

* Translated using Weblate (Esperanto)

Currently translated at 100.0% (17 of 17 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/eo/

* Translated using Weblate (Esperanto)

Currently translated at 63.6% (505 of 793 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/

---------

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>
Co-authored-by: Eji-san <ejierubani@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Radoŝ Porka <animatorzPolski@gmail.com>
Co-authored-by: Deniz <denizgezgin365@gmail.com>
Co-authored-by: Uzuki Shimamura <hzy980512@126.com>
Co-authored-by: sebastians17 <sebastians117.ss@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Co-authored-by: B4LiN7 <B4LiN7@users.noreply.hosted.weblate.org>
Co-authored-by: Saft Octavian <saftoctavian@gmail.com>
Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Naga <yz2000.pro@gmail.com>
(cherry picked from commit 7edecae57f77ece7a5a3b457620c61e225fdc906)
2024-03-02 13:48:00 -05:00
Maddie Witman ed34807a58 Fix some issues from 7ff95e2 (#415)
* Fixed extra header introduced in 7ff95e2

* Removed parentheses to make detekt happy

* Updated relative date display for dates in the future

* Small cleanup for header creation logic

* replaced "and" with "&&" for better formatting

(cherry picked from commit 07f963d5ae16c3c8d7be025a7e9439ad2110ac71)
2024-03-02 13:47:43 -05:00
beerpsi 31acbbdcdc [ExtensionLoader] Prioritize extension classpath over app classpath (#433)
(cherry picked from commit ab02568ac6e9dabc7a41036bb3d8c77138125544)
2024-03-02 13:47:21 -05:00
MajorTanya ef7708e324 Fix DelayedTrackingUpdateJob spam on update errors (#411)
* Fix DelayedTrackingUpdateJob spam on update errors

DelayedTrackingUpdateJob would start spamming when it encountered an
error (e.g. a tracker has an issue) and never stop.
This seems to stem from a circular dependency between the Job's
`doWork` and TrackChapter's `await`.

TrackChapter sets up a completely new instance of the
DelayedTrackingUpdateJob if any Exception was thrown during the track
update.

This causes the Job to get replaced (as per the WorkManager's set
ExistingWorkPolicy).

Because of this, the guard clause at the start of doWork would never
trigger, as all instances of the Job would report being the 0th try
(because they were completely new instances).

This simple fix introduces a boolean `isRetry` parameter to
TrackChapter's await method, which is set to `false` by default.
DelayedTrackingUpdateJob however sets this parameter to `true`, which
means TrackChapter won't try to set up the Job again.

* Rename isRetry parameter to setupJobOnFailure

This also inverts the logic, so true & false were swapped.

(cherry picked from commit 617bf491eee1d1010dc23c3f6df5d148700feb44)
2024-03-02 13:47:09 -05:00
MajorTanya de353c3334 Address overridePendingTransition deprecation (#410)
This function is deprecated starting with API 34 "UpsideDownCake" and
should be replaced with `overrideActivityTransition`.

(cherry picked from commit 840b647b4b4e738fac546b7437dd5449679232a1)
2024-03-02 13:46:42 -05:00
AntsyLich b47a317c48 Tweak detekt config
(cherry picked from commit 1b0bbb84401005801e61107a0c36443af691b8e6)
2024-03-02 13:44:22 -05:00
AntsyLich 2b163c91a9 Cleanup [BaseColorScheme.getColorScheme]
(cherry picked from commit 95d4df9ca80a88e87e633fc24c7fec677bc9d9b6)
2024-03-02 13:44:13 -05:00
AntsyLich d380a078a2 Update gradle.properties
(cherry picked from commit fb86c470f6cfcb0b6c1bb7b2790366e0d0c0662e)
2024-03-02 13:43:09 -05:00
AntsyLich 556afacd13 Small cleanup in WorkerInfoScreen
(cherry picked from commit 5aec8f8018236a38106483da08f9cbc28261ac9b)
2024-03-02 13:42:56 -05:00
AntsyLich 598d622d0b Revert a mishap in 7ff95e21babda98dd1b479912278d6029cd15f0d
(cherry picked from commit e183cbb231c6d48a17c573dbd3f2c8ce04ff7031)
2024-03-02 13:42:37 -05:00
AntsyLich 3a1d0d65bf Ignore detekt [LongParameterList] for composables
(cherry picked from commit 6bdb37be65757ca903c5c2a249ca03331b04d673)
2024-03-02 13:41:52 -05:00
Jobobby04 cc7b8a9b69 Improve duplicate chapter set as read 2024-03-02 13:41:35 -05:00
Maddie Witman 6c6f09ac5a Refactor use of Java.util.date to Java.time.*, to fix localized date issues. (#402)
* Add support for localdate based relative times

* Update History Screen to use new localdate based relative times

* Update Updates Screen to use new localdate based relative times

* Cleaned up date util classes

* Updated build time display

* Code cleanup

* Fixed crash in settings

* Updated Preferences item

* Worker Info works

* Fixed Tracker date display

* Code changes to pass detekt

(cherry picked from commit 7ff95e21babda98dd1b479912278d6029cd15f0d)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt
2024-03-02 13:41:18 -05:00
MajorTanya 1fe309f363 Minor refactor of theming when expressions (#396)
* Minor refactor of theming when expressions

Avoids triggering detekt's CyclomaticComplexMethod warning because of
too many when branches, which would happen with one more theme being
added in these two locations.

In TachiyomiTheme, the Monet theme is separated because it requires
the current Compose context to function. The other themes do not and
are delegated to a Map.

* Implement requested changes

- moved themeResources out of the ThemingDelegate interface
- replaced single condition when with if expression

(cherry picked from commit 96c236e5c38248c875f2ac7596cd51845aa651ea)
2024-03-02 12:10:21 -05:00
renovate[bot] 3417fdb1a4 Update dependency androidx.test.ext:junit-ktx to v1.2.0-alpha03 (#340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 72f3756a3b89de292d75d41fe6b5a59172039c45)
2024-03-02 12:10:13 -05:00
renovate[bot] a6394672e7 Update dependency androidx.test.espresso:espresso-core to v3.6.0-alpha03 (#339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 0780385d2ebc9caca0bda0151d828ea6e036c768)
2024-03-02 12:10:07 -05:00
renovate[bot] 1fc97e4b7a Update lifecycle.version to v2.7.0 (#268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 31e9273b1ff4ecfd1992beaa8ad10fa27f726cc2)
2024-03-02 12:09:58 -05:00
renovate[bot] fe853aa1c5 Update dependency com.github.requery:sqlite-android to v3.45.0 (#260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 088e37b2d8d4d790e926eda34119377c2f94ccf6)

# Conflicts:
#	gradle/libs.versions.toml
2024-03-02 12:09:47 -05:00
renovate[bot] 9c3f805eab Update dependency io.github.fornewid:material-motion-compose-core to v1.2.0 (#257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 5b88f1bd94ae3c696681fe34a22453575e747ff6)
2024-03-02 12:08:30 -05:00
renovate[bot] 410eda6d6c Update dependency androidx.benchmark:benchmark-macro-junit4 to v1.2.3 (#255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 18beb20aac774a83d6cb13f6dc276c3683aaf9a2)
2024-03-02 12:08:21 -05:00
renovate[bot] 719c24fb38 Update dependency gradle to v8.6 (#341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 9bff20cb1a0918d7789b281952624fed890fbab7)
2024-03-02 12:08:04 -05:00
Jobobby04 a7cb182bbe Cleanup 2024-03-02 12:01:49 -05:00
Fermín Cirella dbb970d7b5 Make manga page preview row count configurable (#1087)
* Make manga page preview row count configurable

* Replace string with plural
2024-03-02 11:58:16 -05:00
Dexroneum 887a27cf3e [RU] Translations (#1085) 2024-03-02 11:57:24 -05:00
Shamicen eed8ffb9d4 fix password protect downloads and copying ComicInfo files in LocalSource (#1084)
* fix password protect downloads

* fixed copying of ComicInfo file in LocalSource.kt

* Return correct archive file

* Applied upstream fix

* Use tempFileManager instead of file path

* Use streams instead of files
2024-03-02 11:56:57 -05:00
Cuong M. Tran dd412e33ad Rename MangaDex's FollowStatus's property to better reflect its type (#1082) 2024-03-02 11:56:11 -05:00
ɴᴇᴋᴏ 94e5c33785 Update zh-rTW strings.xml (#1080)
* Update strings.xml

* Update new zh-rTW strings.xml

* Update strings.xml

https://github.com/jobobby04/TachiyomiSY/pull/1079#issue-2140674661
2024-03-02 11:55:47 -05:00
Luqman 6f3f109723 add mark read dupes on reading (#1079)
* Feature: mark read dupes

* dupe chapter reading add summary

* Update ReaderViewModel.kt

* Update i18n/src/commonMain/resources/MR/base/strings.xml

* Update app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt

---------

Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>
2024-03-02 11:55:09 -05:00
Jobobby04 d8082de1db Ignore chapters with 0 or under chapter numbers 2024-02-18 11:47:19 -05:00
120 changed files with 1417 additions and 957 deletions
+2 -2
View File
@@ -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.10.4" Example: "1.10.5"
validations: validations:
required: true required: true
@@ -96,7 +96,7 @@ 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.10.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**. - label: I have updated the app to version **[1.10.5](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
required: true required: true
- label: I have updated all installed extensions. - label: I have updated all installed 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.10.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**. - label: I have updated the app to version **[1.10.5](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
+4 -3
View File
@@ -32,10 +32,11 @@ jobs:
java-version: 17 java-version: 17
distribution: adopt distribution: adopt
- name: Set up gradle
uses: gradle/actions/setup-gradle@v3
- name: Build app - name: Build app
uses: gradle/gradle-command-action@v2 run: ./gradlew assembleDevDebug
with:
arguments: assembleDevDebug
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
+9 -3
View File
@@ -26,8 +26,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi.sy" applicationId = "eu.kanade.tachiyomi.sy"
versionCode = 64 versionCode = 65
versionName = "1.10.4" versionName = "1.10.5"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -310,7 +310,7 @@ tasks {
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=coil.annotation.ExperimentalCoilApi", "-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi", "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.FlowPreview",
@@ -330,6 +330,12 @@ tasks {
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath, project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
) )
} }
// https://developer.android.com/jetpack/androidx/releases/compose-compiler#1.5.9
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:nonSkippingGroupOptimization=true",
)
} }
} }
@@ -104,7 +104,13 @@ fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
/** /**
* Creates a ComicInfo instance based on the manga and chapter metadata. * Creates a ComicInfo instance based on the manga and chapter metadata.
*/ */
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories: List<String>?) = ComicInfo( fun getComicInfo(
manga: Manga,
chapter: Chapter,
urls: List<String>,
categories: List<String>?,
sourceName: String,
) = ComicInfo(
title = ComicInfo.Title(chapter.name), title = ComicInfo.Title(chapter.name),
series = ComicInfo.Series(manga.title), series = ComicInfo.Series(manga.title),
number = chapter.chapterNumber.takeIf { it >= 0 }?.let { number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
@@ -114,7 +120,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories:
ComicInfo.Number(it.toString()) ComicInfo.Number(it.toString())
} }
}, },
web = ComicInfo.Web(chapterUrl), web = ComicInfo.Web(urls.joinToString(" ")),
summary = manga.description?.let { ComicInfo.Summary(it) }, summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) }, writer = manga.author?.let { ComicInfo.Writer(it) },
penciller = manga.artist?.let { ComicInfo.Penciller(it) }, penciller = manga.artist?.let { ComicInfo.Penciller(it) },
@@ -124,6 +130,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories:
ComicInfoPublishingStatus.toComicInfoValue(manga.status), ComicInfoPublishingStatus.toComicInfoValue(manga.status),
), ),
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) }, categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
source = ComicInfo.SourceMihon(sourceName),
// SY --> // SY -->
padding = CbzCrypto.createComicInfoPadding()?.let { ComicInfo.PaddingTachiyomiSY(it) }, padding = CbzCrypto.createComicInfoPadding()?.let { ComicInfo.PaddingTachiyomiSY(it) },
// SY <-- // SY <--
@@ -23,7 +23,7 @@ class TrackChapter(
private val delayedTrackingStore: DelayedTrackingStore, private val delayedTrackingStore: DelayedTrackingStore,
) { ) {
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) { suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) {
withNonCancellableContext { withNonCancellableContext {
val tracks = getTracks.await(mangaId) val tracks = getTracks.await(mangaId)
if (tracks.isEmpty()) return@withNonCancellableContext if (tracks.isEmpty()) return@withNonCancellableContext
@@ -34,7 +34,7 @@ class TrackChapter(
service == null || service == null ||
!service.isLoggedIn || !service.isLoggedIn ||
chapterNumber <= track.lastChapterRead /* SY --> */ || chapterNumber <= track.lastChapterRead /* SY --> */ ||
(service is MdList && track.status == FollowStatus.UNFOLLOWED.int.toLong())/* SY <-- */ (service is MdList && track.status == FollowStatus.UNFOLLOWED.long)/* SY <-- */
) { ) {
return@mapNotNull null return@mapNotNull null
} }
@@ -50,7 +50,9 @@ class TrackChapter(
delayedTrackingStore.remove(track.id) delayedTrackingStore.remove(track.id)
} catch (e: Exception) { } catch (e: Exception) {
delayedTrackingStore.add(track.id, chapterNumber) delayedTrackingStore.add(track.id, chapterNumber)
DelayedTrackingUpdateJob.setupTask(context) if (setupJobOnFailure) {
DelayedTrackingUpdateJob.setupTask(context)
}
throw e throw e
} }
} }
@@ -45,7 +45,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
logcat(LogPriority.DEBUG) { logcat(LogPriority.DEBUG) {
"Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
} }
trackChapter.await(context, track.mangaId, track.lastChapterRead) trackChapter.await(context, track.mangaId, track.lastChapterRead, setupJobOnFailure = false)
} }
} }
@@ -8,8 +8,8 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum import tachiyomi.core.common.preference.getEnum
import java.text.DateFormat import java.time.format.DateTimeFormatter
import java.text.SimpleDateFormat import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
class UiPreferences( class UiPreferences(
@@ -46,6 +46,8 @@ class UiPreferences(
fun mergeInOverflow() = preferenceStore.getBoolean("merge_in_overflow", true) fun mergeInOverflow() = preferenceStore.getBoolean("merge_in_overflow", true)
fun previewsRowCount() = preferenceStore.getInt("pref_previews_row_count", 4)
fun useNewSourceNavigation() = preferenceStore.getBoolean("use_new_source_navigation", true) fun useNewSourceNavigation() = preferenceStore.getBoolean("use_new_source_navigation", true)
fun bottomBarLabels() = preferenceStore.getBoolean("pref_show_bottom_bar_labels", true) fun bottomBarLabels() = preferenceStore.getBoolean("pref_show_bottom_bar_labels", true)
@@ -57,9 +59,9 @@ class UiPreferences(
// SY <-- // SY <--
companion object { companion object {
fun dateFormat(format: String): DateFormat = when (format) { fun dateFormat(format: String): DateTimeFormatter = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT) "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault()) else -> DateTimeFormatter.ofPattern(format, Locale.getDefault())
} }
} }
} }
@@ -25,7 +25,7 @@ import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import eu.kanade.domain.source.model.icon import eu.kanade.domain.source.model.icon
import eu.kanade.presentation.util.rememberResourceBitmapPainter import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -50,7 +50,8 @@ import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.components.BadgeGroup import tachiyomi.presentation.core.components.BadgeGroup
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 java.util.Date import java.time.Instant
import java.time.ZoneId
@Composable @Composable
fun BrowseSourceEHentaiList( fun BrowseSourceEHentaiList(
@@ -128,9 +129,11 @@ fun BrowseSourceEHentaiListItem(
} }
val datePosted by produceState("", metadata) { val datePosted by produceState("", metadata) {
value = withIOContext { value = withIOContext {
runCatching { metadata.datePosted?.let { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) } } runCatching {
.getOrNull() metadata.datePosted?.let {
.orEmpty() MetadataUtil.EX_DATE_FORMAT.format(Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()))
}
}.getOrNull().orEmpty()
} }
} }
val genre by produceState<Pair<GenreColor, StringResource>?>(null, metadata) { val genre by produceState<Pair<GenreColor, StringResource>?>(null, metadata) {
@@ -9,20 +9,26 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Date import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
@Composable @Composable
fun relativeDateText( fun relativeDateText(
dateEpochMillis: Long, dateEpochMillis: Long,
): String { ): String {
return relativeDateText( return relativeDateText(
date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L }, localDate = LocalDate.ofInstant(
Instant.ofEpochMilli(dateEpochMillis),
ZoneId.systemDefault(),
)
.takeIf { dateEpochMillis > 0L },
) )
} }
@Composable @Composable
fun relativeDateText( fun relativeDateText(
date: Date?, localDate: LocalDate?,
): String { ): String {
val context = LocalContext.current val context = LocalContext.current
@@ -30,11 +36,10 @@ fun relativeDateText(
val relativeTime = remember { preferences.relativeTime().get() } val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
return date return localDate?.toRelativeString(
?.toRelativeString( context = context,
context = context, relative = relativeTime,
relative = relativeTime, dateFormat = dateFormat,
dateFormat = dateFormat, )
)
?: stringResource(MR.strings.not_applicable) ?: stringResource(MR.strings.not_applicable)
} }
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -29,7 +30,6 @@ import androidx.compose.ui.util.fastForEachIndexed
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.components.material.TabText
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -78,9 +78,8 @@ fun TabbedDialog(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
state = pagerState, state = pagerState,
verticalAlignment = Alignment.Top, verticalAlignment = Alignment.Top,
) { page -> pageContent = { page -> content(page) }
content(page) )
}
} }
} }
} }
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.PrimaryTabRow
@@ -24,7 +25,6 @@ import dev.icerock.moko.resources.StringResource
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.components.material.TabText
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -29,7 +29,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import java.util.Date import java.time.LocalDate
@Composable @Composable
fun HistoryScreen( fun HistoryScreen(
@@ -134,7 +134,7 @@ private fun HistoryScreenContent(
} }
sealed interface HistoryUiModel { sealed interface HistoryUiModel {
data class Header(val date: Date) : HistoryUiModel data class Header(val date: LocalDate) : HistoryUiModel
data class Item(val item: HistoryWithRelations) : HistoryUiModel data class Item(val item: HistoryWithRelations) : HistoryUiModel
} }
@@ -7,6 +7,7 @@ import kotlinx.collections.immutable.toImmutableList
import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
import java.time.Instant import java.time.Instant
import java.time.LocalDate
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.Date import java.util.Date
import kotlin.random.Random import kotlin.random.Random
@@ -73,10 +74,10 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
private object HistoryUiModelExamples { private object HistoryUiModelExamples {
val headerToday = header() val headerToday = header()
val headerTomorrow = val headerTomorrow =
HistoryUiModel.Header(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))) HistoryUiModel.Header(LocalDate.now().plusDays(1))
fun header(instantBuilder: (Instant) -> Instant = { it }) = fun header(instantBuilder: (Instant) -> Instant = { it }) =
HistoryUiModel.Header(Date.from(instantBuilder(Instant.now()))) HistoryUiModel.Header(LocalDate.from(instantBuilder(Instant.now())))
fun items() = sequence { fun items() = sequence {
var count = 1 var count = 1
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -22,7 +23,6 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.util.plus import tachiyomi.presentation.core.util.plus
@@ -106,7 +106,8 @@ import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp import tachiyomi.presentation.core.util.isScrollingUp
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import java.time.Instant import java.time.Instant
import java.util.Date import java.time.ZoneId
import java.time.ZonedDateTime
@Composable @Composable
fun MangaScreen( fun MangaScreen(
@@ -150,6 +151,7 @@ fun MangaScreen(
onMergeWithAnotherClicked: () -> Unit, onMergeWithAnotherClicked: () -> Unit,
onOpenPagePreview: (Int) -> Unit, onOpenPagePreview: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit, onMorePreviewsClicked: () -> Unit,
previewsRowCount: Int,
// SY <-- // SY <--
// For bottom action menu // For bottom action menu
@@ -208,6 +210,7 @@ fun MangaScreen(
onMergeWithAnotherClicked = onMergeWithAnotherClicked, onMergeWithAnotherClicked = onMergeWithAnotherClicked,
onOpenPagePreview = onOpenPagePreview, onOpenPagePreview = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked, onMorePreviewsClicked = onMorePreviewsClicked,
previewsRowCount = previewsRowCount,
// SY <-- // SY <--
onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
@@ -253,6 +256,7 @@ fun MangaScreen(
onMergeWithAnotherClicked = onMergeWithAnotherClicked, onMergeWithAnotherClicked = onMergeWithAnotherClicked,
onOpenPagePreview = onOpenPagePreview, onOpenPagePreview = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked, onMorePreviewsClicked = onMorePreviewsClicked,
previewsRowCount = previewsRowCount,
// SY <-- // SY <--
onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
@@ -308,6 +312,7 @@ private fun MangaScreenSmallImpl(
onMergeWithAnotherClicked: () -> Unit, onMergeWithAnotherClicked: () -> Unit,
onOpenPagePreview: (Int) -> Unit, onOpenPagePreview: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit, onMorePreviewsClicked: () -> Unit,
previewsRowCount: Int,
// SY <-- // SY <--
// For bottom action menu // For bottom action menu
@@ -544,13 +549,14 @@ private fun MangaScreenSmallImpl(
} }
} }
if (state.pagePreviewsState !is PagePreviewState.Unused) { if (state.pagePreviewsState !is PagePreviewState.Unused && previewsRowCount > 0) {
PagePreviewItems( PagePreviewItems(
pagePreviewState = state.pagePreviewsState, pagePreviewState = state.pagePreviewsState,
onOpenPage = onOpenPagePreview, onOpenPage = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked, onMorePreviewsClicked = onMorePreviewsClicked,
maxWidth = maxWidth, maxWidth = maxWidth,
setMaxWidth = { maxWidth = it } setMaxWidth = { maxWidth = it },
rowCount = previewsRowCount,
) )
} }
// SY <-- // SY <--
@@ -632,6 +638,7 @@ fun MangaScreenLargeImpl(
onMergeWithAnotherClicked: () -> Unit, onMergeWithAnotherClicked: () -> Unit,
onOpenPagePreview: (Int) -> Unit, onOpenPagePreview: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit, onMorePreviewsClicked: () -> Unit,
previewsRowCount: Int,
// SY <-- // SY <--
// For bottom action menu // For bottom action menu
@@ -832,11 +839,12 @@ fun MangaScreenLargeImpl(
onMergeWithAnotherClicked = onMergeWithAnotherClicked, onMergeWithAnotherClicked = onMergeWithAnotherClicked,
) )
} }
if (state.pagePreviewsState !is PagePreviewState.Unused) { if (state.pagePreviewsState !is PagePreviewState.Unused && previewsRowCount > 0) {
PagePreviews( PagePreviews(
pagePreviewState = state.pagePreviewsState, pagePreviewState = state.pagePreviewsState,
onOpenPage = onOpenPagePreview, onOpenPage = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked, onMorePreviewsClicked = onMorePreviewsClicked,
rowCount = previewsRowCount,
) )
} }
// SY <-- // SY <--
@@ -979,9 +987,10 @@ private fun LazyListScope.sharedChapterItems(
?.let { ?.let {
// SY --> // SY -->
if (manga.isEhBasedManga()) { if (manga.isEhBasedManga()) {
MetadataUtil.EX_DATE_FORMAT.format(Date(it)) MetadataUtil.EX_DATE_FORMAT
.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault()))
} else { } else {
relativeDateText(Date(item.chapter.dateUpload)) relativeDateText(item.chapter.dateUpload)
} }
// SY <-- // SY <--
}, },
@@ -2,7 +2,6 @@ package eu.kanade.presentation.manga.components
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@@ -24,8 +23,8 @@ import androidx.compose.runtime.remember
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.composed
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -86,11 +85,13 @@ private fun NotDownloadedIndicator(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit, onClick: (ChapterDownloadAction) -> Unit,
) { ) {
val hapticFeedback = LocalHapticFeedback.current
Box( Box(
modifier = modifier modifier = modifier
.size(IconButtonTokens.StateLayerSize) .size(IconButtonTokens.StateLayerSize)
.commonClickable( .commonClickable(
enabled = enabled, enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { onClick(ChapterDownloadAction.START_NOW) }, onLongClick = { onClick(ChapterDownloadAction.START_NOW) },
onClick = { onClick(ChapterDownloadAction.START) }, onClick = { onClick(ChapterDownloadAction.START) },
) )
@@ -114,12 +115,14 @@ private fun DownloadingIndicator(
onClick: (ChapterDownloadAction) -> Unit, onClick: (ChapterDownloadAction) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val hapticFeedback = LocalHapticFeedback.current
var isMenuExpanded by remember { mutableStateOf(false) } var isMenuExpanded by remember { mutableStateOf(false) }
Box( Box(
modifier = modifier modifier = modifier
.size(IconButtonTokens.StateLayerSize) .size(IconButtonTokens.StateLayerSize)
.commonClickable( .commonClickable(
enabled = enabled, enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { onClick(ChapterDownloadAction.CANCEL) }, onLongClick = { onClick(ChapterDownloadAction.CANCEL) },
onClick = { isMenuExpanded = true }, onClick = { isMenuExpanded = true },
), ),
@@ -185,12 +188,14 @@ private fun DownloadedIndicator(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit, onClick: (ChapterDownloadAction) -> Unit,
) { ) {
val hapticFeedback = LocalHapticFeedback.current
var isMenuExpanded by remember { mutableStateOf(false) } var isMenuExpanded by remember { mutableStateOf(false) }
Box( Box(
modifier = modifier modifier = modifier
.size(IconButtonTokens.StateLayerSize) .size(IconButtonTokens.StateLayerSize)
.commonClickable( .commonClickable(
enabled = enabled, enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { isMenuExpanded = true }, onLongClick = { isMenuExpanded = true },
onClick = { isMenuExpanded = true }, onClick = { isMenuExpanded = true },
), ),
@@ -220,11 +225,13 @@ private fun ErrorIndicator(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit, onClick: (ChapterDownloadAction) -> Unit,
) { ) {
val hapticFeedback = LocalHapticFeedback.current
Box( Box(
modifier = modifier modifier = modifier
.size(IconButtonTokens.StateLayerSize) .size(IconButtonTokens.StateLayerSize)
.commonClickable( .commonClickable(
enabled = enabled, enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { onClick(ChapterDownloadAction.START) }, onLongClick = { onClick(ChapterDownloadAction.START) },
onClick = { onClick(ChapterDownloadAction.START) }, onClick = { onClick(ChapterDownloadAction.START) },
), ),
@@ -241,26 +248,23 @@ private fun ErrorIndicator(
private fun Modifier.commonClickable( private fun Modifier.commonClickable(
enabled: Boolean, enabled: Boolean,
hapticFeedback: HapticFeedback,
onLongClick: () -> Unit, onLongClick: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
) = composed { ) = this.combinedClickable(
val haptic = LocalHapticFeedback.current enabled = enabled,
onLongClick = {
Modifier.combinedClickable( onLongClick()
enabled = enabled, hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onLongClick = { },
onLongClick() onClick = onClick,
haptic.performHapticFeedback(HapticFeedbackType.LongPress) role = Role.Button,
}, interactionSource = null,
onClick = onClick, indication = ripple(
role = Role.Button, bounded = false,
interactionSource = remember { MutableInteractionSource() }, radius = IconButtonTokens.StateLayerSize / 2,
indication = ripple( ),
bounded = false, )
radius = IconButtonTokens.StateLayerSize / 2,
),
)
}
private val IndicatorSize = 26.dp private val IndicatorSize = 26.dp
private val IndicatorPadding = 2.dp private val IndicatorPadding = 2.dp
@@ -24,36 +24,27 @@ import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
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.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import me.saket.swipe.SwipeableActionsBox import me.saket.swipe.SwipeableActionsBox
import me.saket.swipe.rememberSwipeableActionsState
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.ReadItemAlpha
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.selectedBackground import tachiyomi.presentation.core.util.selectedBackground
import kotlin.math.absoluteValue
@Composable @Composable
fun MangaChapterListItem( fun MangaChapterListItem(
@@ -78,158 +69,132 @@ fun MangaChapterListItem(
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val haptic = LocalHapticFeedback.current
val density = LocalDensity.current
val textAlpha = if (read) ReadItemAlpha else 1f val textAlpha = if (read) ReadItemAlpha else 1f
val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha
// Increase touch slop of swipe action to reduce accidental trigger val start = getSwipeAction(
val configuration = LocalViewConfiguration.current action = chapterSwipeStartAction,
CompositionLocalProvider( read = read,
LocalViewConfiguration provides object : ViewConfiguration by configuration { bookmark = bookmark,
override val touchSlop: Float = configuration.touchSlop * 3f downloadState = downloadStateProvider(),
}, background = MaterialTheme.colorScheme.primaryContainer,
onSwipe = { onChapterSwipe(chapterSwipeStartAction) },
)
val end = getSwipeAction(
action = chapterSwipeEndAction,
read = read,
bookmark = bookmark,
downloadState = downloadStateProvider(),
background = MaterialTheme.colorScheme.primaryContainer,
onSwipe = { onChapterSwipe(chapterSwipeEndAction) },
)
SwipeableActionsBox(
modifier = Modifier.clipToBounds(),
startActions = listOfNotNull(start),
endActions = listOfNotNull(end),
swipeThreshold = swipeActionThreshold,
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
) { ) {
val start = getSwipeAction( Row(
action = chapterSwipeStartAction, modifier = modifier
read = read, .selectedBackground(selected)
bookmark = bookmark, .combinedClickable(
downloadState = downloadStateProvider(), onClick = onClick,
background = MaterialTheme.colorScheme.primaryContainer, onLongClick = onLongClick,
onSwipe = { onChapterSwipe(chapterSwipeStartAction) }, )
) .padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
val end = getSwipeAction(
action = chapterSwipeEndAction,
read = read,
bookmark = bookmark,
downloadState = downloadStateProvider(),
background = MaterialTheme.colorScheme.primaryContainer,
onSwipe = { onChapterSwipe(chapterSwipeEndAction) },
)
val swipeableActionsState = rememberSwipeableActionsState()
LaunchedEffect(Unit) {
// Haptic effect when swipe over threshold
val swipeActionThresholdPx = with(density) { swipeActionThreshold.toPx() }
snapshotFlow { swipeableActionsState.offset.value.absoluteValue > swipeActionThresholdPx }
.collect { if (it) haptic.performHapticFeedback(HapticFeedbackType.LongPress) }
}
SwipeableActionsBox(
modifier = Modifier.clipToBounds(),
state = swipeableActionsState,
startActions = listOfNotNull(start),
endActions = listOfNotNull(end),
swipeThreshold = swipeActionThreshold,
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
) { ) {
Row( Column(
modifier = modifier modifier = Modifier.weight(1f),
.selectedBackground(selected) verticalArrangement = Arrangement.spacedBy(6.dp),
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
) { ) {
Column( Row(
modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically,
) { ) {
Row( var textHeight by remember { mutableIntStateOf(0) }
horizontalArrangement = Arrangement.spacedBy(2.dp), if (!read) {
verticalAlignment = Alignment.CenterVertically, Icon(
) { imageVector = Icons.Filled.Circle,
var textHeight by remember { mutableIntStateOf(0) } contentDescription = stringResource(MR.strings.unread),
if (!read) { modifier = Modifier
Icon( .height(8.dp)
imageVector = Icons.Filled.Circle, .padding(end = 4.dp),
contentDescription = stringResource(MR.strings.unread), tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.height(8.dp)
.padding(end = 4.dp),
tint = MaterialTheme.colorScheme.primary,
)
}
if (bookmark) {
Icon(
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(MR.strings.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
tint = MaterialTheme.colorScheme.primary,
)
}
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = textAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onTextLayout = { textHeight = it.size.height },
) )
} }
if (bookmark) {
Icon(
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(MR.strings.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
tint = MaterialTheme.colorScheme.primary,
)
}
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = textAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onTextLayout = { textHeight = it.size.height },
)
}
Row { Row {
ProvideTextStyle( ProvideTextStyle(
value = MaterialTheme.typography.bodyMedium.copy( value = MaterialTheme.typography.bodyMedium.copy(
fontSize = 12.sp, fontSize = 12.sp,
color = LocalContentColor.current.copy(alpha = textSubtitleAlpha), color = LocalContentColor.current.copy(alpha = textSubtitleAlpha),
), ),
) { ) {
if (date != null) { if (date != null) {
Text( Text(
text = date, text = date,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
if ( if (readProgress != null || scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
readProgress != null || }
scanlator != null/* SY --> */ || if (readProgress != null) {
sourceName != null/* SY <-- */ Text(
) { text = readProgress,
DotSeparatorText() maxLines = 1,
} overflow = TextOverflow.Ellipsis,
} color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
if (readProgress != null) { )
Text( if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
text = readProgress, }
maxLines = 1, // SY -->
overflow = TextOverflow.Ellipsis, if (sourceName != null) {
color = LocalContentColor.current.copy(alpha = ReadItemAlpha), Text(
) text = sourceName,
if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText() maxLines = 1,
} overflow = TextOverflow.Ellipsis,
// SY --> )
if (sourceName != null) { if (scanlator != null) DotSeparatorText()
Text( }
text = sourceName, // SY <--
maxLines = 1, if (scanlator != null) {
overflow = TextOverflow.Ellipsis, Text(
) text = scanlator,
if (scanlator != null) DotSeparatorText() maxLines = 1,
} overflow = TextOverflow.Ellipsis,
// SY <-- )
if (scanlator != null) {
Text(
text = scanlator,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
} }
} }
} }
ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,
onClick = { onDownloadClick?.invoke(it) },
)
} }
ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,
onClick = { onDownloadClick?.invoke(it) },
)
} }
} }
} }
@@ -11,7 +11,7 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import eu.kanade.presentation.util.rememberResourceBitmapPainter import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -38,10 +38,10 @@ import androidx.compose.ui.viewinterop.AndroidView
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.view.updatePadding import androidx.core.view.updatePadding
import coil.imageLoader import coil3.imageLoader
import coil.request.CachePolicy import coil3.request.CachePolicy
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.size.Size import coil3.size.Size
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.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
@@ -169,7 +169,9 @@ fun MangaCoverDialog(
.data(coverDataProvider()) .data(coverDataProvider())
.size(Size.ORIGINAL) .size(Size.ORIGINAL)
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.target { drawable -> .target { image ->
val drawable = image.asDrawable(view.context.resources)
// Copy bitmap in case it came from memory cache // Copy bitmap in case it came from memory cache
// Because SSIV needs to thoroughly read the image // Because SSIV needs to thoroughly read the image
val copy = (drawable as? BitmapDrawable)?.let { val copy = (drawable as? BitmapDrawable)?.let {
@@ -73,7 +73,7 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
@@ -25,14 +25,13 @@ 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.draw.clipToBounds
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent import coil3.compose.SubcomposeAsyncImageContent
import eu.kanade.domain.manga.model.PagePreview import eu.kanade.domain.manga.model.PagePreview
import eu.kanade.presentation.manga.MangaScreenItem import eu.kanade.presentation.manga.MangaScreenItem
import eu.kanade.tachiyomi.ui.manga.PagePreviewState import eu.kanade.tachiyomi.ui.manga.PagePreviewState
@@ -102,6 +101,7 @@ fun PagePreviews(
pagePreviewState: PagePreviewState, pagePreviewState: PagePreviewState,
onOpenPage: (Int) -> Unit, onOpenPage: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit, onMorePreviewsClicked: () -> Unit,
rowCount: Int,
) { ) {
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
var maxWidth by remember { var maxWidth by remember {
@@ -113,7 +113,7 @@ fun PagePreviews(
} }
pagePreviewState is PagePreviewState.Success -> { pagePreviewState is PagePreviewState.Success -> {
val itemPerRowCount = (maxWidth / 120.dp).floor() val itemPerRowCount = (maxWidth / 120.dp).floor()
pagePreviewState.pagePreviews.take(4 * itemPerRowCount).chunked(itemPerRowCount).forEach { pagePreviewState.pagePreviews.take(rowCount * itemPerRowCount).chunked(itemPerRowCount).forEach {
PagePreviewRow( PagePreviewRow(
onOpenPage = onOpenPage, onOpenPage = onOpenPage,
items = remember(it) { it.toImmutableList() } items = remember(it) { it.toImmutableList() }
@@ -132,7 +132,8 @@ fun LazyListScope.PagePreviewItems(
onOpenPage: (Int) -> Unit, onOpenPage: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit, onMorePreviewsClicked: () -> Unit,
maxWidth: Dp, maxWidth: Dp,
setMaxWidth: (Dp) -> Unit setMaxWidth: (Dp) -> Unit,
rowCount: Int,
) { ) {
when { when {
pagePreviewState is PagePreviewState.Loading || maxWidth == Dp.Hairline -> { pagePreviewState is PagePreviewState.Loading || maxWidth == Dp.Hairline -> {
@@ -148,7 +149,7 @@ fun LazyListScope.PagePreviewItems(
items( items(
key = { "${MangaScreenItem.CHAPTER_PREVIEW_ROW}-$it" }, key = { "${MangaScreenItem.CHAPTER_PREVIEW_ROW}-$it" },
contentType = { MangaScreenItem.CHAPTER_PREVIEW_ROW }, contentType = { MangaScreenItem.CHAPTER_PREVIEW_ROW },
items = pagePreviewState.pagePreviews.take(4 * itemPerRowCount).chunked(itemPerRowCount), items = pagePreviewState.pagePreviews.take(rowCount * itemPerRowCount).chunked(itemPerRowCount),
) { ) {
PagePreviewRow( PagePreviewRow(
onOpenPage = onOpenPage, onOpenPage = onOpenPage,
@@ -23,11 +23,12 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
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.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.Instant import java.time.LocalDate
object SettingsAppearanceScreen : SearchableSettings { object SettingsAppearanceScreen : SearchableSettings {
@@ -106,7 +107,7 @@ object SettingsAppearanceScreen : SearchableSettings {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val now = remember { Instant.now().toEpochMilli() } val now = remember { LocalDate.now() }
val dateFormat by uiPreferences.dateFormat().collectAsState() val dateFormat by uiPreferences.dateFormat().collectAsState()
val formattedNow = remember(dateFormat) { val formattedNow = remember(dateFormat) {
@@ -157,6 +158,8 @@ object SettingsAppearanceScreen : SearchableSettings {
// SY --> // SY -->
@Composable @Composable
fun getForkGroup(uiPreferences: UiPreferences): Preference.PreferenceGroup { fun getForkGroup(uiPreferences: UiPreferences): Preference.PreferenceGroup {
val previewsRowCount by uiPreferences.previewsRowCount().collectAsState()
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
stringResource(SYMR.strings.pref_category_fork), stringResource(SYMR.strings.pref_category_fork),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
@@ -174,6 +177,21 @@ object SettingsAppearanceScreen : SearchableSettings {
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),
), ),
Preference.PreferenceItem.SliderPreference(
value = previewsRowCount,
title = stringResource(SYMR.strings.pref_previews_row_count),
subtitle = if (previewsRowCount > 0) pluralStringResource(
SYMR.plurals.row_count,
previewsRowCount,
previewsRowCount,
) else stringResource(MR.strings.disabled),
min = 0,
max = 10,
onValueChanged = {
uiPreferences.previewsRowCount().set(it)
true
},
),
), ),
) )
} }
@@ -178,6 +178,11 @@ object SettingsReaderScreen : SearchableSettings {
pref = readerPreferences.skipDupe(), pref = readerPreferences.skipDupe(),
title = stringResource(MR.strings.pref_skip_dupe_chapters), title = stringResource(MR.strings.pref_skip_dupe_chapters),
), ),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.markReadDupe(),
title = stringResource(MR.strings.pref_mark_read_dupe_chapters),
subtitle = stringResource(MR.strings.pref_mark_read_dupe_chapters_summary),
),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.alwaysShowChapterTransition(), pref = readerPreferences.alwaysShowChapterTransition(),
title = stringResource(MR.strings.pref_always_show_chapter_transition), title = stringResource(MR.strings.pref_always_show_chapter_transition),
@@ -382,17 +387,16 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonDoubleTapZoomEnabled(), pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
title = stringResource(MR.strings.pref_double_tap_zoom), title = stringResource(MR.strings.pref_double_tap_zoom),
enabled = true, ),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonDisableZoomOut(),
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
), ),
// SY --> // SY -->
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.pageTransitionsWebtoon(), pref = readerPreferences.pageTransitionsWebtoon(),
title = stringResource(MR.strings.pref_page_transitions), title = stringResource(MR.strings.pref_page_transitions),
), ),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonEnableZoomOut(),
title = stringResource(SYMR.strings.enable_zoom_out),
),
// SY <-- // SY <--
), ),
) )
@@ -56,10 +56,10 @@ import tachiyomi.presentation.core.icons.Reddit
import tachiyomi.presentation.core.icons.X import tachiyomi.presentation.core.icons.X
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.text.DateFormat import java.time.LocalDateTime
import java.text.SimpleDateFormat import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale import java.util.Locale
import java.util.TimeZone
object AboutScreen : Screen() { object AboutScreen : Screen() {
@@ -293,16 +293,9 @@ object AboutScreen : Screen() {
internal fun getFormattedBuildTime(): String { internal fun getFormattedBuildTime(): String {
return try { return try {
val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US) val df = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'", Locale.US)
inputDf.timeZone = TimeZone.getTimeZone("UTC") .withZone(ZoneId.of("UTC"))
val buildTime = inputDf.parse(BuildConfig.BUILD_TIME) val buildTime = LocalDateTime.from(df.parse(BuildConfig.BUILD_TIME))
val outputDf = DateFormat.getDateTimeInstance(
DateFormat.MEDIUM,
DateFormat.SHORT,
Locale.getDefault(),
)
outputDf.timeZone = TimeZone.getDefault()
buildTime!!.toDateTimestampString(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get())) buildTime!!.toDateTimestampString(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
} catch (e: Exception) { } catch (e: Exception) {
@@ -42,7 +42,9 @@ import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.plus import tachiyomi.presentation.core.util.plus
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Date import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
class WorkerInfoScreen : Screen() { class WorkerInfoScreen : Screen() {
@@ -148,13 +150,16 @@ class WorkerInfoScreen : Screen() {
} }
appendLine("State: ${workInfo.state}") appendLine("State: ${workInfo.state}")
if (workInfo.state == WorkInfo.State.ENQUEUED) { if (workInfo.state == WorkInfo.State.ENQUEUED) {
appendLine( val timestamp = LocalDateTime.ofInstant(
"Next scheduled run: ${Date(workInfo.nextScheduleTimeMillis).toDateTimestampString( Instant.ofEpochMilli(workInfo.nextScheduleTimeMillis),
ZoneId.systemDefault(),
)
.toDateTimestampString(
UiPreferences.dateFormat( UiPreferences.dateFormat(
Injekt.get<UiPreferences>().dateFormat().get(), Injekt.get<UiPreferences>().dateFormat().get(),
), ),
)}", )
) appendLine("Next scheduled run: $timestamp",)
appendLine("Attempt #${workInfo.runAttemptCount + 1}") appendLine("Attempt #${workInfo.runAttemptCount + 1}")
} }
appendLine() appendLine()
@@ -22,7 +22,10 @@ import exh.source.isEhBasedManga
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import java.util.Date import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZonedDateTime
@Composable @Composable
fun ChapterListDialog( fun ChapterListDialog(
@@ -56,9 +59,13 @@ fun ChapterListDialog(
?.let { ?.let {
// SY --> // SY -->
if (manga?.isEhBasedManga() == true) { if (manga?.isEhBasedManga() == true) {
MetadataUtil.EX_DATE_FORMAT.format(Date(it)) MetadataUtil.EX_DATE_FORMAT
.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault()))
} else { } else {
Date(it).toRelativeString(context, dateRelativeTime, chapterItem.dateFormat) LocalDate.ofInstant(
Instant.ofEpochMilli(it),
ZoneId.systemDefault(),
).toRelativeString(context, dateRelativeTime, chapterItem.dateFormat)
} }
// SY <-- // SY <--
}, },
@@ -217,11 +217,6 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
label = stringResource(MR.strings.pref_page_transitions), label = stringResource(MR.strings.pref_page_transitions),
pref = screenModel.preferences.pageTransitionsWebtoon(), pref = screenModel.preferences.pageTransitionsWebtoon(),
) )
CheckboxItem(
label = stringResource(SYMR.strings.enable_zoom_out),
pref = screenModel.preferences.webtoonEnableZoomOut(),
)
// SY <-- // SY <--
val dualPageSplitWebtoon by screenModel.preferences.dualPageSplitWebtoon().collectAsState() val dualPageSplitWebtoon by screenModel.preferences.dualPageSplitWebtoon().collectAsState()
@@ -254,6 +249,10 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
label = stringResource(MR.strings.pref_double_tap_zoom), label = stringResource(MR.strings.pref_double_tap_zoom),
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(), pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(),
) )
CheckboxItem(
label = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
pref = screenModel.preferences.webtoonDisableZoomOut(),
)
} }
// SY --> // SY -->
@@ -8,6 +8,7 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.AppTheme import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.presentation.theme.colorscheme.BaseColorScheme
import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme 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
@@ -62,23 +63,27 @@ private fun getThemeColorScheme(
appTheme: AppTheme, appTheme: AppTheme,
isAmoled: Boolean, isAmoled: Boolean,
): ColorScheme { ): ColorScheme {
val colorScheme = when (appTheme) { val colorScheme = if (appTheme == AppTheme.MONET) {
AppTheme.DEFAULT -> TachiyomiColorScheme MonetColorScheme(LocalContext.current)
AppTheme.MONET -> MonetColorScheme(LocalContext.current) } else {
AppTheme.GREEN_APPLE -> GreenAppleColorScheme colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
AppTheme.LAVENDER -> LavenderColorScheme
AppTheme.MIDNIGHT_DUSK -> MidnightDuskColorScheme
AppTheme.NORD -> NordColorScheme
AppTheme.STRAWBERRY_DAIQUIRI -> StrawberryColorScheme
AppTheme.TAKO -> TakoColorScheme
AppTheme.TEALTURQUOISE -> TealTurqoiseColorScheme
AppTheme.TIDAL_WAVE -> TidalWaveColorScheme
AppTheme.YINYANG -> YinYangColorScheme
AppTheme.YOTSUBA -> YotsubaColorScheme
else -> TachiyomiColorScheme
} }
return colorScheme.getColorScheme( return colorScheme.getColorScheme(
isSystemInDarkTheme(), isSystemInDarkTheme(),
isAmoled, isAmoled,
) )
} }
private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
AppTheme.DEFAULT to TachiyomiColorScheme,
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
AppTheme.LAVENDER to LavenderColorScheme,
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
AppTheme.NORD to NordColorScheme,
AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme,
AppTheme.TAKO to TakoColorScheme,
AppTheme.TEALTURQUOISE to TealTurqoiseColorScheme,
AppTheme.TIDAL_WAVE to TidalWaveColorScheme,
AppTheme.YINYANG to YinYangColorScheme,
AppTheme.YOTSUBA to YotsubaColorScheme,
)
@@ -9,18 +9,15 @@ internal abstract class BaseColorScheme {
abstract val lightScheme: ColorScheme abstract val lightScheme: ColorScheme
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme { fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
return (if (isDark) darkScheme else lightScheme) if (!isDark) return lightScheme
.let {
if (isDark && isAmoled) { if (!isAmoled) return darkScheme
it.copy(
background = Color.Black, return darkScheme.copy(
onBackground = Color.White, background = Color.Black,
surface = Color.Black, onBackground = Color.White,
onSurface = Color.White, surface = Color.Black,
) onSurface = Color.White,
} else { )
it
}
}
} }
} }
@@ -52,17 +52,18 @@ import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.presentation.track.components.TrackLogoIcon import eu.kanade.presentation.track.components.TrackLogoIcon
import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.lang.toLocalDate
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import java.text.DateFormat import java.time.format.DateTimeFormatter
private const val UnsetStatusTextAlpha = 0.5F private const val UnsetStatusTextAlpha = 0.5F
@Composable @Composable
fun TrackInfoDialogHome( fun TrackInfoDialogHome(
trackItems: List<TrackItem>, trackItems: List<TrackItem>,
dateFormat: DateFormat, dateFormat: DateTimeFormatter,
onStatusClick: (TrackItem) -> Unit, onStatusClick: (TrackItem) -> Unit,
onChapterClick: (TrackItem) -> Unit, onChapterClick: (TrackItem) -> Unit,
onScoreClick: (TrackItem) -> Unit, onScoreClick: (TrackItem) -> Unit,
@@ -104,11 +105,11 @@ fun TrackInfoDialogHome(
.takeIf { supportsScoring && item.track.score != 0.0 }, .takeIf { supportsScoring && item.track.score != 0.0 },
onScoreClick = { onScoreClick(item) } onScoreClick = { onScoreClick(item) }
.takeIf { supportsScoring }, .takeIf { supportsScoring },
startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate) } startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate.toLocalDate()) }
.takeIf { supportsReadingDates && item.track.startDate != 0L }, .takeIf { supportsReadingDates && item.track.startDate != 0L },
onStartDateClick = { onStartDateEdit(item) } // TODO onStartDateClick = { onStartDateEdit(item) } // TODO
.takeIf { supportsReadingDates }, .takeIf { supportsReadingDates },
endDate = dateFormat.format(item.track.finishDate) endDate = dateFormat.format(item.track.finishDate.toLocalDate())
.takeIf { supportsReadingDates && item.track.finishDate != 0L }, .takeIf { supportsReadingDates && item.track.finishDate != 0L },
onEndDateClick = { onEndDateEdit(item) } onEndDateClick = { onEndDateEdit(item) }
.takeIf { supportsReadingDates }, .takeIf { supportsReadingDates },
@@ -5,7 +5,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.test.DummyTracker import eu.kanade.test.DummyTracker
import tachiyomi.domain.track.model.Track import tachiyomi.domain.track.model.Track
import java.text.DateFormat import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
internal class TrackInfoDialogHomePreviewProvider : internal class TrackInfoDialogHomePreviewProvider :
PreviewParameterProvider<@Composable () -> Unit> { PreviewParameterProvider<@Composable () -> Unit> {
@@ -46,7 +47,7 @@ internal class TrackInfoDialogHomePreviewProvider :
trackItemWithoutTrack, trackItemWithoutTrack,
trackItemWithTrack, trackItemWithTrack,
), ),
dateFormat = DateFormat.getDateInstance(), dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
onStatusClick = {}, onStatusClick = {},
onChapterClick = {}, onChapterClick = {},
onScoreClick = {}, onScoreClick = {},
@@ -61,7 +62,7 @@ internal class TrackInfoDialogHomePreviewProvider :
private val noTrackers = @Composable { private val noTrackers = @Composable {
TrackInfoDialogHome( TrackInfoDialogHome(
trackItems = listOf(), trackItems = listOf(),
dateFormat = DateFormat.getDateInstance(), dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
onStatusClick = {}, onStatusClick = {},
onChapterClick = {}, onChapterClick = {},
onScoreClick = {}, onScoreClick = {},
@@ -36,7 +36,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import java.util.Date import java.time.LocalDate
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@Composable @Composable
@@ -212,6 +212,6 @@ private fun UpdatesBottomBar(
} }
sealed interface UpdatesUiModel { sealed interface UpdatesUiModel {
data class Header(val date: Date) : UpdatesUiModel data class Header(val date: LocalDate) : UpdatesUiModel
data class Item(val item: UpdatesItem) : UpdatesUiModel data class Item(val item: UpdatesItem) : UpdatesUiModel
} }
+17 -21
View File
@@ -15,12 +15,14 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import coil.ImageLoader import coil3.ImageLoader
import coil.ImageLoaderFactory import coil3.SingletonImageLoader
import coil.decode.GifDecoder import coil3.disk.DiskCache
import coil.decode.ImageDecoderDecoder import coil3.disk.directory
import coil.disk.DiskCache import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil.util.DebugLogger import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.util.DebugLogger
import com.elvishew.xlog.LogConfiguration import com.elvishew.xlog.LogConfiguration
import com.elvishew.xlog.LogLevel import com.elvishew.xlog.LogLevel
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
@@ -81,7 +83,7 @@ import java.security.Security
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
private val basePreferences: BasePreferences by injectLazy() private val basePreferences: BasePreferences by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy() private val networkPreferences: NetworkPreferences by injectLazy()
@@ -166,28 +168,23 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
}*/ }*/
} }
override fun newImageLoader(): ImageLoader { override fun newImageLoader(context: Context): ImageLoader {
return ImageLoader.Builder(this).apply { return ImageLoader.Builder(this).apply {
val callFactoryInit = { Injekt.get<NetworkHelper>().client } val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
val diskCacheInit = { CoilDiskCache.get(this@App) } val diskCacheLazy = lazy { CoilDiskCache.get(this@App) }
components { components {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
add(TachiyomiImageDecoder.Factory()) add(TachiyomiImageDecoder.Factory())
add(MangaCoverFetcher.MangaFactory(lazy(callFactoryInit), lazy(diskCacheInit))) add(MangaCoverFetcher.MangaFactory(callFactoryLazy, diskCacheLazy))
add(MangaCoverFetcher.MangaCoverFactory(lazy(callFactoryInit), lazy(diskCacheInit))) add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy, diskCacheLazy))
add(MangaKeyer()) add(MangaKeyer())
add(MangaCoverKeyer()) add(MangaCoverKeyer())
// SY --> // SY -->
add(PagePreviewKeyer()) add(PagePreviewKeyer())
add(PagePreviewFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit))) add(PagePreviewFetcher.Factory(callFactoryLazy, diskCacheLazy))
// SY <-- // SY <--
} }
callFactory(callFactoryInit) diskCache(diskCacheLazy::value)
diskCache(diskCacheInit)
crossfade((300 * this@App.animatorDurationScale).toInt()) crossfade((300 * this@App.animatorDurationScale).toInt())
allowRgb565(DeviceUtil.isLowRamDevice(this@App)) allowRgb565(DeviceUtil.isLowRamDevice(this@App))
if (networkPreferences.verboseLogging().get()) logger(DebugLogger()) if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
@@ -195,7 +192,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
// Coil spawns a new thread for every image load by default // Coil spawns a new thread for every image load by default
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8)) fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
decoderDispatcher(Dispatchers.IO.limitedParallelism(2)) decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
}.build() }.build()
} }
@@ -1,19 +1,19 @@
package eu.kanade.tachiyomi.data.coil package eu.kanade.tachiyomi.data.coil
import androidx.core.net.toUri import androidx.core.net.toUri
import coil.ImageLoader import coil3.Extras
import coil.decode.DataSource import coil3.ImageLoader
import coil.decode.ImageSource import coil3.decode.DataSource
import coil.disk.DiskCache import coil3.decode.ImageSource
import coil.fetch.FetchResult import coil3.disk.DiskCache
import coil.fetch.Fetcher import coil3.fetch.FetchResult
import coil.fetch.SourceResult import coil3.fetch.Fetcher
import coil.network.HttpException import coil3.fetch.SourceFetchResult
import coil.request.Options import coil3.getOrDefault
import coil.request.Parameters import coil3.request.Options
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER_KEY
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import logcat.LogPriority import logcat.LogPriority
@@ -22,6 +22,7 @@ import okhttp3.Call
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.http.HTTP_NOT_MODIFIED import okhttp3.internal.http.HTTP_NOT_MODIFIED
import okio.FileSystem
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import okio.Source import okio.Source
import okio.buffer import okio.buffer
@@ -33,6 +34,7 @@ import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.IOException
/** /**
* A [Fetcher] that fetches cover image for [Manga] object. * A [Fetcher] that fetches cover image for [Manga] object.
@@ -42,7 +44,7 @@ import java.io.File
* handled by Coil's [DiskCache]. * handled by Coil's [DiskCache].
* *
* Available request parameter: * Available request parameter:
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true * - [USE_CUSTOM_COVER_KEY]: Use custom cover if set by user, default is true
*/ */
class MangaCoverFetcher( class MangaCoverFetcher(
private val url: String?, private val url: String?,
@@ -61,7 +63,7 @@ class MangaCoverFetcher(
override suspend fun fetch(): FetchResult { override suspend fun fetch(): FetchResult {
// Use custom cover if exists // Use custom cover if exists
val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true val useCustomCover = options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)
if (useCustomCover) { if (useCustomCover) {
val customCoverFile = customCoverFileLazy.value val customCoverFile = customCoverFileLazy.value
if (customCoverFile.exists()) { if (customCoverFile.exists()) {
@@ -80,8 +82,12 @@ class MangaCoverFetcher(
} }
private fun fileLoader(file: File): FetchResult { private fun fileLoader(file: File): FetchResult {
return SourceResult( return SourceFetchResult(
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey), source = ImageSource(
file = file.toOkioPath(),
fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey
),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
) )
@@ -92,8 +98,8 @@ class MangaCoverFetcher(
.openInputStream() .openInputStream()
.source() .source()
.buffer() .buffer()
return SourceResult( return SourceFetchResult(
source = ImageSource(source = source, context = options.context), source = ImageSource(source = source, fileSystem = FileSystem.SYSTEM),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
) )
@@ -121,7 +127,7 @@ class MangaCoverFetcher(
} }
// Read from snapshot // Read from snapshot
return SourceResult( return SourceFetchResult(
source = snapshot.toImageSource(), source = snapshot.toImageSource(),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
@@ -141,7 +147,7 @@ class MangaCoverFetcher(
// Read from disk cache // Read from disk cache
snapshot = writeToDiskCache(response) snapshot = writeToDiskCache(response)
if (snapshot != null) { if (snapshot != null) {
return SourceResult( return SourceFetchResult(
source = snapshot.toImageSource(), source = snapshot.toImageSource(),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.NETWORK, dataSource = DataSource.NETWORK,
@@ -149,8 +155,8 @@ class MangaCoverFetcher(
} }
// Read from response if cache is unused or unusable // Read from response if cache is unused or unusable
return SourceResult( return SourceFetchResult(
source = ImageSource(source = responseBody.source(), context = options.context), source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM),
mimeType = "image/*", mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK, dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
) )
@@ -169,17 +175,20 @@ class MangaCoverFetcher(
val response = client.newCall(newRequest()).await() val response = client.newCall(newRequest()).await()
if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) { if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) {
response.close() response.close()
throw HttpException(response) throw IOException(response.message)
} }
return response return response
} }
private fun newRequest(): Request { private fun newRequest(): Request {
val request = Request.Builder() val request = Request.Builder().apply {
.url(url!!) url(url!!)
.headers(sourceLazy.value?.headers ?: options.headers)
// Support attaching custom data to the network request. val sourceHeaders = sourceLazy.value?.headers
.tag(Parameters::class.java, options.parameters) if (sourceHeaders != null) {
headers(sourceHeaders)
}
}
when { when {
options.networkCachePolicy.readEnabled -> { options.networkCachePolicy.readEnabled -> {
@@ -264,7 +273,12 @@ class MangaCoverFetcher(
} }
private fun DiskCache.Snapshot.toImageSource(): ImageSource { private fun DiskCache.Snapshot.toImageSource(): ImageSource {
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this) return ImageSource(
file = data,
fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey,
closeable = this,
)
} }
private fun getResourceType(cover: String?): Type? { private fun getResourceType(cover: String?): Type? {
@@ -330,7 +344,7 @@ class MangaCoverFetcher(
} }
companion object { companion object {
const val USE_CUSTOM_COVER = "use_custom_cover" val USE_CUSTOM_COVER_KEY = Extras.Key(true)
private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build() private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build()
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build() private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.coil package eu.kanade.tachiyomi.data.coil
import coil.key.Keyer import coil3.key.Keyer
import coil.request.Options import coil3.request.Options
import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
@@ -1,15 +1,13 @@
package eu.kanade.tachiyomi.data.coil package eu.kanade.tachiyomi.data.coil
import coil.ImageLoader import coil3.ImageLoader
import coil.decode.DataSource import coil3.decode.DataSource
import coil.decode.ImageSource import coil3.decode.ImageSource
import coil.disk.DiskCache import coil3.disk.DiskCache
import coil.fetch.FetchResult import coil3.fetch.FetchResult
import coil.fetch.Fetcher import coil3.fetch.Fetcher
import coil.fetch.SourceResult import coil3.fetch.SourceFetchResult
import coil.network.HttpException import coil3.request.Options
import coil.request.Options
import coil.request.Parameters
import eu.kanade.domain.manga.model.PagePreview import eu.kanade.domain.manga.model.PagePreview
import eu.kanade.tachiyomi.data.cache.PagePreviewCache import eu.kanade.tachiyomi.data.cache.PagePreviewCache
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
@@ -21,12 +19,14 @@ import okhttp3.Call
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.http.HTTP_NOT_MODIFIED import okhttp3.internal.http.HTTP_NOT_MODIFIED
import okio.FileSystem
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import okio.Source import okio.Source
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.IOException
/** /**
* A [Fetcher] that fetches page preview image for [PagePreview] object. * A [Fetcher] that fetches page preview image for [PagePreview] object.
@@ -54,8 +54,12 @@ class PagePreviewFetcher(
} }
private fun fileLoader(file: File): FetchResult { private fun fileLoader(file: File): FetchResult {
return SourceResult( return SourceFetchResult(
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey), source = ImageSource(
file = file.toOkioPath(),
fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey
),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
) )
@@ -76,7 +80,7 @@ class PagePreviewFetcher(
} }
// Read from snapshot // Read from snapshot
return SourceResult( return SourceFetchResult(
source = snapshot.toImageSource(), source = snapshot.toImageSource(),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
@@ -96,7 +100,7 @@ class PagePreviewFetcher(
// Read from disk cache // Read from disk cache
snapshot = writeToDiskCache(response) snapshot = writeToDiskCache(response)
if (snapshot != null) { if (snapshot != null) {
return SourceResult( return SourceFetchResult(
source = snapshot.toImageSource(), source = snapshot.toImageSource(),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.NETWORK, dataSource = DataSource.NETWORK,
@@ -104,8 +108,8 @@ class PagePreviewFetcher(
} }
// Read from response if cache is unused or unusable // Read from response if cache is unused or unusable
return SourceResult( return SourceFetchResult(
source = ImageSource(source = responseBody.source(), context = options.context), source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM),
mimeType = "image/*", mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK, dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
) )
@@ -125,7 +129,7 @@ class PagePreviewFetcher(
) ?: callFactoryLazy.value.newCall(newRequest()).await() ) ?: callFactoryLazy.value.newCall(newRequest()).await()
if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) { if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) {
response.close() response.close()
throw HttpException(response) throw IOException(response.message)
} }
return response return response
} }
@@ -144,11 +148,14 @@ class PagePreviewFetcher(
} }
private fun newRequest(): Request { private fun newRequest(): Request {
val request = Request.Builder() val request = Request.Builder().apply {
.url(page.imageUrl) url(page.imageUrl)
.headers((sourceLazy.value as? HttpSource)?.headers ?: options.headers)
// Support attaching custom data to the network request. val sourceHeaders = (sourceLazy.value as? HttpSource)?.headers
.tag(Parameters::class.java, options.parameters) if (sourceHeaders != null) {
headers(sourceHeaders)
}
}
request.cacheControl(getCacheControl()) request.cacheControl(getCacheControl())
@@ -218,7 +225,12 @@ class PagePreviewFetcher(
} }
private fun DiskCache.Snapshot.toImageSource(): ImageSource { private fun DiskCache.Snapshot.toImageSource(): ImageSource {
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this) return ImageSource(
file = data,
fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey,
closeable = this
)
} }
class Factory( class Factory(
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.coil package eu.kanade.tachiyomi.data.coil
import coil.key.Keyer import coil3.key.Keyer
import coil.request.Options import coil3.request.Options
import eu.kanade.domain.manga.model.PagePreview import eu.kanade.domain.manga.model.PagePreview
class PagePreviewKeyer : Keyer<PagePreview> { class PagePreviewKeyer : Keyer<PagePreview> {
@@ -2,13 +2,14 @@ package eu.kanade.tachiyomi.data.coil
import android.os.Build import android.os.Build
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader import coil3.ImageLoader
import coil.decode.DecodeResult import coil3.asCoilImage
import coil.decode.Decoder import coil3.decode.DecodeResult
import coil.decode.ImageDecoderDecoder import coil3.decode.Decoder
import coil.decode.ImageSource import coil3.decode.ImageSource
import coil.fetch.SourceResult import coil3.fetch.SourceFetchResult
import coil.request.Options import coil3.request.Options
import coil3.request.allowRgb565
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.FileHeader import net.lingala.zip4j.model.FileHeader
@@ -51,14 +52,14 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
check(bitmap != null) { "Failed to decode image" } check(bitmap != null) { "Failed to decode image" }
return DecodeResult( return DecodeResult(
drawable = bitmap.toDrawable(options.context.resources), image = bitmap.asCoilImage(),
isSampled = false, isSampled = false,
) )
} }
class Factory : Decoder.Factory { class Factory : Decoder.Factory {
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? { override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
if (!isApplicable(result.source.source())) return null if (!isApplicable(result.source.source())) return null
return TachiyomiImageDecoder(result.source, options) return TachiyomiImageDecoder(result.source, options)
} }
@@ -79,7 +80,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
} }
} }
override fun equals(other: Any?) = other is ImageDecoderDecoder.Factory override fun equals(other: Any?) = other is Factory
override fun hashCode() = javaClass.hashCode() override fun hashCode() = javaClass.hashCode()
} }
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.os.Build
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.domain.chapter.model.toSChapter import eu.kanade.domain.chapter.model.toSChapter
import eu.kanade.domain.manga.model.getComicInfo import eu.kanade.domain.manga.model.getComicInfo
@@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.CbzCrypto.addFilesToZip
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
@@ -43,8 +43,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import logcat.LogPriority import logcat.LogPriority
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.ZipParameters
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response import okhttp3.Response
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
@@ -61,12 +59,12 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import java.nio.charset.StandardCharsets
import java.util.Locale import java.util.Locale
import java.util.zip.CRC32 import java.util.zip.CRC32
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@@ -86,6 +84,7 @@ class Downloader(
private val downloadPreferences: DownloadPreferences = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val xml: XML = Injekt.get(), private val xml: XML = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
// SY --> // SY -->
private val sourcePreferences: SourcePreferences = Injekt.get(), private val sourcePreferences: SourcePreferences = Injekt.get(),
// SY <-- // SY <--
@@ -663,31 +662,15 @@ class Downloader(
dirname: String, dirname: String,
tmpDir: UniFile, tmpDir: UniFile,
) { ) {
val zipFile = File(context.externalCacheDir, "$dirname.cbz$TMP_DIR_SUFFIX")
val zip = ZipFile(zipFile)
val zipParameters = ZipParameters()
CbzCrypto.setZipParametersEncrypted(zipParameters)
zip.setPassword(CbzCrypto.getDecryptedPasswordCbz())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) zip.charset = StandardCharsets.ISO_8859_1
tmpDir.filePath?.let { addPaddingToImage(File(it)) } tmpDir.filePath?.let { addPaddingToImage(File(it)) }
zip.addFiles( tmpDir.listFiles()?.toList()?.let { files ->
tmpDir.listFiles()?.map { img -> img.filePath?.let { File(it) } }, mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
zipParameters, ?.addFilesToZip(files, CbzCrypto.getDecryptedPasswordCbz())
)
zip.close()
val realZip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
realZip.openOutputStream().use { out ->
zipFile.inputStream().use {
it.copyTo(out)
}
} }
mangaDir.findFile("$dirname.cbz$TMP_DIR_SUFFIX")?.renameTo("$dirname.cbz") mangaDir.findFile("$dirname.cbz$TMP_DIR_SUFFIX")?.renameTo("$dirname.cbz")
tmpDir.delete() tmpDir.delete()
zipFile.delete()
} }
private fun addPaddingToImage(imageDir: File) { private fun addPaddingToImage(imageDir: File) {
@@ -713,9 +696,22 @@ class Downloader(
chapter: Chapter, chapter: Chapter,
source: HttpSource, source: HttpSource,
) { ) {
val chapterUrl = source.getChapterUrl(chapter.toSChapter())
val categories = getCategories.await(manga.id).map { it.name.trim() }.takeUnless { it.isEmpty() } val categories = getCategories.await(manga.id).map { it.name.trim() }.takeUnless { it.isEmpty() }
val comicInfo = getComicInfo(manga, chapter, chapterUrl, categories) val urls = getTracks.await(manga.id)
.mapNotNull { track ->
track.remoteUrl.takeUnless { url -> url.isBlank() }?.trim()
}
.plus(source.getChapterUrl(chapter.toSChapter()).trim())
.distinct()
val comicInfo = getComicInfo(
manga,
chapter,
urls,
categories,
source.name
)
// Remove the old file // Remove the old file
dir.findFile(COMIC_INFO_FILE, true)?.delete() dir.findFile(COMIC_INFO_FILE, true)?.delete()
dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use { dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use {
@@ -28,7 +28,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.track.TrackStatus import eu.kanade.tachiyomi.data.track.TrackStatus
import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.MergedSource
@@ -64,6 +63,7 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.model.NoChaptersException import tachiyomi.domain.chapter.model.NoChaptersException
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
@@ -101,7 +101,6 @@ import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) : class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
@@ -400,8 +399,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
.sortedByDescending { it.sourceOrder }.run { .sortedByDescending { it.sourceOrder }.run {
if (libraryPreferences.libraryReadDuplicateChapters().get()) { if (libraryPreferences.libraryReadDuplicateChapters().get()) {
val readChapters = getChaptersByMangaId.await(manga.id).filter { it.read } val readChapters = getChaptersByMangaId.await(manga.id).filter { it.read }
val newReadChapters = this.filter { chapter -> readChapters.any { it.chapterNumber == chapter.chapterNumber } } val newReadChapters = this.filter { chapter ->
.also { setReadStatus.await(true, *it.toTypedArray()) } chapter.chapterNumber > 0 &&
readChapters.any { it.chapterNumber == chapter.chapterNumber }
}
if (newReadChapters.isNotEmpty()) {
setReadStatus.await(true, *newReadChapters.toTypedArray())
}
this.filterNot { newReadChapters.contains(it) } this.filterNot { newReadChapters.contains(it) }
} else { } else {
@@ -631,9 +636,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
var tracker = dbTracks.firstOrNull { it.trackerId == TrackerManager.MDLIST } var tracker = dbTracks.firstOrNull { it.trackerId == TrackerManager.MDLIST }
?: mdList.createInitialTracker(manga).toDomainTrack(idRequired = false) ?: mdList.createInitialTracker(manga).toDomainTrack(idRequired = false)
if (tracker?.status == FollowStatus.UNFOLLOWED.int.toLong()) { if (tracker?.status == FollowStatus.UNFOLLOWED.long) {
tracker = tracker.copy( tracker = tracker.copy(
status = FollowStatus.READING.int.toLong(), status = FollowStatus.READING.long,
) )
val updatedTrack = mdList.update(tracker.toDbTrack()) val updatedTrack = mdList.update(tracker.toDbTrack())
insertTrack.await(updatedTrack.toDomainTrack(false)!!) insertTrack.await(updatedTrack.toDomainTrack(false)!!)
@@ -9,9 +9,10 @@ import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import coil.imageLoader import coil3.imageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.transform.CircleCropTransformation import coil3.request.transformations
import coil3.transform.CircleCropTransformation
import eu.kanade.presentation.util.formatChapterNumber import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences
@@ -294,7 +295,7 @@ class LibraryUpdateNotifier(
.transformations(CircleCropTransformation()) .transformations(CircleCropTransformation())
.size(NOTIF_ICON_SIZE) .size(NOTIF_ICON_SIZE)
.build() .build()
val drawable = context.imageLoader.execute(request).drawable val drawable = context.imageLoader.execute(request).image?.asDrawable(context.resources)
return drawable?.getBitmapOrNull() return drawable?.getBitmapOrNull()
} }
@@ -26,7 +26,7 @@ enum class TrackStatus(val int: Int, val res: StringResource) {
fun parseTrackerStatus(trackerManager: TrackerManager, tracker: Long, status: Long): TrackStatus? { fun parseTrackerStatus(trackerManager: TrackerManager, tracker: Long, status: Long): TrackStatus? {
return when (tracker) { return when (tracker) {
trackerManager.mdList.id -> { trackerManager.mdList.id -> {
when (FollowStatus.fromInt(status)) { when (FollowStatus.fromLong(status)) {
FollowStatus.UNFOLLOWED -> null FollowStatus.UNFOLLOWED -> null
FollowStatus.READING -> READING FollowStatus.READING -> READING
FollowStatus.COMPLETED -> COMPLETED FollowStatus.COMPLETED -> COMPLETED
@@ -41,7 +41,7 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
} }
override fun getStatusList(): List<Long> { override fun getStatusList(): List<Long> {
return FollowStatus.entries.map { it.int } return FollowStatus.entries.map { it.long }
} }
override fun getStatus(status: Long): StringResource? = when (status) { override fun getStatus(status: Long): StringResource? = when (status) {
@@ -64,13 +64,13 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
val mdex = mdex ?: throw MangaDexNotFoundException() val mdex = mdex ?: throw MangaDexNotFoundException()
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url) val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
val followStatus = FollowStatus.fromInt(track.status) val followStatus = FollowStatus.fromLong(track.status)
// this updates the follow status in the metadata // this updates the follow status in the metadata
// allow follow status to update // allow follow status to update
if (remoteTrack.status != followStatus.int) { if (remoteTrack.status != followStatus.long) {
if (mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)) { if (mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)) {
remoteTrack.status = followStatus.int remoteTrack.status = followStatus.long
} else { } else {
track.status = remoteTrack.status track.status = remoteTrack.status
} }
@@ -103,19 +103,19 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
} }
} }
override fun getCompletionStatus(): Long = FollowStatus.COMPLETED.int override fun getCompletionStatus(): Long = FollowStatus.COMPLETED.long
override fun getReadingStatus(): Long = FollowStatus.READING.int override fun getReadingStatus(): Long = FollowStatus.READING.long
override fun getRereadingStatus(): Long = FollowStatus.RE_READING.int override fun getRereadingStatus(): Long = FollowStatus.RE_READING.long
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track = update( override suspend fun bind(track: Track, hasReadChapters: Boolean): Track = update(
refresh(track).also { refresh(track).also {
if (it.status == FollowStatus.UNFOLLOWED.int) { if (it.status == FollowStatus.UNFOLLOWED.long) {
it.status = if (hasReadChapters) { it.status = if (hasReadChapters) {
FollowStatus.READING.int FollowStatus.READING.long
} else { } else {
FollowStatus.PLAN_TO_READ.int FollowStatus.PLAN_TO_READ.long
} }
} }
}, },
@@ -136,7 +136,7 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track { fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track {
return Track.create(id).apply { return Track.create(id).apply {
manga_id = dbManga.id manga_id = dbManga.id
status = FollowStatus.UNFOLLOWED.int status = FollowStatus.UNFOLLOWED.long
tracking_url = MdUtil.baseUrl + mdManga.url tracking_url = MdUtil.baseUrl + mdManga.url
title = mdManga.title title = mdManga.title
} }
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.myanimelist package eu.kanade.tachiyomi.data.track.myanimelist
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Interceptor import okhttp3.Interceptor
@@ -32,7 +31,8 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
// Add the authorization header to the original request // Add the authorization header to the original request
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.header("User-Agent", "TachiyomiSY v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") // TODO(antsy): Add back custom user agent when they stop blocking us for no apparent reason
// .header("User-Agent", "TachiyomiSY v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)
@@ -6,7 +6,6 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import dalvik.system.PathClassLoader
import eu.kanade.domain.extension.interactor.TrustExtension import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.util.lang.Hash import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
import eu.kanade.tachiyomi.util.system.ChildFirstPathClassLoader
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -272,7 +272,7 @@ internal object ExtensionLoader {
} }
val classLoader = try { val classLoader = try {
PathClassLoader(appInfo.sourceDir, null, context.classLoader) ChildFirstPathClassLoader(appInfo.sourceDir, null, context.classLoader)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($pkgName)" } logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($pkgName)" }
return LoadResult.Error return LoadResult.Error
@@ -89,6 +89,8 @@ import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.net.URLEncoder import java.net.URLEncoder
import java.time.ZoneOffset
import java.time.ZonedDateTime
// TODO Consider gallery updating when doing tabbed browsing // TODO Consider gallery updating when doing tabbed browsing
class EHentai( class EHentai(
@@ -271,8 +273,9 @@ class EHentai(
private fun getDateTag(element: Element?): Long? { private fun getDateTag(element: Element?): Long? {
val text = element?.text()?.nullIfBlank() val text = element?.text()?.nullIfBlank()
return if (text != null) { return if (text != null) {
val date = MetadataUtil.EX_DATE_FORMAT.parse(text) println(text)
date?.time val date = ZonedDateTime.parse(text, MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC))
date?.toInstant()?.toEpochMilli()
} else { } else {
null null
} }
@@ -376,11 +379,12 @@ class EHentai(
url = EHentaiSearchMetadata.normalizeUrl(location), url = EHentaiSearchMetadata.normalizeUrl(location),
name = "v1: " + doc.selectFirst("#gn")!!.text(), name = "v1: " + doc.selectFirst("#gn")!!.text(),
chapter_number = 1f, chapter_number = 1f,
date_upload = MetadataUtil.EX_DATE_FORMAT.parse( date_upload = ZonedDateTime.parse(
doc.select("#gdd .gdt1").find { el -> doc.select("#gdd .gdt1").find { el ->
el.text().lowercase() == "posted:" el.text().lowercase() == "posted:"
}!!.nextElementSibling()!!.text(), }!!.nextElementSibling()!!.text(),
)!!.time, MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC)
)!!.toInstant().toEpochMilli(),
scanlator = EHentaiSearchMetadata.galleryId(location), scanlator = EHentaiSearchMetadata.galleryId(location),
) )
// Build and append the rest of the galleries // Build and append the rest of the galleries
@@ -395,7 +399,10 @@ class EHentai(
url = EHentaiSearchMetadata.normalizeUrl(link), url = EHentaiSearchMetadata.normalizeUrl(link),
name = "v${index + 2}: $name", name = "v${index + 2}: $name",
chapter_number = index + 2f, chapter_number = index + 2f,
date_upload = MetadataUtil.EX_DATE_FORMAT.parse(posted)!!.time, date_upload = ZonedDateTime.parse(
posted,
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC)
).toInstant().toEpochMilli(),
scanlator = EHentaiSearchMetadata.galleryId(link), scanlator = EHentaiSearchMetadata.galleryId(link),
) )
}.reversed() + self }.reversed() + self
@@ -706,7 +713,10 @@ class EHentai(
if (left != null && right != null) { if (left != null && right != null) {
ignore { ignore {
when (left.removeSuffix(":").lowercase()) { when (left.removeSuffix(":").lowercase()) {
"posted" -> datePosted = MetadataUtil.EX_DATE_FORMAT.parse(right)!!.time "posted" -> datePosted = ZonedDateTime.parse(
right,
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC)
).toInstant().toEpochMilli()
// Example gallery with parent: https://e-hentai.org/g/1390451/7f181c2426/ // Example gallery with parent: https://e-hentai.org/g/1390451/7f181c2426/
// Example JP gallery: https://exhentai.org/g/1375385/03519d541b/ // Example JP gallery: https://exhentai.org/g/1375385/03519d541b/
// Parent is older variation of the gallery // Parent is older variation of the gallery
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.ui.base.delegate package eu.kanade.tachiyomi.ui.base.delegate
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Build
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@@ -149,7 +151,12 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
if (activity.isAuthenticationSupported()) { if (activity.isAuthenticationSupported()) {
if (!SecureActivityDelegate.requireUnlock) return if (!SecureActivityDelegate.requireUnlock) return
activity.startActivity(Intent(activity, UnlockActivity::class.java)) activity.startActivity(Intent(activity, UnlockActivity::class.java))
activity.overridePendingTransition(0, 0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
activity.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 0, 0)
} else {
@Suppress("DEPRECATION")
activity.overridePendingTransition(0, 0)
}
} else { } else {
securityPreferences.useAuthenticator().set(false) securityPreferences.useAuthenticator().set(false)
} }
@@ -12,51 +12,10 @@ interface ThemingDelegate {
companion object { companion object {
fun getThemeResIds(appTheme: AppTheme, isAmoled: Boolean): List<Int> { fun getThemeResIds(appTheme: AppTheme, isAmoled: Boolean): List<Int> {
val resIds = mutableListOf<Int>() return buildList(2) {
when (appTheme) { add(themeResources.getOrDefault(appTheme, R.style.Theme_Tachiyomi))
AppTheme.MONET -> { if (isAmoled) add(R.style.ThemeOverlay_Tachiyomi_Amoled)
resIds += R.style.Theme_Tachiyomi_Monet
}
AppTheme.GREEN_APPLE -> {
resIds += R.style.Theme_Tachiyomi_GreenApple
}
AppTheme.LAVENDER -> {
resIds += R.style.Theme_Tachiyomi_Lavender
}
AppTheme.MIDNIGHT_DUSK -> {
resIds += R.style.Theme_Tachiyomi_MidnightDusk
}
AppTheme.NORD -> {
resIds += R.style.Theme_Tachiyomi_Nord
}
AppTheme.STRAWBERRY_DAIQUIRI -> {
resIds += R.style.Theme_Tachiyomi_StrawberryDaiquiri
}
AppTheme.TAKO -> {
resIds += R.style.Theme_Tachiyomi_Tako
}
AppTheme.TEALTURQUOISE -> {
resIds += R.style.Theme_Tachiyomi_TealTurquoise
}
AppTheme.YINYANG -> {
resIds += R.style.Theme_Tachiyomi_YinYang
}
AppTheme.YOTSUBA -> {
resIds += R.style.Theme_Tachiyomi_Yotsuba
}
AppTheme.TIDAL_WAVE -> {
resIds += R.style.Theme_Tachiyomi_TidalWave
}
else -> {
resIds += R.style.Theme_Tachiyomi
}
} }
if (isAmoled) {
resIds += R.style.ThemeOverlay_Tachiyomi_Amoled
}
return resIds
} }
} }
} }
@@ -68,3 +27,17 @@ class ThemingDelegateImpl : ThemingDelegate {
.forEach(activity::setTheme) .forEach(activity::setTheme)
} }
} }
private val themeResources: Map<AppTheme, Int> = mapOf(
AppTheme.MONET to R.style.Theme_Tachiyomi_Monet,
AppTheme.GREEN_APPLE to R.style.Theme_Tachiyomi_GreenApple,
AppTheme.LAVENDER to R.style.Theme_Tachiyomi_Lavender,
AppTheme.MIDNIGHT_DUSK to R.style.Theme_Tachiyomi_MidnightDusk,
AppTheme.NORD to R.style.Theme_Tachiyomi_Nord,
AppTheme.STRAWBERRY_DAIQUIRI to R.style.Theme_Tachiyomi_StrawberryDaiquiri,
AppTheme.TAKO to R.style.Theme_Tachiyomi_Tako,
AppTheme.TEALTURQUOISE to R.style.Theme_Tachiyomi_TealTurquoise,
AppTheme.YINYANG to R.style.Theme_Tachiyomi_YinYang,
AppTheme.YOTSUBA to R.style.Theme_Tachiyomi_Yotsuba,
AppTheme.TIDAL_WAVE to R.style.Theme_Tachiyomi_TidalWave,
)
@@ -5,7 +5,7 @@ import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.util.insertSeparators import eu.kanade.core.util.insertSeparators
import eu.kanade.presentation.history.HistoryUiModel import eu.kanade.presentation.history.HistoryUiModel
import eu.kanade.tachiyomi.util.lang.toDateKey import eu.kanade.tachiyomi.util.lang.toLocalDate
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -30,7 +30,6 @@ import tachiyomi.domain.history.interactor.RemoveHistory
import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.history.model.HistoryWithRelations
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Date
class HistoryScreenModel( class HistoryScreenModel(
private val getHistory: GetHistory = Injekt.get(), private val getHistory: GetHistory = Injekt.get(),
@@ -62,10 +61,10 @@ class HistoryScreenModel(
private fun List<HistoryWithRelations>.toHistoryUiModels(): List<HistoryUiModel> { private fun List<HistoryWithRelations>.toHistoryUiModels(): List<HistoryUiModel> {
return map { HistoryUiModel.Item(it) } return map { HistoryUiModel.Item(it) }
.insertSeparators { before, after -> .insertSeparators { before, after ->
val beforeDate = before?.item?.readAt?.time?.toDateKey() ?: Date(0) val beforeDate = before?.item?.readAt?.time?.toLocalDate()
val afterDate = after?.item?.readAt?.time?.toDateKey() ?: Date(0) val afterDate = after?.item?.readAt?.time?.toLocalDate()
when { when {
beforeDate.time != afterDate.time && afterDate.time != 0L -> HistoryUiModel.Header(afterDate) beforeDate != afterDate && afterDate != null -> HistoryUiModel.Header(afterDate)
// Return null to avoid adding a separator between two items. // Return null to avoid adding a separator between two items.
else -> null else -> null
} }
@@ -20,8 +20,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.children import androidx.core.view.children
import coil.load import coil3.load
import coil.transform.RoundedCornersTransformation import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -5,9 +5,9 @@ import android.net.Uri
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import coil.imageLoader import coil3.imageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.size.Size import coil3.size.Size
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.saver.Image import eu.kanade.tachiyomi.data.saver.Image
@@ -96,7 +96,7 @@ class MangaCoverScreenModel(
.build() .build()
return withIOContext { return withIOContext {
val result = context.imageLoader.execute(req).drawable val result = context.imageLoader.execute(req).image?.asDrawable(context.resources)
// TODO: Handle animated cover // TODO: Handle animated cover
val bitmap = result?.getBitmapOrNull() ?: return@withIOContext null val bitmap = result?.getBitmapOrNull() ?: return@withIOContext null
@@ -197,6 +197,7 @@ class MangaScreen(
onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf { onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf {
successState.manga.favorite successState.manga.favorite
}, },
previewsRowCount = successState.previewsRowCount,
// SY --> // SY -->
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite }, onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) }, onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
@@ -418,6 +418,7 @@ class MangaScreenModel(
PagePreviewState.Unused PagePreviewState.Unused
}, },
alwaysShowReadingProgress = readerPreferences.preserveReadingPosition().get() && manga.isEhBasedManga(), alwaysShowReadingProgress = readerPreferences.preserveReadingPosition().get() && manga.isEhBasedManga(),
previewsRowCount = uiPreferences.previewsRowCount().get(),
// SY <-- // SY <--
) )
} }
@@ -1634,6 +1635,7 @@ class MangaScreenModel(
val showMergeWithAnother: Boolean, val showMergeWithAnother: Boolean,
val pagePreviewsState: PagePreviewState, val pagePreviewsState: PagePreviewState,
val alwaysShowReadingProgress: Boolean, val alwaysShowReadingProgress: Boolean,
val previewsRowCount: Int,
// SY <-- // SY <--
) : State { ) : State {
val processedChapters by lazy { val processedChapters by lazy {
@@ -1679,7 +1681,7 @@ class MangaScreenModel(
val trackingCount: Int val trackingCount: Int
get() = trackItems.count { get() = trackItems.count {
it.track != null && ((it.tracker is MdList && it.track.status != FollowStatus.UNFOLLOWED.int.toLong()) || it.tracker !is MdList) it.track != null && ((it.tracker is MdList && it.track.status != FollowStatus.UNFOLLOWED.long) || it.tracker !is MdList)
} }
/** /**
@@ -1,8 +1,9 @@
package eu.kanade.tachiyomi.ui.manga.merged package eu.kanade.tachiyomi.ui.manga.merged
import android.view.View import android.view.View
import coil.load import coil3.load
import coil.transform.RoundedCornersTransformation import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.EditMergedSettingsItemBinding import eu.kanade.tachiyomi.databinding.EditMergedSettingsItemBinding
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader package eu.kanade.tachiyomi.ui.reader
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.assist.AssistContent import android.app.assist.AssistContent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -178,7 +179,16 @@ class ReaderActivity : BaseActivity() {
*/ */
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
registerSecureActivity(this) registerSecureActivity(this)
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_OPEN,
R.anim.shared_axis_x_push_enter,
R.anim.shared_axis_x_push_exit,
)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -312,7 +322,16 @@ class ReaderActivity : BaseActivity() {
override fun finish() { override fun finish() {
viewModel.onActivityFinish() viewModel.onActivityFinish()
super.finish() super.finish()
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_CLOSE,
R.anim.shared_axis_x_pop_enter,
R.anim.shared_axis_x_pop_exit,
)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
}
} }
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
@@ -9,6 +9,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.chapter.model.toDbChapter import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.model.readerOrientation import eu.kanade.domain.manga.model.readerOrientation
@@ -129,6 +130,7 @@ class ReaderViewModel @JvmOverloads constructor(
private val getMergedMangaById: GetMergedMangaById = Injekt.get(), private val getMergedMangaById: GetMergedMangaById = Injekt.get(),
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(), private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
private val getMergedChaptersByMangaId: GetMergedChaptersByMangaId = Injekt.get(), private val getMergedChaptersByMangaId: GetMergedChaptersByMangaId = Injekt.get(),
private val setReadStatus: SetReadStatus = Injekt.get()
// SY <-- // SY <--
) : ViewModel() { ) : ViewModel() {
@@ -691,6 +693,16 @@ class ReaderViewModel @JvmOverloads constructor(
// SY <-- // SY <--
readerChapter.chapter.read = true readerChapter.chapter.read = true
// SY --> // SY -->
if (readerChapter.chapter.chapter_number > 0 && readerPreferences.markReadDupe().get()) {
getChaptersByMangaId.await(manga!!.id).sortedByDescending { it.sourceOrder }
.filter {
it.id != readerChapter.chapter.id &&
!it.read &&
it.chapterNumber.toFloat() == readerChapter.chapter.chapter_number
}
.ifEmpty { null }
?.also { setReadStatus.await(true, *it.toTypedArray()) }
}
if (manga?.isEhBasedManga() == true) { if (manga?.isEhBasedManga() == true) {
viewModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
val chapterUpdates = chapterList val chapterUpdates = chapterList
@@ -4,9 +4,9 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import coil.imageLoader import coil3.imageLoader
import coil.request.CachePolicy import coil3.request.CachePolicy
import coil.request.ImageRequest import coil3.request.ImageRequest
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@@ -37,7 +37,7 @@ class SaveImageNotifier(private val context: Context) {
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.size(720, 1280) .size(720, 1280)
.target( .target(
onSuccess = { showCompleteNotification(uri, it.getBitmapOrNull()) }, onSuccess = { showCompleteNotification(uri, it.asDrawable(context.resources).getBitmapOrNull()) },
onError = { onError(null) }, onError = { onError(null) },
) )
.build() .build()
@@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.reader.chapter
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import java.text.DateFormat import java.time.format.DateTimeFormatter
data class ReaderChapterItem( data class ReaderChapterItem(
val chapter: Chapter, val chapter: Chapter,
val manga: Manga, val manga: Manga,
val isCurrent: Boolean, val isCurrent: Boolean,
val dateFormat: DateFormat, val dateFormat: DateTimeFormatter,
) )
@@ -37,10 +37,6 @@ internal class ZipPageLoader(file: File) : PageLoader() {
} }
init { init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
zip4j.charset = StandardCharsets.ISO_8859_1
}
Zip4jFile(file).use { zip -> Zip4jFile(file).use { zip ->
if (zip.isEncrypted) { if (zip.isEncrypted) {
if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) { if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) {
@@ -78,6 +78,8 @@ class ReaderPreferences(
fun skipDupe() = preferenceStore.getBoolean("skip_dupe", false) fun skipDupe() = preferenceStore.getBoolean("skip_dupe", false)
fun webtoonDisableZoomOut() = preferenceStore.getBoolean("webtoon_disable_zoom_out", false)
// endregion // endregion
// region Split two page spread // region Split two page spread
@@ -160,8 +162,6 @@ class ReaderPreferences(
fun useAutoWebtoon() = preferenceStore.getBoolean("eh_use_auto_webtoon", true) fun useAutoWebtoon() = preferenceStore.getBoolean("eh_use_auto_webtoon", true)
fun webtoonEnableZoomOut() = preferenceStore.getBoolean("webtoon_enable_zoom_out", false)
fun continuousVerticalTappingByPage() = preferenceStore.getBoolean("continuous_vertical_tapping_by_page", false) fun continuousVerticalTappingByPage() = preferenceStore.getBoolean("continuous_vertical_tapping_by_page", false)
fun cropBordersContinuousVertical() = preferenceStore.getBoolean("crop_borders_continues_vertical", false) fun cropBordersContinuousVertical() = preferenceStore.getBoolean("crop_borders_continues_vertical", false)
@@ -181,6 +181,8 @@ class ReaderPreferences(
fun centerMarginType() = preferenceStore.getInt("center_margin_type", PagerConfig.CenterMarginType.NONE) fun centerMarginType() = preferenceStore.getInt("center_margin_type", PagerConfig.CenterMarginType.NONE)
fun cacheArchiveMangaOnDisk() = preferenceStore.getBoolean("cache_archive_manga_on_disk", false) fun cacheArchiveMangaOnDisk() = preferenceStore.getBoolean("cache_archive_manga_on_disk", false)
fun markReadDupe() = preferenceStore.getBoolean("mark_read_dupe", false)
// SY <-- // SY <--
enum class TappingInvertMode( enum class TappingInvertMode(
@@ -18,10 +18,11 @@ import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.dispose import coil3.dispose
import coil.imageLoader import coil3.imageLoader
import coil.request.CachePolicy import coil3.request.CachePolicy
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.crossfade
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT_QUAD import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT_QUAD
@@ -342,7 +343,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
.diskCachePolicy(CachePolicy.DISABLED) .diskCachePolicy(CachePolicy.DISABLED)
.target( .target(
onSuccess = { result -> onSuccess = { result ->
setImageDrawable(result) setImageDrawable(result.asDrawable(context.resources))
(result as? Animatable)?.start() (result as? Animatable)?.start()
isVisible = true isVisible = true
this@ReaderPageImageView.onImageLoaded() this@ReaderPageImageView.onImageLoaded()
@@ -29,6 +29,11 @@ class WebtoonConfig(
var imageCropBorders = false var imageCropBorders = false
private set private set
var zoomOutDisabled = false
private set
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
var sidePadding = 0 var sidePadding = 0
private set private set
@@ -42,14 +47,9 @@ class WebtoonConfig(
// SY --> // SY -->
var usePageTransitions = false var usePageTransitions = false
var enableZoomOut = false
private set
var continuousCropBorders = false var continuousCropBorders = false
private set private set
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
// SY <-- // SY <--
init { init {
readerPreferences.cropBordersWebtoon() readerPreferences.cropBordersWebtoon()
@@ -86,6 +86,12 @@ class WebtoonConfig(
{ imagePropertyChangedListener?.invoke() }, { imagePropertyChangedListener?.invoke() },
) )
readerPreferences.webtoonDisableZoomOut()
.register(
{ zoomOutDisabled = it },
{ zoomPropertyChangedListener?.invoke(it) }
)
readerPreferences.webtoonDoubleTapZoomEnabled() readerPreferences.webtoonDoubleTapZoomEnabled()
.register( .register(
{ doubleTapZoom = it }, { doubleTapZoom = it },
@@ -99,9 +105,6 @@ class WebtoonConfig(
.launchIn(scope) .launchIn(scope)
// SY --> // SY -->
readerPreferences.webtoonEnableZoomOut()
.register({ enableZoomOut = it }, { zoomPropertyChangedListener?.invoke(it) })
readerPreferences.cropBordersContinuousVertical() readerPreferences.cropBordersContinuousVertical()
.register({ continuousCropBorders = it }, { imagePropertyChangedListener?.invoke() }) .register({ continuousCropBorders = it }, { imagePropertyChangedListener?.invoke() })
@@ -33,13 +33,11 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
scaleDetector.isQuickScaleEnabled = value scaleDetector.isQuickScaleEnabled = value
} }
// SY --> var zoomOutDisabled = false
var enableZoomOut = false
set(value) { set(value) {
field = value field = value
recycler?.canZoomOut = value recycler?.zoomOutDisabled = value
} }
// SY <--
/** /**
* Recycler view added in this frame. * Recycler view added in this frame.
@@ -33,19 +33,15 @@ class WebtoonRecyclerView @JvmOverloads constructor(
private var firstVisibleItemPosition = 0 private var firstVisibleItemPosition = 0
private var lastVisibleItemPosition = 0 private var lastVisibleItemPosition = 0
private var currentScale = DEFAULT_RATE private var currentScale = DEFAULT_RATE
var zoomOutDisabled = false
// SY -->
var canZoomOut = false
set(value) { set(value) {
field = value field = value
if (!value) { if (value && currentScale < DEFAULT_RATE) {
zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f) zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f)
} }
} }
private val minRate private val minRate
get() = if (canZoomOut) MIN_RATE else DEFAULT_RATE get() = if (zoomOutDisabled) DEFAULT_RATE else MIN_RATE
// SY <--
private val listener = GestureListener() private val listener = GestureListener()
private val detector = Detector() private val detector = Detector()
@@ -179,9 +175,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
fun onScale(scaleFactor: Float) { fun onScale(scaleFactor: Float) {
currentScale *= scaleFactor currentScale *= scaleFactor
currentScale = currentScale.coerceIn( currentScale = currentScale.coerceIn(
// SY -->
minRate, minRate,
// SY <--
MAX_SCALE_RATE, MAX_SCALE_RATE,
) )
@@ -208,8 +202,8 @@ class WebtoonRecyclerView @JvmOverloads constructor(
} }
fun onScaleEnd() { fun onScaleEnd() {
if (scaleX < /* SY --> */ minRate /* SY <-- */) { if (scaleX < minRate) {
zoom(currentScale, /* SY --> */ minRate /* SY <-- */, x, 0f, y, 0f) zoom(currentScale, minRate, x, 0f, y, 0f)
} }
} }
@@ -159,17 +159,15 @@ class WebtoonViewer(
frame.doubleTapZoom = it frame.doubleTapZoom = it
} }
config.zoomPropertyChangedListener = {
frame.zoomOutDisabled = it
}
config.navigationModeChangedListener = { config.navigationModeChangedListener = {
val showOnStart = config.navigationOverlayOnStart || config.forceNavigationOverlay val showOnStart = config.navigationOverlayOnStart || config.forceNavigationOverlay
activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart) activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart)
} }
// SY -->
config.zoomPropertyChangedListener = {
frame.enableZoomOut = it
}
// SY <--
frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
frame.addView(recycler) frame.addView(recycler)
} }
@@ -17,7 +17,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.toDateKey import eu.kanade.tachiyomi.util.lang.toLocalDate
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
@@ -49,7 +49,6 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.Date
class UpdatesScreenModel( class UpdatesScreenModel(
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
@@ -386,12 +385,10 @@ class UpdatesScreenModel(
return items return items
.map { UpdatesUiModel.Item(it) } .map { UpdatesUiModel.Item(it) }
.insertSeparators { before, after -> .insertSeparators { before, after ->
val beforeDate = before?.item?.update?.dateFetch?.toDateKey() ?: Date(0) val beforeDate = before?.item?.update?.dateFetch?.toLocalDate()
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0) val afterDate = after?.item?.update?.dateFetch?.toLocalDate()
when { when {
beforeDate.time != afterDate.time && afterDate.time != 0L -> { beforeDate != afterDate && afterDate != null -> UpdatesUiModel.Header(afterDate)
UpdatesUiModel.Header(afterDate)
}
// Return null to avoid adding a separator between two items. // Return null to avoid adding a separator between two items.
else -> null else -> null
} }
@@ -1,8 +1,10 @@
package eu.kanade.tachiyomi.ui.webview package eu.kanade.tachiyomi.ui.webview
import android.app.Activity
import android.app.assist.AssistContent import android.app.assist.AssistContent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.core.net.toUri import androidx.core.net.toUri
@@ -35,7 +37,16 @@ class WebViewActivity : BaseActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_OPEN,
R.anim.shared_axis_x_push_enter,
R.anim.shared_axis_x_push_exit,
)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (!WebViewUtil.supportsWebView(this)) { if (!WebViewUtil.supportsWebView(this)) {
@@ -77,7 +88,16 @@ class WebViewActivity : BaseActivity() {
override fun finish() { override fun finish() {
super.finish() super.finish()
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_CLOSE,
R.anim.shared_axis_x_pop_enter,
R.anim.shared_axis_x_pop_exit,
)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
}
} }
private fun shareWebpage(url: String) { private fun shareWebpage(url: String) {
@@ -6,15 +6,18 @@ import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import java.text.DateFormat import java.text.DateFormat
import java.time.Instant import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.Calendar
import java.util.Date import java.util.Date
import kotlin.math.absoluteValue
fun Date.toDateTimestampString(dateFormatter: DateFormat): String { fun LocalDateTime.toDateTimestampString(dateTimeFormatter: DateTimeFormatter): String {
val date = dateFormatter.format(this) val date = dateTimeFormatter.format(this)
val time = DateFormat.getTimeInstance(DateFormat.SHORT).format(this) val time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(this)
return "$date $time" return "$date $time"
} }
@@ -32,51 +35,35 @@ fun Long.convertEpochMillisZone(
.toEpochMilli() .toEpochMilli()
} }
/** fun Long.toLocalDate(): LocalDate {
* Get date as time key return LocalDate.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault())
*
* @param date desired date
* @return date as time key
*/
fun Long.toDateKey(): Date {
val instant = Instant.ofEpochMilli(this)
return Date.from(instant.truncatedTo(ChronoUnit.DAYS))
} }
fun Date.toRelativeString( fun LocalDate.toRelativeString(
context: Context, context: Context,
relative: Boolean = true, relative: Boolean = true,
dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT), dateFormat: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT),
): String { ): String {
if (!relative) { if (!relative) {
return dateFormat.format(this) return dateFormat.format(this)
} }
val now = Date() val now = LocalDate.now()
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) val difference = ChronoUnit.DAYS.between(this, now)
val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt()
return when { return when {
difference < 0 -> dateFormat.format(this) difference < -7 -> dateFormat.format(this)
difference < MILLISECONDS_IN_DAY -> context.stringResource(MR.strings.relative_time_today) difference < 0 -> context.pluralStringResource(
difference < MILLISECONDS_IN_DAY.times(7) -> context.pluralStringResource( MR.plurals.upcoming_relative_time,
MR.plurals.relative_time, difference.toInt().absoluteValue,
days, difference.toInt().absoluteValue,
days,
) )
difference < 1 -> context.stringResource(MR.strings.relative_time_today)
difference < 7 -> context.pluralStringResource(
MR.plurals.relative_time,
difference.toInt(),
difference.toInt(),
)
else -> dateFormat.format(this) else -> dateFormat.format(this)
} }
} }
private const val MILLISECONDS_IN_DAY = 86_400_000L
private val Date.timeWithOffset: Long
get() {
return Calendar.getInstance().run {
time = this@timeWithOffset
val dstOffset = get(Calendar.DST_OFFSET)
this@timeWithOffset.time + timeZone.rawOffset + dstOffset
}
}
private fun Long.floorNearest(to: Long): Long {
return this.floorDiv(to) * to
}
@@ -0,0 +1,86 @@
package eu.kanade.tachiyomi.util.system
import dalvik.system.PathClassLoader
import java.io.IOException
import java.io.InputStream
import java.net.URL
import java.util.Enumeration
/**
* A parent-last class loader that will try in order:
* - the system class loader
* - the child class loader
* - the parent class loader.
*/
class ChildFirstPathClassLoader(
dexPath: String,
librarySearchPath: String?,
parent: ClassLoader
) : PathClassLoader(dexPath, librarySearchPath, parent) {
private val systemClassLoader: ClassLoader? = getSystemClassLoader()
override fun loadClass(name: String?, resolve: Boolean): Class<*> {
var c = findLoadedClass(name)
if (c == null && systemClassLoader != null) {
try {
c = systemClassLoader.loadClass(name)
} catch (_: ClassNotFoundException) {}
}
if (c == null) {
c = try {
findClass(name)
} catch (_: ClassNotFoundException) {
super.loadClass(name, resolve)
}
}
if (resolve) {
resolveClass(c)
}
return c
}
override fun getResource(name: String?): URL? {
return systemClassLoader?.getResource(name)
?: findResource(name)
?: super.getResource(name)
}
override fun getResources(name: String?): Enumeration<URL> {
val systemUrls = systemClassLoader?.getResources(name)
val localUrls = findResources(name)
val parentUrls = parent?.getResources(name)
val urls = buildList {
while (systemUrls?.hasMoreElements() == true) {
add(systemUrls.nextElement())
}
while (localUrls?.hasMoreElements() == true) {
add(localUrls.nextElement())
}
while (parentUrls?.hasMoreElements() == true) {
add(parentUrls.nextElement())
}
}
return object : Enumeration<URL> {
val iterator = urls.iterator()
override fun hasMoreElements() = iterator.hasNext()
override fun nextElement() = iterator.next()
}
}
override fun getResourceAsStream(name: String?): InputStream? {
return try {
getResource(name)?.openStream()
} catch (_: IOException) {
return null
}
}
}
@@ -4,7 +4,7 @@ import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import coil.drawable.ScaleDrawable import coil3.gif.ScaleDrawable
fun Drawable.getBitmapOrNull(): Bitmap? = when (this) { fun Drawable.getBitmapOrNull(): Bitmap? = when (this) {
is BitmapDrawable -> bitmap is BitmapDrawable -> bitmap
@@ -57,7 +57,7 @@ class FollowsHandler(
it, it,
lang, lang,
) to MangaDexSearchMetadata().apply { ) to MangaDexSearchMetadata().apply {
followStatus = FollowStatus.fromDex(statuses[it.id]).int.toInt() followStatus = FollowStatus.fromDex(statuses[it.id]).long.toInt()
} }
}.sortedWith(comparator) }.sortedWith(comparator)
} }
@@ -155,7 +155,7 @@ class FollowsHandler(
val (followStatus, rating) = followStatusDef.await() to ratingDef.await() val (followStatus, rating) = followStatusDef.await() to ratingDef.await()
Track.create(TrackerManager.MDLIST).apply { Track.create(TrackerManager.MDLIST).apply {
title = "" title = ""
status = followStatus.int status = followStatus.long
tracking_url = url tracking_url = url
score = rating?.rating?.toDouble() ?: 0.0 score = rating?.rating?.toDouble() ?: 0.0
} }
@@ -2,14 +2,14 @@ package exh.md.utils
import java.util.Locale import java.util.Locale
enum class FollowStatus(val int: Long) { enum class FollowStatus(val long: Long) {
UNFOLLOWED(0), UNFOLLOWED(0L),
READING(1), READING(1L),
COMPLETED(2), COMPLETED(2L),
ON_HOLD(3), ON_HOLD(3L),
PLAN_TO_READ(4), PLAN_TO_READ(4L),
DROPPED(5), DROPPED(5L),
RE_READING(6), RE_READING(6L),
; ;
fun toDex(): String = this.name.lowercase(Locale.US) fun toDex(): String = this.name.lowercase(Locale.US)
@@ -18,6 +18,6 @@ enum class FollowStatus(val int: Long) {
fun fromDex( fun fromDex(
value: String?, value: String?,
): FollowStatus = entries.firstOrNull { it.name.lowercase(Locale.US) == value } ?: UNFOLLOWED ): FollowStatus = entries.firstOrNull { it.name.lowercase(Locale.US) == value } ?: UNFOLLOWED
fun fromInt(value: Long): FollowStatus = entries.firstOrNull { it.int == value } ?: UNFOLLOWED fun fromLong(value: Long): FollowStatus = entries.firstOrNull { it.long == value } ?: UNFOLLOWED
} }
} }
@@ -1,6 +1,8 @@
package exh.ui.intercept package exh.ui.intercept
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -46,7 +48,16 @@ class InterceptActivity : BaseActivity() {
private val status: MutableStateFlow<InterceptResult> = MutableStateFlow(InterceptResult.Idle) private val status: MutableStateFlow<InterceptResult> = MutableStateFlow(InterceptResult.Idle)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_OPEN,
R.anim.shared_axis_x_push_enter,
R.anim.shared_axis_x_push_exit,
)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setComposeContent { setComposeContent {
@@ -142,7 +153,16 @@ class InterceptActivity : BaseActivity() {
override fun finish() { override fun finish() {
super.finish() super.finish()
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_CLOSE,
R.anim.shared_axis_x_pop_enter,
R.anim.shared_axis_x_pop_exit,
)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
}
} }
private val galleryAdder = GalleryAdder() private val galleryAdder = GalleryAdder()
@@ -1,8 +1,10 @@
package exh.ui.login package exh.ui.login
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebView import android.webkit.WebView
@@ -32,7 +34,16 @@ class EhLoginActivity : BaseActivity() {
private val preferenceManager: UnsortedPreferences by injectLazy() private val preferenceManager: UnsortedPreferences by injectLazy()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_OPEN,
R.anim.shared_axis_x_push_enter,
R.anim.shared_axis_x_push_exit,
)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (!WebViewUtil.supportsWebView(this)) { if (!WebViewUtil.supportsWebView(this)) {
@@ -162,7 +173,16 @@ class EhLoginActivity : BaseActivity() {
override fun finish() { override fun finish() {
super.finish() super.finish()
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_CLOSE,
R.anim.shared_axis_x_pop_enter,
R.anim.shared_axis_x_pop_exit,
)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
}
} }
init { init {
@@ -18,7 +18,9 @@ import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import java.util.Date import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
@Composable @Composable
fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) { fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) {
@@ -50,7 +52,11 @@ fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) {
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp) binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp)
} }
binding.whenPosted.text = MetadataUtil.EX_DATE_FORMAT.format(Date((meta.uploadDate ?: 0) * 1000)) binding.whenPosted.text = MetadataUtil.EX_DATE_FORMAT
.format(
ZonedDateTime
.ofInstant(Instant.ofEpochSecond(meta.uploadDate ?: 0), ZoneId.systemDefault())
)
binding.pages.text = context.pluralStringResource( binding.pages.text = context.pluralStringResource(
SYMR.plurals.num_pages, SYMR.plurals.num_pages,
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<changelog bulletedList="true"> <changelog bulletedList="true">
<changelogversion versionName="1.10.5" changeDate="Mar 2,2024">
<changelogtext>[b]Based on Mihon stable 0.16.4(from 0.16.3)[/b]</changelogtext>
<changelogtext>Minor fix for mark duplicate chapters as read</changelogtext>
<changelogtext>Include the delayed tracker update fix</changelogtext>
</changelogversion>
<changelogversion versionName="1.10.4" changeDate="Jan 17,2024"> <changelogversion versionName="1.10.4" changeDate="Jan 17,2024">
<changelogtext>Hotfix for 1.10.3</changelogtext> <changelogtext>Hotfix for 1.10.3</changelogtext>
</changelogversion> </changelogversion>
+5 -2
View File
@@ -6,14 +6,17 @@ naming:
constantPattern: '[A-Z][A-Za-z0-9]*' constantPattern: '[A-Z][A-Za-z0-9]*'
complexity: complexity:
LongMethod:
ignoreAnnotated: [ 'Composable' ]
LongParameterList: LongParameterList:
functionThreshold: 6
constructorThreshold: 7
ignoreDefaultParameters: true ignoreDefaultParameters: true
ignoreAnnotated: [ 'Composable' ]
style: style:
MagicNumber: MagicNumber:
ignorePropertyDeclaration: true ignorePropertyDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true ignoreCompanionObjectPropertyDeclaration: true
ReturnCount:
excludeGuardClauses: true
UnusedPrivateMember: UnusedPrivateMember:
ignoreAnnotated: [ 'Preview' ] ignoreAnnotated: [ 'Preview' ]
@@ -27,6 +27,7 @@ fun SManga.getComicInfo() = ComicInfo(
coverArtist = null, coverArtist = null,
tags = null, tags = null,
categories = null, categories = null,
source = null,
padding = null, padding = null,
) )
@@ -82,6 +83,7 @@ data class ComicInfo(
val web: Web?, val web: Web?,
val publishingStatus: PublishingStatusTachiyomi?, val publishingStatus: PublishingStatusTachiyomi?,
val categories: CategoriesTachiyomi?, val categories: CategoriesTachiyomi?,
val source: SourceMihon?,
// SY --> // SY -->
val padding: PaddingTachiyomiSY?, val padding: PaddingTachiyomiSY?,
// SY <-- // SY <--
@@ -159,6 +161,10 @@ data class ComicInfo(
@XmlSerialName("Categories", "http://www.w3.org/2001/XMLSchema", "ty") @XmlSerialName("Categories", "http://www.w3.org/2001/XMLSchema", "ty")
data class CategoriesTachiyomi(@XmlValue(true) val value: String = "") data class CategoriesTachiyomi(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("SourceMihon", "http://www.w3.org/2001/XMLSchema", "mh")
data class SourceMihon(@XmlValue(true) val value: String = "")
// SY --> // SY -->
@Serializable @Serializable
@XmlSerialName("PaddingTachiyomiSY", "http://www.w3.org/2001/XMLSchema", "tysy") @XmlSerialName("PaddingTachiyomiSY", "http://www.w3.org/2001/XMLSchema", "tysy")
@@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.util.storage
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -11,9 +13,14 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority import logcat.LogPriority
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.exception.ZipException
import net.lingala.zip4j.io.inputstream.ZipInputStream
import net.lingala.zip4j.io.outputstream.ZipOutputStream
import net.lingala.zip4j.model.LocalFileHeader
import net.lingala.zip4j.model.ZipParameters import net.lingala.zip4j.model.ZipParameters
import net.lingala.zip4j.model.enums.AesKeyStrength import net.lingala.zip4j.model.enums.AesKeyStrength
import net.lingala.zip4j.model.enums.EncryptionMethod import net.lingala.zip4j.model.enums.EncryptionMethod
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@@ -221,6 +228,133 @@ object CbzCrypto {
} }
return String(bytes).contains(DEFAULT_COVER_NAME, ignoreCase = true) return String(bytes).contains(DEFAULT_COVER_NAME, ignoreCase = true)
} }
fun UniFile.isEncryptedZip(): Boolean {
return try {
val stream = ZipInputStream(this.openInputStream())
stream.nextEntry
stream.close()
false
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
true
} else throw zipException
}
}
fun UniFile.testCbzPassword(): Boolean {
return try {
val stream = ZipInputStream(this.openInputStream())
stream.setPassword(getDecryptedPasswordCbz())
stream.nextEntry
stream.close()
true
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
false
} else throw zipException
}
}
fun UniFile.addStreamToZip(inputStream: InputStream, filename: String, password: CharArray? = null) {
val zipOutputStream =
if (password != null) ZipOutputStream(this.openOutputStream(), password)
else ZipOutputStream(this.openOutputStream())
val zipParameters = ZipParameters()
zipParameters.fileNameInZip = filename
if (password != null) setZipParametersEncrypted(zipParameters)
zipOutputStream.putNextEntry(zipParameters)
zipOutputStream.use { output ->
inputStream.use { input ->
input.copyTo(output)
}
}
}
fun UniFile.addFilesToZip(files: List<UniFile>, password: CharArray? = null) {
val zipOutputStream =
if (password != null) ZipOutputStream(this.openOutputStream(), password)
else ZipOutputStream(this.openOutputStream())
files.forEach {
val zipParameters = ZipParameters()
if (password != null) setZipParametersEncrypted(zipParameters)
zipParameters.fileNameInZip = it.name
zipOutputStream.putNextEntry(zipParameters)
it.openInputStream().use { input ->
input.copyTo(zipOutputStream)
}
zipOutputStream.closeEntry()
}
zipOutputStream.close()
}
fun UniFile.getZipInputStream(filename: String): InputStream? {
val zipInputStream = ZipInputStream(this.openInputStream())
var fileHeader: LocalFileHeader?
if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz())
try {
while (run {
fileHeader = zipInputStream.nextEntry
fileHeader != null
}) {
if (fileHeader?.fileName == filename) return zipInputStream
}
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
logcat(LogPriority.WARN) {
"Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}"
}
} else throw zipException
}
return null
}
fun UniFile.getCoverStreamFromZip(): InputStream? {
val zipInputStream = ZipInputStream(this.openInputStream())
var fileHeader: LocalFileHeader?
val fileHeaderList: MutableList<LocalFileHeader?> = mutableListOf()
if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz())
try {
while (run {
fileHeader = zipInputStream.nextEntry
fileHeader != null
}) {
fileHeaderList.add(fileHeader)
}
var coverHeader = fileHeaderList
.mapNotNull { it }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) }
val coverStream = coverHeader?.fileName?.let { this.getZipInputStream(it) }
if (coverStream != null) {
if (!ImageUtil.isImage(coverHeader?.fileName) { coverStream }) coverHeader = null
}
return coverHeader?.fileName?.let { getZipInputStream(it) }
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
logcat(LogPriority.WARN) {
"Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}"
}
return null
} else throw zipException
}
}
} }
private const val BUFFER_SIZE = 2048 private const val BUFFER_SIZE = 2048
+7 -23
View File
@@ -1,26 +1,10 @@
# Project-wide Gradle settings. android.nonTransitiveRClass=false
android.useAndroidX=true
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx5120m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
org.gradle.parallel=true
org.gradle.caching=true
kotlin.code.style=official
kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidSourceSetLayoutVersion=2
android.useAndroidX=true org.gradle.caching=true
android.nonTransitiveRClass=false org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
org.gradle.parallel=true
+5 -5
View File
@@ -1,6 +1,6 @@
[versions] [versions]
agp_version = "8.2.2" agp_version = "8.2.2"
lifecycle_version = "2.6.2" lifecycle_version = "2.7.0"
paging_version = "3.2.1" paging_version = "3.2.1"
[libraries] [libraries]
@@ -25,10 +25,10 @@ workmanager = "androidx.work:work-runtime:2.9.0"
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" } paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" } paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.2" benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.3"
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha02" test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha03"
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha02" test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha03"
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-beta01" test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0"
[bundles] [bundles]
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"] lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
+3 -3
View File
@@ -1,7 +1,7 @@
[versions] [versions]
compiler = "1.5.8" compiler = "1.5.10"
compose-bom = "2024.01.00-alpha03" compose-bom = "2024.02.00-alpha02"
accompanist = "0.34.0" accompanist = "0.35.0-alpha"
[libraries] [libraries]
activity = "androidx.activity:activity-compose:1.8.2" activity = "androidx.activity:activity-compose:1.8.2"
+2 -2
View File
@@ -1,6 +1,6 @@
[versions] [versions]
kotlin_version = "1.9.22" kotlin_version = "1.9.22"
serialization_version = "1.6.2" serialization_version = "1.6.3"
xml_serialization_version = "0.86.3" xml_serialization_version = "0.86.3"
[libraries] [libraries]
@@ -9,7 +9,7 @@ gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "
immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.7" } immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.7" }
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.7.3" } coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.8.0" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" } coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" }
+14 -13
View File
@@ -9,13 +9,13 @@ shizuku_version = "12.2.0"
sqldelight = "2.0.0" sqldelight = "2.0.0"
sqlite = "2.4.0" sqlite = "2.4.0"
voyager = "1.0.0" voyager = "1.0.0"
detekt = "1.23.1" detekt = "1.23.5"
detektCompose = "0.3.11" detektCompose = "0.3.11"
[libraries] [libraries]
desugar = "com.android.tools:desugar_jdk_libs:2.0.4" desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2" android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
google-services-gradle = "com.google.gms:google-services:4.4.0" google-services-gradle = "com.google.gms:google-services:4.4.1"
rxjava = "io.reactivex:rxjava:1.3.8" rxjava = "io.reactivex:rxjava:1.3.8"
@@ -23,7 +23,7 @@ okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_ve
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp_version" } okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp_version" }
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" } okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
okio = "com.squareup.okio:okio:3.7.0" okio = "com.squareup.okio:okio:3.8.0"
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2" conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
@@ -38,17 +38,18 @@ zip4j = "net.lingala.zip4j:zip4j:2.11.5"
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" } sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" } sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" }
sqlite-android = "com.github.requery:sqlite-android:3.43.0" sqlite-android = "com.github.requery:sqlite-android:3.45.0"
sqlcipher = "net.zetetic:sqlcipher-android:4.5.4" sqlcipher = "net.zetetic:sqlcipher-android:4.5.4"
preferencektx = "androidx.preference:preference-ktx:1.2.1" preferencektx = "androidx.preference:preference-ktx:1.2.1"
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440" injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
coil-bom = { module = "io.coil-kt:coil-bom", version = "2.5.0" } coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.0.0-alpha06" }
coil-core = { module = "io.coil-kt:coil" } coil-core = { module = "io.coil-kt.coil3:coil" }
coil-gif = { module = "io.coil-kt:coil-gif" } coil-gif = { module = "io.coil-kt.coil3:coil-gif" }
coil-compose = { module = "io.coil-kt:coil-compose" } coil-compose = { module = "io.coil-kt.coil3:coil-compose" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp" }
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:7e57335" subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:7e57335"
image-decoder = "com.github.tachiyomiorg:image-decoder:fbd6601290" image-decoder = "com.github.tachiyomiorg:image-decoder:fbd6601290"
@@ -64,9 +65,9 @@ flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013
photoview = "com.github.chrisbanes:PhotoView:2.3.0" photoview = "com.github.chrisbanes:PhotoView:2.3.0"
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0" directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
insetter = "dev.chrisbanes.insetter:insetter:0.6.1" insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.1.0" compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.2.0"
swipe = "me.saket.swipe:swipe:1.2.0" swipe = "me.saket.swipe:swipe:1.3.0"
moko-core = { module = "dev.icerock.moko:resources", version.ref = "moko" } moko-core = { module = "dev.icerock.moko:resources", version.ref = "moko" }
moko-gradle = { module = "dev.icerock.moko:resources-generator", version.ref = "moko" } moko-gradle = { module = "dev.icerock.moko:resources-generator", version.ref = "moko" }
@@ -75,7 +76,7 @@ logcat = "com.squareup.logcat:logcat:0.1"
acra-http = { module = "ch.acra:acra-http", version.ref = "acra" } acra-http = { module = "ch.acra:acra-http", version.ref = "acra" }
acra-scheduler = { module = "ch.acra:acra-advanced-scheduler", version.ref = "acra" } acra-scheduler = { module = "ch.acra:acra-advanced-scheduler", version.ref = "acra" }
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.5.0" firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.5.1"
aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" } aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlib_version" } aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlib_version" }
@@ -92,7 +93,7 @@ sqldelight-android-paging = { module = "app.cash.sqldelight:androidx-paging3-ext
sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" } sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref = "sqldelight" } sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref = "sqldelight" }
junit = "org.junit.jupiter:junit-jupiter:5.10.1" junit = "org.junit.jupiter:junit-jupiter:5.10.2"
kotest-assertions = "io.kotest:kotest-assertions-core:5.8.0" kotest-assertions = "io.kotest:kotest-assertions-core:5.8.0"
mockk = "io.mockk:mockk:1.13.9" mockk = "io.mockk:mockk:1.13.9"
@@ -110,7 +111,7 @@ acra = ["acra-http", "acra-scheduler"]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"]
js-engine = ["quickjs-android"] js-engine = ["quickjs-android"]
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
coil = ["coil-core", "coil-gif", "coil-compose"] coil = ["coil-core", "coil-gif", "coil-compose", "coil-network-okhttp"]
shizuku = ["shizuku-api", "shizuku-provider"] shizuku = ["shizuku-api", "shizuku-provider"]
sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"] sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"]
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"] voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"]
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
@@ -71,4 +71,9 @@
<item quantity="one">%1$d second ago</item> <item quantity="one">%1$d second ago</item>
<item quantity="other">%1$d seconds ago</item> <item quantity="other">%1$d seconds ago</item>
</plurals> </plurals>
<plurals name="row_count">
<item quantity="one">%d row</item>
<item quantity="other">%d rows</item>
</plurals>
</resources> </resources>
@@ -168,6 +168,7 @@
<string name="put_recommends_in_overflow_summary">Put the recommendations button in the overflow menu instead of on the entry page</string> <string name="put_recommends_in_overflow_summary">Put the recommendations button in the overflow menu instead of on the entry page</string>
<string name="put_merge_in_overflow">Merge in overflow</string> <string name="put_merge_in_overflow">Merge in overflow</string>
<string name="put_merge_in_overflow_summary">Put the merge button in the overflow menu instead of on the entry page</string> <string name="put_merge_in_overflow_summary">Put the merge button in the overflow menu instead of on the entry page</string>
<string name="pref_previews_row_count">Previews row count</string>
<!-- Appearance Settings --> <!-- Appearance Settings -->
<string name="pref_category_navbar">Navbar</string> <string name="pref_category_navbar">Navbar</string>
@@ -176,6 +176,7 @@
<string name="library_group_updates_all">Постоянно запускать обновления категорий</string> <string name="library_group_updates_all">Постоянно запускать обновления категорий</string>
<!-- Browse settings --> <!-- Browse settings -->
<string name="pref_hide_feed">Скрыть вкладку «Лента»</string>
<string name="pref_feed_position">Позиция вкладки (Лента)</string> <string name="pref_feed_position">Позиция вкладки (Лента)</string>
<string name="pref_feed_position_summery">Поместить вкладку «Лента» на место первой вкладки в «Поисковик»? Это сделает её вкладкой по умолчанию при открытии «Поисковик». Не рекомендуется, если вы используете ограниченные сети(Моб. данные)</string> <string name="pref_feed_position_summery">Поместить вкладку «Лента» на место первой вкладки в «Поисковик»? Это сделает её вкладкой по умолчанию при открытии «Поисковик». Не рекомендуется, если вы используете ограниченные сети(Моб. данные)</string>
<string name="pref_source_source_filtering">Фильтровать источники по категориям</string> <string name="pref_source_source_filtering">Фильтровать источники по категориям</string>
@@ -196,6 +197,8 @@
<string name="biometric_lock_time_conflicts">Биометрическое время блокировки конфликтует с тем, которое уже существует!</string> <string name="biometric_lock_time_conflicts">Биометрическое время блокировки конфликтует с тем, которое уже существует!</string>
<string name="biometric_lock_start_time">Ввести время начала</string> <string name="biometric_lock_start_time">Ввести время начала</string>
<string name="biometric_lock_end_time">Ввести время окончания</string> <string name="biometric_lock_end_time">Ввести время окончания</string>
<string name="delete_time_range">Удалить время блокировки</string>
<string name="delete_time_range_confirmation">Хотите ли вы удалить время блокировки %s?</string>
<string name="biometric_lock_days">Дни биометрической блокировки</string> <string name="biometric_lock_days">Дни биометрической блокировки</string>
<string name="biometric_lock_days_summary">Выбрать дни биометрической блокировки приложения</string> <string name="biometric_lock_days_summary">Выбрать дни биометрической блокировки приложения</string>
<string name="sunday">Воскресенье</string> <string name="sunday">Воскресенье</string>
@@ -331,6 +334,7 @@
<string name="description_hint">Описание: %1$s</string> <string name="description_hint">Описание: %1$s</string>
<string name="author_hint">Автор: %1$s</string> <string name="author_hint">Автор: %1$s</string>
<string name="artist_hint">Художник: %1$s</string> <string name="artist_hint">Художник: %1$s</string>
<string name="thumbnail_url_hint">URL-адрес миниатюры: %1$s</string>
<!-- Browse --> <!-- Browse -->
<!-- Sources Tab --> <!-- Sources Tab -->
@@ -374,13 +378,10 @@
<string name="error_tag_exists">Этот тэг уже существует!</string> <string name="error_tag_exists">Этот тэг уже существует!</string>
<string name="delete_tag">Удалить тэг</string> <string name="delete_tag">Удалить тэг</string>
<string name="delete_tag_confirmation">Хотите ли вы удалить тэг %s?</string> <string name="delete_tag_confirmation">Хотите ли вы удалить тэг %s?</string>
<string name="delete_time_range">Удалить время блокировки</string>
<string name="delete_time_range_confirmation">Хотите ли вы удалить время блокировки %s?</string>
<!-- Extension section --> <!-- Extension section -->
<string name="ext_redundant">Избыточное</string> <string name="ext_redundant">Избыточное</string>
<string name="redundant_extension_message">Это расширение является избыточным и не будет использоваться внутри этой версии Tachiyomi.</string> <string name="redundant_extension_message">Это расширение является избыточным и не будет использоваться внутри этой версии Tachiyomi.</string>
<string name="no_repos_found">Репозитории расширений не найдены. Пожалуйста, добавьте парочку, перейдя по "Больше → Репозитории расширений"!</string>
<!-- Migration --> <!-- Migration -->
<string name="select_sources">Выберите источники</string> <string name="select_sources">Выберите источники</string>
@@ -9,10 +9,10 @@
<string name="action_start_reading">開始閱讀</string> <string name="action_start_reading">開始閱讀</string>
<string name="action_edit_info">編輯資訊</string> <string name="action_edit_info">編輯資訊</string>
<!-- Entry Type --> <!-- Entry Type -->
<string name="entry_type_manga">日本漫畫</string> <string name="entry_type_manga">漫畫(日本)</string>
<string name="entry_type_manhwa">韓國漫畫</string> <string name="entry_type_manhwa">漫畫(韓國)</string>
<string name="entry_type_manhua">中國漫畫</string> <string name="entry_type_manhua">漫畫(中國)</string>
<string name="entry_type_comic">美國漫畫</string> <string name="entry_type_comic">漫畫(美國)</string>
<string name="entry_type_webtoon">條漫</string> <string name="entry_type_webtoon">條漫</string>
<!-- Preferences --> <!-- Preferences -->
@@ -173,6 +173,7 @@
<string name="library_group_updates_all">每次啟動後載入目錄更新</string> <string name="library_group_updates_all">每次啟動後載入目錄更新</string>
<!-- Browse settings --> <!-- Browse settings -->
<string name="pref_hide_feed">隱藏訂閱標籤</string>
<string name="pref_feed_position">訂閱標籤位置</string> <string name="pref_feed_position">訂閱標籤位置</string>
<string name="pref_feed_position_summery">你想讓訂閱標籤成為瀏覽中的第一個標籤嗎?這將使它成為開啟瀏覽時的預設標籤,如果你使用的是資料或計費網路,則不建議使用。</string> <string name="pref_feed_position_summery">你想讓訂閱標籤成為瀏覽中的第一個標籤嗎?這將使它成為開啟瀏覽時的預設標籤,如果你使用的是資料或計費網路,則不建議使用。</string>
<string name="pref_source_source_filtering">在目錄中過濾來源</string> <string name="pref_source_source_filtering">在目錄中過濾來源</string>
@@ -193,6 +194,8 @@
<string name="biometric_lock_time_conflicts">鎖定時間與已經存在的時間衝突!</string> <string name="biometric_lock_time_conflicts">鎖定時間與已經存在的時間衝突!</string>
<string name="biometric_lock_start_time">輸入開始時間</string> <string name="biometric_lock_start_time">輸入開始時間</string>
<string name="biometric_lock_end_time">輸入結束時間</string> <string name="biometric_lock_end_time">輸入結束時間</string>
<string name="delete_time_range">刪除時間範圍</string>
<string name="delete_time_range_confirmation">您是否要刪除時間範圍%s</string>
<string name="biometric_lock_days">生物識別鎖週期</string> <string name="biometric_lock_days">生物識別鎖週期</string>
<string name="biometric_lock_days_summary">需要鎖定應用程式的週期</string> <string name="biometric_lock_days_summary">需要鎖定應用程式的週期</string>
@@ -328,7 +331,8 @@
<string name="title_hint">標題:%1$s</string> <string name="title_hint">標題:%1$s</string>
<string name="description_hint">描述:%1$s</string> <string name="description_hint">描述:%1$s</string>
<string name="author_hint">作者:%1$s</string> <string name="author_hint">作者:%1$s</string>
<string name="artist_hint">藝術家:%1$s</string> <string name="artist_hint">家:%1$s</string>
<string name="thumbnail_url_hint">縮圖網址: %1$s</string>
<!-- Browse --> <!-- Browse -->
<!-- Sources Tab --> <!-- Sources Tab -->
@@ -10,6 +10,11 @@
<item quantity="other">%1$d days ago</item> <item quantity="other">%1$d days ago</item>
</plurals> </plurals>
<plurals name="upcoming_relative_time">
<item quantity="one">Tomorrow</item>
<item quantity="other">In %1$d days</item>
</plurals>
<plurals name="num_categories"> <plurals name="num_categories">
<item quantity="one">%d category</item> <item quantity="one">%d category</item>
<item quantity="other">%d categories</item> <item quantity="other">%d categories</item>
@@ -384,6 +384,8 @@
<string name="pref_skip_read_chapters">Skip chapters marked read</string> <string name="pref_skip_read_chapters">Skip chapters marked read</string>
<string name="pref_skip_filtered_chapters">Skip filtered chapters</string> <string name="pref_skip_filtered_chapters">Skip filtered chapters</string>
<string name="pref_skip_dupe_chapters">Skip duplicate chapters</string> <string name="pref_skip_dupe_chapters">Skip duplicate chapters</string>
<string name="pref_mark_read_dupe_chapters">Mark duplicate chapters as read</string>
<string name="pref_mark_read_dupe_chapters_summary">Mark duplicate chapters as read after reading</string>
<string name="pref_reader_navigation">Navigation</string> <string name="pref_reader_navigation">Navigation</string>
<string name="pref_read_with_volume_keys">Volume keys</string> <string name="pref_read_with_volume_keys">Volume keys</string>
<string name="pref_read_with_volume_keys_inverted">Invert volume keys</string> <string name="pref_read_with_volume_keys_inverted">Invert volume keys</string>
@@ -456,6 +458,7 @@
<string name="pref_high">High</string> <string name="pref_high">High</string>
<string name="pref_low">Low</string> <string name="pref_low">Low</string>
<string name="pref_lowest">Lowest</string> <string name="pref_lowest">Lowest</string>
<string name="pref_webtoon_disable_zoom_out">Disable zoom out</string>
<!-- Downloads section --> <!-- Downloads section -->
<string name="pref_category_delete_chapters">Delete chapters</string> <string name="pref_category_delete_chapters">Delete chapters</string>
+14 -10
View File
@@ -5,28 +5,28 @@
<item quantity="other">%d пухмӑш</item> <item quantity="other">%d пухмӑш</item>
</plurals> </plurals>
<plurals name="lock_after_mins"> <plurals name="lock_after_mins">
<item quantity="one">1 минут хыҫҫӑн</item> <item quantity="one">%1$s минут хыҫҫӑн</item>
<item quantity="other">%1$s минут хыҫҫӑн</item> <item quantity="other">%1$s минут хыҫҫӑн</item>
</plurals> </plurals>
<plurals name="restore_completed_message"> <plurals name="restore_completed_message">
<item quantity="one">%1$s,%2$s йӑнӑшпа тӑвӑннӑ</item> <item quantity="one">%1$s хушши %2$s йӑнӑшпа тӑвӑннӑ</item>
<item quantity="other">%1$s, %2$s йӑнӑшпа тӑвӑннӑ</item> <item quantity="other">%1$s хушши %2$s йӑнӑшпа тӑвӑннӑ</item>
</plurals> </plurals>
<plurals name="update_check_notification_ext_updates"> <plurals name="update_check_notification_ext_updates">
<item quantity="one">Хушма валли ҫӗнетӳ пур</item> <item quantity="one">Хушма валли ҫӗнетӳ пур</item>
<item quantity="other">%d хушма валли ҫӗнетӳ пур</item> <item quantity="other">%d хушма валли ҫӗнетӳ пур</item>
</plurals> </plurals>
<plurals name="notification_chapters_multiple_and_more"> <plurals name="notification_chapters_multiple_and_more">
<item quantity="one">%1$s сыпӑкӗсем</item> <item quantity="one">%1$s сыпӑкӗсем тата тепӗр 1</item>
<item quantity="other">%1$s сыпӑкӗсем тата ытти %2$d</item> <item quantity="other">%1$s сыпӑкӗсем тата тепӗр %2$d</item>
</plurals> </plurals>
<plurals name="notification_chapters_generic"> <plurals name="notification_chapters_generic">
<item quantity="one">1 ҫӗнӗ сыпӑк</item> <item quantity="one">%1$d ҫӗнӗ сыпӑк</item>
<item quantity="other">%1$d ҫӗнӗ сыпӑк</item> <item quantity="other">%1$d ҫӗнӗ сыпӑк</item>
</plurals> </plurals>
<plurals name="notification_new_chapters_summary"> <plurals name="notification_new_chapters_summary">
<item quantity="one">Ҫӗнӗ сыпӑксем 1 хайлав валли тупӑннӑ</item> <item quantity="one">%d хайлав валли</item>
<item quantity="other">Ҫӗнӗ сыпӑксем %d хайлав валли тупӑннӑ</item> <item quantity="other">%d хайлав валли</item>
</plurals> </plurals>
<plurals name="manga_num_chapters"> <plurals name="manga_num_chapters">
<item quantity="one">%1$s сыпӑк</item> <item quantity="one">%1$s сыпӑк</item>
@@ -37,8 +37,8 @@
<item quantity="other">%1$s йулчӗ</item> <item quantity="other">%1$s йулчӗ</item>
</plurals> </plurals>
<plurals name="num_trackers"> <plurals name="num_trackers">
<item quantity="one">1 сӑнану</item> <item quantity="one">%d йӗрлев</item>
<item quantity="other">%d сӑнану</item> <item quantity="other">%d йӗрлев</item>
</plurals> </plurals>
<plurals name="missing_chapters_warning"> <plurals name="missing_chapters_warning">
<item quantity="one">%d сыпӑк ҫук</item> <item quantity="one">%d сыпӑк ҫук</item>
@@ -64,4 +64,8 @@
<item quantity="one">Тепӗр сыпӑк</item> <item quantity="one">Тепӗр сыпӑк</item>
<item quantity="other">Тепӗр %d сыпӑк</item> <item quantity="other">Тепӗр %d сыпӑк</item>
</plurals> </plurals>
<plurals name="num_repos">
<item quantity="one">%d усрав</item>
<item quantity="other">%d усрав</item>
</plurals>
</resources> </resources>
+55 -24
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="manga">Вулавӑшри серилӗхсем</string> <string name="manga">Вулавӑшри хайлавсем</string>
<string name="used_cache">Усӑ курнӑ: %1$s</string> <string name="used_cache">Усӑ курнӑ: %1$s</string>
<string name="cookies_cleared">Куккисем катертнӗ</string> <string name="cookies_cleared">Куккисем катертнӗ</string>
<string name="pref_clear_cookies">Кукки тасат</string> <string name="pref_clear_cookies">Кукки тасат</string>
@@ -49,8 +49,8 @@
<string name="secure_screen">Ыкран сыхлавӗ</string> <string name="secure_screen">Ыкран сыхлавӗ</string>
<string name="pref_category_security">Сыхлав тата вӑрттӑнлӑх</string> <string name="pref_category_security">Сыхлав тата вӑрттӑнлӑх</string>
<string name="pref_manage_notifications">Систерӳсене ӗнер</string> <string name="pref_manage_notifications">Систерӳсене ӗнер</string>
<string name="pref_date_format">Ҫул-кун хармачӗ</string> <string name="pref_date_format">Вӑхӑт хармачӗ</string>
<string name="theme_dark">Ҫутнӑ</string> <string name="theme_dark">Тӗттӗм</string>
<string name="theme_light">Сӳнтернӗ</string> <string name="theme_light">Сӳнтернӗ</string>
<string name="pref_category_advanced">Тата</string> <string name="pref_category_advanced">Тата</string>
<string name="pref_category_downloads">Тийевсем</string> <string name="pref_category_downloads">Тийевсем</string>
@@ -72,8 +72,8 @@
<string name="action_cancel">Пӑрахӑҫла</string> <string name="action_cancel">Пӑрахӑҫла</string>
<string name="action_pin">Ҫаклат</string> <string name="action_pin">Ҫаклат</string>
<string name="action_disable">Сӳнтер</string> <string name="action_disable">Сӳнтер</string>
<string name="action_display_download_badge">Тийене сыпӑксем шучӗ</string> <string name="action_display_download_badge">Тийенӗ сыпӑксен шучӗ</string>
<string name="action_display_list">Йат-йыш</string> <string name="action_display_list">Йат йышӗ</string>
<string name="action_display">Кӑтарт</string> <string name="action_display">Кӑтарт</string>
<string name="action_display_mode">Кӑтарту тытӑмӗ</string> <string name="action_display_mode">Кӑтарту тытӑмӗ</string>
<string name="action_open_in_web_view">WebView-ра уҫ</string> <string name="action_open_in_web_view">WebView-ра уҫ</string>
@@ -83,12 +83,12 @@
<string name="action_remove">Катерт</string> <string name="action_remove">Катерт</string>
<string name="action_retry">Ҫӗнӗрен</string> <string name="action_retry">Ҫӗнӗрен</string>
<string name="action_pause">Чар</string> <string name="action_pause">Чар</string>
<string name="action_view_chapters">Сыпӑксем пӑх</string> <string name="action_view_chapters">Сыпӑксене пӑх</string>
<string name="action_edit_cover">Хуплашка улӑштар</string> <string name="action_edit_cover">Хуплашкана улӑштар</string>
<string name="action_move_category">Пухмӑша хуш</string> <string name="action_move_category">Пухмӑша кӗрт</string>
<string name="action_rename_category">Пухмӑш йатне улӑштар</string> <string name="action_rename_category">Пухмӑш йатне улӑштар</string>
<string name="action_edit_categories">Пухмӑшсене улӑштар</string> <string name="action_edit_categories">Пухмӑшсене улӑштар</string>
<string name="action_add_category">Пухмӑш хуш</string> <string name="action_add_category">Пухмӑша хуш</string>
<string name="action_add">Хуш</string> <string name="action_add">Хуш</string>
<string name="action_mark_previous_as_read">Умӗнхине вуланӑ пек паллӑ ту</string> <string name="action_mark_previous_as_read">Умӗнхине вуланӑ пек паллӑ ту</string>
<string name="action_edit">Улӑштар</string> <string name="action_edit">Улӑштар</string>
@@ -198,7 +198,7 @@
<string name="untrusted_extension">Шанчӑклӑ мар хушма</string> <string name="untrusted_extension">Шанчӑклӑ мар хушма</string>
<string name="lock_when_idle">Ним туман чух ҫаклатни</string> <string name="lock_when_idle">Ним туман чух ҫаклатни</string>
<string name="display_mode_chapter">%1$s-мӗш сыпӑк</string> <string name="display_mode_chapter">%1$s-мӗш сыпӑк</string>
<string name="lock_with_biometrics">Ҫаклатӑва уҫма пӳрне йӗрӗ ыйтни</string> <string name="lock_with_biometrics">Ҫаклатӑва уҫма пӳрне йӗрре ыйтни</string>
<string name="pref_webtoon_side_padding">Аяккинчи чаку</string> <string name="pref_webtoon_side_padding">Аяккинчи чаку</string>
<string name="pref_always_show_chapter_transition">Сыпӑксем урлӑ каҫнине яланах кӑтарт</string> <string name="pref_always_show_chapter_transition">Сыпӑксем урлӑ каҫнине яланах кӑтарт</string>
<string name="color_filter_a_value">Тӑрӑлӑх мар</string> <string name="color_filter_a_value">Тӑрӑлӑх мар</string>
@@ -400,7 +400,7 @@
<string name="pref_create_backup">Янтӑв ту</string> <string name="pref_create_backup">Янтӑв ту</string>
<string name="updated_version">v%1$s верссиччен ҫӗнетнӗ</string> <string name="updated_version">v%1$s верссиччен ҫӗнетнӗ</string>
<string name="whats_new">Мӗн ҫӗнни</string> <string name="whats_new">Мӗн ҫӗнни</string>
<string name="pref_category_theme">Темӑ</string> <string name="pref_category_theme">Темӗ</string>
<string name="action_sort_date_added">Хушни вӑхӑчӗпе</string> <string name="action_sort_date_added">Хушни вӑхӑчӗпе</string>
<string name="download_insufficient_space">Тиск ҫинче вырӑн ҫитмен пирки сыпӑксем тийенеймерӗҫ</string> <string name="download_insufficient_space">Тиск ҫинче вырӑн ҫитмен пирки сыпӑксем тийенеймерӗҫ</string>
<string name="action_global_search_query">\"%1$s\" пур ҫӗрте шыра</string> <string name="action_global_search_query">\"%1$s\" пур ҫӗрте шыра</string>
@@ -436,7 +436,7 @@
<string name="pref_category_nsfw_content">NSFW (18+) ҫӑл куҫӗсем</string> <string name="pref_category_nsfw_content">NSFW (18+) ҫӑл куҫӗсем</string>
<string name="file_picker_error">Файлсене суйламалли хушӑм тупӑнман</string> <string name="file_picker_error">Файлсене суйламалли хушӑм тупӑнман</string>
<string name="myanimelist_relogin">Тархасшӑн MAL-а ҫӗнӗрен кӗр</string> <string name="myanimelist_relogin">Тархасшӑн MAL-а ҫӗнӗрен кӗр</string>
<string name="pref_show_nsfw_source">Ҫӑл куҫсен тата хушмасен йат-йышӗнче кӑтартни</string> <string name="pref_show_nsfw_source">Ҫӑл куҫсен тата хушмасен йат йышӗнче кӑтартни</string>
<string name="track_finished_reading_date">Вулама вӗҫленӗ вӑхӑчӗ</string> <string name="track_finished_reading_date">Вулама вӗҫленӗ вӑхӑчӗ</string>
<string name="track_started_reading_date">Вулама пуҫланӑ вӑхӑчӗ</string> <string name="track_started_reading_date">Вулама пуҫланӑ вӑхӑчӗ</string>
<string name="edge_nav">Хӗрри</string> <string name="edge_nav">Хӗрри</string>
@@ -477,31 +477,31 @@
<string name="action_show_errors">Йӑнӑшсене кӑтарт</string> <string name="action_show_errors">Йӑнӑшсене кӑтарт</string>
<string name="cancel_all_for_series">Ҫак серилӗх валли пурне те пӑрахӑҫла</string> <string name="cancel_all_for_series">Ҫак серилӗх валли пурне те пӑрахӑҫла</string>
<string name="pref_downloads_summary">Хӑй-хальлӗн тийесе илни, малтанах тийени</string> <string name="pref_downloads_summary">Хӑй-хальлӗн тийесе илни, малтанах тийени</string>
<string name="action_sort_last_manga_update">Йулашки ҫӗнетӳ тӗрӗсленипе</string> <string name="action_sort_last_manga_update">Йулашки хут ҫӗнетӳ пуррине тӗрӗсленипе</string>
<string name="action_sort_unread_count">Йулнӑ сыпӑксемпе</string> <string name="action_sort_unread_count">Йулнӑ сыпӑксемпе</string>
<string name="action_sort_count">Ҫырав шучӗпе</string> <string name="action_sort_count">Ҫырав шучӗпе</string>
<string name="action_remove_everything">Пурне те катерт</string> <string name="action_remove_everything">Пурне те катерт</string>
<string name="delete_category_confirmation">«%s» пухмӑш катертесшӗнех-и\?</string> <string name="delete_category_confirmation">«%s» пухмӑша катертесшӗнех-и?</string>
<string name="delete_category">Пухмӑш катерт</string> <string name="delete_category">Пухмӑша катерт</string>
<string name="action_display_local_badge">Вырӑнти ҫӑл куҫран</string> <string name="action_display_local_badge">Вырӑнти ҫӑл куҫран</string>
<string name="label_warning">Асӑрхаттару</string> <string name="label_warning">Асӑрхаттару</string>
<string name="action_display_cover_only_grid">Йатсӑр сетке</string> <string name="action_display_cover_only_grid">Йатсӑр сетке</string>
<string name="action_move_to_top_all_for_series">Серилӗхе пуҫламӑша куҫар</string> <string name="action_move_to_top_all_for_series">Хайлава пуҫламӑша куҫар</string>
<string name="confirm_lock_change">Улшӑнӑва ҫирӗплетме есӗлӗхе ҫирӗплет</string> <string name="confirm_lock_change">Улшӑнӑва ҫирӗплетме есӗлӗхе ҫирӗплет</string>
<string name="action_show_manga">Ҫырава кӑтарт</string> <string name="action_show_manga">Хайлава кӑтарт</string>
<string name="action_display_language_badge">Чӗлхе</string> <string name="action_display_language_badge">Чӗлхе</string>
<string name="action_search_hint">Шыра…</string> <string name="action_search_hint">Шыра…</string>
<string name="action_close">Хуп</string> <string name="action_close">Хуп</string>
<string name="action_start_downloading_now">Тийеве халех пуҫла</string> <string name="action_start_downloading_now">Тийеве халех пуҫла</string>
<string name="internal_error">InternalError: Хушма пӗлӗме пӑхма тӑвӑм-пулӑм кӗнекине пӑх</string> <string name="internal_error">InternalError: Хушма хыпар-пӗлӳ пӑхма тӑвӑм-пулӑм кӗнекине пӑх</string>
<string name="pref_category_appearance">Кӑтартӑну</string> <string name="pref_category_appearance">Кӑтартӑну</string>
<string name="on">Ҫутнӑ</string> <string name="on">Ҫутнӑ</string>
<string name="off">Сӳнтернӗ</string> <string name="off">Сӳнтернӗ</string>
<string name="pref_appearance_summary">Темӑ, кун тата вӑхӑт хармачӗ</string> <string name="pref_appearance_summary">Темӗ, кун тата вӑхӑт хармачӗ</string>
<string name="pref_library_summary">Пухмӑшсем, пӗтӗмӗшле ҫӗнетӳ, сыпӑксене туртни</string> <string name="pref_library_summary">Пухмӑшсем, пӗтӗмӗшле ҫӗнетӳ, сыпӑксене туртни</string>
<string name="pref_reader_summary">Вулав тытӑмӗ, кӑтартӑнни, куҫӑм</string> <string name="pref_reader_summary">Вулав тытӑмӗ, кӑтартӑнни, куҫӑм</string>
<string name="label_default">Йаланхилле</string> <string name="label_default">Йаланхилле</string>
<string name="action_open_random_manga">Ӑнсӑрт ҫырав уҫ</string> <string name="action_open_random_manga">Ӑнсӑрт ҫырава уҫ</string>
<string name="theme_strawberrydaiquiri">Ҫӗр ҫырли тайккирийӗ</string> <string name="theme_strawberrydaiquiri">Ҫӗр ҫырли тайккирийӗ</string>
<string name="theme_midnightdusk">Ҫур ҫӗр ӗнтрӗкӗ</string> <string name="theme_midnightdusk">Ҫур ҫӗр ӗнтрӗкӗ</string>
<string name="pref_tracking_summary">Пӗр йенлӗ ӳсӗм килӗштерӗвӗ, анлӑлатнӑ килӗштерӳ</string> <string name="pref_tracking_summary">Пӗр йенлӗ ӳсӗм килӗштерӗвӗ, анлӑлатнӑ килӗштерӳ</string>
@@ -524,7 +524,7 @@
<string name="network_not_metered">Чараксӑр тетел урлӑ ҫеҫ</string> <string name="network_not_metered">Чараксӑр тетел урлӑ ҫеҫ</string>
<string name="pref_update_only_started">Серилӗхе пуҫламан</string> <string name="pref_update_only_started">Серилӗхе пуҫламан</string>
<string name="theme_tako">Такку</string> <string name="theme_tako">Такку</string>
<string name="pref_dark_theme_pure_black">Хуп-хура темӑ</string> <string name="pref_dark_theme_pure_black">Хуп-хура темӗ</string>
<string name="pref_app_language">Ап чӗлхи</string> <string name="pref_app_language">Ап чӗлхи</string>
<string name="action_not_now">Халь мар</string> <string name="action_not_now">Халь мар</string>
<string name="update_72hour">Кашни 3 кун</string> <string name="update_72hour">Кашни 3 кун</string>
@@ -533,8 +533,8 @@
<string name="label_stats">Шутлавсем</string> <string name="label_stats">Шутлавсем</string>
<string name="action_copy_to_clipboard">Пайлашу асне ӑт</string> <string name="action_copy_to_clipboard">Пайлашу асне ӑт</string>
<string name="pref_backup_summary">Хӑй тӗллӗн тата хӑй-хальлӗн йантӑлав</string> <string name="pref_backup_summary">Хӑй тӗллӗн тата хӑй-хальлӗн йантӑлав</string>
<string name="action_update_category">Пухмӑш ҫӗнет</string> <string name="action_update_category">Пухмӑша ҫӗнет</string>
<string name="pref_advanced_summary">Йӑнӑш кӗнекине тийесе йани, петтерей лайӑхлатни</string> <string name="pref_advanced_summary">Йӑнӑш кӗнекине тийесе йани, петтерейе лайӑхлатни</string>
<string name="pref_library_update_show_tab_badge">Вуламан сыпӑксен шутне «Ҫӗнӗлӗх» ыккун ҫинче кӑтартни</string> <string name="pref_library_update_show_tab_badge">Вуламан сыпӑксен шутне «Ҫӗнӗлӗх» ыккун ҫинче кӑтартни</string>
<string name="pref_landscape_zoom">Сӑна тӑрӑх пысӑклатни</string> <string name="pref_landscape_zoom">Сӑна тӑрӑх пысӑклатни</string>
<string name="multi_lang">Нумай чӗлхеллӗ</string> <string name="multi_lang">Нумай чӗлхеллӗ</string>
@@ -568,6 +568,37 @@
<string name="ext_installer_shizuku_unavailable_dialog">Shizuku-н хушма ларткӑча усӑ курма Shizuku ларт тата ҫут.</string> <string name="ext_installer_shizuku_unavailable_dialog">Shizuku-н хушма ларткӑча усӑ курма Shizuku ларт тата ҫут.</string>
<string name="action_ok">Йурӗ</string> <string name="action_ok">Йурӗ</string>
<string name="unlock_app_title">%s уҫ</string> <string name="unlock_app_title">%s уҫ</string>
<string name="action_move_to_bottom_all_for_series">Серилӗхе вӗҫе куҫар</string> <string name="action_move_to_bottom_all_for_series">Хайлава вӗҫе куҫар</string>
<string name="pref_library_update_categories_details">Кӗртнӗ пухмӑшсенче пулнӑ пулсан та кӑларса пӑрахнӑ пухмӑшсенче пулнӑ серилӗхсем ҫӗнелмӗҫ.</string> <string name="pref_library_update_categories_details">Кӗртнӗ пухмӑшсенче пулнӑ пулсан та кӑларса пӑрахнӑ пухмӑшсенче пулнӑ серилӗхсем ҫӗнелмӗҫ.</string>
<string name="selected">Суйланӑ</string>
<string name="not_selected">Суйламан</string>
<string name="action_bar_up_description">Ҫӳлелле куҫ</string>
<string name="scanlator">Куҫаруҫӑ</string>
<string name="label_data_storage">Пӗлӗмсем тата усрав</string>
<string name="action_sort_tracker_score">Йӗрлевҫӗн хаклавӗ</string>
<string name="sort_category_confirmation">Пултмӑшсене сас паллисен йӗркипе аласшӑн-им?</string>
<string name="action_apply">Кӳр</string>
<string name="action_revert_to_default">Йаланхилле тавӑр</string>
<string name="pref_onboarding_guide">Пуҫлама пӗлкӗч</string>
<string name="onboarding_heading">Килӗрех!</string>
<string name="onboarding_description">Айтӑр темиҫе йапала ӗнерӗпӗр. Есӗ вӗсене йаланах кайран ӗнерӳсенче улӑштарма пултаратӑн.</string>
<string name="onboarding_action_next">Малалла</string>
<string name="onboarding_action_finish">Пуҫла</string>
<string name="onboarding_action_skip">Ирттер</string>
<string name="onboarding_storage_action_select">Папка суйла</string>
<string name="onboarding_storage_selection_required">Папка суйламалла</string>
<string name="onboarding_storage_help_action">Усрав пӗлкӗчӗ</string>
<string name="onboarding_permission_install_apps">Апсене лартма ирӗк</string>
<string name="onboarding_permission_install_apps_description">Ҫӑл куҫсен хушмисене лартма ирӗк парӗ.</string>
<string name="onboarding_permission_notifications">Систерӳсем килме ирӗк</string>
<string name="onboarding_permission_notifications_description">Вулавӑшри ҫӗнетӳсем пурри пирки тата ытти пирки пӗлесси.</string>
<string name="onboarding_permission_ignore_battery_opts">Петтерейе хыҫра усӑ курма ирӗк</string>
<string name="onboarding_permission_ignore_battery_opts_description">Тӑсӑлакан вулавӑш ҫӗнелни, тийени тата йантӑ ӑтава тавӑрни чарӑнасран хӑтӑлтарӗ.</string>
<string name="onboarding_permission_action_grant">Ирӗк пар</string>
<string name="onboarding_guides_new_user">%s-ра ҫӗнни? Епӗр пуҫлав пӗлкӗчӗпе паллашма сӗнетпӗр.</string>
<string name="theme_nord">Ҫур ҫӗр</string>
<string name="action_sort_category">Путмӑшсене ала</string>
<string name="onboarding_storage_help_info">Кивӗ верссирен ҫӗнӗлетӗн те мӗн суламаллине пӗлместӗн? Нумайрах пӗлме усрав пӗлкӗчне кӗрсе пӑх.</string>
<string name="onboarding_guides_returning_user">%s ҫӗнӗрен лартатӑн?</string>
<string name="pref_relative_format_summary">«%1$s» вырӑнне «%2$s»</string>
</resources> </resources>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<plurals name="lock_after_mins"> <plurals name="lock_after_mins">
<item quantity="one">Post 1 minuto</item> <item quantity="one">Post %1$s minuto</item>
<item quantity="other">Post %1$s minutoj</item> <item quantity="other">Post %1$s minutoj</item>
</plurals> </plurals>
<plurals name="num_categories"> <plurals name="num_categories">
@@ -17,7 +17,7 @@
<item quantity="other">Ĉapitroj %1$s kaj %2$d pli</item> <item quantity="other">Ĉapitroj %1$s kaj %2$d pli</item>
</plurals> </plurals>
<plurals name="notification_chapters_generic"> <plurals name="notification_chapters_generic">
<item quantity="one">1 nova ĉapitro</item> <item quantity="one">%1$d nova ĉapitro</item>
<item quantity="other">%1$d novaj ĉapitroj</item> <item quantity="other">%1$d novaj ĉapitroj</item>
</plurals> </plurals>
<plurals name="notification_new_chapters_summary"> <plurals name="notification_new_chapters_summary">
@@ -25,8 +25,8 @@
<item quantity="other">Por %d titoloj</item> <item quantity="other">Por %d titoloj</item>
</plurals> </plurals>
<plurals name="missing_chapters_warning"> <plurals name="missing_chapters_warning">
<item quantity="one">Mankas 1 ĉapitron</item> <item quantity="one">Preterpasas %d ĉapitron, aŭ ĝi mankas ĉe la fonto aŭ ĝi estis elfiltrita</item>
<item quantity="other">Mankas %d ĉapitrojn</item> <item quantity="other">Preterpasas %d ĉapitrojn, aŭ ili mankas ĉe la fonto aŭ ili estis elfiltritaj</item>
</plurals> </plurals>
<plurals name="num_trackers"> <plurals name="num_trackers">
<item quantity="one">1 ŝanĝspurilo</item> <item quantity="one">1 ŝanĝspurilo</item>
@@ -60,4 +60,12 @@
<item quantity="one">Sekva nelegita ĉapitro</item> <item quantity="one">Sekva nelegita ĉapitro</item>
<item quantity="other">Sekvaj %d nelegitaj ĉapitroj</item> <item quantity="other">Sekvaj %d nelegitaj ĉapitroj</item>
</plurals> </plurals>
<plurals name="num_repos">
<item quantity="one">%d deponejo</item>
<item quantity="other">%d deponejoj</item>
</plurals>
<plurals name="update_check_notification_ext_updates">
<item quantity="one">Disponebla ĝisdatigo de etendaĵo</item>
<item quantity="other">Disponeblaj ĝisdatigoj de %d etendaĵoj</item>
</plurals>
</resources> </resources>
@@ -481,4 +481,28 @@
<string name="action_remove_everything">Forigi ĉiojn</string> <string name="action_remove_everything">Forigi ĉiojn</string>
<string name="onboarding_heading">Bonvenon!</string> <string name="onboarding_heading">Bonvenon!</string>
<string name="onboarding_action_skip">Preterpasi</string> <string name="onboarding_action_skip">Preterpasi</string>
<string name="selected">Elektitaj</string>
<string name="not_selected">Ne elektitaj</string>
<string name="action_menu_overflow_description">Pli da opcioj</string>
<string name="delete_downloaded">Forigi elŝutitaĵn</string>
<string name="label_data_storage">Datumoj kaj konservejo</string>
<string name="action_update_category">Ĝisdatigi kategorion</string>
<string name="onboarding_action_next">Sekva</string>
<string name="onboarding_storage_selection_required">Dosierujo devas esti elektita</string>
<string name="pref_appearance_summary">Eloso, dato &amp; tempoformo</string>
<string name="pref_security_summary">Apa ŝlosado, sekura ekrano</string>
<string name="unlock_app_title">Malŝlosi %s</string>
<string name="disabled_nav">Malŝaltita</string>
<string name="invalid_backup_file_error">Tuta eraro:</string>
<string name="source_settings">Agordoj de fonto</string>
<string name="app_settings">Apa agordoj</string>
<string name="theme_nord">Norda</string>
<string name="theme_tidalwave">Cunami</string>
<string name="pref_relative_format_summary">\"%1$s\" anstataŭ \"%2$s\"</string>
<string name="pref_app_language">Apa lingvo</string>
<string name="ext_update_all">Ĝisdatigi ĉiujn</string>
<string name="ext_info_version">Versio</string>
<string name="ext_info_language">Lingvo</string>
<string name="ext_installer_pref">Instalilo</string>
<string name="ext_installer_legacy">Malmoderna</string>
</resources> </resources>
@@ -506,7 +506,7 @@
<string name="local_invalid_format">El formato del capítulo no es correcto</string> <string name="local_invalid_format">El formato del capítulo no es correcto</string>
<string name="chapter_not_found">No se ha encontrado el capítulo</string> <string name="chapter_not_found">No se ha encontrado el capítulo</string>
<string name="notification_incognito_text">Desactivar el modo incógnito</string> <string name="notification_incognito_text">Desactivar el modo incógnito</string>
<string name="enhanced_tracking_info">Ofrece funciones mejoradas para ciertas fuentes. Se hace un seguimiento automático de los elementos al añadirlos a la biblioteca.</string> <string name="enhanced_tracking_info">Ofrecen funciones mejoradas para ciertas fuentes. Se hace un seguimiento automático de los elementos al añadirlos a la biblioteca.</string>
<string name="enhanced_services">Servicios de seguimiento mejorados</string> <string name="enhanced_services">Servicios de seguimiento mejorados</string>
<string name="tracking_guide">Guía de seguimiento</string> <string name="tracking_guide">Guía de seguimiento</string>
<string name="automatic_background">Automático</string> <string name="automatic_background">Automático</string>
@@ -669,7 +669,7 @@
<string name="hour_short">%dh</string> <string name="hour_short">%dh</string>
<string name="minute_short">%dm</string> <string name="minute_short">%dm</string>
<string name="label_used">En uso</string> <string name="label_used">En uso</string>
<string name="not_applicable">¿?</string> <string name="not_applicable">N/D</string>
<string name="label_tracked_titles">En servicios de seguimiento</string> <string name="label_tracked_titles">En servicios de seguimiento</string>
<string name="label_downloaded">Descargados</string> <string name="label_downloaded">Descargados</string>
<string name="label_stats">Estadísticas</string> <string name="label_stats">Estadísticas</string>
@@ -681,7 +681,7 @@
<string name="pref_library_update_show_tab_badge">Ver número de capítulos por leer en el icono de actualizaciones</string> <string name="pref_library_update_show_tab_badge">Ver número de capítulos por leer en el icono de actualizaciones</string>
<string name="copied_to_clipboard_plain">Se ha copiado al portapapeles</string> <string name="copied_to_clipboard_plain">Se ha copiado al portapapeles</string>
<string name="pref_skip_dupe_chapters">Saltarse los capítulos repetidos</string> <string name="pref_skip_dupe_chapters">Saltarse los capítulos repetidos</string>
<string name="enhanced_services_not_installed">Está disponible, pero la fuente todavía no se ha instalado: %s</string> <string name="enhanced_services_not_installed">Están disponibles, pero las fuentes todavía no se han instalado: %s</string>
<string name="confirm_add_duplicate_manga">Ya tienes un elemento en la biblioteca con este mismo nombre. <string name="confirm_add_duplicate_manga">Ya tienes un elemento en la biblioteca con este mismo nombre.
\n \n
\n¿Seguro que quieres continuar\?</string> \n¿Seguro que quieres continuar\?</string>
@@ -742,7 +742,7 @@
<string name="pref_storage_usage">Almacenamiento utilizado</string> <string name="pref_storage_usage">Almacenamiento utilizado</string>
<string name="action_sort_tracker_score">Puntuación del rastreador</string> <string name="action_sort_tracker_score">Puntuación del rastreador</string>
<string name="action_apply">Aplicar</string> <string name="action_apply">Aplicar</string>
<string name="action_revert_to_default">Volver a la configuración predeterminada</string> <string name="action_revert_to_default">Restablecer vista</string>
<string name="action_create">Crear</string> <string name="action_create">Crear</string>
<string name="no_scanlators_found">No se ha encontrado ningún equipo de traducción</string> <string name="no_scanlators_found">No se ha encontrado ningún equipo de traducción</string>
<string name="scanlator">Equipo de traducción</string> <string name="scanlator">Equipo de traducción</string>
@@ -780,7 +780,7 @@
<string name="ext_permission_install_apps_warning">Toca aquí para conceder los permisos necesarios para instalar extensiones.</string> <string name="ext_permission_install_apps_warning">Toca aquí para conceder los permisos necesarios para instalar extensiones.</string>
<string name="private_settings">Incluir datos privados, como las claves de inicio de sesión en plataformas de seguimiento</string> <string name="private_settings">Incluir datos privados, como las claves de inicio de sesión en plataformas de seguimiento</string>
<string name="invalid_backup_file_error">Descripción completa del problema:</string> <string name="invalid_backup_file_error">Descripción completa del problema:</string>
<string name="manga_interval_expected_update">Se espera que se publiquen nuevos capítulos en torno a %1$s, el ciclo aproximado de comprobación entre números es de %2$s.</string> <string name="manga_interval_expected_update">Se espera que el siguiente número salga en aproximadamente %1$s, la aplicación busca actualizaciones cada %2$s.</string>
<string name="manga_interval_custom_amount">Frecuencia de actualización personalizada:</string> <string name="manga_interval_custom_amount">Frecuencia de actualización personalizada:</string>
<string name="error_repo_exists">El repositorio ya existe</string> <string name="error_repo_exists">El repositorio ya existe</string>
<string name="pref_library_update_smart_update">Actualizaciones inteligentes</string> <string name="pref_library_update_smart_update">Actualizaciones inteligentes</string>
@@ -62,8 +62,8 @@
</plurals> </plurals>
<plurals name="next_unread_chapters"> <plurals name="next_unread_chapters">
<item quantity="one">Chapitre suivant non lu</item> <item quantity="one">Chapitre suivant non lu</item>
<item quantity="many">Les %d suivants non lus</item> <item quantity="many">Les %d chapitres suivants non lus</item>
<item quantity="other">Les %d suivants non lus</item> <item quantity="other">Les %d chapitres suivants non lus</item>
</plurals> </plurals>
<plurals name="download_amount"> <plurals name="download_amount">
<item quantity="one">Chapitre suivant</item> <item quantity="one">Chapitre suivant</item>
@@ -5,15 +5,15 @@
<item quantity="other">%d bővítményfrissítés érhető el</item> <item quantity="other">%d bővítményfrissítés érhető el</item>
</plurals> </plurals>
<plurals name="lock_after_mins"> <plurals name="lock_after_mins">
<item quantity="one">1 perc után</item> <item quantity="one">%1$s perc múlva</item>
<item quantity="other">%1$s percek után</item> <item quantity="other">%1$s perc múlva</item>
</plurals> </plurals>
<plurals name="num_categories"> <plurals name="num_categories">
<item quantity="one">%d kategória</item> <item quantity="one">%d kategória</item>
<item quantity="other">%d kategóriák</item> <item quantity="other">%d kategória</item>
</plurals> </plurals>
<plurals name="manga_num_chapters"> <plurals name="manga_num_chapters">
<item quantity="one">1 fejezet</item> <item quantity="one">%1$s fejezet</item>
<item quantity="other">%1$s fejezet</item> <item quantity="other">%1$s fejezet</item>
</plurals> </plurals>
<plurals name="notification_chapters_generic"> <plurals name="notification_chapters_generic">
@@ -26,7 +26,7 @@
</plurals> </plurals>
<plurals name="num_trackers"> <plurals name="num_trackers">
<item quantity="one">%d tracker</item> <item quantity="one">%d tracker</item>
<item quantity="other">%d trackerek</item> <item quantity="other">%d tracker</item>
</plurals> </plurals>
<plurals name="notification_new_chapters_summary"> <plurals name="notification_new_chapters_summary">
<item quantity="one">%d-nak/nek</item> <item quantity="one">%d-nak/nek</item>
@@ -54,11 +54,11 @@
</plurals> </plurals>
<plurals name="download_amount"> <plurals name="download_amount">
<item quantity="one">Következő fejezet</item> <item quantity="one">Következő fejezet</item>
<item quantity="other">Következő %d fejezetek</item> <item quantity="other">Következő %d fejezet</item>
</plurals> </plurals>
<plurals name="missing_chapters"> <plurals name="missing_chapters">
<item quantity="one">Hiányzó %1$s fejezet</item> <item quantity="one">%1$s fejezet hiányzik</item>
<item quantity="other">Hiányzó %1$s fejezetek</item> <item quantity="other">%1$s fejezet hiányzik</item>
</plurals> </plurals>
<plurals name="day"> <plurals name="day">
<item quantity="one">1 nap</item> <item quantity="one">1 nap</item>

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