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
description: You can find your TachiyomiSY version in **More → About**.
placeholder: |
Example: "1.10.4"
Example: "1.10.5"
validations:
required: true
@@ -96,7 +96,7 @@ body:
required: true
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https:/mihon.app/docs/guides/troubleshooting/).
required: true
- label: I have updated the app to version **[1.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
- label: I have updated all installed extensions.
required: true
+1 -1
View File
@@ -31,7 +31,7 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to version **[1.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
- label: I will fill out all of the requested information in this form.
required: true
+4 -3
View File
@@ -32,10 +32,11 @@ jobs:
java-version: 17
distribution: adopt
- name: Set up gradle
uses: gradle/actions/setup-gradle@v3
- name: Build app
uses: gradle/gradle-command-action@v2
with:
arguments: assembleDevDebug
run: ./gradlew assembleDevDebug
- name: Upload APK
uses: actions/upload-artifact@v3
+9 -3
View File
@@ -26,8 +26,8 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi.sy"
versionCode = 64
versionName = "1.10.4"
versionCode = 65
versionName = "1.10.5"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -310,7 +310,7 @@ tasks {
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-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=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
@@ -330,6 +330,12 @@ tasks {
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.
*/
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),
series = ComicInfo.Series(manga.title),
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
@@ -114,7 +120,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories:
ComicInfo.Number(it.toString())
}
},
web = ComicInfo.Web(chapterUrl),
web = ComicInfo.Web(urls.joinToString(" ")),
summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(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),
),
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
source = ComicInfo.SourceMihon(sourceName),
// SY -->
padding = CbzCrypto.createComicInfoPadding()?.let { ComicInfo.PaddingTachiyomiSY(it) },
// SY <--
@@ -23,7 +23,7 @@ class TrackChapter(
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 {
val tracks = getTracks.await(mangaId)
if (tracks.isEmpty()) return@withNonCancellableContext
@@ -34,7 +34,7 @@ class TrackChapter(
service == null ||
!service.isLoggedIn ||
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
}
@@ -50,7 +50,9 @@ class TrackChapter(
delayedTrackingStore.remove(track.id)
} catch (e: Exception) {
delayedTrackingStore.add(track.id, chapterNumber)
DelayedTrackingUpdateJob.setupTask(context)
if (setupJobOnFailure) {
DelayedTrackingUpdateJob.setupTask(context)
}
throw e
}
}
@@ -45,7 +45,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
logcat(LogPriority.DEBUG) {
"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 tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
class UiPreferences(
@@ -46,6 +46,8 @@ class UiPreferences(
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 bottomBarLabels() = preferenceStore.getBoolean("pref_show_bottom_bar_labels", true)
@@ -57,9 +59,9 @@ class UiPreferences(
// SY <--
companion object {
fun dateFormat(format: String): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault())
fun dateFormat(format: String): DateTimeFormatter = when (format) {
"" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
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.unit.dp
import androidx.core.graphics.drawable.toBitmap
import coil.compose.AsyncImage
import coil3.compose.AsyncImage
import eu.kanade.domain.source.model.icon
import eu.kanade.presentation.util.rememberResourceBitmapPainter
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.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import java.util.Date
import java.time.Instant
import java.time.ZoneId
@Composable
fun BrowseSourceEHentaiList(
@@ -128,9 +129,11 @@ fun BrowseSourceEHentaiListItem(
}
val datePosted by produceState("", metadata) {
value = withIOContext {
runCatching { metadata.datePosted?.let { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) } }
.getOrNull()
.orEmpty()
runCatching {
metadata.datePosted?.let {
MetadataUtil.EX_DATE_FORMAT.format(Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()))
}
}.getOrNull().orEmpty()
}
}
val genre by produceState<Pair<GenreColor, StringResource>?>(null, metadata) {
@@ -9,20 +9,26 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
@Composable
fun relativeDateText(
dateEpochMillis: Long,
): String {
return relativeDateText(
date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L },
localDate = LocalDate.ofInstant(
Instant.ofEpochMilli(dateEpochMillis),
ZoneId.systemDefault(),
)
.takeIf { dateEpochMillis > 0L },
)
}
@Composable
fun relativeDateText(
date: Date?,
localDate: LocalDate?,
): String {
val context = LocalContext.current
@@ -30,11 +36,10 @@ fun relativeDateText(
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
return date
?.toRelativeString(
context = context,
relative = relativeTime,
dateFormat = dateFormat,
)
return localDate?.toRelativeString(
context = context,
relative = relativeTime,
dateFormat = dateFormat,
)
?: 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.Row
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
@@ -29,7 +30,6 @@ import androidx.compose.ui.util.fastForEachIndexed
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.components.material.TabText
import tachiyomi.presentation.core.i18n.stringResource
@@ -78,9 +78,8 @@ fun TabbedDialog(
modifier = Modifier.animateContentSize(),
state = pagerState,
verticalAlignment = Alignment.Top,
) { page ->
content(page)
}
pageContent = { page -> content(page) }
)
}
}
}
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
@@ -24,7 +25,6 @@ import dev.icerock.moko.resources.StringResource
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.TabText
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.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import java.util.Date
import java.time.LocalDate
@Composable
fun HistoryScreen(
@@ -134,7 +134,7 @@ private fun HistoryScreenContent(
}
sealed interface HistoryUiModel {
data class Header(val date: Date) : HistoryUiModel
data class Header(val date: LocalDate) : 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.manga.model.MangaCover
import java.time.Instant
import java.time.LocalDate
import java.time.temporal.ChronoUnit
import java.util.Date
import kotlin.random.Random
@@ -73,10 +74,10 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
private object HistoryUiModelExamples {
val headerToday = header()
val headerTomorrow =
HistoryUiModel.Header(Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))
HistoryUiModel.Header(LocalDate.now().plusDays(1))
fun header(instantBuilder: (Instant) -> Instant = { it }) =
HistoryUiModel.Header(Date.from(instantBuilder(Instant.now())))
HistoryUiModel.Header(LocalDate.from(instantBuilder(Instant.now())))
fun items() = sequence {
var count = 1
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.rememberScrollState
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.LibraryManga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.util.plus
@@ -106,7 +106,8 @@ import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp
import tachiyomi.source.local.isLocal
import java.time.Instant
import java.util.Date
import java.time.ZoneId
import java.time.ZonedDateTime
@Composable
fun MangaScreen(
@@ -150,6 +151,7 @@ fun MangaScreen(
onMergeWithAnotherClicked: () -> Unit,
onOpenPagePreview: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit,
previewsRowCount: Int,
// SY <--
// For bottom action menu
@@ -208,6 +210,7 @@ fun MangaScreen(
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
onOpenPagePreview = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked,
previewsRowCount = previewsRowCount,
// SY <--
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
@@ -253,6 +256,7 @@ fun MangaScreen(
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
onOpenPagePreview = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked,
previewsRowCount = previewsRowCount,
// SY <--
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
@@ -308,6 +312,7 @@ private fun MangaScreenSmallImpl(
onMergeWithAnotherClicked: () -> Unit,
onOpenPagePreview: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit,
previewsRowCount: Int,
// SY <--
// 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(
pagePreviewState = state.pagePreviewsState,
onOpenPage = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked,
maxWidth = maxWidth,
setMaxWidth = { maxWidth = it }
setMaxWidth = { maxWidth = it },
rowCount = previewsRowCount,
)
}
// SY <--
@@ -632,6 +638,7 @@ fun MangaScreenLargeImpl(
onMergeWithAnotherClicked: () -> Unit,
onOpenPagePreview: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit,
previewsRowCount: Int,
// SY <--
// For bottom action menu
@@ -832,11 +839,12 @@ fun MangaScreenLargeImpl(
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
)
}
if (state.pagePreviewsState !is PagePreviewState.Unused) {
if (state.pagePreviewsState !is PagePreviewState.Unused && previewsRowCount > 0) {
PagePreviews(
pagePreviewState = state.pagePreviewsState,
onOpenPage = onOpenPagePreview,
onMorePreviewsClicked = onMorePreviewsClicked,
rowCount = previewsRowCount,
)
}
// SY <--
@@ -979,9 +987,10 @@ private fun LazyListScope.sharedChapterItems(
?.let {
// SY -->
if (manga.isEhBasedManga()) {
MetadataUtil.EX_DATE_FORMAT.format(Date(it))
MetadataUtil.EX_DATE_FORMAT
.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault()))
} else {
relativeDateText(Date(item.chapter.dateUpload))
relativeDateText(item.chapter.dateUpload)
}
// SY <--
},
@@ -2,7 +2,6 @@ package eu.kanade.presentation.manga.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -24,8 +23,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
@@ -86,11 +85,13 @@ private fun NotDownloadedIndicator(
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
val hapticFeedback = LocalHapticFeedback.current
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { onClick(ChapterDownloadAction.START_NOW) },
onClick = { onClick(ChapterDownloadAction.START) },
)
@@ -114,12 +115,14 @@ private fun DownloadingIndicator(
onClick: (ChapterDownloadAction) -> Unit,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
var isMenuExpanded by remember { mutableStateOf(false) }
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { onClick(ChapterDownloadAction.CANCEL) },
onClick = { isMenuExpanded = true },
),
@@ -185,12 +188,14 @@ private fun DownloadedIndicator(
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
val hapticFeedback = LocalHapticFeedback.current
var isMenuExpanded by remember { mutableStateOf(false) }
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { isMenuExpanded = true },
onClick = { isMenuExpanded = true },
),
@@ -220,11 +225,13 @@ private fun ErrorIndicator(
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
val hapticFeedback = LocalHapticFeedback.current
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { onClick(ChapterDownloadAction.START) },
onClick = { onClick(ChapterDownloadAction.START) },
),
@@ -241,26 +248,23 @@ private fun ErrorIndicator(
private fun Modifier.commonClickable(
enabled: Boolean,
hapticFeedback: HapticFeedback,
onLongClick: () -> Unit,
onClick: () -> Unit,
) = composed {
val haptic = LocalHapticFeedback.current
Modifier.combinedClickable(
enabled = enabled,
onLongClick = {
onLongClick()
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = onClick,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
)
}
) = this.combinedClickable(
enabled = enabled,
onLongClick = {
onLongClick()
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = onClick,
role = Role.Button,
interactionSource = null,
indication = ripple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
)
private val IndicatorSize = 26.dp
private val IndicatorPadding = 2.dp
@@ -24,36 +24,27 @@ import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
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.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.tachiyomi.data.download.model.Download
import me.saket.swipe.SwipeableActionsBox
import me.saket.swipe.rememberSwipeableActionsState
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ReadItemAlpha
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.selectedBackground
import kotlin.math.absoluteValue
@Composable
fun MangaChapterListItem(
@@ -78,158 +69,132 @@ fun MangaChapterListItem(
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
modifier: Modifier = Modifier,
) {
val haptic = LocalHapticFeedback.current
val density = LocalDensity.current
val textAlpha = if (read) ReadItemAlpha else 1f
val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha
// Increase touch slop of swipe action to reduce accidental trigger
val configuration = LocalViewConfiguration.current
CompositionLocalProvider(
LocalViewConfiguration provides object : ViewConfiguration by configuration {
override val touchSlop: Float = configuration.touchSlop * 3f
},
val start = getSwipeAction(
action = chapterSwipeStartAction,
read = read,
bookmark = bookmark,
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(
action = chapterSwipeStartAction,
read = read,
bookmark = bookmark,
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) },
)
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(
modifier = modifier
.selectedBackground(selected)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
) {
Row(
modifier = modifier
.selectedBackground(selected)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp),
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
var textHeight by remember { mutableIntStateOf(0) }
if (!read) {
Icon(
imageVector = Icons.Filled.Circle,
contentDescription = stringResource(MR.strings.unread),
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 },
var textHeight by remember { mutableIntStateOf(0) }
if (!read) {
Icon(
imageVector = Icons.Filled.Circle,
contentDescription = stringResource(MR.strings.unread),
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 },
)
}
Row {
ProvideTextStyle(
value = MaterialTheme.typography.bodyMedium.copy(
fontSize = 12.sp,
color = LocalContentColor.current.copy(alpha = textSubtitleAlpha),
),
) {
if (date != null) {
Text(
text = date,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (
readProgress != null ||
scanlator != null/* SY --> */ ||
sourceName != null/* SY <-- */
) {
DotSeparatorText()
}
}
if (readProgress != null) {
Text(
text = readProgress,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
)
if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
}
// SY -->
if (sourceName != null) {
Text(
text = sourceName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (scanlator != null) DotSeparatorText()
}
// SY <--
if (scanlator != null) {
Text(
text = scanlator,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Row {
ProvideTextStyle(
value = MaterialTheme.typography.bodyMedium.copy(
fontSize = 12.sp,
color = LocalContentColor.current.copy(alpha = textSubtitleAlpha),
),
) {
if (date != null) {
Text(
text = date,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (readProgress != null || scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
}
if (readProgress != null) {
Text(
text = readProgress,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
)
if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
}
// SY -->
if (sourceName != null) {
Text(
text = sourceName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (scanlator != null) DotSeparatorText()
}
// 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.layout.ContentScale
import androidx.compose.ui.semantics.Role
import coil.compose.AsyncImage
import coil3.compose.AsyncImage
import eu.kanade.presentation.util.rememberResourceBitmapPainter
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.DialogProperties
import androidx.core.view.updatePadding
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.size.Size
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.size.Size
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.DropdownMenu
@@ -169,7 +169,9 @@ fun MangaCoverDialog(
.data(coverDataProvider())
.size(Size.ORIGINAL)
.memoryCachePolicy(CachePolicy.DISABLED)
.target { drawable ->
.target { image ->
val drawable = image.asDrawable(view.context.resources)
// Copy bitmap in case it came from memory cache
// Because SSIV needs to thoroughly read the image
val copy = (drawable as? BitmapDrawable)?.let {
@@ -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.sp
import coil.compose.AsyncImage
import coil3.compose.AsyncImage
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R
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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent
import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent
import eu.kanade.domain.manga.model.PagePreview
import eu.kanade.presentation.manga.MangaScreenItem
import eu.kanade.tachiyomi.ui.manga.PagePreviewState
@@ -102,6 +101,7 @@ fun PagePreviews(
pagePreviewState: PagePreviewState,
onOpenPage: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit,
rowCount: Int,
) {
Column(Modifier.fillMaxWidth()) {
var maxWidth by remember {
@@ -113,7 +113,7 @@ fun PagePreviews(
}
pagePreviewState is PagePreviewState.Success -> {
val itemPerRowCount = (maxWidth / 120.dp).floor()
pagePreviewState.pagePreviews.take(4 * itemPerRowCount).chunked(itemPerRowCount).forEach {
pagePreviewState.pagePreviews.take(rowCount * itemPerRowCount).chunked(itemPerRowCount).forEach {
PagePreviewRow(
onOpenPage = onOpenPage,
items = remember(it) { it.toImmutableList() }
@@ -132,7 +132,8 @@ fun LazyListScope.PagePreviewItems(
onOpenPage: (Int) -> Unit,
onMorePreviewsClicked: () -> Unit,
maxWidth: Dp,
setMaxWidth: (Dp) -> Unit
setMaxWidth: (Dp) -> Unit,
rowCount: Int,
) {
when {
pagePreviewState is PagePreviewState.Loading || maxWidth == Dp.Hairline -> {
@@ -148,7 +149,7 @@ fun LazyListScope.PagePreviewItems(
items(
key = { "${MangaScreenItem.CHAPTER_PREVIEW_ROW}-$it" },
contentType = { MangaScreenItem.CHAPTER_PREVIEW_ROW },
items = pagePreviewState.pagePreviews.take(4 * itemPerRowCount).chunked(itemPerRowCount),
items = pagePreviewState.pagePreviews.take(rowCount * itemPerRowCount).chunked(itemPerRowCount),
) {
PagePreviewRow(
onOpenPage = onOpenPage,
@@ -23,11 +23,12 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.LocalDate
object SettingsAppearanceScreen : SearchableSettings {
@@ -106,7 +107,7 @@ object SettingsAppearanceScreen : SearchableSettings {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val now = remember { Instant.now().toEpochMilli() }
val now = remember { LocalDate.now() }
val dateFormat by uiPreferences.dateFormat().collectAsState()
val formattedNow = remember(dateFormat) {
@@ -157,6 +158,8 @@ object SettingsAppearanceScreen : SearchableSettings {
// SY -->
@Composable
fun getForkGroup(uiPreferences: UiPreferences): Preference.PreferenceGroup {
val previewsRowCount by uiPreferences.previewsRowCount().collectAsState()
return Preference.PreferenceGroup(
stringResource(SYMR.strings.pref_category_fork),
preferenceItems = persistentListOf(
@@ -174,6 +177,21 @@ object SettingsAppearanceScreen : SearchableSettings {
title = stringResource(SYMR.strings.put_merge_in_overflow),
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(),
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(
pref = readerPreferences.alwaysShowChapterTransition(),
title = stringResource(MR.strings.pref_always_show_chapter_transition),
@@ -382,17 +387,16 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
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 -->
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.pageTransitionsWebtoon(),
title = stringResource(MR.strings.pref_page_transitions),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonEnableZoomOut(),
title = stringResource(SYMR.strings.enable_zoom_out),
),
// SY <--
),
)
@@ -56,10 +56,10 @@ import tachiyomi.presentation.core.icons.Reddit
import tachiyomi.presentation.core.icons.X
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
import java.util.TimeZone
object AboutScreen : Screen() {
@@ -293,16 +293,9 @@ object AboutScreen : Screen() {
internal fun getFormattedBuildTime(): String {
return try {
val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US)
inputDf.timeZone = TimeZone.getTimeZone("UTC")
val buildTime = inputDf.parse(BuildConfig.BUILD_TIME)
val outputDf = DateFormat.getDateTimeInstance(
DateFormat.MEDIUM,
DateFormat.SHORT,
Locale.getDefault(),
)
outputDf.timeZone = TimeZone.getDefault()
val df = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'", Locale.US)
.withZone(ZoneId.of("UTC"))
val buildTime = LocalDateTime.from(df.parse(BuildConfig.BUILD_TIME))
buildTime!!.toDateTimestampString(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
} catch (e: Exception) {
@@ -42,7 +42,9 @@ import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.plus
import uy.kohesive.injekt.Injekt
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() {
@@ -148,13 +150,16 @@ class WorkerInfoScreen : Screen() {
}
appendLine("State: ${workInfo.state}")
if (workInfo.state == WorkInfo.State.ENQUEUED) {
appendLine(
"Next scheduled run: ${Date(workInfo.nextScheduleTimeMillis).toDateTimestampString(
val timestamp = LocalDateTime.ofInstant(
Instant.ofEpochMilli(workInfo.nextScheduleTimeMillis),
ZoneId.systemDefault(),
)
.toDateTimestampString(
UiPreferences.dateFormat(
Injekt.get<UiPreferences>().dateFormat().get(),
),
)}",
)
)
appendLine("Next scheduled run: $timestamp",)
appendLine("Attempt #${workInfo.runAttemptCount + 1}")
}
appendLine()
@@ -22,7 +22,10 @@ import exh.source.isEhBasedManga
import kotlinx.collections.immutable.ImmutableList
import tachiyomi.domain.chapter.model.Chapter
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
fun ChapterListDialog(
@@ -56,9 +59,13 @@ fun ChapterListDialog(
?.let {
// SY -->
if (manga?.isEhBasedManga() == true) {
MetadataUtil.EX_DATE_FORMAT.format(Date(it))
MetadataUtil.EX_DATE_FORMAT
.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault()))
} else {
Date(it).toRelativeString(context, dateRelativeTime, chapterItem.dateFormat)
LocalDate.ofInstant(
Instant.ofEpochMilli(it),
ZoneId.systemDefault(),
).toRelativeString(context, dateRelativeTime, chapterItem.dateFormat)
}
// SY <--
},
@@ -217,11 +217,6 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
label = stringResource(MR.strings.pref_page_transitions),
pref = screenModel.preferences.pageTransitionsWebtoon(),
)
CheckboxItem(
label = stringResource(SYMR.strings.enable_zoom_out),
pref = screenModel.preferences.webtoonEnableZoomOut(),
)
// SY <--
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),
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(),
)
CheckboxItem(
label = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
pref = screenModel.preferences.webtoonDisableZoomOut(),
)
}
// SY -->
@@ -8,6 +8,7 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.ui.UiPreferences
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.LavenderColorScheme
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
@@ -62,23 +63,27 @@ private fun getThemeColorScheme(
appTheme: AppTheme,
isAmoled: Boolean,
): ColorScheme {
val colorScheme = when (appTheme) {
AppTheme.DEFAULT -> TachiyomiColorScheme
AppTheme.MONET -> MonetColorScheme(LocalContext.current)
AppTheme.GREEN_APPLE -> GreenAppleColorScheme
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
val colorScheme = if (appTheme == AppTheme.MONET) {
MonetColorScheme(LocalContext.current)
} else {
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
}
return colorScheme.getColorScheme(
isSystemInDarkTheme(),
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
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
return (if (isDark) darkScheme else lightScheme)
.let {
if (isDark && isAmoled) {
it.copy(
background = Color.Black,
onBackground = Color.White,
surface = Color.Black,
onSurface = Color.White,
)
} else {
it
}
}
if (!isDark) return lightScheme
if (!isAmoled) return darkScheme
return darkScheme.copy(
background = Color.Black,
onBackground = Color.White,
surface = Color.Black,
onSurface = Color.White,
)
}
}
@@ -52,17 +52,18 @@ import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.presentation.track.components.TrackLogoIcon
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.lang.toLocalDate
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import java.text.DateFormat
import java.time.format.DateTimeFormatter
private const val UnsetStatusTextAlpha = 0.5F
@Composable
fun TrackInfoDialogHome(
trackItems: List<TrackItem>,
dateFormat: DateFormat,
dateFormat: DateTimeFormatter,
onStatusClick: (TrackItem) -> Unit,
onChapterClick: (TrackItem) -> Unit,
onScoreClick: (TrackItem) -> Unit,
@@ -104,11 +105,11 @@ fun TrackInfoDialogHome(
.takeIf { supportsScoring && item.track.score != 0.0 },
onScoreClick = { onScoreClick(item) }
.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 },
onStartDateClick = { onStartDateEdit(item) } // TODO
.takeIf { supportsReadingDates },
endDate = dateFormat.format(item.track.finishDate)
endDate = dateFormat.format(item.track.finishDate.toLocalDate())
.takeIf { supportsReadingDates && item.track.finishDate != 0L },
onEndDateClick = { onEndDateEdit(item) }
.takeIf { supportsReadingDates },
@@ -5,7 +5,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.test.DummyTracker
import tachiyomi.domain.track.model.Track
import java.text.DateFormat
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
internal class TrackInfoDialogHomePreviewProvider :
PreviewParameterProvider<@Composable () -> Unit> {
@@ -46,7 +47,7 @@ internal class TrackInfoDialogHomePreviewProvider :
trackItemWithoutTrack,
trackItemWithTrack,
),
dateFormat = DateFormat.getDateInstance(),
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
onStatusClick = {},
onChapterClick = {},
onScoreClick = {},
@@ -61,7 +62,7 @@ internal class TrackInfoDialogHomePreviewProvider :
private val noTrackers = @Composable {
TrackInfoDialogHome(
trackItems = listOf(),
dateFormat = DateFormat.getDateInstance(),
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
onStatusClick = {},
onChapterClick = {},
onScoreClick = {},
@@ -36,7 +36,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import java.util.Date
import java.time.LocalDate
import kotlin.time.Duration.Companion.seconds
@Composable
@@ -212,6 +212,6 @@ private fun UpdatesBottomBar(
}
sealed interface UpdatesUiModel {
data class Header(val date: Date) : UpdatesUiModel
data class Header(val date: LocalDate) : 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.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.disk.DiskCache
import coil.util.DebugLogger
import coil3.ImageLoader
import coil3.SingletonImageLoader
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.util.DebugLogger
import com.elvishew.xlog.LogConfiguration
import com.elvishew.xlog.LogLevel
import com.elvishew.xlog.XLog
@@ -81,7 +83,7 @@ import java.security.Security
import java.text.SimpleDateFormat
import java.util.Locale
class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
private val basePreferences: BasePreferences 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 {
val callFactoryInit = { Injekt.get<NetworkHelper>().client }
val diskCacheInit = { CoilDiskCache.get(this@App) }
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
val diskCacheLazy = lazy { CoilDiskCache.get(this@App) }
components {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
add(TachiyomiImageDecoder.Factory())
add(MangaCoverFetcher.MangaFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(MangaCoverFetcher.MangaCoverFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(MangaCoverFetcher.MangaFactory(callFactoryLazy, diskCacheLazy))
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy, diskCacheLazy))
add(MangaKeyer())
add(MangaCoverKeyer())
// SY -->
add(PagePreviewKeyer())
add(PagePreviewFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(PagePreviewFetcher.Factory(callFactoryLazy, diskCacheLazy))
// SY <--
}
callFactory(callFactoryInit)
diskCache(diskCacheInit)
diskCache(diskCacheLazy::value)
crossfade((300 * this@App.animatorDurationScale).toInt())
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
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
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
}.build()
}
@@ -1,19 +1,19 @@
package eu.kanade.tachiyomi.data.coil
import androidx.core.net.toUri
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.disk.DiskCache
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.network.HttpException
import coil.request.Options
import coil.request.Parameters
import coil3.Extras
import coil3.ImageLoader
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.disk.DiskCache
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.getOrDefault
import coil3.request.Options
import com.hippo.unifile.UniFile
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.source.online.HttpSource
import logcat.LogPriority
@@ -22,6 +22,7 @@ import okhttp3.Call
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.http.HTTP_NOT_MODIFIED
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import okio.Source
import okio.buffer
@@ -33,6 +34,7 @@ import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
/**
* A [Fetcher] that fetches cover image for [Manga] object.
@@ -42,7 +44,7 @@ import java.io.File
* handled by Coil's [DiskCache].
*
* 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(
private val url: String?,
@@ -61,7 +63,7 @@ class MangaCoverFetcher(
override suspend fun fetch(): FetchResult {
// 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) {
val customCoverFile = customCoverFileLazy.value
if (customCoverFile.exists()) {
@@ -80,8 +82,12 @@ class MangaCoverFetcher(
}
private fun fileLoader(file: File): FetchResult {
return SourceResult(
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
return SourceFetchResult(
source = ImageSource(
file = file.toOkioPath(),
fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey
),
mimeType = "image/*",
dataSource = DataSource.DISK,
)
@@ -92,8 +98,8 @@ class MangaCoverFetcher(
.openInputStream()
.source()
.buffer()
return SourceResult(
source = ImageSource(source = source, context = options.context),
return SourceFetchResult(
source = ImageSource(source = source, fileSystem = FileSystem.SYSTEM),
mimeType = "image/*",
dataSource = DataSource.DISK,
)
@@ -121,7 +127,7 @@ class MangaCoverFetcher(
}
// Read from snapshot
return SourceResult(
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = DataSource.DISK,
@@ -141,7 +147,7 @@ class MangaCoverFetcher(
// Read from disk cache
snapshot = writeToDiskCache(response)
if (snapshot != null) {
return SourceResult(
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = DataSource.NETWORK,
@@ -149,8 +155,8 @@ class MangaCoverFetcher(
}
// Read from response if cache is unused or unusable
return SourceResult(
source = ImageSource(source = responseBody.source(), context = options.context),
return SourceFetchResult(
source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM),
mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
)
@@ -169,17 +175,20 @@ class MangaCoverFetcher(
val response = client.newCall(newRequest()).await()
if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) {
response.close()
throw HttpException(response)
throw IOException(response.message)
}
return response
}
private fun newRequest(): Request {
val request = Request.Builder()
.url(url!!)
.headers(sourceLazy.value?.headers ?: options.headers)
// Support attaching custom data to the network request.
.tag(Parameters::class.java, options.parameters)
val request = Request.Builder().apply {
url(url!!)
val sourceHeaders = sourceLazy.value?.headers
if (sourceHeaders != null) {
headers(sourceHeaders)
}
}
when {
options.networkCachePolicy.readEnabled -> {
@@ -264,7 +273,12 @@ class MangaCoverFetcher(
}
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? {
@@ -330,7 +344,7 @@ class MangaCoverFetcher(
}
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_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.coil
import coil.key.Keyer
import coil.request.Options
import coil3.key.Keyer
import coil3.request.Options
import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.CoverCache
import tachiyomi.domain.manga.model.MangaCover
@@ -1,15 +1,13 @@
package eu.kanade.tachiyomi.data.coil
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.disk.DiskCache
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.network.HttpException
import coil.request.Options
import coil.request.Parameters
import coil3.ImageLoader
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.disk.DiskCache
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import eu.kanade.domain.manga.model.PagePreview
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
import eu.kanade.tachiyomi.network.await
@@ -21,12 +19,14 @@ import okhttp3.Call
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.http.HTTP_NOT_MODIFIED
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import okio.Source
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
/**
* A [Fetcher] that fetches page preview image for [PagePreview] object.
@@ -54,8 +54,12 @@ class PagePreviewFetcher(
}
private fun fileLoader(file: File): FetchResult {
return SourceResult(
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
return SourceFetchResult(
source = ImageSource(
file = file.toOkioPath(),
fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey
),
mimeType = "image/*",
dataSource = DataSource.DISK,
)
@@ -76,7 +80,7 @@ class PagePreviewFetcher(
}
// Read from snapshot
return SourceResult(
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = DataSource.DISK,
@@ -96,7 +100,7 @@ class PagePreviewFetcher(
// Read from disk cache
snapshot = writeToDiskCache(response)
if (snapshot != null) {
return SourceResult(
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = DataSource.NETWORK,
@@ -104,8 +108,8 @@ class PagePreviewFetcher(
}
// Read from response if cache is unused or unusable
return SourceResult(
source = ImageSource(source = responseBody.source(), context = options.context),
return SourceFetchResult(
source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM),
mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
)
@@ -125,7 +129,7 @@ class PagePreviewFetcher(
) ?: callFactoryLazy.value.newCall(newRequest()).await()
if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) {
response.close()
throw HttpException(response)
throw IOException(response.message)
}
return response
}
@@ -144,11 +148,14 @@ class PagePreviewFetcher(
}
private fun newRequest(): Request {
val request = Request.Builder()
.url(page.imageUrl)
.headers((sourceLazy.value as? HttpSource)?.headers ?: options.headers)
// Support attaching custom data to the network request.
.tag(Parameters::class.java, options.parameters)
val request = Request.Builder().apply {
url(page.imageUrl)
val sourceHeaders = (sourceLazy.value as? HttpSource)?.headers
if (sourceHeaders != null) {
headers(sourceHeaders)
}
}
request.cacheControl(getCacheControl())
@@ -218,7 +225,12 @@ class PagePreviewFetcher(
}
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(
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.coil
import coil.key.Keyer
import coil.request.Options
import coil3.key.Keyer
import coil3.request.Options
import eu.kanade.domain.manga.model.PagePreview
class PagePreviewKeyer : Keyer<PagePreview> {
@@ -2,13 +2,14 @@ package eu.kanade.tachiyomi.data.coil
import android.os.Build
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.decode.DecodeResult
import coil.decode.Decoder
import coil.decode.ImageDecoderDecoder
import coil.decode.ImageSource
import coil.fetch.SourceResult
import coil.request.Options
import coil3.ImageLoader
import coil3.asCoilImage
import coil3.decode.DecodeResult
import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.request.allowRgb565
import eu.kanade.tachiyomi.util.storage.CbzCrypto
import net.lingala.zip4j.ZipFile
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" }
return DecodeResult(
drawable = bitmap.toDrawable(options.context.resources),
image = bitmap.asCoilImage(),
isSampled = false,
)
}
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
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()
}
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.os.Build
import com.hippo.unifile.UniFile
import eu.kanade.domain.chapter.model.toSChapter
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.online.HttpSource
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.NOMEDIA_FILE
import eu.kanade.tachiyomi.util.storage.saveTo
@@ -43,8 +43,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import logcat.LogPriority
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.ZipParameters
import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response
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.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.BufferedOutputStream
import java.io.File
import java.nio.charset.StandardCharsets
import java.util.Locale
import java.util.zip.CRC32
import java.util.zip.ZipEntry
@@ -86,6 +84,7 @@ class Downloader(
private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val xml: XML = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
// SY -->
private val sourcePreferences: SourcePreferences = Injekt.get(),
// SY <--
@@ -663,31 +662,15 @@ class Downloader(
dirname: String,
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)) }
zip.addFiles(
tmpDir.listFiles()?.map { img -> img.filePath?.let { File(it) } },
zipParameters,
)
zip.close()
val realZip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
realZip.openOutputStream().use { out ->
zipFile.inputStream().use {
it.copyTo(out)
}
tmpDir.listFiles()?.toList()?.let { files ->
mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
?.addFilesToZip(files, CbzCrypto.getDecryptedPasswordCbz())
}
mangaDir.findFile("$dirname.cbz$TMP_DIR_SUFFIX")?.renameTo("$dirname.cbz")
tmpDir.delete()
zipFile.delete()
}
private fun addPaddingToImage(imageDir: File) {
@@ -713,9 +696,22 @@ class Downloader(
chapter: Chapter,
source: HttpSource,
) {
val chapterUrl = source.getChapterUrl(chapter.toSChapter())
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
dir.findFile(COMIC_INFO_FILE, true)?.delete()
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.track.TrackStatus
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.UpdateStrategy
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.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.model.NoChaptersException
import tachiyomi.domain.download.service.DownloadPreferences
@@ -101,7 +101,6 @@ import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
@@ -400,8 +399,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
.sortedByDescending { it.sourceOrder }.run {
if (libraryPreferences.libraryReadDuplicateChapters().get()) {
val readChapters = getChaptersByMangaId.await(manga.id).filter { it.read }
val newReadChapters = this.filter { chapter -> readChapters.any { it.chapterNumber == chapter.chapterNumber } }
.also { setReadStatus.await(true, *it.toTypedArray()) }
val newReadChapters = this.filter { chapter ->
chapter.chapterNumber > 0 &&
readChapters.any { it.chapterNumber == chapter.chapterNumber }
}
if (newReadChapters.isNotEmpty()) {
setReadStatus.await(true, *newReadChapters.toTypedArray())
}
this.filterNot { newReadChapters.contains(it) }
} else {
@@ -631,9 +636,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
var tracker = dbTracks.firstOrNull { it.trackerId == TrackerManager.MDLIST }
?: mdList.createInitialTracker(manga).toDomainTrack(idRequired = false)
if (tracker?.status == FollowStatus.UNFOLLOWED.int.toLong()) {
if (tracker?.status == FollowStatus.UNFOLLOWED.long) {
tracker = tracker.copy(
status = FollowStatus.READING.int.toLong(),
status = FollowStatus.READING.long,
)
val updatedTrack = mdList.update(tracker.toDbTrack())
insertTrack.await(updatedTrack.toDomainTrack(false)!!)
@@ -9,9 +9,10 @@ import android.graphics.BitmapFactory
import android.net.Uri
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import coil.imageLoader
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import coil3.imageLoader
import coil3.request.ImageRequest
import coil3.request.transformations
import coil3.transform.CircleCropTransformation
import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
@@ -294,7 +295,7 @@ class LibraryUpdateNotifier(
.transformations(CircleCropTransformation())
.size(NOTIF_ICON_SIZE)
.build()
val drawable = context.imageLoader.execute(request).drawable
val drawable = context.imageLoader.execute(request).image?.asDrawable(context.resources)
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? {
return when (tracker) {
trackerManager.mdList.id -> {
when (FollowStatus.fromInt(status)) {
when (FollowStatus.fromLong(status)) {
FollowStatus.UNFOLLOWED -> null
FollowStatus.READING -> READING
FollowStatus.COMPLETED -> COMPLETED
@@ -41,7 +41,7 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
}
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) {
@@ -64,13 +64,13 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
val mdex = mdex ?: throw MangaDexNotFoundException()
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
// allow follow status to update
if (remoteTrack.status != followStatus.int) {
if (remoteTrack.status != followStatus.long) {
if (mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)) {
remoteTrack.status = followStatus.int
remoteTrack.status = followStatus.long
} else {
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(
refresh(track).also {
if (it.status == FollowStatus.UNFOLLOWED.int) {
if (it.status == FollowStatus.UNFOLLOWED.long) {
it.status = if (hasReadChapters) {
FollowStatus.READING.int
FollowStatus.READING.long
} 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 {
return Track.create(id).apply {
manga_id = dbManga.id
status = FollowStatus.UNFOLLOWED.int
status = FollowStatus.UNFOLLOWED.long
tracking_url = MdUtil.baseUrl + mdManga.url
title = mdManga.title
}
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
@@ -32,7 +31,8 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
// Add the authorization header to the original request
val authRequest = originalRequest.newBuilder()
.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()
return chain.proceed(authRequest)
@@ -6,7 +6,6 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import dalvik.system.PathClassLoader
import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.domain.source.service.SourcePreferences
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.util.lang.Hash
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
import eu.kanade.tachiyomi.util.system.ChildFirstPathClassLoader
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
@@ -272,7 +272,7 @@ internal object ExtensionLoader {
}
val classLoader = try {
PathClassLoader(appInfo.sourceDir, null, context.classLoader)
ChildFirstPathClassLoader(appInfo.sourceDir, null, context.classLoader)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($pkgName)" }
return LoadResult.Error
@@ -89,6 +89,8 @@ import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.net.URLEncoder
import java.time.ZoneOffset
import java.time.ZonedDateTime
// TODO Consider gallery updating when doing tabbed browsing
class EHentai(
@@ -271,8 +273,9 @@ class EHentai(
private fun getDateTag(element: Element?): Long? {
val text = element?.text()?.nullIfBlank()
return if (text != null) {
val date = MetadataUtil.EX_DATE_FORMAT.parse(text)
date?.time
println(text)
val date = ZonedDateTime.parse(text, MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC))
date?.toInstant()?.toEpochMilli()
} else {
null
}
@@ -376,11 +379,12 @@ class EHentai(
url = EHentaiSearchMetadata.normalizeUrl(location),
name = "v1: " + doc.selectFirst("#gn")!!.text(),
chapter_number = 1f,
date_upload = MetadataUtil.EX_DATE_FORMAT.parse(
date_upload = ZonedDateTime.parse(
doc.select("#gdd .gdt1").find { el ->
el.text().lowercase() == "posted:"
}!!.nextElementSibling()!!.text(),
)!!.time,
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC)
)!!.toInstant().toEpochMilli(),
scanlator = EHentaiSearchMetadata.galleryId(location),
)
// Build and append the rest of the galleries
@@ -395,7 +399,10 @@ class EHentai(
url = EHentaiSearchMetadata.normalizeUrl(link),
name = "v${index + 2}: $name",
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),
)
}.reversed() + self
@@ -706,7 +713,10 @@ class EHentai(
if (left != null && right != null) {
ignore {
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 JP gallery: https://exhentai.org/g/1375385/03519d541b/
// Parent is older variation of the gallery
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.ui.base.delegate
import android.app.Activity
import android.content.Intent
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
@@ -149,7 +151,12 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
if (activity.isAuthenticationSupported()) {
if (!SecureActivityDelegate.requireUnlock) return
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 {
securityPreferences.useAuthenticator().set(false)
}
@@ -12,51 +12,10 @@ interface ThemingDelegate {
companion object {
fun getThemeResIds(appTheme: AppTheme, isAmoled: Boolean): List<Int> {
val resIds = mutableListOf<Int>()
when (appTheme) {
AppTheme.MONET -> {
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
}
return buildList(2) {
add(themeResources.getOrDefault(appTheme, R.style.Theme_Tachiyomi))
if (isAmoled) add(R.style.ThemeOverlay_Tachiyomi_Amoled)
}
if (isAmoled) {
resIds += R.style.ThemeOverlay_Tachiyomi_Amoled
}
return resIds
}
}
}
@@ -68,3 +27,17 @@ class ThemingDelegateImpl : ThemingDelegate {
.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 eu.kanade.core.util.insertSeparators
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.toImmutableList
import kotlinx.coroutines.Dispatchers
@@ -30,7 +30,6 @@ import tachiyomi.domain.history.interactor.RemoveHistory
import tachiyomi.domain.history.model.HistoryWithRelations
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
class HistoryScreenModel(
private val getHistory: GetHistory = Injekt.get(),
@@ -62,10 +61,10 @@ class HistoryScreenModel(
private fun List<HistoryWithRelations>.toHistoryUiModels(): List<HistoryUiModel> {
return map { HistoryUiModel.Item(it) }
.insertSeparators { before, after ->
val beforeDate = before?.item?.readAt?.time?.toDateKey() ?: Date(0)
val afterDate = after?.item?.readAt?.time?.toDateKey() ?: Date(0)
val beforeDate = before?.item?.readAt?.time?.toLocalDate()
val afterDate = after?.item?.readAt?.time?.toLocalDate()
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.
else -> null
}
@@ -20,8 +20,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.view.children
import coil.load
import coil.transform.RoundedCornersTransformation
import coil3.load
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -5,9 +5,9 @@ import android.net.Uri
import androidx.compose.material3.SnackbarHostState
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import coil.imageLoader
import coil.request.ImageRequest
import coil.size.Size
import coil3.imageLoader
import coil3.request.ImageRequest
import coil3.size.Size
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.saver.Image
@@ -96,7 +96,7 @@ class MangaCoverScreenModel(
.build()
return withIOContext {
val result = context.imageLoader.execute(req).drawable
val result = context.imageLoader.execute(req).image?.asDrawable(context.resources)
// TODO: Handle animated cover
val bitmap = result?.getBitmapOrNull() ?: return@withIOContext null
@@ -197,6 +197,7 @@ class MangaScreen(
onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf {
successState.manga.favorite
},
previewsRowCount = successState.previewsRowCount,
// SY -->
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
@@ -418,6 +418,7 @@ class MangaScreenModel(
PagePreviewState.Unused
},
alwaysShowReadingProgress = readerPreferences.preserveReadingPosition().get() && manga.isEhBasedManga(),
previewsRowCount = uiPreferences.previewsRowCount().get(),
// SY <--
)
}
@@ -1634,6 +1635,7 @@ class MangaScreenModel(
val showMergeWithAnother: Boolean,
val pagePreviewsState: PagePreviewState,
val alwaysShowReadingProgress: Boolean,
val previewsRowCount: Int,
// SY <--
) : State {
val processedChapters by lazy {
@@ -1679,7 +1681,7 @@ class MangaScreenModel(
val trackingCount: Int
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
import android.view.View
import coil.load
import coil.transform.RoundedCornersTransformation
import coil3.load
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.EditMergedSettingsItemBinding
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader
import android.annotation.SuppressLint
import android.app.Activity
import android.app.assist.AssistContent
import android.content.Context
import android.content.Intent
@@ -178,7 +179,16 @@ class ReaderActivity : BaseActivity() {
*/
override fun onCreate(savedInstanceState: Bundle?) {
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)
@@ -312,7 +322,16 @@ class ReaderActivity : BaseActivity() {
override fun finish() {
viewModel.onActivityFinish()
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 {
@@ -9,6 +9,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.model.readerOrientation
@@ -129,6 +130,7 @@ class ReaderViewModel @JvmOverloads constructor(
private val getMergedMangaById: GetMergedMangaById = Injekt.get(),
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
private val getMergedChaptersByMangaId: GetMergedChaptersByMangaId = Injekt.get(),
private val setReadStatus: SetReadStatus = Injekt.get()
// SY <--
) : ViewModel() {
@@ -691,6 +693,16 @@ class ReaderViewModel @JvmOverloads constructor(
// SY <--
readerChapter.chapter.read = true
// 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) {
viewModelScope.launchNonCancellable {
val chapterUpdates = chapterList
@@ -4,9 +4,9 @@ import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import androidx.core.app.NotificationCompat
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@@ -37,7 +37,7 @@ class SaveImageNotifier(private val context: Context) {
.memoryCachePolicy(CachePolicy.DISABLED)
.size(720, 1280)
.target(
onSuccess = { showCompleteNotification(uri, it.getBitmapOrNull()) },
onSuccess = { showCompleteNotification(uri, it.asDrawable(context.resources).getBitmapOrNull()) },
onError = { onError(null) },
)
.build()
@@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.reader.chapter
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import java.text.DateFormat
import java.time.format.DateTimeFormatter
data class ReaderChapterItem(
val chapter: Chapter,
val manga: Manga,
val isCurrent: Boolean,
val dateFormat: DateFormat,
val dateFormat: DateTimeFormatter,
)
@@ -37,10 +37,6 @@ internal class ZipPageLoader(file: File) : PageLoader() {
}
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
zip4j.charset = StandardCharsets.ISO_8859_1
}
Zip4jFile(file).use { zip ->
if (zip.isEncrypted) {
if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) {
@@ -78,6 +78,8 @@ class ReaderPreferences(
fun skipDupe() = preferenceStore.getBoolean("skip_dupe", false)
fun webtoonDisableZoomOut() = preferenceStore.getBoolean("webtoon_disable_zoom_out", false)
// endregion
// region Split two page spread
@@ -160,8 +162,6 @@ class ReaderPreferences(
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 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 cacheArchiveMangaOnDisk() = preferenceStore.getBoolean("cache_archive_manga_on_disk", false)
fun markReadDupe() = preferenceStore.getBoolean("mark_read_dupe", false)
// SY <--
enum class TappingInvertMode(
@@ -18,10 +18,11 @@ import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.os.postDelayed
import androidx.core.view.isVisible
import coil.dispose
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil3.dispose
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT_QUAD
@@ -342,7 +343,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
.diskCachePolicy(CachePolicy.DISABLED)
.target(
onSuccess = { result ->
setImageDrawable(result)
setImageDrawable(result.asDrawable(context.resources))
(result as? Animatable)?.start()
isVisible = true
this@ReaderPageImageView.onImageLoaded()
@@ -29,6 +29,11 @@ class WebtoonConfig(
var imageCropBorders = false
private set
var zoomOutDisabled = false
private set
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
var sidePadding = 0
private set
@@ -42,14 +47,9 @@ class WebtoonConfig(
// SY -->
var usePageTransitions = false
var enableZoomOut = false
private set
var continuousCropBorders = false
private set
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
// SY <--
init {
readerPreferences.cropBordersWebtoon()
@@ -86,6 +86,12 @@ class WebtoonConfig(
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.webtoonDisableZoomOut()
.register(
{ zoomOutDisabled = it },
{ zoomPropertyChangedListener?.invoke(it) }
)
readerPreferences.webtoonDoubleTapZoomEnabled()
.register(
{ doubleTapZoom = it },
@@ -99,9 +105,6 @@ class WebtoonConfig(
.launchIn(scope)
// SY -->
readerPreferences.webtoonEnableZoomOut()
.register({ enableZoomOut = it }, { zoomPropertyChangedListener?.invoke(it) })
readerPreferences.cropBordersContinuousVertical()
.register({ continuousCropBorders = it }, { imagePropertyChangedListener?.invoke() })
@@ -33,13 +33,11 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
scaleDetector.isQuickScaleEnabled = value
}
// SY -->
var enableZoomOut = false
var zoomOutDisabled = false
set(value) {
field = value
recycler?.canZoomOut = value
recycler?.zoomOutDisabled = value
}
// SY <--
/**
* Recycler view added in this frame.
@@ -33,19 +33,15 @@ class WebtoonRecyclerView @JvmOverloads constructor(
private var firstVisibleItemPosition = 0
private var lastVisibleItemPosition = 0
private var currentScale = DEFAULT_RATE
// SY -->
var canZoomOut = false
var zoomOutDisabled = false
set(value) {
field = value
if (!value) {
if (value && currentScale < DEFAULT_RATE) {
zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f)
}
}
private val minRate
get() = if (canZoomOut) MIN_RATE else DEFAULT_RATE
// SY <--
get() = if (zoomOutDisabled) DEFAULT_RATE else MIN_RATE
private val listener = GestureListener()
private val detector = Detector()
@@ -179,9 +175,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
fun onScale(scaleFactor: Float) {
currentScale *= scaleFactor
currentScale = currentScale.coerceIn(
// SY -->
minRate,
// SY <--
MAX_SCALE_RATE,
)
@@ -208,8 +202,8 @@ class WebtoonRecyclerView @JvmOverloads constructor(
}
fun onScaleEnd() {
if (scaleX < /* SY --> */ minRate /* SY <-- */) {
zoom(currentScale, /* SY --> */ minRate /* SY <-- */, x, 0f, y, 0f)
if (scaleX < minRate) {
zoom(currentScale, minRate, x, 0f, y, 0f)
}
}
@@ -159,17 +159,15 @@ class WebtoonViewer(
frame.doubleTapZoom = it
}
config.zoomPropertyChangedListener = {
frame.zoomOutDisabled = it
}
config.navigationModeChangedListener = {
val showOnStart = config.navigationOverlayOnStart || config.forceNavigationOverlay
activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart)
}
// SY -->
config.zoomPropertyChangedListener = {
frame.enableZoomOut = it
}
// SY <--
frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
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.library.LibraryUpdateJob
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.EXH_SOURCE_ID
import kotlinx.collections.immutable.PersistentList
@@ -49,7 +49,6 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime
import java.util.Date
class UpdatesScreenModel(
private val sourceManager: SourceManager = Injekt.get(),
@@ -386,12 +385,10 @@ class UpdatesScreenModel(
return items
.map { UpdatesUiModel.Item(it) }
.insertSeparators { before, after ->
val beforeDate = before?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
val beforeDate = before?.item?.update?.dateFetch?.toLocalDate()
val afterDate = after?.item?.update?.dateFetch?.toLocalDate()
when {
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
UpdatesUiModel.Header(afterDate)
}
beforeDate != afterDate && afterDate != null -> UpdatesUiModel.Header(afterDate)
// Return null to avoid adding a separator between two items.
else -> null
}
@@ -1,8 +1,10 @@
package eu.kanade.tachiyomi.ui.webview
import android.app.Activity
import android.app.assist.AssistContent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.core.net.toUri
@@ -35,7 +37,16 @@ class WebViewActivity : BaseActivity() {
}
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)
if (!WebViewUtil.supportsWebView(this)) {
@@ -77,7 +88,16 @@ class WebViewActivity : BaseActivity() {
override fun 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) {
@@ -6,15 +6,18 @@ import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR
import java.text.DateFormat
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.time.temporal.ChronoUnit
import java.util.Calendar
import java.util.Date
import kotlin.math.absoluteValue
fun Date.toDateTimestampString(dateFormatter: DateFormat): String {
val date = dateFormatter.format(this)
val time = DateFormat.getTimeInstance(DateFormat.SHORT).format(this)
fun LocalDateTime.toDateTimestampString(dateTimeFormatter: DateTimeFormatter): String {
val date = dateTimeFormatter.format(this)
val time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(this)
return "$date $time"
}
@@ -32,51 +35,35 @@ fun Long.convertEpochMillisZone(
.toEpochMilli()
}
/**
* Get date as time key
*
* @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 Long.toLocalDate(): LocalDate {
return LocalDate.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault())
}
fun Date.toRelativeString(
fun LocalDate.toRelativeString(
context: Context,
relative: Boolean = true,
dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT),
dateFormat: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT),
): String {
if (!relative) {
return dateFormat.format(this)
}
val now = Date()
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY)
val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt()
val now = LocalDate.now()
val difference = ChronoUnit.DAYS.between(this, now)
return when {
difference < 0 -> dateFormat.format(this)
difference < MILLISECONDS_IN_DAY -> context.stringResource(MR.strings.relative_time_today)
difference < MILLISECONDS_IN_DAY.times(7) -> context.pluralStringResource(
MR.plurals.relative_time,
days,
days,
difference < -7 -> dateFormat.format(this)
difference < 0 -> context.pluralStringResource(
MR.plurals.upcoming_relative_time,
difference.toInt().absoluteValue,
difference.toInt().absoluteValue,
)
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)
}
}
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.Drawable
import androidx.core.graphics.drawable.toBitmap
import coil.drawable.ScaleDrawable
import coil3.gif.ScaleDrawable
fun Drawable.getBitmapOrNull(): Bitmap? = when (this) {
is BitmapDrawable -> bitmap
@@ -57,7 +57,7 @@ class FollowsHandler(
it,
lang,
) to MangaDexSearchMetadata().apply {
followStatus = FollowStatus.fromDex(statuses[it.id]).int.toInt()
followStatus = FollowStatus.fromDex(statuses[it.id]).long.toInt()
}
}.sortedWith(comparator)
}
@@ -155,7 +155,7 @@ class FollowsHandler(
val (followStatus, rating) = followStatusDef.await() to ratingDef.await()
Track.create(TrackerManager.MDLIST).apply {
title = ""
status = followStatus.int
status = followStatus.long
tracking_url = url
score = rating?.rating?.toDouble() ?: 0.0
}
@@ -2,14 +2,14 @@ package exh.md.utils
import java.util.Locale
enum class FollowStatus(val int: Long) {
UNFOLLOWED(0),
READING(1),
COMPLETED(2),
ON_HOLD(3),
PLAN_TO_READ(4),
DROPPED(5),
RE_READING(6),
enum class FollowStatus(val long: Long) {
UNFOLLOWED(0L),
READING(1L),
COMPLETED(2L),
ON_HOLD(3L),
PLAN_TO_READ(4L),
DROPPED(5L),
RE_READING(6L),
;
fun toDex(): String = this.name.lowercase(Locale.US)
@@ -18,6 +18,6 @@ enum class FollowStatus(val int: Long) {
fun fromDex(
value: String?,
): 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
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -46,7 +48,16 @@ class InterceptActivity : BaseActivity() {
private val status: MutableStateFlow<InterceptResult> = MutableStateFlow(InterceptResult.Idle)
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)
setComposeContent {
@@ -142,7 +153,16 @@ class InterceptActivity : BaseActivity() {
override fun 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()
@@ -1,8 +1,10 @@
package exh.ui.login
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.webkit.CookieManager
import android.webkit.WebView
@@ -32,7 +34,16 @@ class EhLoginActivity : BaseActivity() {
private val preferenceManager: UnsortedPreferences by injectLazy()
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)
if (!WebViewUtil.supportsWebView(this)) {
@@ -162,7 +173,16 @@ class EhLoginActivity : BaseActivity() {
override fun 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 {
@@ -18,7 +18,9 @@ import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import java.util.Date
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
@Composable
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.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(
SYMR.plurals.num_pages,
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<changelogtext>Hotfix for 1.10.3</changelogtext>
</changelogversion>
+5 -2
View File
@@ -6,14 +6,17 @@ naming:
constantPattern: '[A-Z][A-Za-z0-9]*'
complexity:
LongMethod:
ignoreAnnotated: [ 'Composable' ]
LongParameterList:
functionThreshold: 6
constructorThreshold: 7
ignoreDefaultParameters: true
ignoreAnnotated: [ 'Composable' ]
style:
MagicNumber:
ignorePropertyDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ReturnCount:
excludeGuardClauses: true
UnusedPrivateMember:
ignoreAnnotated: [ 'Preview' ]
@@ -27,6 +27,7 @@ fun SManga.getComicInfo() = ComicInfo(
coverArtist = null,
tags = null,
categories = null,
source = null,
padding = null,
)
@@ -82,6 +83,7 @@ data class ComicInfo(
val web: Web?,
val publishingStatus: PublishingStatusTachiyomi?,
val categories: CategoriesTachiyomi?,
val source: SourceMihon?,
// SY -->
val padding: PaddingTachiyomiSY?,
// SY <--
@@ -159,6 +161,10 @@ data class ComicInfo(
@XmlSerialName("Categories", "http://www.w3.org/2001/XMLSchema", "ty")
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 -->
@Serializable
@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.KeyProperties
import android.util.Base64
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -11,9 +13,14 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority
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.enums.AesKeyStrength
import net.lingala.zip4j.model.enums.EncryptionMethod
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayInputStream
@@ -221,6 +228,133 @@ object CbzCrypto {
}
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
+7 -23
View File
@@ -1,26 +1,10 @@
# Project-wide Gradle settings.
# 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
android.nonTransitiveRClass=false
android.useAndroidX=true
kotlin.code.style=official
kotlin.mpp.androidSourceSetLayoutVersion=2
android.useAndroidX=true
android.nonTransitiveRClass=false
org.gradle.caching=true
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]
agp_version = "8.2.2"
lifecycle_version = "2.6.2"
lifecycle_version = "2.7.0"
paging_version = "3.2.1"
[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-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.2"
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha02"
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha02"
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-beta01"
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.3"
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha03"
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha03"
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0"
[bundles]
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
+3 -3
View File
@@ -1,7 +1,7 @@
[versions]
compiler = "1.5.8"
compose-bom = "2024.01.00-alpha03"
accompanist = "0.34.0"
compiler = "1.5.10"
compose-bom = "2024.02.00-alpha02"
accompanist = "0.35.0-alpha"
[libraries]
activity = "androidx.activity:activity-compose:1.8.2"
+2 -2
View File
@@ -1,6 +1,6 @@
[versions]
kotlin_version = "1.9.22"
serialization_version = "1.6.2"
serialization_version = "1.6.3"
xml_serialization_version = "0.86.3"
[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" }
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-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
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"
sqlite = "2.4.0"
voyager = "1.0.0"
detekt = "1.23.1"
detekt = "1.23.5"
detektCompose = "0.3.11"
[libraries]
desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
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"
@@ -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-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", 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"
@@ -38,17 +38,18 @@ zip4j = "net.lingala.zip4j:zip4j:2.11.5"
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", 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"
preferencektx = "androidx.preference:preference-ktx:1.2.1"
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
coil-bom = { module = "io.coil-kt:coil-bom", version = "2.5.0" }
coil-core = { module = "io.coil-kt:coil" }
coil-gif = { module = "io.coil-kt:coil-gif" }
coil-compose = { module = "io.coil-kt:coil-compose" }
coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.0.0-alpha06" }
coil-core = { module = "io.coil-kt.coil3:coil" }
coil-gif = { module = "io.coil-kt.coil3:coil-gif" }
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"
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"
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
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-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-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-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-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"
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"]
js-engine = ["quickjs-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"]
sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"]
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"]
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -71,4 +71,9 @@
<item quantity="one">%1$d second ago</item>
<item quantity="other">%1$d seconds ago</item>
</plurals>
<plurals name="row_count">
<item quantity="one">%d row</item>
<item quantity="other">%d rows</item>
</plurals>
</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_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="pref_previews_row_count">Previews row count</string>
<!-- Appearance Settings -->
<string name="pref_category_navbar">Navbar</string>
@@ -176,6 +176,7 @@
<string name="library_group_updates_all">Постоянно запускать обновления категорий</string>
<!-- Browse settings -->
<string name="pref_hide_feed">Скрыть вкладку «Лента»</string>
<string name="pref_feed_position">Позиция вкладки (Лента)</string>
<string name="pref_feed_position_summery">Поместить вкладку «Лента» на место первой вкладки в «Поисковик»? Это сделает её вкладкой по умолчанию при открытии «Поисковик». Не рекомендуется, если вы используете ограниченные сети(Моб. данные)</string>
<string name="pref_source_source_filtering">Фильтровать источники по категориям</string>
@@ -196,6 +197,8 @@
<string name="biometric_lock_time_conflicts">Биометрическое время блокировки конфликтует с тем, которое уже существует!</string>
<string name="biometric_lock_start_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_summary">Выбрать дни биометрической блокировки приложения</string>
<string name="sunday">Воскресенье</string>
@@ -331,6 +334,7 @@
<string name="description_hint">Описание: %1$s</string>
<string name="author_hint">Автор: %1$s</string>
<string name="artist_hint">Художник: %1$s</string>
<string name="thumbnail_url_hint">URL-адрес миниатюры: %1$s</string>
<!-- Browse -->
<!-- Sources Tab -->
@@ -374,13 +378,10 @@
<string name="error_tag_exists">Этот тэг уже существует!</string>
<string name="delete_tag">Удалить тэг</string>
<string name="delete_tag_confirmation">Хотите ли вы удалить тэг %s?</string>
<string name="delete_time_range">Удалить время блокировки</string>
<string name="delete_time_range_confirmation">Хотите ли вы удалить время блокировки %s?</string>
<!-- Extension section -->
<string name="ext_redundant">Избыточное</string>
<string name="redundant_extension_message">Это расширение является избыточным и не будет использоваться внутри этой версии Tachiyomi.</string>
<string name="no_repos_found">Репозитории расширений не найдены. Пожалуйста, добавьте парочку, перейдя по "Больше → Репозитории расширений"!</string>
<!-- Migration -->
<string name="select_sources">Выберите источники</string>
@@ -9,10 +9,10 @@
<string name="action_start_reading">開始閱讀</string>
<string name="action_edit_info">編輯資訊</string>
<!-- Entry Type -->
<string name="entry_type_manga">日本漫畫</string>
<string name="entry_type_manhwa">韓國漫畫</string>
<string name="entry_type_manhua">中國漫畫</string>
<string name="entry_type_comic">美國漫畫</string>
<string name="entry_type_manga">漫畫(日本)</string>
<string name="entry_type_manhwa">漫畫(韓國)</string>
<string name="entry_type_manhua">漫畫(中國)</string>
<string name="entry_type_comic">漫畫(美國)</string>
<string name="entry_type_webtoon">條漫</string>
<!-- Preferences -->
@@ -173,6 +173,7 @@
<string name="library_group_updates_all">每次啟動後載入目錄更新</string>
<!-- Browse settings -->
<string name="pref_hide_feed">隱藏訂閱標籤</string>
<string name="pref_feed_position">訂閱標籤位置</string>
<string name="pref_feed_position_summery">你想讓訂閱標籤成為瀏覽中的第一個標籤嗎?這將使它成為開啟瀏覽時的預設標籤,如果你使用的是資料或計費網路,則不建議使用。</string>
<string name="pref_source_source_filtering">在目錄中過濾來源</string>
@@ -193,6 +194,8 @@
<string name="biometric_lock_time_conflicts">鎖定時間與已經存在的時間衝突!</string>
<string name="biometric_lock_start_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_summary">需要鎖定應用程式的週期</string>
@@ -328,7 +331,8 @@
<string name="title_hint">標題:%1$s</string>
<string name="description_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 -->
<!-- Sources Tab -->
@@ -10,6 +10,11 @@
<item quantity="other">%1$d days ago</item>
</plurals>
<plurals name="upcoming_relative_time">
<item quantity="one">Tomorrow</item>
<item quantity="other">In %1$d days</item>
</plurals>
<plurals name="num_categories">
<item quantity="one">%d category</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_filtered_chapters">Skip filtered 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_read_with_volume_keys">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_low">Low</string>
<string name="pref_lowest">Lowest</string>
<string name="pref_webtoon_disable_zoom_out">Disable zoom out</string>
<!-- Downloads section -->
<string name="pref_category_delete_chapters">Delete chapters</string>
+14 -10
View File
@@ -5,28 +5,28 @@
<item quantity="other">%d пухмӑш</item>
</plurals>
<plurals name="lock_after_mins">
<item quantity="one">1 минут хыҫҫӑн</item>
<item quantity="one">%1$s минут хыҫҫӑн</item>
<item quantity="other">%1$s минут хыҫҫӑн</item>
</plurals>
<plurals name="restore_completed_message">
<item quantity="one">%1$s,%2$s йӑнӑшпа тӑвӑннӑ</item>
<item quantity="other">%1$s, %2$s йӑнӑшпа тӑвӑннӑ</item>
<item quantity="one">%1$s хушши %2$s йӑнӑшпа тӑвӑннӑ</item>
<item quantity="other">%1$s хушши %2$s йӑнӑшпа тӑвӑннӑ</item>
</plurals>
<plurals name="update_check_notification_ext_updates">
<item quantity="one">Хушма валли ҫӗнетӳ пур</item>
<item quantity="other">%d хушма валли ҫӗнетӳ пур</item>
</plurals>
<plurals name="notification_chapters_multiple_and_more">
<item quantity="one">%1$s сыпӑкӗсем</item>
<item quantity="other">%1$s сыпӑкӗсем тата ытти %2$d</item>
<item quantity="one">%1$s сыпӑкӗсем тата тепӗр 1</item>
<item quantity="other">%1$s сыпӑкӗсем тата тепӗр %2$d</item>
</plurals>
<plurals name="notification_chapters_generic">
<item quantity="one">1 ҫӗнӗ сыпӑк</item>
<item quantity="one">%1$d ҫӗнӗ сыпӑк</item>
<item quantity="other">%1$d ҫӗнӗ сыпӑк</item>
</plurals>
<plurals name="notification_new_chapters_summary">
<item quantity="one">Ҫӗнӗ сыпӑксем 1 хайлав валли тупӑннӑ</item>
<item quantity="other">Ҫӗнӗ сыпӑксем %d хайлав валли тупӑннӑ</item>
<item quantity="one">%d хайлав валли</item>
<item quantity="other">%d хайлав валли</item>
</plurals>
<plurals name="manga_num_chapters">
<item quantity="one">%1$s сыпӑк</item>
@@ -37,8 +37,8 @@
<item quantity="other">%1$s йулчӗ</item>
</plurals>
<plurals name="num_trackers">
<item quantity="one">1 сӑнану</item>
<item quantity="other">%d сӑнану</item>
<item quantity="one">%d йӗрлев</item>
<item quantity="other">%d йӗрлев</item>
</plurals>
<plurals name="missing_chapters_warning">
<item quantity="one">%d сыпӑк ҫук</item>
@@ -64,4 +64,8 @@
<item quantity="one">Тепӗр сыпӑк</item>
<item quantity="other">Тепӗр %d сыпӑк</item>
</plurals>
<plurals name="num_repos">
<item quantity="one">%d усрав</item>
<item quantity="other">%d усрав</item>
</plurals>
</resources>
+55 -24
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="manga">Вулавӑшри серилӗхсем</string>
<string name="manga">Вулавӑшри хайлавсем</string>
<string name="used_cache">Усӑ курнӑ: %1$s</string>
<string name="cookies_cleared">Куккисем катертнӗ</string>
<string name="pref_clear_cookies">Кукки тасат</string>
@@ -49,8 +49,8 @@
<string name="secure_screen">Ыкран сыхлавӗ</string>
<string name="pref_category_security">Сыхлав тата вӑрттӑнлӑх</string>
<string name="pref_manage_notifications">Систерӳсене ӗнер</string>
<string name="pref_date_format">Ҫул-кун хармачӗ</string>
<string name="theme_dark">Ҫутнӑ</string>
<string name="pref_date_format">Вӑхӑт хармачӗ</string>
<string name="theme_dark">Тӗттӗм</string>
<string name="theme_light">Сӳнтернӗ</string>
<string name="pref_category_advanced">Тата</string>
<string name="pref_category_downloads">Тийевсем</string>
@@ -72,8 +72,8 @@
<string name="action_cancel">Пӑрахӑҫла</string>
<string name="action_pin">Ҫаклат</string>
<string name="action_disable">Сӳнтер</string>
<string name="action_display_download_badge">Тийене сыпӑксем шучӗ</string>
<string name="action_display_list">Йат-йыш</string>
<string name="action_display_download_badge">Тийенӗ сыпӑксен шучӗ</string>
<string name="action_display_list">Йат йышӗ</string>
<string name="action_display">Кӑтарт</string>
<string name="action_display_mode">Кӑтарту тытӑмӗ</string>
<string name="action_open_in_web_view">WebView-ра уҫ</string>
@@ -83,12 +83,12 @@
<string name="action_remove">Катерт</string>
<string name="action_retry">Ҫӗнӗрен</string>
<string name="action_pause">Чар</string>
<string name="action_view_chapters">Сыпӑксем пӑх</string>
<string name="action_edit_cover">Хуплашка улӑштар</string>
<string name="action_move_category">Пухмӑша хуш</string>
<string name="action_view_chapters">Сыпӑксене пӑх</string>
<string name="action_edit_cover">Хуплашкана улӑштар</string>
<string name="action_move_category">Пухмӑша кӗрт</string>
<string name="action_rename_category">Пухмӑш йатне улӑштар</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_mark_previous_as_read">Умӗнхине вуланӑ пек паллӑ ту</string>
<string name="action_edit">Улӑштар</string>
@@ -198,7 +198,7 @@
<string name="untrusted_extension">Шанчӑклӑ мар хушма</string>
<string name="lock_when_idle">Ним туман чух ҫаклатни</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_always_show_chapter_transition">Сыпӑксем урлӑ каҫнине яланах кӑтарт</string>
<string name="color_filter_a_value">Тӑрӑлӑх мар</string>
@@ -400,7 +400,7 @@
<string name="pref_create_backup">Янтӑв ту</string>
<string name="updated_version">v%1$s верссиччен ҫӗнетнӗ</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="download_insufficient_space">Тиск ҫинче вырӑн ҫитмен пирки сыпӑксем тийенеймерӗҫ</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="file_picker_error">Файлсене суйламалли хушӑм тупӑнман</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_started_reading_date">Вулама пуҫланӑ вӑхӑчӗ</string>
<string name="edge_nav">Хӗрри</string>
@@ -477,31 +477,31 @@
<string name="action_show_errors">Йӑнӑшсене кӑтарт</string>
<string name="cancel_all_for_series">Ҫак серилӗх валли пурне те пӑрахӑҫла</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_count">Ҫырав шучӗпе</string>
<string name="action_remove_everything">Пурне те катерт</string>
<string name="delete_category_confirmation">«%s» пухмӑш катертесшӗнех-и\?</string>
<string name="delete_category">Пухмӑш катерт</string>
<string name="delete_category_confirmation">«%s» пухмӑша катертесшӗнех-и?</string>
<string name="delete_category">Пухмӑша катерт</string>
<string name="action_display_local_badge">Вырӑнти ҫӑл куҫран</string>
<string name="label_warning">Асӑрхаттару</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="action_show_manga">Ҫырава кӑтарт</string>
<string name="action_show_manga">Хайлава кӑтарт</string>
<string name="action_display_language_badge">Чӗлхе</string>
<string name="action_search_hint">Шыра…</string>
<string name="action_close">Хуп</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="on">Ҫутнӑ</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_reader_summary">Вулав тытӑмӗ, кӑтартӑнни, куҫӑм</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_midnightdusk">Ҫур ҫӗр ӗнтрӗкӗ</string>
<string name="pref_tracking_summary">Пӗр йенлӗ ӳсӗм килӗштерӗвӗ, анлӑлатнӑ килӗштерӳ</string>
@@ -524,7 +524,7 @@
<string name="network_not_metered">Чараксӑр тетел урлӑ ҫеҫ</string>
<string name="pref_update_only_started">Серилӗхе пуҫламан</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="action_not_now">Халь мар</string>
<string name="update_72hour">Кашни 3 кун</string>
@@ -533,8 +533,8 @@
<string name="label_stats">Шутлавсем</string>
<string name="action_copy_to_clipboard">Пайлашу асне ӑт</string>
<string name="pref_backup_summary">Хӑй тӗллӗн тата хӑй-хальлӗн йантӑлав</string>
<string name="action_update_category">Пухмӑш ҫӗнет</string>
<string name="pref_advanced_summary">Йӑнӑш кӗнекине тийесе йани, петтерей лайӑхлатни</string>
<string name="action_update_category">Пухмӑша ҫӗнет</string>
<string name="pref_advanced_summary">Йӑнӑш кӗнекине тийесе йани, петтерейе лайӑхлатни</string>
<string name="pref_library_update_show_tab_badge">Вуламан сыпӑксен шутне «Ҫӗнӗлӗх» ыккун ҫинче кӑтартни</string>
<string name="pref_landscape_zoom">Сӑна тӑрӑх пысӑклатни</string>
<string name="multi_lang">Нумай чӗлхеллӗ</string>
@@ -568,6 +568,37 @@
<string name="ext_installer_shizuku_unavailable_dialog">Shizuku-н хушма ларткӑча усӑ курма Shizuku ларт тата ҫут.</string>
<string name="action_ok">Йурӗ</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="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>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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>
</plurals>
<plurals name="num_categories">
@@ -17,7 +17,7 @@
<item quantity="other">Ĉapitroj %1$s kaj %2$d pli</item>
</plurals>
<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>
</plurals>
<plurals name="notification_new_chapters_summary">
@@ -25,8 +25,8 @@
<item quantity="other">Por %d titoloj</item>
</plurals>
<plurals name="missing_chapters_warning">
<item quantity="one">Mankas 1 ĉapitron</item>
<item quantity="other">Mankas %d ĉapitrojn</item>
<item quantity="one">Preterpasas %d ĉapitron, aŭ ĝi mankas ĉe la fonto aŭ ĝi estis elfiltrita</item>
<item quantity="other">Preterpasas %d ĉapitrojn, aŭ ili mankas ĉe la fonto aŭ ili estis elfiltritaj</item>
</plurals>
<plurals name="num_trackers">
<item quantity="one">1 ŝanĝspurilo</item>
@@ -60,4 +60,12 @@
<item quantity="one">Sekva nelegita ĉapitro</item>
<item quantity="other">Sekvaj %d nelegitaj ĉapitroj</item>
</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>
@@ -481,4 +481,28 @@
<string name="action_remove_everything">Forigi ĉiojn</string>
<string name="onboarding_heading">Bonvenon!</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>
@@ -506,7 +506,7 @@
<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="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="tracking_guide">Guía de seguimiento</string>
<string name="automatic_background">Automático</string>
@@ -669,7 +669,7 @@
<string name="hour_short">%dh</string>
<string name="minute_short">%dm</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_downloaded">Descargados</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="copied_to_clipboard_plain">Se ha copiado al portapapeles</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.
\n
\n¿Seguro que quieres continuar\?</string>
@@ -742,7 +742,7 @@
<string name="pref_storage_usage">Almacenamiento utilizado</string>
<string name="action_sort_tracker_score">Puntuación del rastreador</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="no_scanlators_found">No se ha encontrado ningún 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="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="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="error_repo_exists">El repositorio ya existe</string>
<string name="pref_library_update_smart_update">Actualizaciones inteligentes</string>
@@ -62,8 +62,8 @@
</plurals>
<plurals name="next_unread_chapters">
<item quantity="one">Chapitre suivant non lu</item>
<item quantity="many">Les %d suivants non lus</item>
<item quantity="other">Les %d suivants non lus</item>
<item quantity="many">Les %d chapitres suivants non lus</item>
<item quantity="other">Les %d chapitres suivants non lus</item>
</plurals>
<plurals name="download_amount">
<item quantity="one">Chapitre suivant</item>
@@ -5,15 +5,15 @@
<item quantity="other">%d bővítményfrissítés érhető el</item>
</plurals>
<plurals name="lock_after_mins">
<item quantity="one">1 perc után</item>
<item quantity="other">%1$s percek után</item>
<item quantity="one">%1$s perc múlva</item>
<item quantity="other">%1$s perc múlva</item>
</plurals>
<plurals name="num_categories">
<item quantity="one">%d kategória</item>
<item quantity="other">%d kategóriák</item>
<item quantity="other">%d kategória</item>
</plurals>
<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>
</plurals>
<plurals name="notification_chapters_generic">
@@ -26,7 +26,7 @@
</plurals>
<plurals name="num_trackers">
<item quantity="one">%d tracker</item>
<item quantity="other">%d trackerek</item>
<item quantity="other">%d tracker</item>
</plurals>
<plurals name="notification_new_chapters_summary">
<item quantity="one">%d-nak/nek</item>
@@ -54,11 +54,11 @@
</plurals>
<plurals name="download_amount">
<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 name="missing_chapters">
<item quantity="one">Hiányzó %1$s fejezet</item>
<item quantity="other">Hiányzó %1$s fejezetek</item>
<item quantity="one">%1$s fejezet hiányzik</item>
<item quantity="other">%1$s fejezet hiányzik</item>
</plurals>
<plurals name="day">
<item quantity="one">1 nap</item>

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