Compare commits

..

96 Commits

Author SHA1 Message Date
AntsyLich
412815af06 Revert "Fix reader tap zones triggering after scrolling was stopped by the user" (#2670) 2025-11-07 13:07:14 +00:00
Weblate (bot)
f7fb68692a Translations update from Hosted Weblate (#2656)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/cs/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cs/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/jv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Doministo <doministo@seznam.cz>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Co-authored-by: TheKingTermux <50316075+TheKingTermux@users.noreply.github.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: amigo browser <juniperforest1@proton.me>
2025-11-07 19:03:52 +06:00
AntsyLich
aa300cb53e Fix extra padding appearing in reader after user interactions (#2669) 2025-11-07 10:00:59 +00:00
Trevor Paley
855eea2ada Improve WebView multi-window UX (#2662)
- Navigation history for lower windows is preserved when a popup is opened
- Back gesture will close a popup window rather than the entire WebView activity when there is no previous page
- The leftmost close button closes the entire activity as before
- When a popup window is shown, a new button appears to close just that window
2025-11-07 15:26:04 +06:00
Mend Renovate
f4703ed83a Update dependency androidx.core:core-splashscreen to v1.2.0 (#2661) 2025-11-07 15:22:41 +06:00
NGB-Was-Taken
506d51a007 Fix flaky migration tests (#2663) 2025-11-07 15:21:17 +06:00
NGB-Was-Taken
9f9155121c Upload test report as artifact on failure (#2664) 2025-11-07 15:20:32 +06:00
AntsyLich
282110ef21 Release v0.19.3 2025-11-04 13:05:40 +06:00
AntsyLich
ace387f8bf Revert "Update dependency androidx.compose:compose-bom to v2025.10.01 (#2522)"
This reverts commit e8bdf58530.
2025-11-04 13:05:23 +06:00
Weblate (bot)
5e428071c9 Translations update from Hosted Weblate (#2646)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/nl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/jv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ms/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Co-authored-by: Siebrenvde <siebren@siebrenvde.dev>
2025-11-04 13:04:46 +06:00
AntsyLich
0acd80dd95 Fix long strip reader not scrolling on consecutive taps (#2650) 2025-11-04 06:46:48 +00:00
bapeey
bdb0ce4779 Fix WebView crash introduced in v0.19.2 (#2649) 2025-11-04 11:43:40 +06:00
Mend Renovate
e8bdf58530 Update dependency androidx.compose:compose-bom to v2025.10.01 (#2522) 2025-11-03 10:52:45 +06:00
Weblate (bot)
e36b4ce60b Translations update from Hosted Weblate (#2639)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/
Translation: Mihon/Mihon

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
2025-11-03 10:38:47 +06:00
AntsyLich
6d543024a3 Migrated to the Android specific about libraries gradle plugin 2025-11-02 22:59:32 +06:00
AntsyLich
0e0b6d9283 Handle reader cutout setting with Insets to support Android 15+ (#2640) 2025-11-02 16:28:25 +00:00
AntsyLich
38b1bd7383 Release v0.19.2 2025-11-02 19:46:31 +06:00
Weblate (bot)
8609553896 Translations update from Hosted Weblate (#2373)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/jv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ko/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ms/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/pt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/el/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/jv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ko/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ms/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ro/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/th/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Efe Akın <efeakin1122@gmail.com>
Co-authored-by: Evan Jones (原文轩) <evanjones1883@gmail.com>
Co-authored-by: Farith <mail2@farithadnan.net>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Itsmechinmoy <167056923+itsmechinmoy@users.noreply.github.com>
Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Co-authored-by: Jakub Fabijan <jakubfabijan@tuta.io>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Madddog1997 <madddog1997@gmail.com>
Co-authored-by: Manjul Tamrakar <manjultamrakar4@gmail.com>
Co-authored-by: Manuela Silva <mmsrs@sky.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mohamed kh <mohamedkhamekhami@gmail.com>
Co-authored-by: MuhamadSyabitHidayattulloh <tebepc@gmail.com>
Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Co-authored-by: Omgeta <anooptiger@hotmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Saft Octavian <saftoctavian@gmail.com>
Co-authored-by: Siebrenvde <siebren@siebrenvde.dev>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: TheKingTermux <50316075+TheKingTermux@users.noreply.github.com>
Co-authored-by: Throw Away <throwawayacc4gulshan@gmail.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: keegang 6705 <darunphobwi@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Co-authored-by: 안세훈 <on9686@gmail.com>
2025-11-02 19:46:06 +06:00
AntsyLich
5f0c460668 Make reader edge-to-edge (#1908) 2025-11-02 13:41:33 +00:00
Naputt1
ac28b6c80c Fix reader tap zones triggering after scrolling was stopped by the user (#2518)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-11-02 07:39:42 +00:00
Constantin Piber
cc28776735 Update Suwayomi tracker to use GraphQL API instead of REST API (#2585) 2025-11-02 06:26:48 +00:00
Trevor Paley
6ab87c7931 Added proper multi window support in WebView instead of treating everything as a redirect (#2584) 2025-11-02 06:23:01 +00:00
Kashish Aggarwal
8662f80fbf Fix date picker not allowing the same start and finish date in negative time zones (#2617)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-11-01 15:38:33 +00:00
AntsyLich
6508766ccd Fix CHANGELOG.md [skip ci] 2025-11-01 20:36:41 +06:00
anirudhn
09ec9fc8c5 Fix scrollbar not showing when animator duration scale animation is turned off (#2398) 2025-11-01 14:34:07 +00:00
c2y5
87c6f34a55 Fix extension download stuck at pending state in some cases (#2483)
Also auto update extension list whenever a repository is added or removed

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-11-01 14:09:22 +00:00
AntsyLich
643762f913 Add option to customize concurrent downloads, increase page concurrency (#2637) 2025-11-01 20:07:30 +06:00
Mend Renovate
7e880014b0 Update markdown to v0.38.1 (#2636) 2025-11-01 12:48:18 +00:00
AntsyLich
f36c259c1f Add subtitle support to slider preference and general cleanup (#2635) 2025-11-01 12:47:06 +00:00
AntsyLich
aef3beb15f Fix reader "Unable to edit key" error (#2634) 2025-11-01 09:03:51 +00:00
NGB-Was-Taken
e9469451ac Update shizuku.version to v13.1.5 (#2566)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-11-01 08:08:10 +00:00
AntsyLich
f9793d3323 Bump app version code and default user agent 2025-11-01 01:47:20 +06:00
AntsyLich
93ba6acea5 Fix migration "Attempt to invoke virtual method" crash (#2632) 2025-10-31 19:42:42 +00:00
AntsyLich
5e7fecc2c1 Fix migration dialog migrating to wrong entry (#2631) 2025-10-31 19:21:29 +00:00
Mend Renovate
343074da5f Update dependency org.junit.jupiter:junit-jupiter to v6.0.1 (#2630) 2025-11-01 01:06:55 +06:00
AntsyLich
7c08b75555 Fix mass migration advanced search query building (#2629) 2025-10-31 18:50:19 +00:00
Constantin Piber
cbf72f4c60 Migrate Kitsu to use library_id and remote_id properly (#2609) 2025-10-31 23:52:44 +06:00
Mend Renovate
0b6de39f2f Update okhttp monorepo to v5.3.0 (#2628) 2025-10-31 17:19:26 +00:00
Mend Renovate
72c4d1fdee Update plugin google-services to v4.4.4 (#2573) 2025-10-31 17:17:09 +00:00
Mend Renovate
fa96366b55 Update GitHub Actions (major) (#2627) 2025-10-31 17:10:37 +00:00
Mend Renovate
3ff25bc984 Update dependency androidx.work:work-runtime to v2.11.0 (#2626) 2025-10-31 17:07:58 +00:00
Mend Renovate
e9224bc2ba Update dependency com.squareup.okio:okio to v3.16.2 (#2576) 2025-10-31 17:07:38 +00:00
Mend Renovate
3c731c2cf5 Update GitHub Actions (#2581) 2025-10-31 17:06:58 +00:00
Mend Renovate
5ac58d01b8 Update dependency com.google.firebase:firebase-bom to v34.5.0 (#2575) 2025-10-31 23:06:40 +06:00
Mend Renovate
eefaf028ce Update xml.serialization.version to v0.91.3 (#2625) 2025-10-31 17:01:34 +00:00
Mend Renovate
582ccca1ab Update kotlin monorepo to v2.2.21 (#2624) 2025-10-31 16:59:21 +00:00
Mend Renovate
8f972115a8 Update dependency io.kotest:kotest-assertions-core to v6.0.4 (#2594) 2025-10-31 22:45:16 +06:00
Mend Renovate
6f6c033811 Update aboutlib.version to v13 (major) (#2580)
Update aboutlib.version to v13
2025-10-31 22:45:02 +06:00
Mend Renovate
57a0ab6711 Update okhttp monorepo to v5.2.1 (#2577) 2025-10-10 20:05:51 +06:00
Radon Rosborough
58b25d697f Improve handling of downloads for chapters with same metadata and optionally for OSes that don't support Unicode in filename (#2305)
Co-authored-by: jkim <jhskim@hotmail.com>
Co-authored-by: fatotak <111342761+fatotak@users.noreply.github.com>
Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-10-08 05:07:09 +06:00
Mend Renovate
1a31c7c7ee Update okhttp monorepo to v5.2.0 (#2564) 2025-10-07 18:51:34 +00:00
Mend Renovate
ad6b651b37 Update softprops/action-gh-release action to v2.4.0 (#2562) 2025-10-08 00:49:25 +06:00
NGB-Was-Taken
96e5131358 Fix disabling incognito mode from notification (#2512)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-10-05 16:03:45 +00:00
Mend Renovate
1d5bc8d2c2 Update dependency com.google.firebase:firebase-bom to v34.3.0 (#2508) 2025-10-05 14:47:15 +06:00
Mend Renovate
6cee911239 Update gradle/actions action to v5 (#2554) 2025-10-05 14:47:01 +06:00
Mend Renovate
96347e3f76 Update GitHub Actions (#2552) 2025-10-05 07:58:31 +00:00
Mend Renovate
9a45d248b1 Update dependency org.junit.jupiter:junit-jupiter to v6 (#2553) 2025-10-05 07:57:34 +00:00
Mend Renovate
04168ecec8 Update moko to v0.25.1 (#2550) 2025-10-05 07:41:46 +00:00
Mend Renovate
607f0ea9cd Update dependency io.mockk:mockk to v1.14.6 (#2549) 2025-10-05 07:40:48 +00:00
Secozzi
27a4f6f45c Update markdown to 0.37.0 (#2516) 2025-10-05 13:36:13 +06:00
Mend Renovate
5236d003d2 Update kotlin monorepo to v2.2.20 (#2498) 2025-10-05 13:29:27 +06:00
Mend Renovate
d61a41e819 Update dependency androidx.work:work-runtime to v2.10.5 (#2523) 2025-10-05 13:28:49 +06:00
Mend Renovate
5637860dd2 Update sqlite to v2.6.1 (#2525) 2025-10-05 13:28:37 +06:00
Mend Renovate
d4d18d0898 Update dependency com.diffplug.spotless:spotless-plugin-gradle to v8 (#2526) 2025-10-05 13:28:09 +06:00
Guzmazow
065147472e Improve spoofing of X-Requested-With header to support newer WebView versions (#2491) 2025-09-19 23:35:23 +06:00
Constantin Piber
6f635782c2 Delegate Suwayomi tracker authentication to extension (#2476)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-09-18 21:19:09 +00:00
Mend Renovate
86d85f74c0 Update lifecycle.version to v2.9.4 (#2503)
Update dependency androidx.lifecycle:lifecycle-process to v2.9.4
2025-09-18 15:48:14 +06:00
Mend Renovate
29e6a2c4a6 Update sqlite to v2.6.0 (#2504) 2025-09-18 15:47:48 +06:00
Mend Renovate
60c66bbd3a Update dependency androidx.core:core-ktx to v1.17.0 (#2402) 2025-09-17 21:29:03 +06:00
Mend Renovate
060e5b2e2e Update dependency androidx.activity:activity-compose to v1.11.0 (#2499) 2025-09-17 12:21:50 +00:00
AntsyLich
4ac9fcd4d3 Replace compose-stable-marker with compose-runtime-annotation 2025-09-17 18:08:25 +06:00
AntsyLich
d3b7f7e55f Bump compile and target sdk 2025-09-17 18:08:25 +06:00
Mend Renovate
0d926626a1 Update dependency com.google.firebase:firebase-bom to v34.2.0 (#2376) 2025-09-17 17:51:11 +06:00
Mend Renovate
6495a2ea43 Update dependency androidx.benchmark:benchmark-macro-junit4 to v1.4.1 (#2496) 2025-09-17 11:48:09 +00:00
Mend Renovate
94f711ba2a Update dependency androidx.work:work-runtime to v2.10.4 (#2497) 2025-09-17 11:47:49 +00:00
Mend Renovate
9f5c4e03b2 Update dependency androidx.compose:compose-bom to v2025.09.00 (#2401) 2025-09-17 17:39:08 +06:00
Mend Renovate
49562e1915 Update lifecycle.version to v2.9.3 (#2447) 2025-09-17 11:37:56 +00:00
Mend Renovate
57c82b30ba Update dependency org.jsoup:jsoup to v1.21.2 (#2438) 2025-09-17 17:30:26 +06:00
Mend Renovate
e573f72cfd Update dependency io.kotest:kotest-assertions-core to v6.0.3 (#2439) 2025-09-17 17:30:04 +06:00
Mend Renovate
95357a8625 Update GitHub Actions (#2443) 2025-09-17 17:29:14 +06:00
Mend Renovate
bd90307df9 Update dependency com.android.tools.build:gradle to v8.13.0 (#2449) 2025-09-17 17:26:02 +06:00
Secozzi
16b5317b90 Fix migration progress not updating and category flag mischeck (#2484)
- Fixed an issue where migration progress wasn't updated after a manual source search
- Fixed incorrect logic where the category migration flag was ignored due to checking the chapter flag instead
2025-09-17 17:12:21 +06:00
Mend Renovate
83f4b48629 Update plugin firebase-crashlytics to v3.0.6 (#2374) 2025-08-21 07:50:25 +00:00
Mend Renovate
4665dc50f6 Update actions/setup-java action to v5 (#2429) 2025-08-21 13:41:06 +06:00
Mend Renovate
85f5e5019e Update dependency com.github.skydoves:compose-stable-marker to v1.0.7 (#2428) 2025-08-21 13:40:56 +06:00
AntsyLich
4bc3b9f3b6 Bump targetSdk to 35 2025-08-21 12:51:32 +06:00
Mend Renovate
2c0d3678d9 Update actions/dependency-review-action action to v4.7.2 (#2418) 2025-08-21 12:49:14 +06:00
Mend Renovate
feda410152 Update dependency com.android.tools.build:gradle to v8.12.1 (#2417) 2025-08-21 12:48:59 +06:00
Mend Renovate
200c2df5ba Update dependency sh.calvin.reorderable:reorderable to v3 (#2419) 2025-08-21 12:48:43 +06:00
Mend Renovate
be09cddde2 Update dependency io.kotest:kotest-assertions-core to v6 (#2416) 2025-08-21 12:47:04 +06:00
AntsyLich
498317de52 Switch to a fork of QuickJS Java 2025-08-21 12:45:15 +06:00
Mend Renovate
33b876edc6 Update kotlin monorepo to v2.2.10 (#2404) 2025-08-15 08:56:05 +06:00
Mend Renovate
e7251f2034 Update actions/checkout action to v5 (#2395) 2025-08-15 08:55:39 +06:00
Secozzi
3d3c36078a Don't hardcode app name in strings.xml (#2394) 2025-08-11 22:01:07 +06:00
Secozzi
c6a96b3970 Fix height of description not being calculated correctly if images are present (#2382) 2025-08-10 00:21:25 +06:00
AntsyLich
fb3dc1c984 Fix readme CI badge [skip ci] 2025-08-07 20:48:44 +06:00
124 changed files with 2789 additions and 1129 deletions

View File

@@ -7,7 +7,7 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{xml,sq,sqm}]
[*.{xml,sq,sqm,aidl}]
indent_size = 4
# noinspection EditorConfigKeyCorrectness

View File

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

View File

@@ -52,7 +52,7 @@ body:
label: Mihon version
description: You can find your Mihon version in **More → About**.
placeholder: |
Example: "0.19.1"
Example: "0.19.3"
validations:
required: true
@@ -95,7 +95,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 **[0.19.1](https://github.com/mihonapp/mihon/releases/latest)**.
- label: I have updated the app to version **[0.19.3](https://github.com/mihonapp/mihon/releases/latest)**.
required: true
- label: I have filled out all of the requested information in this form, including specific version numbers.
required: true

View File

@@ -26,20 +26,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Dependency Review
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
- name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Check code format
run: ./gradlew spotlessCheck
@@ -48,16 +48,24 @@ jobs:
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
- name: Run unit tests
id: unit_tests
run: ./gradlew testReleaseUnitTest
- name: Upload test report
if: steps.unit_tests.outcome == 'failure'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: test-report-${{ github.sha }}
path: app/build/reports/tests/testReleaseUnitTest
- name: Upload APK
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
- name: Upload mapping
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/release

View File

@@ -29,16 +29,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Build
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
@@ -65,7 +65,7 @@ jobs:
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk
- name: Upload APK
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: mihon
path: |
@@ -83,16 +83,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
cache-disabled: true
@@ -117,7 +117,7 @@ jobs:
mv app/build/outputs/apk/foss/app-universal-foss-unsigned-signed.apk mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
- name: Upload APK
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: mihon-foss
path: mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
@@ -130,7 +130,7 @@ jobs:
steps:
- name: Download all artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
merge-multiple: true
@@ -143,7 +143,7 @@ jobs:
mihon-foss
- name: Create GitHub Release
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with:
tag_name: ${{ needs.get_tag.outputs.tag }}
name: Mihon ${{ needs.get_tag.outputs.tag }}

View File

@@ -10,7 +10,53 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- `Fixed` - for any bug fixes.
- `Other` - for technical stuff.
## [Unreleased]
## [v0.19.3] - 2025-11-07
### Improved
- Improved various aspects of the WebView multi window support ([@TheUnlocked](https://github.com/TheUnlocked)) ([#2662](https://github.com/mihonapp/mihon/pull/2662))
### Removed
- Revert "Fix reader tap zones triggering after scrolling was stopped by the user" due to introduction of regression ([@AntsyLich](https://github.com/AntsyLich)) ([#2670](https://github.com/mihonapp/mihon/pull/2670))
### Fixed
- Fix WebView crash introduced in 0.19.2 ([@bapeey](https://github.com/bapeey)) ([#2649](https://github.com/mihonapp/mihon/pull/2649))
- Fix extra padding appearing in reader after user interactions ([@AntsyLich](https://github.com/AntsyLich)) ([#2669](https://github.com/mihonapp/mihon/pull/2669))
- Fix long strip reader not scrolling on consecutive taps ([@AntsyLich](https://github.com/AntsyLich)) ([#2670](https://github.com/mihonapp/mihon/pull/2670))
## [v0.19.2] - 2025-11-02
### Added
- Advanced setting to limit download filenames to ASCII characters. This is provided only as a workaround for OSes that do not properly handle standard Unicode filenames. This setting is generally not recommended and should only be used as a last resort ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
- Option to customize the number of concurrent source and page downloads ([@AntsyLich](https://github.com/AntsyLich)) ([#2637](https://github.com/mihonapp/mihon/pull/2637))
### Changed
- Increased default concurrent page downloads to 5 ([@AntsyLich](https://github.com/AntsyLich)) ([#2637](https://github.com/mihonapp/mihon/pull/2637))
### Improved
- Spoofing of `X-Requested-With` header to support newer WebView versions ([@Guzmazow](https://github.com/Guzmazow)) ([#2491](https://github.com/mihonapp/mihon/pull/2491))
- Download support for chapters with the same metadata. Now a hash based on chapter's url is appended to download filename to tell them apart, letting you download both. Existing downloaded chapters will continue to work normally ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
- Auto refresh extension list whenever a repository is added or removed ([@c2y5](https://github.com/c2y5)) ([#2483](https://github.com/mihonapp/mihon/pull/2483))
- Added proper multi window support in WebView instead of treating everything as a redirect ([@TheUnlocked](https://github.com/TheUnlocked)) ([#2584](https://github.com/mihonapp/mihon/pull/2584))
### Fixed
- Fix height of description not being calculated correctly if images are present ([@Secozzi](https://github.com/Secozzi)) ([#2382](https://github.com/mihonapp/mihon/pull/2382))
- Fix migration progress not updating after manual search ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
- Fix category migration flag being ignored due to incorrect check against chapter flag ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
- Fix disabling incognito mode from notification ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2512](https://github.com/mihonapp/mihon/pull/2512))
- Fix mass migration advanced search query building ([@AntsyLich](https://github.com/AntsyLich)) ([#2629](https://github.com/mihonapp/mihon/pull/2629))
- Fix migration dialog migrating to wrong entry ([@AntsyLich](https://github.com/AntsyLich)) ([#2631](https://github.com/mihonapp/mihon/pull/2631))
- Fix migration "Attempt to invoke virtual method" crash ([@AntsyLich](https://github.com/AntsyLich)) ([#2632](https://github.com/mihonapp/mihon/pull/2632))
- Fix reader "Unable to edit key" error ([@AntsyLich](https://github.com/AntsyLich)) ([#2634](https://github.com/mihonapp/mihon/pull/2634))
- Fix extension download stuck in pending state in some cases ([@c2y5](https://github.com/c2y5)) ([#2483](https://github.com/mihonapp/mihon/pull/2483))
- Fix scrollbar not showing when animator duration scale animation is turned off ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2398](https://github.com/mihonapp/mihon/pull/2398))
- Fix date picker not allowing the same start and finish date in negative time zones ([@AntsyLich](https://github.com/AntsyLich), [@kashish-aggarwal21](https://github.com/kashish-aggarwal21)) ([#2617](https://github.com/mihonapp/mihon/pull/2617))
- Fix reader tap zones triggering after scrolling was stopped by the user ([@Naputt1](https://github.com/Naputt1), [@AntsyLich](https://github.com/AntsyLich)) ([#2518](https://github.com/mihonapp/mihon/pull/2518))
- Fix reader page indicator being partially visible on some devices ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
- Fix inconsistent system bar and reader app bar background ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
- Fix transparent system bar background in reader on Android 15+ ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
### Other
- Delegate Suwayomi tracker authentication to extension ([@cpiber](https://github.com/cpiber)) ([#2476](https://github.com/mihonapp/mihon/pull/2476))
- Fix Kitsu tracker to conform to tracker data structure properly ([@cpiber](https://github.com/cpiber)) ([#2609](https://github.com/mihonapp/mihon/pull/2609))
- Update Suwayomi tracker to use GraphQL API instead of REST API ([@cpiber](https://github.com/cpiber)) ([#2585](https://github.com/mihonapp/mihon/pull/2585))
## [v0.19.1] - 2025-08-07
### Changed
@@ -19,7 +65,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
### Removed
- Predictive back support ([@AntsyLich](https://github.com/AntsyLich)) ([#2362](https://github.com/mihonapp/mihon/pull/2362))
### Fixes
### Fixed
- Fix scrollbar sometimes not showing during scroll or not reaching the bottom with few items ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2304](https://github.com/mihonapp/mihon/pull/2304))
- Fix local source EPUB files not loading ([@AntsyLich](https://github.com/AntsyLich)) ([#2369](https://github.com/mihonapp/mihon/pull/2369))
- Fix title text color in light mode on mass migration list ([@AntsyLich](https://github.com/AntsyLich)) ([#2370](https://github.com/mihonapp/mihon/pull/2370))
@@ -65,7 +111,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Make local source default chapter sorting match file explorer behavior ([@AntsyLich](https://github.com/AntsyLich)) ([#2224](https://github.com/mihonapp/mihon/pull/224))
- Include Manga `initialized` status in backup ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2285](https://github.com/mihonapp/mihon/pull/2285))
### Fixes
### Fixed
- Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885))
- Fix next chapter button occasionally jumping to the last page of the current chapter ([@perokhe](https://github.com/perokhe)) ([#1920](https://github.com/mihonapp/mihon/pull/1920))
- Fix page number not appearing when opening chapter ([@perokhe](https://github.com/perokhe)) ([#1936](https://github.com/mihonapp/mihon/pull/1936))
@@ -397,7 +443,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
- Minimum supported Android version to 8 ([@AntsyLich](https://github.com/AntsyLich)) ([`dfb3091`](https://github.com/mihonapp/mihon/commit/dfb3091e380dda3e9bfb64bf5c9a685cf3a03d0e))
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.19.1...main
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.19.3...main
[v0.19.3]: https://github.com/mihonapp/mihon/compare/v0.19.2...v0.19.3
[v0.19.2]: https://github.com/mihonapp/mihon/compare/v0.19.1...v0.19.2
[v0.19.1]: https://github.com/mihonapp/mihon/compare/v0.19.0...v0.19.1
[v0.19.0]: https://github.com/mihonapp/mihon/compare/v0.18.0...v0.19.0
[v0.18.0]: https://github.com/mihonapp/mihon/compare/v0.17.1...v0.18.0

View File

@@ -12,7 +12,7 @@ Discover and read manga, webtoons, comics, and more easier than ever on your
[![Discord server](https://img.shields.io/discord/1195734228319617024.svg?label=&labelColor=6A7EC2&color=7389D8&logo=discord&logoColor=FFFFFF)](https://discord.gg/mihon)
[![GitHub downloads](https://img.shields.io/github/downloads/mihonapp/mihon/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://mihon.app/download)
[![CI](https://img.shields.io/github/actions/workflow/status/mihonapp/mihon/build_push.yml?labelColor=27303D)](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
[![CI](https://img.shields.io/github/actions/workflow/status/mihonapp/mihon/build.yml?labelColor=27303D)](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
[![License: Apache-2.0](https://img.shields.io/github/license/mihonapp/mihon?labelColor=27303D&color=0877d2)](/LICENSE)
[![Translation status](https://img.shields.io/weblate/progress/mihon?labelColor=27303D&color=946300)](https://hosted.weblate.org/engage/mihon/)

View File

@@ -26,8 +26,8 @@ android {
defaultConfig {
applicationId = "app.mihon"
versionCode = 13
versionName = "0.19.1"
versionCode = 16
versionName = "0.19.3"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -138,9 +138,9 @@ android {
buildFeatures {
viewBinding = true
buildConfig = true
aidl = true
// Disable some unused things
aidl = false
renderScript = false
shaders = false
}
@@ -261,7 +261,6 @@ dependencies {
implementation(libs.directionalviewpager) {
exclude(group = "androidx.viewpager", module = "viewpager")
}
implementation(libs.insetter)
implementation(libs.richeditor.compose)
implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager)

View File

@@ -0,0 +1,7 @@
package mihon.app.shizuku;
interface IShellInterface {
void install(in AssetFileDescriptor apk) = 1;
void destroy() = 16777114;
}

View File

@@ -114,6 +114,7 @@ class SyncChaptersWithSource(
downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
manga.title,
manga.source,
)
@@ -121,12 +122,14 @@ class SyncChaptersWithSource(
if (shouldRenameChapter) {
downloadManager.renameChapter(source, manga, dbChapter, chapter)
}
var toChangeChapter = dbChapter.copy(
name = chapter.name,
chapterNumber = chapter.chapterNumber,
scanlator = chapter.scanlator,
sourceOrder = chapter.sourceOrder,
)
if (chapter.dateUpload != 0L) {
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
}

View File

@@ -26,6 +26,7 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
val downloaded = downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
chapter.url,
manga.title,
manga.source,
)

View File

@@ -255,7 +255,7 @@ private fun ColumnScope.DisplayPage(
value = columns,
valueRange = 0..10,
label = stringResource(MR.strings.pref_library_columns),
valueText = if (columns > 0) {
valueString = if (columns > 0) {
columns.toString()
} else {
stringResource(MR.strings.label_auto)

View File

@@ -53,6 +53,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
@@ -68,6 +69,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
@@ -618,6 +620,7 @@ private fun MangaSummary(
targetValue = if (expanded) 1f else 0f,
label = "summary",
)
var infoHeight by remember { mutableIntStateOf(0) }
Layout(
modifier = modifier.clipToBounds(),
contents = listOf(
@@ -630,25 +633,11 @@ private fun MangaSummary(
)
},
{
Column {
MangaNotesSection(
content = notes,
expanded = true,
onEditNotes = onEditNotesClicked,
)
MarkdownRender(
content = description,
modifier = Modifier.secondaryItemAlpha(),
annotator = descriptionAnnotator(
loadImages = loadImages,
linkStyle = getMarkdownLinkStyle().toSpanStyle(),
),
loadImages = loadImages,
)
}
},
{
Column {
Column(
modifier = Modifier.onSizeChanged { size ->
infoHeight = size.height
},
) {
MangaNotesSection(
content = notes,
expanded = expanded,
@@ -685,14 +674,11 @@ private fun MangaSummary(
}
},
),
) { (shrunk, expanded, actual, scrim), constraints ->
) { (shrunk, actual, scrim), constraints ->
val shrunkHeight = shrunk.single()
.measure(constraints)
.height
val expandedHeight = expanded.single()
.measure(constraints)
.height
val heightDelta = expandedHeight - shrunkHeight
val heightDelta = infoHeight - shrunkHeight
val scrimHeight = 24.dp.roundToPx()
val actualPlaceable = actual.single()

View File

@@ -102,13 +102,9 @@ private fun getMarkdownColors(): MarkdownColors {
val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
return DefaultMarkdownColors(
text = MaterialTheme.colorScheme.onSurface,
codeText = Color.Unspecified,
inlineCodeText = Color.Unspecified,
linkText = Color.Unspecified,
codeBackground = codeBackground,
inlineCodeBackground = codeBackground,
dividerColor = MaterialTheme.colorScheme.outlineVariant,
tableText = Color.Unspecified,
tableBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f),
)
}
@@ -139,7 +135,6 @@ private fun getMarkdownTypography(): MarkdownTypography {
ordered = MaterialTheme.typography.bodyMedium,
bullet = MaterialTheme.typography.bodyMedium,
list = MaterialTheme.typography.bodyMedium,
link = link,
textLink = TextLinkStyles(style = link.toSpanStyle()),
table = MaterialTheme.typography.bodyMedium,
)

View File

@@ -15,10 +15,10 @@ sealed class Preference {
abstract val title: String
abstract val enabled: Boolean
sealed class PreferenceItem<T> : Preference() {
sealed class PreferenceItem<T, R> : Preference() {
abstract val subtitle: String?
abstract val icon: ImageVector?
abstract val onValueChanged: suspend (value: T) -> Boolean
abstract val onValueChanged: suspend (value: T) -> R
/**
* A basic [PreferenceItem] that only displays texts.
@@ -28,9 +28,9 @@ sealed class Preference {
override val subtitle: String? = null,
override val enabled: Boolean = true,
val onClick: (() -> Unit)? = null,
) : PreferenceItem<String>() {
) : PreferenceItem<String, Unit>() {
override val icon: ImageVector? = null
override val onValueChanged: suspend (value: String) -> Boolean = { true }
override val onValueChanged: suspend (value: String) -> Unit = {}
}
/**
@@ -42,7 +42,7 @@ sealed class Preference {
override val subtitle: String? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true },
) : PreferenceItem<Boolean>() {
) : PreferenceItem<Boolean, Boolean>() {
override val icon: ImageVector? = null
}
@@ -52,12 +52,13 @@ sealed class Preference {
data class SliderPreference(
val value: Int,
override val title: String,
override val subtitle: String? = null,
val valueString: String? = null,
val valueRange: IntProgression = 0..1,
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
override val subtitle: String? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (value: Int) -> Boolean = { true },
) : PreferenceItem<Int>() {
override val onValueChanged: suspend (value: Int) -> Unit = {},
) : PreferenceItem<Int, Unit>() {
override val icon: ImageVector? = null
}
@@ -75,7 +76,7 @@ sealed class Preference {
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (value: T) -> Boolean = { true },
) : PreferenceItem<T>() {
) : PreferenceItem<T, Boolean>() {
internal fun internalSet(value: Any) = preference.set(value as T)
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T)
@@ -96,8 +97,8 @@ sealed class Preference {
{ v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (value: String) -> Boolean = { true },
) : PreferenceItem<String>()
override val onValueChanged: suspend (value: String) -> Unit = {},
) : PreferenceItem<String, Unit>()
/**
* A [PreferenceItem] that displays a list of entries as a dialog.
@@ -121,7 +122,7 @@ sealed class Preference {
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
) : PreferenceItem<Set<String>>()
) : PreferenceItem<Set<String>, Boolean>()
/**
* A [PreferenceItem] that shows a EditText in the dialog.
@@ -132,7 +133,7 @@ sealed class Preference {
override val subtitle: String? = "%s",
override val enabled: Boolean = true,
override val onValueChanged: suspend (value: String) -> Boolean = { true },
) : PreferenceItem<String>() {
) : PreferenceItem<String, Boolean>() {
override val icon: ImageVector? = null
}
@@ -143,31 +144,31 @@ sealed class Preference {
val tracker: Tracker,
val login: () -> Unit,
val logout: () -> Unit,
) : PreferenceItem<String>() {
) : PreferenceItem<String, Unit>() {
override val title: String = ""
override val enabled: Boolean = true
override val subtitle: String? = null
override val icon: ImageVector? = null
override val onValueChanged: suspend (value: String) -> Boolean = { true }
override val onValueChanged: suspend (value: String) -> Unit = {}
}
data class InfoPreference(
override val title: String,
) : PreferenceItem<String>() {
) : PreferenceItem<String, Unit>() {
override val enabled: Boolean = true
override val subtitle: String? = null
override val icon: ImageVector? = null
override val onValueChanged: suspend (value: String) -> Boolean = { true }
override val onValueChanged: suspend (value: String) -> Unit = {}
}
data class CustomPreference(
override val title: String,
val content: @Composable () -> Unit,
) : PreferenceItem<Unit>() {
) : PreferenceItem<Unit, Unit>() {
override val enabled: Boolean = true
override val subtitle: String? = null
override val icon: ImageVector? = null
override val onValueChanged: suspend (value: Unit) -> Boolean = { true }
override val onValueChanged: suspend (value: Unit) -> Unit = {}
}
}
@@ -175,6 +176,6 @@ sealed class Preference {
override val title: String,
override val enabled: Boolean = true,
val preferenceItems: ImmutableList<PreferenceItem<out Any>>,
val preferenceItems: ImmutableList<PreferenceItem<out Any, out Any>>,
) : Preference()
}

View File

@@ -35,7 +35,7 @@ val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) {
@Composable
fun StatusWrapper(
item: Preference.PreferenceItem<*>,
item: Preference.PreferenceItem<*, *>,
highlightKey: String?,
content: @Composable () -> Unit,
) {
@@ -56,7 +56,7 @@ fun StatusWrapper(
@Composable
internal fun PreferenceItem(
item: Preference.PreferenceItem<*>,
item: Preference.PreferenceItem<*, *>,
highlightKey: String?,
) {
val scope = rememberCoroutineScope()
@@ -83,17 +83,18 @@ internal fun PreferenceItem(
}
is Preference.PreferenceItem.SliderPreference -> {
BaseSliderItem(
label = item.title,
value = item.value,
valueRange = item.valueRange,
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
steps = item.steps,
labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
title = item.title,
subtitle = item.subtitle,
valueString = item.valueString.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
onChange = {
scope.launch {
item.onValueChanged(it)
}
},
titleStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
modifier = Modifier.padding(
horizontal = PrefsHorizontalPadding,
vertical = PrefsVerticalPadding,

View File

@@ -71,7 +71,7 @@ fun PreferenceScreen(
}
// Create Preference Item
is Preference.PreferenceItem<*> -> item {
is Preference.PreferenceItem<*, *> -> item {
PreferenceItem(
item = preference,
highlightKey = highlightKey,

View File

@@ -323,6 +323,11 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(MR.strings.pref_update_library_manga_titles),
subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary),
),
Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.disallowNonAsciiFilenames(),
title = stringResource(MR.strings.pref_disallow_non_ascii_filenames),
subtitle = stringResource(MR.strings.pref_disallow_non_ascii_filenames_details),
),
),
)
}

View File

@@ -37,6 +37,8 @@ object SettingsDownloadScreen : SearchableSettings {
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
val parallelSourceLimit by downloadPreferences.parallelSourceLimit().collectAsState()
val parallelPageLimit by downloadPreferences.parallelPageLimit().collectAsState()
return listOf(
Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.downloadOnlyOverWifi(),
@@ -51,6 +53,19 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.split_tall_images),
subtitle = stringResource(MR.strings.split_tall_images_summary),
),
Preference.PreferenceItem.SliderPreference(
value = parallelSourceLimit,
valueRange = 1..10,
title = stringResource(MR.strings.pref_download_concurrent_sources),
onValueChanged = { downloadPreferences.parallelSourceLimit().set(it) },
),
Preference.PreferenceItem.SliderPreference(
value = parallelPageLimit,
valueRange = 1..15,
title = stringResource(MR.strings.pref_download_concurrent_pages),
subtitle = stringResource(MR.strings.pref_download_concurrent_pages_summary),
onValueChanged = { downloadPreferences.parallelPageLimit().set(it) },
),
getDeleteChaptersGroup(
downloadPreferences = downloadPreferences,
categories = allCategories,

View File

@@ -1,6 +1,5 @@
package eu.kanade.presentation.more.settings.screen
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
@@ -10,6 +9,7 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
@@ -101,11 +101,9 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_fullscreen),
),
Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.cutoutShort(),
preference = readerPreferences.drawUnderCutout(),
title = stringResource(MR.strings.pref_cutout_short),
enabled = fullscreen &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
enabled = LocalView.current.hasDisplayCutout() && fullscreen,
),
Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.keepScreenOn(),
@@ -143,23 +141,17 @@ object SettingsReaderScreen : SearchableSettings {
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
valueRange = 1..15,
title = stringResource(MR.strings.pref_flash_duration),
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
valueString = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
enabled = flashPageState,
onValueChanged = {
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
true
},
onValueChanged = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
),
Preference.PreferenceItem.SliderPreference(
value = flashInterval,
valueRange = 1..10,
title = stringResource(MR.strings.pref_flash_page_interval),
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
valueString = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
enabled = flashPageState,
onValueChanged = {
flashIntervalPref.set(it)
true
},
onValueChanged = { flashIntervalPref.set(it) },
),
Preference.PreferenceItem.ListPreference(
preference = flashColorPref,
@@ -342,11 +334,8 @@ object SettingsReaderScreen : SearchableSettings {
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
},
title = stringResource(MR.strings.pref_webtoon_side_padding),
subtitle = numberFormat.format(webtoonSidePadding / 100f),
onValueChanged = {
webtoonSidePaddingPref.set(it)
true
},
valueString = numberFormat.format(webtoonSidePadding / 100f),
onValueChanged = { webtoonSidePaddingPref.set(it) },
),
Preference.PreferenceItem.ListPreference(
preference = readerPreferences.readerHideThreshold(),

View File

@@ -183,7 +183,7 @@ private fun SearchResult(
emptySequence()
}
}
is Preference.PreferenceItem<*> -> sequenceOf(null to p)
is Preference.PreferenceItem<*, *> -> sequenceOf(null to p)
}
}
// Don't show info preference

View File

@@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.extension.ExtensionManager
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.channels.Channel
@@ -27,6 +28,7 @@ class ExtensionReposScreenModel(
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
@@ -53,6 +55,7 @@ class ExtensionReposScreenModel(
fun createRepo(baseUrl: String) {
screenModelScope.launchIO {
when (val result = createExtensionRepo.await(baseUrl)) {
CreateExtensionRepo.Result.Success -> extensionManager.findAvailableExtensions()
CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists)
is CreateExtensionRepo.Result.DuplicateFingerprint -> {
@@ -93,6 +96,7 @@ class ExtensionReposScreenModel(
fun deleteRepo(baseUrl: String) {
screenModelScope.launchIO {
deleteExtensionRepo.await(baseUrl)
extensionManager.findAvailableExtensions()
}
}

View File

@@ -57,7 +57,7 @@ fun ExtensionRepoCreateDialog(
},
text = {
Column {
Text(text = stringResource(MR.strings.action_add_repo_message))
Text(text = stringResource(MR.strings.action_add_repo_message, stringResource(MR.strings.app_name)))
OutlinedTextField(
modifier = Modifier

View File

@@ -99,7 +99,7 @@ class DebugInfoScreen : Screen() {
}
private fun getDeviceInfoGroup(): Preference.PreferenceGroup {
val items = persistentListOf<Preference.PreferenceItem<out Any>>().mutate {
val items = persistentListOf<Preference.PreferenceItem<out Any, out Any>>().mutate {
it.add(
Preference.PreferenceItem.TextPreference(
title = "Model",

View File

@@ -6,6 +6,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.TextStyle
@@ -15,9 +16,10 @@ import androidx.compose.ui.unit.sp
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
@Composable
fun PageIndicatorText(
fun ReaderPageIndicator(
currentPage: Int,
totalPages: Int,
modifier: Modifier = Modifier,
) {
if (currentPage <= 0 || totalPages <= 0) return
@@ -36,6 +38,7 @@ fun PageIndicatorText(
Box(
contentAlignment = Alignment.Center,
modifier = modifier,
) {
Text(
text = text,
@@ -50,10 +53,10 @@ fun PageIndicatorText(
@PreviewLightDark
@Composable
private fun PageIndicatorTextPreview() {
private fun ReaderPageIndicatorPreview() {
TachiyomiPreviewTheme {
Surface {
PageIndicatorText(currentPage = 10, totalPages = 69)
ReaderPageIndicator(currentPage = 10, totalPages = 69)
}
}
}

View File

@@ -2,42 +2,41 @@ package eu.kanade.presentation.reader.appbars
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bookmark
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.reader.components.ChapterNavigator
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
private val animationSpec = tween<IntOffset>(200)
private val readerBarsSlideAnimationSpec = tween<IntOffset>(200)
private val readerBarsFadeAnimationSpec = tween<Float>(150)
@Composable
fun ReaderAppBars(
visible: Boolean,
fullscreen: Boolean,
mangaTitle: String?,
chapterTitle: String?,
@@ -71,83 +70,26 @@ fun ReaderAppBars(
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
val modifierWithInsetsPadding = if (fullscreen) {
Modifier.systemBarsPadding()
} else {
Modifier
}
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = Modifier.fillMaxHeight()) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically(
initialOffsetY = { -it },
animationSpec = animationSpec,
),
exit = slideOutVertically(
targetOffsetY = { -it },
animationSpec = animationSpec,
),
enter = slideInVertically(initialOffsetY = { -it }, animationSpec = readerBarsSlideAnimationSpec) +
fadeIn(animationSpec = readerBarsFadeAnimationSpec),
exit = slideOutVertically(targetOffsetY = { -it }, animationSpec = readerBarsSlideAnimationSpec) +
fadeOut(animationSpec = readerBarsFadeAnimationSpec),
) {
AppBar(
modifier = modifierWithInsetsPadding
ReaderTopBar(
modifier = Modifier
.background(backgroundColor)
.clickable(onClick = onClickTopAppBar),
backgroundColor = backgroundColor,
title = mangaTitle,
subtitle = chapterTitle,
mangaTitle = mangaTitle,
chapterTitle = chapterTitle,
navigateUp = navigateUp,
actions = {
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
add(
AppBar.Action(
title = stringResource(
if (bookmarked) {
MR.strings.action_remove_bookmark
} else {
MR.strings.action_bookmark
},
),
icon = if (bookmarked) {
Icons.Outlined.Bookmark
} else {
Icons.Outlined.BookmarkBorder
},
onClick = onToggleBookmarked,
),
)
onOpenInWebView?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_web_view),
onClick = it,
),
)
}
onOpenInBrowser?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_browser),
onClick = it,
),
)
}
onShare?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_share),
onClick = it,
),
)
}
}
.build(),
)
},
bookmarked = bookmarked,
onToggleBookmarked = onToggleBookmarked,
onOpenInWebView = onOpenInWebView,
onOpenInBrowser = onOpenInBrowser,
onShare = onShare,
)
}
@@ -155,19 +97,12 @@ fun ReaderAppBars(
AnimatedVisibility(
visible = visible,
enter = slideInVertically(
initialOffsetY = { it },
animationSpec = animationSpec,
),
exit = slideOutVertically(
targetOffsetY = { it },
animationSpec = animationSpec,
),
enter = slideInVertically(initialOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) +
fadeIn(animationSpec = readerBarsFadeAnimationSpec),
exit = slideOutVertically(targetOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) +
fadeOut(animationSpec = readerBarsFadeAnimationSpec),
) {
Column(
modifier = modifierWithInsetsPadding,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) {
ChapterNavigator(
isRtl = isRtl,
onNextChapter = onNextChapter,
@@ -178,8 +113,12 @@ fun ReaderAppBars(
totalPages = totalPages,
onPageIndexChange = onPageIndexChange,
)
BottomReaderBar(
backgroundColor = backgroundColor,
ReaderBottomBar(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor)
.padding(horizontal = MaterialTheme.padding.small)
.windowInsetsPadding(WindowInsets.navigationBars),
readingMode = readingMode,
onClickReadingMode = onClickReadingMode,
orientation = orientation,

View File

@@ -1,10 +1,7 @@
package eu.kanade.presentation.reader.appbars
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
@@ -12,9 +9,8 @@ import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
@@ -22,8 +18,7 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun BottomReaderBar(
backgroundColor: Color,
fun ReaderBottomBar(
readingMode: ReadingMode,
onClickReadingMode: () -> Unit,
orientation: ReaderOrientation,
@@ -31,12 +26,11 @@ fun BottomReaderBar(
cropEnabled: Boolean,
onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor)
.padding(8.dp),
modifier = modifier
.pointerInput(Unit) {},
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {

View File

@@ -0,0 +1,83 @@
package eu.kanade.presentation.reader.appbars
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bookmark
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun ReaderTopBar(
mangaTitle: String?,
chapterTitle: String?,
navigateUp: () -> Unit,
bookmarked: Boolean,
onToggleBookmarked: () -> Unit,
onOpenInWebView: (() -> Unit)?,
onOpenInBrowser: (() -> Unit)?,
onShare: (() -> Unit)?,
modifier: Modifier = Modifier,
) {
AppBar(
modifier = modifier,
backgroundColor = Color.Transparent,
title = mangaTitle,
subtitle = chapterTitle,
navigateUp = navigateUp,
actions = {
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
add(
AppBar.Action(
title = stringResource(
if (bookmarked) {
MR.strings.action_remove_bookmark
} else {
MR.strings.action_bookmark
},
),
icon = if (bookmarked) {
Icons.Outlined.Bookmark
} else {
Icons.Outlined.BookmarkBorder
},
onClick = onToggleBookmarked,
),
)
onOpenInWebView?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_web_view),
onClick = it,
),
)
}
onOpenInBrowser?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_browser),
onClick = it,
),
)
}
onShare?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_share),
onClick = it,
),
)
}
}
.build(),
)
},
)
}

View File

@@ -1,5 +1,6 @@
package eu.kanade.presentation.reader.settings
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
@@ -8,6 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.SettingsChipRow
@@ -64,10 +66,11 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
pref = screenModel.preferences.fullscreen(),
)
if (screenModel.hasDisplayCutout && screenModel.preferences.fullscreen().get()) {
val isFullscreen by screenModel.preferences.fullscreen().collectAsState()
if (LocalActivity.current?.hasDisplayCutout() == true && isFullscreen) {
CheckboxItem(
label = stringResource(MR.strings.pref_cutout_short),
pref = screenModel.preferences.cutoutShort(),
pref = screenModel.preferences.drawUnderCutout(),
)
}
@@ -100,7 +103,7 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
valueRange = 1..15,
label = stringResource(MR.strings.pref_flash_duration),
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
valueString = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
@@ -108,7 +111,7 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
value = flashInterval,
valueRange = 1..10,
label = stringResource(MR.strings.pref_flash_page_interval),
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
valueString = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
onChange = {
flashIntervalPref.set(it)
},

View File

@@ -156,7 +156,7 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
value = webtoonSidePadding,
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
label = stringResource(MR.strings.pref_webtoon_side_padding),
valueText = numberFormat.format(webtoonSidePadding / 100f),
valueString = numberFormat.format(webtoonSidePadding / 100f),
onChange = {
screenModel.preferences.webtoonSidePadding().set(it)
},

View File

@@ -2,8 +2,10 @@ package eu.kanade.presentation.webview
import android.content.pm.ApplicationInfo
import android.graphics.Bitmap
import android.os.Message
import android.webkit.WebResourceRequest
import android.webkit.WebView
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -19,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -26,17 +29,23 @@ 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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.stack.mutableStateStackOf
import com.kevinnzou.web.AccompanistWebChromeClient
import com.kevinnzou.web.AccompanistWebViewClient
import com.kevinnzou.web.LoadingState
import com.kevinnzou.web.WebContent
import com.kevinnzou.web.WebView
import com.kevinnzou.web.rememberWebViewNavigator
import com.kevinnzou.web.rememberWebViewState
import com.kevinnzou.web.WebViewNavigator
import com.kevinnzou.web.WebViewState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getHtml
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import kotlinx.collections.immutable.persistentListOf
@@ -44,6 +53,18 @@ import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
class WebViewWindow(webContent: WebContent, val navigator: WebViewNavigator) {
var state by mutableStateOf(WebViewState(webContent))
var popupMessage: Message? = null
private set
var webView: WebView? = null
constructor(popupMessage: Message, navigator: WebViewNavigator) : this(WebContent.NavigatorOnly, navigator) {
this.popupMessage = popupMessage
}
}
@Composable
fun WebViewScreenContent(
onNavigateUp: () -> Unit,
@@ -55,8 +76,20 @@ fun WebViewScreenContent(
headers: Map<String, String> = emptyMap(),
onUrlChange: (String) -> Unit = {},
) {
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
val navigator = rememberWebViewNavigator()
val coroutineScope = rememberCoroutineScope()
val windowStack = remember {
mutableStateStackOf(
WebViewWindow(
WebContent.Url(url = url, additionalHttpHeaders = headers),
WebViewNavigator(coroutineScope),
),
)
}
val currentWindow = windowStack.lastItemOrNull!!
val navigator = currentWindow.navigator
val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope()
@@ -97,31 +130,67 @@ fun WebViewScreenContent(
view: WebView?,
request: WebResourceRequest?,
): Boolean {
request?.let {
// Don't attempt to open blobs as webpages
if (it.url.toString().startsWith("blob:http")) {
return false
}
val url = request?.url?.toString() ?: return false
// Ignore intents urls
if (it.url.toString().startsWith("intent://")) {
// Ignore intents urls
if (url.startsWith("intent://")) return true
// Only open valid web urls
if (url.startsWith("http") || url.startsWith("https")) {
if (url != view?.url) {
view?.loadUrl(url, headers)
return true
}
// Continue with request, but with custom headers
view?.loadUrl(it.url.toString(), headers)
}
return super.shouldOverrideUrlLoading(view, request)
return false
}
}
}
val webChromeClient = remember {
object : AccompanistWebChromeClient() {
override fun onCreateWindow(
view: WebView,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: Message,
): Boolean {
// if it wasn't initiated by a user gesture, we should ignore it like a normal browser would
if (isUserGesture) {
windowStack.push(WebViewWindow(resultMsg, WebViewNavigator(coroutineScope)))
return true
}
return false
}
}
}
fun initializePopup(webView: WebView, message: Message): WebView {
val transport = message.obj as WebView.WebViewTransport
transport.webView = webView
message.sendToTarget()
return webView
}
val popState = remember<() -> Unit> {
{
if (windowStack.size == 1) {
onNavigateUp()
} else {
windowStack.pop()
}
}
}
BackHandler(windowStack.size > 1, popState)
Scaffold(
topBar = {
Box {
Column {
AppBar(
title = state.pageTitle ?: initialTitle,
title = currentWindow.state.pageTitle ?: initialTitle,
subtitle = currentUrl,
navigateUp = onNavigateUp,
navigationIcon = Icons.Outlined.Close,
@@ -164,7 +233,18 @@ fun WebViewScreenContent(
title = stringResource(MR.strings.pref_clear_cookies),
onClick = { onClearCookies(currentUrl) },
),
),
).builder().apply {
if (windowStack.size > 1) {
add(
0,
AppBar.Action(
title = stringResource(MR.strings.action_webview_close_tab),
icon = ImageVector.vectorResource(R.drawable.ic_tab_close_24px),
onClick = popState,
),
)
}
}.build(),
)
},
)
@@ -186,7 +266,7 @@ fun WebViewScreenContent(
}
}
}
when (val loadingState = state.loadingState) {
when (val loadingState = currentWindow.state.loadingState) {
is LoadingState.Initializing -> LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
@@ -203,27 +283,55 @@ fun WebViewScreenContent(
}
},
) { contentPadding ->
WebView(
state = state,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding),
navigator = navigator,
onCreated = { webView ->
webView.setDefaultSettings()
// We need to key the WebView composable to the window object since simply updating the WebView composable will
// not cause it to re-invoke the WebView factory and render the new current window's WebView. This lets us
// completely reset the WebView composable when the current window switches.
key(currentWindow) {
WebView(
state = currentWindow.state,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding),
navigator = navigator,
onCreated = { webView ->
webView.setDefaultSettings()
// Debug mode (chrome://inspect/#devices)
if (BuildConfig.DEBUG &&
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
) {
WebView.setWebContentsDebuggingEnabled(true)
}
// Debug mode (chrome://inspect/#devices)
if (BuildConfig.DEBUG &&
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
) {
WebView.setWebContentsDebuggingEnabled(true)
}
headers["user-agent"]?.let {
webView.settings.userAgentString = it
}
},
client = webClient,
)
headers["user-agent"]?.let {
webView.settings.userAgentString = it
}
},
onDispose = { webView ->
val window = windowStack.items.find { it.webView == webView }
if (window == null) {
// If we couldn't find any window on the stack that owns this WebView, it means that we can
// safely dispose of it because the window containing it has been closed.
webView.destroy()
} else {
// The composable is being disposed but the WebView object is not.
// When the WebView element is recomposed, we will want the WebView to resume from its state
// before it was unmounted, we won't want it to reset back to its original target.
window.state.content = WebContent.NavigatorOnly
}
},
client = webClient,
chromeClient = webChromeClient,
factory = { context ->
currentWindow.webView
?: WebView(context).also { webView ->
currentWindow.webView = webView
currentWindow.popupMessage?.let {
initializePopup(webView, it)
}
}
},
)
}
}
}

View File

@@ -122,7 +122,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
val pendingIntent = PendingIntent.getBroadcast(
this@App,
0,
Intent(ACTION_DISABLE_INCOGNITO_MODE),
Intent(ACTION_DISABLE_INCOGNITO_MODE).setPackage(BuildConfig.APPLICATION_ID),
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
setContentIntent(pendingIntent)
@@ -220,8 +220,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
// Override the value passed as X-Requested-With in WebView requests
val stackTrace = Looper.getMainLooper().thread.stackTrace
val isChromiumCall = stackTrace.any { trace ->
trace.className.equals("org.chromium.base.BuildInfo", ignoreCase = true) &&
setOf("getAll", "getPackageName", "<init>").any { trace.methodName.equals(it, ignoreCase = true) }
trace.className.lowercase() in setOf("org.chromium.base.buildinfo", "org.chromium.base.apkinfo") &&
trace.methodName.lowercase() in setOf("getall", "getpackagename", "<init>")
}
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)

View File

@@ -6,7 +6,6 @@ import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import logcat.LogPriority
import okhttp3.Response
@@ -115,7 +114,7 @@ class ChapterCache(
fun isImageInCache(imageUrl: String): Boolean {
return try {
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)).use { it != null }
} catch (e: IOException) {
} catch (_: IOException) {
false
}
}
@@ -147,7 +146,7 @@ class ChapterCache(
try {
// Get editor from md5 key.
val key = DiskUtil.hashKeyForDisk(imageUrl)
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
editor = diskCache.edit(key) ?: return
// Get OutputStream and write image with Okio.
response.body.source().saveTo(editor.newOutputStream(0))

View File

@@ -128,6 +128,7 @@ class DownloadCache(
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query
* @param chapterUrl the url of the chapter to query
* @param mangaTitle the title of the manga to query.
* @param sourceId the id of the source of the chapter.
* @param skipCache whether to skip the directory cache and check in the filesystem.
@@ -135,13 +136,14 @@ class DownloadCache(
fun isChapterDownloaded(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
mangaTitle: String,
sourceId: Long,
skipCache: Boolean,
): Boolean {
if (skipCache) {
val source = sourceManager.getOrStub(sourceId)
return provider.findChapterDir(chapterName, chapterScanlator, mangaTitle, source) != null
return provider.findChapterDir(chapterName, chapterScanlator, chapterUrl, mangaTitle, source) != null
}
renewCache()
@@ -153,6 +155,7 @@ class DownloadCache(
return provider.getValidChapterDirNames(
chapterName,
chapterScanlator,
chapterUrl,
).any { it in mangaDir.chapterDirs }
}
}
@@ -233,7 +236,7 @@ class DownloadCache(
rootDownloadsDirMutex.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
if (it in mangaDir.chapterDirs) {
mangaDir.chapterDirs -= it
}
@@ -254,7 +257,7 @@ class DownloadCache(
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
chapters.forEach { chapter ->
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
if (it in mangaDir.chapterDirs) {
mangaDir.chapterDirs -= it
}

View File

@@ -159,7 +159,7 @@ class DownloadManager(
* @return the list of pages from the chapter.
*/
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): List<Page> {
val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, manga.title, source)
val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, chapter.url, manga.title, source)
val files = chapterDir?.listFiles().orEmpty()
.filter { it.isFile && ImageUtil.isImage(it.name) { it.openInputStream() } }
@@ -185,11 +185,12 @@ class DownloadManager(
fun isChapterDownloaded(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
mangaTitle: String,
sourceId: Long,
skipCache: Boolean = false,
): Boolean {
return cache.isChapterDownloaded(chapterName, chapterScanlator, mangaTitle, sourceId, skipCache)
return cache.isChapterDownloaded(chapterName, chapterScanlator, chapterUrl, mangaTitle, sourceId, skipCache)
}
/**
@@ -368,7 +369,7 @@ class DownloadManager(
* @param newChapter the target chapter with the new name.
*/
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator)
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator, oldChapter.url)
val mangaDir = provider.getMangaDir(manga.title, source).getOrElse { e ->
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
return
@@ -379,7 +380,7 @@ class DownloadManager(
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull() ?: return
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator)
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
if (oldDownload.isFile && oldDownload.extension == "cbz") {
newName += ".cbz"
}

View File

@@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.Hash.md5
import eu.kanade.tachiyomi.util.storage.DiskUtil
import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.displayablePath
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.i18n.MR
@@ -25,6 +27,7 @@ import java.io.IOException
class DownloadProvider(
private val context: Context,
private val storageManager: StorageManager = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
) {
private val downloadsDir: UniFile?
@@ -96,9 +99,15 @@ class DownloadProvider(
* @param mangaTitle the title of the manga to query.
* @param source the source of the chapter.
*/
fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? {
fun findChapterDir(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
mangaTitle: String,
source: Source,
): UniFile? {
val mangaDir = findMangaDir(mangaTitle, source)
return getValidChapterDirNames(chapterName, chapterScanlator).asSequence()
return getValidChapterDirNames(chapterName, chapterScanlator, chapterUrl).asSequence()
.mapNotNull { mangaDir?.findFile(it) }
.firstOrNull()
}
@@ -113,7 +122,7 @@ class DownloadProvider(
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): Pair<UniFile?, List<UniFile>> {
val mangaDir = findMangaDir(manga.title, source) ?: return null to emptyList()
return mangaDir to chapters.mapNotNull { chapter ->
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull()
}
@@ -125,7 +134,10 @@ class DownloadProvider(
* @param source the source to query.
*/
fun getSourceDirName(source: Source): String {
return DiskUtil.buildValidFilename(source.toString())
return DiskUtil.buildValidFilename(
source.toString(),
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames().get(),
)
}
/**
@@ -134,23 +146,75 @@ class DownloadProvider(
* @param mangaTitle the title of the manga to query.
*/
fun getMangaDirName(mangaTitle: String): String {
return DiskUtil.buildValidFilename(mangaTitle)
return DiskUtil.buildValidFilename(
mangaTitle,
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames().get(),
)
}
/**
* Returns the chapter directory name for a chapter.
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query
* @param chapterScanlator scanlator of the chapter to query.
* @param chapterUrl url of the chapter to query.
*/
fun getChapterDirName(chapterName: String, chapterScanlator: String?): String {
val newChapterName = sanitizeChapterName(chapterName)
return DiskUtil.buildValidFilename(
fun getChapterDirName(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
disallowNonAsciiFilenames: Boolean = libraryPreferences.disallowNonAsciiFilenames().get(),
): String {
var dirName = sanitizeChapterName(chapterName)
if (!chapterScanlator.isNullOrBlank()) {
dirName = chapterScanlator + "_" + dirName
}
// Subtract 7 bytes for hash and underscore, 4 bytes for .cbz
dirName = DiskUtil.buildValidFilename(dirName, DiskUtil.MAX_FILE_NAME_BYTES - 11, disallowNonAsciiFilenames)
dirName += "_" + md5(chapterUrl).take(6)
return dirName
}
/**
* Returns list of names that might have been previously used as
* the directory name for a chapter.
* Add to this list if naming pattern ever changes.
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query.
* @param chapterUrl url of the chapter to query.
*/
private fun getLegacyChapterDirNames(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
): List<String> {
val sanitizedChapterName = sanitizeChapterName(chapterName)
val chapterNameV1 = DiskUtil.buildValidFilename(
when {
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$newChapterName"
else -> newChapterName
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$sanitizedChapterName"
else -> sanitizedChapterName
},
)
// Get the filename that would be generated if the user were
// using the other value for the disallow non-ASCII
// filenames setting. This ensures that chapters downloaded
// before the user changed the setting can still be found.
val otherChapterDirName =
getChapterDirName(
chapterName,
chapterScanlator,
chapterUrl,
!libraryPreferences.disallowNonAsciiFilenames().get(),
)
return buildList(2) {
// Chapter name without hash (unable to handle duplicate
// chapter names)
add(chapterNameV1)
add(otherChapterDirName)
}
}
/**
@@ -165,24 +229,30 @@ class DownloadProvider(
}
fun isChapterDirNameChanged(oldChapter: Chapter, newChapter: Chapter): Boolean {
return oldChapter.name != newChapter.name ||
oldChapter.scanlator?.takeIf { it.isNotBlank() } != newChapter.scanlator?.takeIf { it.isNotBlank() }
return getChapterDirName(oldChapter.name, oldChapter.scanlator, oldChapter.url) !=
getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
}
/**
* Returns valid downloaded chapter directory names.
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query
* @param chapter the domain chapter object.
*/
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?): List<String> {
val chapterDirName = getChapterDirName(chapterName, chapterScanlator)
return buildList(2) {
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?, chapterUrl: String): List<String> {
val chapterDirName = getChapterDirName(chapterName, chapterScanlator, chapterUrl)
val legacyChapterDirNames = getLegacyChapterDirNames(chapterName, chapterScanlator, chapterUrl)
return buildList {
// Folder of images
add(chapterDirName)
// Archived chapters
add("$chapterDirName.cbz")
// any legacy names
legacyChapterDirNames.forEach {
add(it)
add("$it.cbz")
}
}
}
}

View File

@@ -191,15 +191,17 @@ class Downloader(
if (isRunning) return
downloaderJob = scope.launch {
val activeDownloadsFlow = queueState.transformLatest { queue ->
val activeDownloadsFlow = combine(
queueState,
downloadPreferences.parallelSourceLimit().changes(),
) { a, b -> a to b }.transformLatest { (queue, parallelCount) ->
while (true) {
val activeDownloads = queue.asSequence()
// Ignore completed downloads, leave them in the queue
.filter { it.status.value <= Download.State.DOWNLOADING.value }
.groupBy { it.source }
.toList()
// Concurrently download from 5 different sources
.take(5)
.take(parallelCount)
.map { (_, downloads) -> downloads.first() }
emit(activeDownloads)
@@ -211,7 +213,8 @@ class Downloader(
}.filter { it }
activeDownloadsErroredFlow.first()
}
}.distinctUntilChanged()
}
.distinctUntilChanged()
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
supervisorScope {
@@ -274,7 +277,7 @@ class Downloader(
val wasEmpty = queueState.value.isEmpty()
val chaptersToQueue = chapters.asSequence()
// Filter out those already downloaded.
.filter { provider.findChapterDir(it.name, it.scanlator, manga.title, source) == null }
.filter { provider.findChapterDir(it.name, it.scanlator, it.url, manga.title, source) == null }
// Add chapters to queue from the start.
.sortedByDescending { it.sourceOrder }
// Filter out those already enqueued.
@@ -299,7 +302,10 @@ class Downloader(
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
) {
notifier.onWarning(
context.stringResource(MR.strings.download_queue_size_warning),
context.stringResource(
MR.strings.download_queue_size_warning,
context.stringResource(MR.strings.app_name),
),
WARNING_NOTIF_TIMEOUT_MS,
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
)
@@ -333,7 +339,11 @@ class Downloader(
return
}
val chapterDirname = provider.getChapterDirName(download.chapter.name, download.chapter.scanlator)
val chapterDirname = provider.getChapterDirName(
download.chapter.name,
download.chapter.scanlator,
download.chapter.url,
)
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
try {
@@ -359,24 +369,23 @@ class Downloader(
download.status = Download.State.DOWNLOADING
// Start downloading images, consider we can have downloaded images already
// Concurrently do 2 pages at a time
pageList.asFlow()
.flatMapMerge(concurrency = 2) { page ->
flow {
// Fetch image URL if necessary
if (page.imageUrl.isNullOrEmpty()) {
page.status = Page.State.LoadPage
try {
page.imageUrl = download.source.getImageUrl(page)
} catch (e: Throwable) {
page.status = Page.State.Error(e)
}
pageList.asFlow().flatMapMerge(concurrency = downloadPreferences.parallelPageLimit().get()) { page ->
flow {
// Fetch image URL if necessary
if (page.imageUrl.isNullOrEmpty()) {
page.status = Page.State.LoadPage
try {
page.imageUrl = download.source.getImageUrl(page)
} catch (e: Throwable) {
page.status = Page.State.Error(e)
}
}
withIOContext { getOrDownloadImage(page, download, tmpDir) }
emit(page)
}.flowOn(Dispatchers.IO)
withIOContext { getOrDownloadImage(page, download, tmpDir) }
emit(page)
}
.flowOn(Dispatchers.IO)
}
.collect {
// Do when page is downloaded.
notifier.onProgressChange(download)

View File

@@ -104,6 +104,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
track.remote_id = remoteTrack.remote_id
track.library_id = remoteTrack.library_id
if (track.status != COMPLETED) {
track.status = if (hasReadChapters) READING else track.status

View File

@@ -76,7 +76,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.awaitSuccess()
.parseAs<KitsuAddMangaResult>()
.let {
track.remote_id = it.data.id
track.library_id = it.data.id
track
}
}
@@ -88,7 +88,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
val data = buildJsonObject {
putJsonObject("data") {
put("type", "libraryEntries")
put("id", track.remote_id)
put("id", track.library_id)
putJsonObject("attributes") {
put("status", track.toApiStatus())
put("progress", track.last_chapter_read.toInt())
@@ -102,7 +102,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
authClient.newCall(
Request.Builder()
.url("${BASE_URL}library-entries/${track.remote_id}")
.url("${BASE_URL}library-entries/${track.library_id}")
.headers(
headersOf("Content-Type", VND_API_JSON),
)
@@ -119,7 +119,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
withIOContext {
authClient.newCall(
DELETE(
"${BASE_URL}library-entries/${track.remoteId}",
"${BASE_URL}library-entries/${track.libraryId}",
headers = headersOf("Content-Type", VND_API_JSON),
),
)
@@ -192,7 +192,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
suspend fun getLibManga(track: Track): Track {
return withIOContext {
val url = "${BASE_URL}library-entries".toUri().buildUpon()
.encodedQuery("filter[id]=${track.remote_id}")
.encodedQuery("filter[id]=${track.library_id}")
.appendQueryParameter("include", "manga")
.build()
with(json) {

View File

@@ -21,7 +21,8 @@ data class KitsuListSearchResult(
val manga = included[0].attributes
return TrackSearch.create(TrackerManager.KITSU).apply {
remote_id = userData.id
remote_id = included[0].id
library_id = userData.id
title = manga.canonicalTitle
total_chapters = manga.chapterCount ?: 0
cover_url = manga.posterImage?.original ?: ""

View File

@@ -70,7 +70,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
}
override suspend fun refresh(track: Track): Track {
val remoteTrack = api.getTrackSearch(track.tracking_url)
val remoteTrack = api.getTrackSearch(track.remote_id)
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
return track
@@ -88,14 +88,13 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
override suspend fun match(manga: DomainManga): TrackSearch? =
try {
api.getTrackSearch(manga.url)
api.getTrackSearch(manga.url.getMangaId())
} catch (e: Exception) {
null
}
override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean = source?.let {
accept(it)
} == true
override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean =
track.remoteUrl == manga.url && source?.let { accept(it) } == true
override fun migrateTrack(track: DomainTrack, manga: DomainManga, newSource: Source): DomainTrack? =
if (accept(newSource)) {
@@ -103,4 +102,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
} else {
null
}
private fun String.getMangaId(): Long =
this.substringAfterLast('/').toLong()
}

View File

@@ -1,22 +1,22 @@
package eu.kanade.tachiyomi.data.track.suwayomi
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.PUT
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.json.Json
import okhttp3.Credentials
import okhttp3.Dns
import okhttp3.FormBody
import okhttp3.Headers
import kotlinx.serialization.json.addAll
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@@ -25,79 +25,147 @@ import java.security.MessageDigest
class SuwayomiApi(private val trackId: Long) {
private val network: NetworkHelper by injectLazy()
private val json: Json by injectLazy()
private val client: OkHttpClient =
network.client.newBuilder()
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
.build()
private val sourceManager: SourceManager by injectLazy()
private val source: HttpSource by lazy { (sourceManager.get(sourceId) as HttpSource) }
private val client: OkHttpClient by lazy { source.client }
private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') }
private val apiUrl: String by lazy { "$baseUrl/api/graphql" }
private fun headersBuilder(): Headers.Builder = Headers.Builder().apply {
if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) {
val credentials = Credentials.basic(baseLogin, basePassword)
add("Authorization", credentials)
suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext {
val query = """
|query GetManga(${'$'}mangaId: Int!) {
| manga(id: ${'$'}mangaId) {
| ...MangaFragment
| }
|}
|
|$MangaFragment
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("mangaId", mangaId)
}
}
}
private val headers: Headers by lazy { headersBuilder().build() }
private val baseUrl by lazy { getPrefBaseUrl() }
private val baseLogin by lazy { getPrefBaseLogin() }
private val basePassword by lazy { getPrefBasePassword() }
suspend fun getTrackSearch(trackUrl: String): TrackSearch = withIOContext {
val url = try {
// test if getting api url or manga id
val mangaId = trackUrl.toLong()
"$baseUrl/api/v1/manga/$mangaId"
} catch (e: NumberFormatException) {
trackUrl
}
val manga = with(json) {
client.newCall(GET("$url/full", headers))
client.newCall(
POST(
apiUrl,
body = payload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
.parseAs<MangaDataClass>()
.parseAs<GetMangaResult>()
.data
.entry
}
TrackSearch.create(trackId).apply {
remote_id = mangaId
title = manga.title
cover_url = "$url/thumbnail"
cover_url = "$baseUrl/${manga.thumbnailUrl}"
summary = manga.description.orEmpty()
tracking_url = url
total_chapters = manga.chapterCount
publishing_status = manga.status
last_chapter_read = manga.lastChapterRead?.chapterNumber ?: 0.0
tracking_url = "$baseUrl/manga/$mangaId"
total_chapters = manga.chapters.totalCount.toLong()
publishing_status = manga.status.name
last_chapter_read = manga.latestReadChapter?.chapterNumber ?: 0.0
status = when (manga.unreadCount) {
manga.chapterCount -> Suwayomi.UNREAD
0L -> Suwayomi.COMPLETED
manga.chapters.totalCount -> Suwayomi.UNREAD
0 -> Suwayomi.COMPLETED
else -> Suwayomi.READING
}
}
}
suspend fun updateProgress(track: Track): Track {
val url = track.tracking_url
val chapters = with(json) {
client.newCall(GET("$url/chapters", headers))
.awaitSuccess()
.parseAs<List<ChapterDataClass>>()
val mangaId = track.remote_id
val chaptersQuery = """
|query GetMangaUnreadChapters(${'$'}mangaId: Int!) {
| chapters(condition: {mangaId: ${'$'}mangaId, isRead: false}) {
| nodes {
| id
| chapterNumber
| }
| }
|}
""".trimMargin()
val chaptersPayload = buildJsonObject {
put("query", chaptersQuery)
putJsonObject("variables") {
put("mangaId", mangaId)
}
}
val chaptersToMark = with(json) {
client.newCall(
POST(
apiUrl,
body = chaptersPayload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
.parseAs<GetMangaUnreadChaptersResult>()
.data
.entry
.nodes
.mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read } }
}
val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index
client.newCall(
PUT(
"$url/chapter/$lastChapterIndex",
headers,
FormBody.Builder(Charset.forName("utf8"))
.add("markPrevRead", "true")
.add("read", "true")
.build(),
),
).awaitSuccess()
val markQuery = """
|mutation MarkChaptersRead(${'$'}chapters: [Int!]!) {
| updateChapters(input: {ids: ${'$'}chapters, patch: {isRead: true}}) {
| chapters {
| id
| }
| }
|}
""".trimMargin()
val markPayload = buildJsonObject {
put("query", markQuery)
putJsonObject("variables") {
putJsonArray("chapters") {
addAll(chaptersToMark)
}
}
}
with(json) {
client.newCall(
POST(
apiUrl,
body = markPayload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
}
return getTrackSearch(track.tracking_url)
val trackQuery = """
|mutation TrackManga(${'$'}mangaId: Int!) {
| trackProgress(input: {mangaId: ${'$'}mangaId}) {
| trackRecords {
| lastChapterRead
| }
| }
|}
""".trimMargin()
val trackPayload = buildJsonObject {
put("query", trackQuery)
putJsonObject("variables") {
put("mangaId", mangaId)
}
}
with(json) {
client.newCall(
POST(
apiUrl,
body = trackPayload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
}
return getTrackSearch(track.remote_id)
}
private val sourceId by lazy {
@@ -106,18 +174,35 @@ class SuwayomiApi(private val trackId: Long) {
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
}
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$sourceId", Context.MODE_PRIVATE)
companion object {
private val MangaFragment = """
|fragment MangaFragment on MangaType {
| artist
| author
| description
| id
| status
| thumbnailUrl
| title
| url
| genre
| inLibraryAt
| chapters {
| totalCount
| }
| latestUploadedChapter {
| uploadDate
| }
| latestFetchedChapter {
| fetchedAt
| }
| latestReadChapter {
| lastReadAt
| chapterNumber
| }
| unreadCount
| downloadCount
|}
""".trimMargin()
}
private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!!
private fun getPrefBaseLogin(): String = preferences.getString(LOGIN_TITLE, LOGIN_DEFAULT)!!
private fun getPrefBasePassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!!
}
private const val ADDRESS_TITLE = "Server URL Address"
private const val ADDRESS_DEFAULT = ""
private const val LOGIN_TITLE = "Login (Basic Auth)"
private const val LOGIN_DEFAULT = ""
private const val PASSWORD_TITLE = "Password (Basic Auth)"
private const val PASSWORD_DEFAULT = ""

View File

@@ -1,100 +1,90 @@
package eu.kanade.tachiyomi.data.track.suwayomi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
public enum class MangaStatus(
public val rawValue: String,
) {
UNKNOWN("UNKNOWN"),
ONGOING("ONGOING"),
COMPLETED("COMPLETED"),
LICENSED("LICENSED"),
PUBLISHING_FINISHED("PUBLISHING_FINISHED"),
CANCELLED("CANCELLED"),
ON_HIATUS("ON_HIATUS"),
}
@Serializable
data class SourceDataClass(
val id: String,
val name: String,
val lang: String,
val iconUrl: String,
public data class MangaFragment(
public val artist: String?,
public val author: String?,
public val description: String?,
public val id: Int,
public val status: MangaStatus,
public val thumbnailUrl: String?,
public val title: String,
public val url: String,
public val genre: List<String>,
public val inLibraryAt: Long,
public val chapters: Chapters,
public val latestUploadedChapter: LatestUploadedChapter?,
public val latestFetchedChapter: LatestFetchedChapter?,
public val latestReadChapter: LatestReadChapter?,
public val unreadCount: Int,
public val downloadCount: Int,
) {
@Serializable
public data class Chapters(
public val totalCount: Int,
)
/** The Source provides a latest listing */
val supportsLatest: Boolean,
@Serializable
public data class LatestUploadedChapter(
public val uploadDate: Long,
)
/** The Source implements [ConfigurableSource] */
val isConfigurable: Boolean,
@Serializable
public data class LatestFetchedChapter(
public val fetchedAt: Long,
)
/** The Source class has a @Nsfw annotation */
val isNsfw: Boolean,
@Serializable
public data class LatestReadChapter(
public val lastReadAt: Long,
public val chapterNumber: Double,
)
}
/** A nicer version of [name] */
val displayName: String,
@Serializable
public data class GetMangaResult(
public val data: GetMangaData,
)
@Serializable
data class MangaDataClass(
val id: Int,
val sourceId: String,
val url: String,
val title: String,
val thumbnailUrl: String?,
val initialized: Boolean,
val artist: String?,
val author: String?,
val description: String?,
val genre: List<String>,
val status: String,
val inLibrary: Boolean,
val inLibraryAt: Long,
val source: SourceDataClass?,
val meta: Map<String, String>,
val realUrl: String?,
val lastFetchedAt: Long?,
val chaptersLastFetchedAt: Long?,
val freshData: Boolean,
val unreadCount: Long?,
val downloadCount: Long?,
val chapterCount: Long, // actually is nullable server side, but should be set at this time
val lastChapterRead: ChapterDataClass?,
val age: Long?,
val chaptersAge: Long?,
public data class GetMangaData(
@SerialName("manga")
public val entry: MangaFragment,
)
@Serializable
data class ChapterDataClass(
val id: Int,
val url: String,
val name: String,
val uploadDate: Long,
val chapterNumber: Double,
val scanlator: String?,
val mangaId: Int,
/** chapter is read */
val read: Boolean,
/** chapter is bookmarked */
val bookmarked: Boolean,
/** last read page, zero means not read/no data */
val lastPageRead: Int,
/** last read page, zero means not read/no data */
val lastReadAt: Long,
/** this chapter's index, starts with 1 */
val index: Int,
/** the date we fist saw this chapter*/
val fetchedAt: Long,
/** is chapter downloaded */
val downloaded: Boolean,
/** used to construct pages in the front-end */
val pageCount: Int,
/** total chapter count, used to calculate if there's a next and prev chapter */
val chapterCount: Int?,
/** used to store client specific values */
val meta: Map<String, String>,
public data class GetMangaUnreadChaptersEntry(
public val nodes: List<GetMangaUnreadChaptersNode>,
)
@Serializable
public data class GetMangaUnreadChaptersNode(
public val id: Int,
public val chapterNumber: Double,
)
@Serializable
public data class GetMangaUnreadChaptersResult(
public val data: GetMangaUnreadChaptersData,
)
@Serializable
public data class GetMangaUnreadChaptersData(
@SerialName("chapters")
public val entry: GetMangaUnreadChaptersEntry,
)

View File

@@ -140,7 +140,7 @@ class ExtensionManager(
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
withUIContext { context.toast(MR.strings.extension_api_error) }
emptyList()
return
}
enableAdditionalSubLanguages(extensions)

View File

@@ -1,27 +1,73 @@
package eu.kanade.tachiyomi.extension.installer
import android.app.Service
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.Process
import android.os.IBinder
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.system.getUriSize
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.app.shizuku.IShellInterface
import mihon.app.shizuku.ShellInterface
import rikka.shizuku.Shizuku
import tachiyomi.core.common.util.system.logcat
import tachiyomi.i18n.MR
import java.io.BufferedReader
import java.io.InputStream
class ShizukuInstaller(private val service: Service) : Installer(service) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var shellInterface: IShellInterface? = null
private val shizukuArgs by lazy {
Shizuku.UserServiceArgs(
ComponentName(service, ShellInterface::class.java),
)
.tag("shizuku_service")
.processNameSuffix("shizuku_service")
.debuggable(BuildConfig.DEBUG)
.daemon(false)
}
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
shellInterface = IShellInterface.Stub.asInterface(service)
ready = true
checkQueue()
}
override fun onServiceDisconnected(name: ComponentName?) {
shellInterface = null
}
}
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Int.MIN_VALUE)
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
if (status == PackageInstaller.STATUS_SUCCESS) {
continueQueue(InstallStep.Installed)
} else {
logcat(LogPriority.ERROR) { "Failed to install extension $packageName: $message" }
continueQueue(InstallStep.Error)
}
}
}
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
logcat { "Shizuku was killed prematurely" }
service.stopSelf()
@@ -31,8 +77,8 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
if (grantResult == PackageManager.PERMISSION_GRANTED) {
ready = true
checkQueue()
Shizuku.bindUserService(shizukuArgs, connection)
} else {
service.stopSelf()
}
@@ -41,40 +87,34 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
}
}
fun initShizuku() {
if (ready) return
if (!Shizuku.pingBinder()) {
logcat(LogPriority.ERROR) { "Shizuku is not ready to use" }
service.toast(MR.strings.ext_installer_shizuku_stopped)
service.stopSelf()
return
}
if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
Shizuku.bindUserService(shizukuArgs, connection)
} else {
Shizuku.addRequestPermissionResultListener(shizukuPermissionListener)
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
}
}
override var ready = false
override fun processEntry(entry: Entry) {
super.processEntry(entry)
scope.launch {
var sessionId: String? = null
try {
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
service.contentResolver.openInputStream(entry.uri)!!.use {
val userId = Process.myUserHandle().hashCode()
val createCommand = "pm install-create --user $userId -r -i ${service.packageName} -S $size"
val createResult = exec(createCommand)
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
?: throw RuntimeException("Failed to create install session")
val writeResult = exec("pm install-write -S $size $sessionId base -", it)
if (writeResult.resultCode != 0) {
throw RuntimeException("Failed to write APK to session $sessionId")
}
val commitResult = exec("pm install-commit $sessionId")
if (commitResult.resultCode != 0) {
throw RuntimeException("Failed to commit install session $sessionId")
}
continueQueue(InstallStep.Installed)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
if (sessionId != null) {
exec("pm install-abandon $sessionId")
}
continueQueue(InstallStep.Error)
}
try {
shellInterface?.install(
service.contentResolver.openAssetFileDescriptor(entry.uri, "r"),
)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
continueQueue(InstallStep.Error)
}
}
@@ -84,41 +124,26 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
override fun onDestroy() {
Shizuku.removeBinderDeadListener(shizukuDeadListener)
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
Shizuku.unbindUserService(shizukuArgs, connection, true)
service.unregisterReceiver(receiver)
logcat { "ShizukuInstaller destroy" }
scope.cancel()
super.onDestroy()
}
private fun exec(command: String, stdin: InputStream? = null): ShellResult {
@Suppress("DEPRECATION")
val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, null)
if (stdin != null) {
process.outputStream.use { stdin.copyTo(it) }
}
val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
val resultCode = process.waitFor()
return ShellResult(resultCode, output)
}
private data class ShellResult(val resultCode: Int, val out: String)
init {
Shizuku.addBinderDeadListener(shizukuDeadListener)
ready = if (Shizuku.pingBinder()) {
if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
true
} else {
Shizuku.addRequestPermissionResultListener(shizukuPermissionListener)
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
false
}
} else {
logcat(LogPriority.ERROR) { "Shizuku is not ready to use" }
service.toast(MR.strings.ext_installer_shizuku_stopped)
service.stopSelf()
false
}
ContextCompat.registerReceiver(
service,
receiver,
IntentFilter(ACTION_INSTALL_RESULT),
ContextCompat.RECEIVER_EXPORTED,
)
initShizuku()
}
}
private const val SHIZUKU_PERMISSION_REQUEST_CODE = 14045
private val SESSION_ID_REGEX = Regex("(?<=\\[).+?(?=])")
const val ACTION_INSTALL_RESULT = "${BuildConfig.APPLICATION_ID}.ACTION_INSTALL_RESULT"

View File

@@ -169,6 +169,7 @@ class ExtensionsScreenModel(
fun cancelInstallUpdateExtension(extension: Extension) {
extensionManager.cancelInstallUpdateExtension(extension)
removeDownloadState(extension)
}
private fun addDownloadState(extension: Extension, installStep: InstallStep) {

View File

@@ -484,6 +484,7 @@ class LibraryScreenModel(
downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
chapter.url,
manga.title,
manga.source,
)

View File

@@ -527,7 +527,13 @@ class MangaScreenModel(
val downloaded = if (isLocal) {
true
} else {
downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
chapter.url,
manga.title,
manga.source,
)
}
val downloadState = when {
activeDownload != null -> activeDownload.status

View File

@@ -54,6 +54,7 @@ import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import eu.kanade.tachiyomi.util.lang.toLocalDate
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
@@ -84,7 +85,6 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZoneOffset
data class TrackInfoDialogHomeScreen(
@@ -220,7 +220,7 @@ data class TrackInfoDialogHomeScreen(
try {
val matchResult = item.tracker.match(manga) ?: throw Exception()
item.tracker.register(matchResult, mangaId)
} catch (e: Exception) {
} catch (_: Exception) {
withUIContext { Injekt.get<Application>().toast(MR.strings.error_no_match) }
}
}
@@ -446,56 +446,46 @@ private data class TrackDateSelectorScreen(
@Transient
private val selectableDates = object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
val dateToCheck = Instant.ofEpochMilli(utcTimeMillis)
.atZone(ZoneOffset.systemDefault())
.toLocalDate()
val targetDate = Instant.ofEpochMilli(utcTimeMillis).toLocalDate(ZoneOffset.UTC)
if (dateToCheck > LocalDate.now()) {
// Disallow future dates
return false
}
// Disallow future dates
if (targetDate > LocalDate.now(ZoneOffset.UTC)) return false
return if (start && track.finishDate > 0) {
// Disallow start date to be set later than finish date
val dateFinished = Instant.ofEpochMilli(track.finishDate)
.atZone(ZoneId.systemDefault())
.toLocalDate()
dateToCheck <= dateFinished
} else if (!start && track.startDate > 0) {
// Disallow end date to be set earlier than start date
val dateStarted = Instant.ofEpochMilli(track.startDate)
.atZone(ZoneId.systemDefault())
.toLocalDate()
dateToCheck >= dateStarted
} else {
// Nothing set before
true
return when {
// Disallow setting start date after finish date
start && track.finishDate > 0 -> {
val finishDate = Instant.ofEpochMilli(track.finishDate).toLocalDate(ZoneOffset.UTC)
targetDate <= finishDate
}
// Disallow setting finish date before start date
!start && track.startDate > 0 -> {
val startDate = Instant.ofEpochMilli(track.startDate).toLocalDate(ZoneOffset.UTC)
startDate <= targetDate
}
else -> {
true
}
}
}
override fun isSelectableYear(year: Int): Boolean {
if (year > LocalDate.now().year) {
// Disallow future dates
return false
}
// Disallow future years
if (year > LocalDate.now(ZoneOffset.UTC).year) return false
return if (start && track.finishDate > 0) {
// Disallow start date to be set later than finish date
val dateFinished = Instant.ofEpochMilli(track.finishDate)
.atZone(ZoneId.systemDefault())
.toLocalDate()
.year
year <= dateFinished
} else if (!start && track.startDate > 0) {
// Disallow end date to be set earlier than start date
val dateStarted = Instant.ofEpochMilli(track.startDate)
.atZone(ZoneId.systemDefault())
.toLocalDate()
.year
year >= dateStarted
} else {
// Nothing set before
true
return when {
// Disallow setting start year after finish year
start && track.finishDate > 0 -> {
val finishDate = Instant.ofEpochMilli(track.finishDate).toLocalDate(ZoneOffset.UTC)
year <= finishDate.year
}
// Disallow setting finish year before start year
!start && track.startDate > 0 -> {
val startDate = Instant.ofEpochMilli(track.startDate).toLocalDate(ZoneOffset.UTC)
startDate.year <= year
}
else -> {
true
}
}
}
}

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.reader
import android.annotation.SuppressLint
import android.app.Activity
import android.app.assist.AssistContent
import android.content.ClipData
import android.content.ClipboardManager
@@ -16,40 +15,45 @@ import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.View.LAYER_TYPE_HARDWARE
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.transition.doOnEnd
import androidx.core.view.WindowCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.lifecycleScope
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.transition.platform.MaterialContainerTransform
import com.hippo.unifile.UniFile
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.core.util.ifSourcesLoaded
import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.reader.DisplayRefreshHost
import eu.kanade.presentation.reader.OrientationSelectDialog
import eu.kanade.presentation.reader.PageIndicatorText
import eu.kanade.presentation.reader.ReaderContentOverlay
import eu.kanade.presentation.reader.ReaderPageActionsDialog
import eu.kanade.presentation.reader.ReaderPageIndicator
import eu.kanade.presentation.reader.ReadingModeSelectDialog
import eu.kanade.presentation.reader.appbars.ReaderAppBars
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
@@ -73,18 +77,17 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import eu.kanade.tachiyomi.util.system.isNightMode
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setComposeContent
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
@@ -121,8 +124,6 @@ class ReaderActivity : BaseActivity() {
val viewModel by viewModels<ReaderViewModel>()
private var assistUrl: String? = null
private val hasCutout by lazy { hasDisplayCutout() }
/**
* Configuration at reader level, like background color or forced orientation.
*/
@@ -132,7 +133,7 @@ class ReaderActivity : BaseActivity() {
private var readingModeToast: Toast? = null
private val displayRefreshHost = DisplayRefreshHost()
private val windowInsetsController by lazy { WindowInsetsControllerCompat(window, binding.root) }
private val windowInsetsController by lazy { WindowInsetsControllerCompat(window, window.decorView) }
private var loadingIndicator: ReaderProgressIndicator? = null
@@ -146,7 +147,7 @@ class ReaderActivity : BaseActivity() {
registerSecureActivity(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_OPEN,
OVERRIDE_TRANSITION_OPEN,
R.anim.shared_axis_x_push_enter,
R.anim.shared_axis_x_push_exit,
)
@@ -155,10 +156,17 @@ class ReaderActivity : BaseActivity() {
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
}
enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
super.onCreate(savedInstanceState)
binding = ReaderActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.setComposeOverlay()
if (viewModel.needsInit()) {
val manga = intent.extras?.getLong("manga", -1) ?: -1L
@@ -181,7 +189,7 @@ class ReaderActivity : BaseActivity() {
}
config = ReaderConfig()
initializeMenu()
setMenuVisibility(viewModel.state.value.menuVisible)
// Finish when incognito mode is disabled
preferences.incognitoMode().changes()
@@ -238,6 +246,92 @@ class ReaderActivity : BaseActivity() {
.launchIn(lifecycleScope)
}
private fun ReaderActivityBinding.setComposeOverlay(): Unit = composeOverlay.setComposeContent {
val state by viewModel.state.collectAsState()
val showPageNumber by readerPreferences.showPageNumber().collectAsState()
val settingsScreenModel = remember {
ReaderSettingsScreenModel(
readerState = viewModel.state,
onChangeReadingMode = viewModel::setMangaReadingMode,
onChangeOrientation = viewModel::setMangaOrientationType,
)
}
Box(modifier = Modifier.fillMaxSize()) {
if (!state.menuVisible && showPageNumber) {
ReaderPageIndicator(
currentPage = state.currentPage,
totalPages = state.totalPages,
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding(),
)
}
ContentOverlay(state = state)
AppBars(state = state)
}
val onDismissRequest = viewModel::closeDialog
when (state.dialog) {
is ReaderViewModel.Dialog.Loading -> {
AlertDialog(
onDismissRequest = {},
confirmButton = {},
text = {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CircularProgressIndicator()
Text(stringResource(MR.strings.loading))
}
},
)
}
is ReaderViewModel.Dialog.Settings -> {
ReaderSettingsDialog(
onDismissRequest = onDismissRequest,
onShowMenus = { setMenuVisibility(true) },
onHideMenus = { setMenuVisibility(false) },
screenModel = settingsScreenModel,
)
}
is ReaderViewModel.Dialog.ReadingModeSelect -> {
ReadingModeSelectDialog(
onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel,
onChange = { stringRes ->
menuToggleToast?.cancel()
if (!readerPreferences.showReadingMode().get()) {
menuToggleToast = toast(stringRes)
}
},
)
}
is ReaderViewModel.Dialog.OrientationModeSelect -> {
OrientationSelectDialog(
onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel,
onChange = { stringRes ->
menuToggleToast?.cancel()
menuToggleToast = toast(stringRes)
},
)
}
is ReaderViewModel.Dialog.PageActions -> {
ReaderPageActionsDialog(
onDismissRequest = onDismissRequest,
onSetAsCover = viewModel::setAsCover,
onShare = viewModel::shareImage,
onSave = viewModel::saveImage,
)
}
null -> {}
}
}
/**
* Called when the activity is destroyed. Cleans up the viewer, configuration and any view.
*/
@@ -289,7 +383,7 @@ class ReaderActivity : BaseActivity() {
super.finish()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_CLOSE,
OVERRIDE_TRANSITION_CLOSE,
R.anim.shared_axis_x_pop_enter,
R.anim.shared_axis_x_pop_exit,
)
@@ -327,180 +421,82 @@ class ReaderActivity : BaseActivity() {
return handled || super.dispatchGenericMotionEvent(event)
}
/**
* Initializes the reader menu. It sets up click listeners and the initial visibility.
*/
private fun initializeMenu() {
binding.pageNumber.setComposeContent {
val state by viewModel.state.collectAsState()
val showPageNumber by viewModel.readerPreferences.showPageNumber().collectAsState()
@Composable
private fun ContentOverlay(state: ReaderViewModel.State) {
val flashOnPageChange by readerPreferences.flashOnPageChange().collectAsState()
if (!state.menuVisible && showPageNumber) {
PageIndicatorText(
currentPage = state.currentPage,
totalPages = state.totalPages,
)
}
val colorOverlayEnabled by readerPreferences.colorFilter().collectAsState()
val colorOverlay by readerPreferences.colorFilterValue().collectAsState()
val colorOverlayMode by readerPreferences.colorFilterMode().collectAsState()
val colorOverlayBlendMode = remember(colorOverlayMode) {
ReaderPreferences.ColorFilterMode.getOrNull(colorOverlayMode)?.second
}
binding.dialogRoot.setComposeContent {
val state by viewModel.state.collectAsState()
val settingsScreenModel = remember {
ReaderSettingsScreenModel(
readerState = viewModel.state,
hasDisplayCutout = hasCutout,
onChangeReadingMode = viewModel::setMangaReadingMode,
onChangeOrientation = viewModel::setMangaOrientationType,
)
}
if (!ifSourcesLoaded()) {
return@setComposeContent
}
val isHttpSource = viewModel.getSource() is HttpSource
val isFullscreen by readerPreferences.fullscreen().collectAsState()
val flashOnPageChange by readerPreferences.flashOnPageChange().collectAsState()
val colorOverlayEnabled by readerPreferences.colorFilter().collectAsState()
val colorOverlay by readerPreferences.colorFilterValue().collectAsState()
val colorOverlayMode by readerPreferences.colorFilterMode().collectAsState()
val colorOverlayBlendMode = remember(colorOverlayMode) {
ReaderPreferences.ColorFilterMode.getOrNull(colorOverlayMode)?.second
}
val cropBorderPaged by readerPreferences.cropBorders().collectAsState()
val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState()
val isPagerType = ReadingMode.isPagerType(viewModel.getMangaReadingMode())
val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon
ReaderContentOverlay(
brightness = state.brightnessOverlayValue,
color = colorOverlay.takeIf { colorOverlayEnabled },
colorBlendMode = colorOverlayBlendMode,
)
ReaderAppBars(
visible = state.menuVisible,
fullscreen = isFullscreen,
mangaTitle = state.manga?.title,
chapterTitle = state.currentChapter?.chapter?.name,
navigateUp = onBackPressedDispatcher::onBackPressed,
onClickTopAppBar = ::openMangaScreen,
bookmarked = state.bookmarked,
onToggleBookmarked = viewModel::toggleChapterBookmark,
onOpenInWebView = ::openChapterInWebView.takeIf { isHttpSource },
onOpenInBrowser = ::openChapterInBrowser.takeIf { isHttpSource },
onShare = ::shareChapter.takeIf { isHttpSource },
viewer = state.viewer,
onNextChapter = ::loadNextChapter,
enabledNext = state.viewerChapters?.nextChapter != null,
onPreviousChapter = ::loadPreviousChapter,
enabledPrevious = state.viewerChapters?.prevChapter != null,
currentPage = state.currentPage,
totalPages = state.totalPages,
onPageIndexChange = {
isScrollingThroughPages = true
moveToPageIndex(it)
},
readingMode = ReadingMode.fromPreference(
viewModel.getMangaReadingMode(resolveDefault = false),
),
onClickReadingMode = viewModel::openReadingModeSelectDialog,
orientation = ReaderOrientation.fromPreference(
viewModel.getMangaOrientation(resolveDefault = false),
),
onClickOrientation = viewModel::openOrientationModeSelectDialog,
cropEnabled = cropEnabled,
onClickCropBorder = {
val enabled = viewModel.toggleCropBorders()
menuToggleToast?.cancel()
menuToggleToast = toast(if (enabled) MR.strings.on else MR.strings.off)
},
onClickSettings = viewModel::openSettingsDialog,
)
if (flashOnPageChange) {
DisplayRefreshHost(
hostState = displayRefreshHost,
)
}
val onDismissRequest = viewModel::closeDialog
when (state.dialog) {
is ReaderViewModel.Dialog.Loading -> {
AlertDialog(
onDismissRequest = {},
confirmButton = {},
text = {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CircularProgressIndicator()
Text(stringResource(MR.strings.loading))
}
},
)
}
is ReaderViewModel.Dialog.Settings -> {
ReaderSettingsDialog(
onDismissRequest = onDismissRequest,
onShowMenus = { setMenuVisibility(true) },
onHideMenus = { setMenuVisibility(false) },
screenModel = settingsScreenModel,
)
}
is ReaderViewModel.Dialog.ReadingModeSelect -> {
ReadingModeSelectDialog(
onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel,
onChange = { stringRes ->
menuToggleToast?.cancel()
if (!readerPreferences.showReadingMode().get()) {
menuToggleToast = toast(stringRes)
}
},
)
}
is ReaderViewModel.Dialog.OrientationModeSelect -> {
OrientationSelectDialog(
onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel,
onChange = { stringRes ->
menuToggleToast?.cancel()
menuToggleToast = toast(stringRes)
},
)
}
is ReaderViewModel.Dialog.PageActions -> {
ReaderPageActionsDialog(
onDismissRequest = onDismissRequest,
onSetAsCover = viewModel::setAsCover,
onShare = viewModel::shareImage,
onSave = viewModel::saveImage,
)
}
null -> {}
}
}
val toolbarColor = ColorUtils.setAlphaComponent(
SurfaceColors.SURFACE_2.getColor(this),
if (isNightMode()) 230 else 242, // 90% dark 95% light
ReaderContentOverlay(
brightness = state.brightnessOverlayValue,
color = colorOverlay.takeIf { colorOverlayEnabled },
colorBlendMode = colorOverlayBlendMode,
)
@Suppress("DEPRECATION")
window.statusBarColor = toolbarColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
@Suppress("DEPRECATION")
window.navigationBarColor = toolbarColor
if (flashOnPageChange) {
DisplayRefreshHost(hostState = displayRefreshHost)
}
}
@Composable
fun AppBars(state: ReaderViewModel.State) {
if (!ifSourcesLoaded()) {
return
}
// Set initial visibility
setMenuVisibility(viewModel.state.value.menuVisible)
val isHttpSource = viewModel.getSource() is HttpSource
val cropBorderPaged by readerPreferences.cropBorders().collectAsState()
val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState()
val isPagerType = ReadingMode.isPagerType(viewModel.getMangaReadingMode())
val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon
ReaderAppBars(
visible = state.menuVisible,
mangaTitle = state.manga?.title,
chapterTitle = state.currentChapter?.chapter?.name,
navigateUp = onBackPressedDispatcher::onBackPressed,
onClickTopAppBar = ::openMangaScreen,
bookmarked = state.bookmarked,
onToggleBookmarked = viewModel::toggleChapterBookmark,
onOpenInWebView = ::openChapterInWebView.takeIf { isHttpSource },
onOpenInBrowser = ::openChapterInBrowser.takeIf { isHttpSource },
onShare = ::shareChapter.takeIf { isHttpSource },
viewer = state.viewer,
onNextChapter = ::loadNextChapter,
enabledNext = state.viewerChapters?.nextChapter != null,
onPreviousChapter = ::loadPreviousChapter,
enabledPrevious = state.viewerChapters?.prevChapter != null,
currentPage = state.currentPage,
totalPages = state.totalPages,
onPageIndexChange = {
isScrollingThroughPages = true
moveToPageIndex(it)
},
readingMode = ReadingMode.fromPreference(
viewModel.getMangaReadingMode(resolveDefault = false),
),
onClickReadingMode = viewModel::openReadingModeSelectDialog,
orientation = ReaderOrientation.fromPreference(
viewModel.getMangaOrientation(resolveDefault = false),
),
onClickOrientation = viewModel::openOrientationModeSelectDialog,
cropEnabled = cropEnabled,
onClickCropBorder = {
val enabled = viewModel.toggleCropBorders()
menuToggleToast?.cancel()
menuToggleToast = toast(if (enabled) MR.strings.on else MR.strings.off)
},
onClickSettings = viewModel::openSettingsDialog,
)
}
/**
@@ -510,13 +506,8 @@ class ReaderActivity : BaseActivity() {
viewModel.showMenus(visible)
if (visible) {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
} else {
if (readerPreferences.fullscreen().get()) {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else if (readerPreferences.fullscreen().get()) {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
}
}
@@ -542,7 +533,7 @@ class ReaderActivity : BaseActivity() {
binding.viewerContainer.removeAllViews()
}
viewModel.onViewerLoaded(newViewer)
updateViewerInset(readerPreferences.fullscreen().get())
updateViewerInset(readerPreferences.fullscreen().get(), readerPreferences.drawUnderCutout().get())
binding.viewerContainer.addView(newViewer.getView())
if (readerPreferences.showReadingMode().get()) {
@@ -593,7 +584,7 @@ class ReaderActivity : BaseActivity() {
try {
readingModeToast?.cancel()
readingModeToast = toast(ReadingMode.fromPreference(mode).stringRes)
} catch (e: ArrayIndexOutOfBoundsException) {
} catch (_: ArrayIndexOutOfBoundsException) {
logcat(LogPriority.ERROR) { "Unknown reading mode: $mode" }
}
}
@@ -785,16 +776,32 @@ class ReaderActivity : BaseActivity() {
/**
* Updates viewer inset depending on fullscreen reader preferences.
*/
private fun updateViewerInset(fullscreen: Boolean) {
viewModel.state.value.viewer?.getView()?.applyInsetter {
if (!fullscreen) {
type(navigationBars = true, statusBars = true) {
padding()
}
}
private fun updateViewerInset(fullscreen: Boolean, drawUnderCutout: Boolean) {
if (!::binding.isInitialized) return
val view = binding.viewerContainer
view.applyInsetsPadding(ViewCompat.getRootWindowInsets(view), fullscreen, drawUnderCutout)
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
view.applyInsetsPadding(windowInsets, fullscreen, drawUnderCutout)
windowInsets
}
}
private fun View.applyInsetsPadding(
windowInsets: WindowInsetsCompat?,
fullscreen: Boolean,
drawUnderCutout: Boolean,
) {
val insets = when {
!fullscreen -> windowInsets?.getInsets(WindowInsetsCompat.Type.systemBars())
!drawUnderCutout -> windowInsets?.getInsets(WindowInsetsCompat.Type.displayCutout())
else -> null
}
?: Insets.NONE
setPadding(insets.left, insets.top, insets.right, insets.bottom)
}
/**
* Class that handles the user preferences of the reader.
*/
@@ -847,10 +854,6 @@ class ReaderActivity : BaseActivity() {
.onEach { setDisplayProfile(it) }
.launchIn(lifecycleScope)
readerPreferences.cutoutShort().changes()
.onEach(::setCutoutShort)
.launchIn(lifecycleScope)
readerPreferences.keepScreenOn().changes()
.onEach(::setKeepScreenOn)
.launchIn(lifecycleScope)
@@ -859,14 +862,21 @@ class ReaderActivity : BaseActivity() {
.onEach(::setCustomBrightness)
.launchIn(lifecycleScope)
merge(readerPreferences.grayscale().changes(), readerPreferences.invertedColors().changes())
.onEach { setLayerPaint(readerPreferences.grayscale().get(), readerPreferences.invertedColors().get()) }
combine(
readerPreferences.grayscale().changes(),
readerPreferences.invertedColors().changes(),
) { grayscale, invertedColors -> grayscale to invertedColors }
.onEach { (grayscale, invertedColors) ->
setLayerPaint(grayscale, invertedColors)
}
.launchIn(lifecycleScope)
readerPreferences.fullscreen().changes()
.onEach {
WindowCompat.setDecorFitsSystemWindows(window, !it)
updateViewerInset(it)
combine(
readerPreferences.fullscreen().changes(),
readerPreferences.drawUnderCutout().changes(),
) { fullscreen, drawUnderCutout -> fullscreen to drawUnderCutout }
.onEach { (fullscreen, drawUnderCutout) ->
updateViewerInset(fullscreen, drawUnderCutout)
}
.launchIn(lifecycleScope)
}
@@ -901,18 +911,6 @@ class ReaderActivity : BaseActivity() {
}
}
private fun setCutoutShort(enabled: Boolean) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return
window.attributes.layoutInDisplayCutoutMode = when (enabled) {
true -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
false -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
// Trigger relayout
setMenuVisibility(viewModel.state.value.menuVisible)
}
/**
* Sets the keep screen on mode according to [enabled].
*/

View File

@@ -38,7 +38,6 @@ import eu.kanade.tachiyomi.util.chapter.filterDownloaded
import eu.kanade.tachiyomi.util.chapter.removeDuplicates
import eu.kanade.tachiyomi.util.editCover
import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.takeBytes
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir
import kotlinx.coroutines.CancellationException
@@ -175,6 +174,7 @@ class ReaderViewModel @JvmOverloads constructor(
!downloadManager.isChapterDownloaded(
it.name,
it.scanlator,
it.url,
manga.title,
manga.source,
)
@@ -184,6 +184,7 @@ class ReaderViewModel @JvmOverloads constructor(
downloadManager.isChapterDownloaded(
it.name,
it.scanlator,
it.url,
manga.title,
manga.source,
)
@@ -397,6 +398,7 @@ class ReaderViewModel @JvmOverloads constructor(
val isDownloaded = downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
manga.title,
manga.source,
skipCache = true,
@@ -473,6 +475,7 @@ class ReaderViewModel @JvmOverloads constructor(
val isNextChapterDownloaded = downloadManager.isChapterDownloaded(
nextChapter.name,
nextChapter.scanlator,
nextChapter.url,
manga.title,
manga.source,
)
@@ -757,7 +760,8 @@ class ReaderViewModel @JvmOverloads constructor(
val chapter = page.chapter.chapter
val filenameSuffix = " - ${page.number}"
return DiskUtil.buildValidFilename(
"${manga.title} - ${chapter.name}".takeBytes(DiskUtil.MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()),
"${manga.title} - ${chapter.name}",
DiskUtil.MAX_FILE_NAME_BYTES - filenameSuffix.byteSize(),
) + filenameSuffix
}

View File

@@ -80,6 +80,7 @@ class ChapterLoader(
val isDownloaded = downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
manga.title,
manga.source,
skipCache = true,

View File

@@ -33,7 +33,13 @@ internal class DownloadPageLoader(
override suspend fun getPages(): List<ReaderPage> {
val dbChapter = chapter.chapter
val chapterPath = downloadProvider.findChapterDir(dbChapter.name, dbChapter.scanlator, manga.title, source)
val chapterPath = downloadProvider.findChapterDir(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
manga.title,
source,
)
return if (chapterPath?.isFile == true) {
getPagesFromArchive(chapterPath)
} else {

View File

@@ -31,7 +31,7 @@ class ReaderPreferences(
fun fullscreen() = preferenceStore.getBoolean("fullscreen", true)
fun cutoutShort() = preferenceStore.getBoolean("cutout_short", true)
fun drawUnderCutout() = preferenceStore.getBoolean("cutout_short", true)
fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", false)

View File

@@ -13,7 +13,6 @@ import uy.kohesive.injekt.api.get
class ReaderSettingsScreenModel(
readerState: StateFlow<ReaderViewModel.State>,
val hasDisplayCutout: Boolean,
val onChangeReadingMode: (ReadingMode) -> Unit,
val onChangeOrientation: (ReaderOrientation) -> Unit,
val preferences: ReaderPreferences = Injekt.get(),

View File

@@ -37,6 +37,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
downloadManager.isChapterDownloaded(
chapterName = goingToChapter.name,
chapterScanlator = goingToChapter.scanlator,
chapterUrl = goingToChapter.url,
mangaTitle = manga.title,
sourceId = manga.source,
skipCache = true,

View File

@@ -107,6 +107,7 @@ class UpdatesScreenModel(
val downloaded = downloadManager.isChapterDownloaded(
update.chapterName,
update.scanlator,
update.chapterUrl,
update.mangaTitle,
update.sourceId,
)

View File

@@ -15,5 +15,5 @@ fun List<Chapter>.filterDownloaded(manga: Manga): List<Chapter> {
val downloadCache: DownloadCache = Injekt.get()
return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source, false) }
return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, it.url, manga.title, manga.source, false) }
}

View File

@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.view.View
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.TabletUiMode
import uy.kohesive.injekt.Injekt
@@ -57,11 +58,19 @@ fun Context.isNightMode(): Boolean {
/**
* Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.).
*
* Only works in Android 9+.
* Only works on Android 9+.
*/
fun Activity.hasDisplayCutout(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
window.decorView.rootWindowInsets?.displayCutout != null
return window.decorView.hasDisplayCutout()
}
/**
* Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.).
*
* Only works on Android 9+.
*/
fun View.hasDisplayCutout(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && rootWindowInsets?.displayCutout != null
}
/**

View File

@@ -0,0 +1,176 @@
/*
* Copyright 2024 Mihon Open Source Project
* Copyright 2015-2024 Javier Tomás
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* The file contains code originally licensed under the MIT license:
*
* Copyright (c) 2024 Zachary Wander
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package mihon.app.shizuku
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.content.pm.PackageInstaller
import android.content.res.AssetFileDescriptor
import android.os.Build
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.os.UserHandle
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.extension.installer.ACTION_INSTALL_RESULT
import rikka.shizuku.SystemServiceHelper
import java.io.OutputStream
import kotlin.system.exitProcess
class ShellInterface : IShellInterface.Stub() {
private val context = createContext()
private val userId = UserHandle::class.java
.getMethod("myUserId")
.invoke(null) as Int
private val packageName = BuildConfig.APPLICATION_ID
@SuppressLint("PrivateApi")
override fun install(apk: AssetFileDescriptor) {
val pmInterface = Class.forName($$"android.content.pm.IPackageManager$Stub")
.getMethod("asInterface", IBinder::class.java)
.invoke(null, SystemServiceHelper.getSystemService("package"))
val packageInstaller = Class.forName("android.content.pm.IPackageManager")
.getMethod("getPackageInstaller")
.invoke(pmInterface)
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
setPackageSource(PackageInstaller.PACKAGE_SOURCE_STORE)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
setInstallerPackageName(packageName)
}
}
val sessionId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
packageInstaller::class.java.getMethod(
"createSession",
PackageInstaller.SessionParams::class.java,
String::class.java,
String::class.java,
Int::class.java,
).invoke(packageInstaller, params, packageName, packageName, userId) as Int
} else {
packageInstaller::class.java.getMethod(
"createSession",
PackageInstaller.SessionParams::class.java,
String::class.java,
Int::class.java,
).invoke(packageInstaller, params, packageName, userId) as Int
}
val session = packageInstaller::class.java
.getMethod("openSession", Int::class.java)
.invoke(packageInstaller, sessionId)
(
session::class.java.getMethod(
"openWrite",
String::class.java,
Long::class.java,
Long::class.java,
).invoke(session, "extension", 0L, apk.length) as ParcelFileDescriptor
).let { fd ->
val revocable = Class.forName("android.os.SystemProperties")
.getMethod("getBoolean", String::class.java, Boolean::class.java)
.invoke(null, "fw.revocable_fd", false) as Boolean
if (revocable) {
ParcelFileDescriptor.AutoCloseOutputStream(fd)
} else {
Class.forName($$"android.os.FileBridge$FileBridgeOutputStream")
.getConstructor(ParcelFileDescriptor::class.java)
.newInstance(fd) as OutputStream
}
}
.use { output ->
apk.createInputStream().use { input -> input.copyTo(output) }
}
val statusIntent = PendingIntent.getBroadcast(
context,
0,
Intent(ACTION_INSTALL_RESULT).setPackage(packageName),
PendingIntent.FLAG_MUTABLE,
)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
session::class.java.getMethod("commit", IntentSender::class.java, Boolean::class.java)
.invoke(session, statusIntent.intentSender, false)
} else {
session::class.java.getMethod("commit", IntentSender::class.java)
.invoke(session, statusIntent.intentSender)
}
}
override fun destroy() {
exitProcess(0)
}
@SuppressLint("PrivateApi")
private fun createContext(): Context {
val activityThread = Class.forName("android.app.ActivityThread")
val systemMain = activityThread.getMethod("systemMain").invoke(null)
val systemContext = activityThread.getMethod("getSystemContext").invoke(systemMain) as Context
val shellUserHandle = UserHandle::class.java
.getConstructor(Int::class.java)
.newInstance(userId)
val shellContext = systemContext::class.java.getMethod(
"createPackageContextAsUser",
String::class.java,
Int::class.java,
UserHandle::class.java,
).invoke(
systemContext,
"com.android.shell",
Context.CONTEXT_INCLUDE_CODE or Context.CONTEXT_IGNORE_SECURITY,
shellUserHandle,
) as Context
return shellContext.createPackageContext("com.android.shell", 0)
}
}

View File

@@ -89,7 +89,7 @@ class MigrateMangaUseCase(
}
// Update categories
if (MigrationFlag.CHAPTER in flags) {
if (MigrationFlag.CATEGORY in flags) {
val categoryIds = getCategories.await(current.id).map { it.id }
setMangaCategories.await(target.id, categoryIds)
}

View File

@@ -315,13 +315,6 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
private val sourceManager: SourceManager = Injekt.get(),
) : StateScreenModel<ScreenModel.State>(State()) {
init {
screenModelScope.launchIO {
initSources()
mutableState.update { it.copy(isLoading = false) }
}
}
private val sourcesComparator = { includedSources: List<Long> ->
compareBy<MigrationSource>(
{ !it.isSelected },
@@ -330,6 +323,13 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
)
}
init {
screenModelScope.launchIO {
initSources()
mutableState.update { it.copy(isLoading = false) }
}
}
private fun updateSources(save: Boolean = true, action: (List<MigrationSource>) -> List<MigrationSource>) {
mutableState.update { state ->
val updatedSources = action(state.sources)

View File

@@ -12,6 +12,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
@@ -49,9 +50,14 @@ internal fun Screen.MigrateMangaDialog(
) {
val scope = rememberCoroutineScope()
val screenModel = rememberScreenModel { MigrateDialogScreenModel(current, target) }
val screenModel = rememberScreenModel { MigrateDialogScreenModel() }
LaunchedEffect(current, target) {
screenModel.init(current, target)
}
val state by screenModel.state.collectAsState()
if (state.isMigrated) return
if (state.isMigrating) {
LoadingScreen(
modifier = Modifier.background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)),
@@ -118,15 +124,13 @@ internal fun Screen.MigrateMangaDialog(
}
private class MigrateDialogScreenModel(
private val current: Manga,
private val target: Manga,
private val sourcePreference: SourcePreferences = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val migrateManga: MigrateMangaUseCase = Injekt.get(),
) : StateScreenModel<MigrateDialogScreenModel.State>(State()) {
init {
fun init(current: Manga, target: Manga) {
val applicableFlags = buildList {
MigrationFlag.entries.forEach {
val applicable = when (it) {
@@ -140,7 +144,14 @@ private class MigrateDialogScreenModel(
}
}
val selectedFlags = sourcePreference.migrationFlags().get()
mutableState.update { it.copy(applicableFlags = applicableFlags, selectedFlags = selectedFlags) }
mutableState.update {
it.copy(
current = current,
target = target,
applicableFlags = applicableFlags,
selectedFlags = selectedFlags,
)
}
}
fun toggleSelection(flag: MigrationFlag) {
@@ -153,15 +164,21 @@ private class MigrateDialogScreenModel(
}
suspend fun migrateManga(replace: Boolean) {
sourcePreference.migrationFlags().set(state.value.selectedFlags)
val state = state.value
val current = state.current ?: return
val target = state.target ?: return
sourcePreference.migrationFlags().set(state.selectedFlags)
mutableState.update { it.copy(isMigrating = true) }
migrateManga(current, target, replace)
mutableState.update { it.copy(isMigrating = false) }
mutableState.update { it.copy(isMigrating = false, isMigrated = true) }
}
data class State(
val current: Manga? = null,
val target: Manga? = null,
val applicableFlags: List<MigrationFlag> = emptyList(),
val selectedFlags: Set<MigrationFlag> = emptySet(),
val isMigrating: Boolean = false,
val isMigrated: Boolean = false,
)
}

View File

@@ -251,6 +251,7 @@ class MigrationListScreenModel(
} catch (_: Exception) {
}
migratingManga.searchResult.value = result.toSuccessSearchResult()
updateMigrationProgress()
}
}

View File

@@ -95,8 +95,8 @@ abstract class BaseSmartSearchEngine<T>(
}
private fun removeTextInBrackets(text: String, readForward: Boolean): String {
val openingChars = if (readForward) "([<{ " else ")]}>"
val closingChars = if (readForward) ")]}>" else "([<{ "
val openingChars = if (readForward) "([<{" else ")]}>"
val closingChars = if (readForward) ")]}>" else "([<{"
var depth = 0
return buildString {

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M476,540L560,456L644,540L700,484L616,400L700,316L644,260L560,344L476,260L420,316L504,400L420,484L476,540ZM320,720Q287,720 263.5,696.5Q240,673 240,640L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L320,720ZM320,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640ZM160,880Q127,880 103.5,856.5Q80,833 80,800L80,240L160,240L160,800Q160,800 160,800Q160,800 160,800L720,800L720,880L160,880ZM320,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640L320,640Q320,640 320,640Q320,640 320,640L320,160Q320,160 320,160Q320,160 320,160Z"/>
</vector>

View File

@@ -13,12 +13,6 @@
android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/page_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal" />
</FrameLayout>
<eu.kanade.tachiyomi.ui.reader.ReaderNavigationOverlayView
@@ -30,7 +24,7 @@
android:visibility="gone" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_root"
android:id="@+id/compose_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@@ -1,9 +1,11 @@
package mihon.core.migration
import io.kotest.assertions.nondeterministic.eventually
import io.mockk.slot
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.newSingleThreadContext
@@ -17,6 +19,7 @@ import org.junit.jupiter.api.Assertions.assertInstanceOf
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.time.Duration.Companion.seconds
class MigratorTest {
@@ -26,7 +29,7 @@ class MigratorTest {
lateinit var migrationStrategyFactory: MigrationStrategyFactory
@BeforeEach
fun initilize() {
fun initialize() {
migrationContext = MigrationContext(false)
migrationJobFactory = spyk(MigrationJobFactory(migrationContext, CoroutineScope(Dispatchers.Main + Job())))
migrationCompletedListener = spyk<MigrationCompletedListener>(block = {})
@@ -45,7 +48,7 @@ class MigratorTest {
verify { migrationJobFactory.create(capture(migrations)) }
assertEquals(1, migrations.captured.size)
verify { migrationCompletedListener() }
eventually(2.seconds) { verify { migrationCompletedListener() } }
}
@Test
@@ -86,7 +89,7 @@ class MigratorTest {
verify { migrationJobFactory.create(capture(migrations)) }
assertEquals(2, migrations.captured.size)
verify { migrationCompletedListener() }
eventually(2.seconds) { verify { migrationCompletedListener() } }
}
@Test
@@ -114,7 +117,7 @@ class MigratorTest {
verify { migrationJobFactory.create(capture(migrations)) }
assertEquals(10, migrations.captured.size)
verify { migrationCompletedListener() }
eventually(2.seconds) { verify { migrationCompletedListener() } }
}
@Test
@@ -135,11 +138,12 @@ class MigratorTest {
verify { migrationJobFactory.create(capture(migrations)) }
assertEquals(2, migrations.captured.size)
verify { migrationCompletedListener() }
eventually(2.seconds) { verify { migrationCompletedListener() } }
}
companion object {
@OptIn(DelicateCoroutinesApi::class)
val mainThreadSurrogate = newSingleThreadContext("UI thread")
@BeforeAll

View File

@@ -4,8 +4,8 @@ import org.gradle.api.JavaVersion as GradleJavaVersion
import org.jetbrains.kotlin.gradle.dsl.JvmTarget as KotlinJvmTarget
object AndroidConfig {
const val COMPILE_SDK = 35
const val TARGET_SDK = 34
const val COMPILE_SDK = 36
const val TARGET_SDK = 36
const val MIN_SDK = 26
const val NDK = "27.1.12297006"
const val BUILD_TOOLS = "35.0.1"

View File

@@ -19,7 +19,7 @@ class NetworkPreferences(
fun defaultUserAgent(): Preference<String> {
return preferenceStore.getString(
"default_user_agent",
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36",
)
}
}

View File

@@ -9,6 +9,9 @@ import androidx.core.content.ContextCompat
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.Hash
import java.io.File
import java.nio.ByteBuffer
import java.nio.CharBuffer
import java.nio.charset.CodingErrorAction
object DiskUtil {
@@ -102,26 +105,84 @@ object DiskUtil {
}
/**
* Mutate the given filename to make it valid for a FAT filesystem,
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
* with a dot), but you can manually add it later.
* Transform a filename fragment to make it safe to use on almost
* all commonly used filesystems. You can pass an entire filename,
* or just part of one, in case you want a specific part of a long
* filename to be truncated, rather than the end of it.
*
* Characters that are potentially unsafe for some filesystems are
* replaced with underscores. This includes the standard ones from
* https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
* but does allow any other valid Unicode code point.
*
* Excessively long filenames are truncated, by default to 240
* bytes. Note that the truncation is based on bytes rather than
* characters (code points), because this is what is relevant to
* filesystem restrictions in most cases.
*
* Leading periods are stripped, to avoid the creation of hidden
* files by default. If a hidden file is desired, a period can be
* prepended to the return value from this function.
*
* If the optional argument disallowNonAscii is set to true,
* then ANYTHING outside the ASCII range is replaced not with underscores,
* but with its hexadecimal encoding. This is to make it so that distinct
* non-English titles of things remain distinct, since not all
* places where this function is used also take care of
* disambiguation.
*
* We could instead replace only non-ASCII characters known to
* be problematic, but so far nobody with a non-Unicode-compliant
* device has been able to provide either directions to reproduce
* their issue nor any documentation or tests that would allow us
* to determine which characters are problems and which are not.
*/
fun buildValidFilename(origName: String): String {
fun buildValidFilename(
origName: String,
maxBytes: Int = MAX_FILE_NAME_BYTES,
disallowNonAscii: Boolean = false,
): String {
val name = origName.trim('.', ' ')
if (name.isEmpty()) {
return "(invalid)"
}
val sb = StringBuilder(name.length)
name.forEach { c ->
if (isValidFatFilenameChar(c)) {
if (disallowNonAscii && c >= 0x80.toChar()) {
sb.append(
c.toString().toByteArray(Charsets.UTF_8).toHexString(
HexFormat {
upperCase = false
},
),
)
} else if (isValidFatFilenameChar(c)) {
sb.append(c)
} else {
sb.append('_')
}
}
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
// ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
return sb.toString().take(240)
return truncateToLength(sb.toString(), maxBytes)
}
/**
* Truncate a string to a maximum length, while maintaining valid Unicode encoding.
*/
fun truncateToLength(s: String, maxBytes: Int): String {
val charset = Charsets.UTF_8
val decoder = charset.newDecoder()
val sba = s.toByteArray(charset)
if (sba.size <= maxBytes) {
return s
}
// Ensure truncation by having byte buffer = maxBytes
val bb = ByteBuffer.wrap(sba, 0, maxBytes)
val cb = CharBuffer.allocate(maxBytes)
// Ignore an incomplete character
decoder.onMalformedInput(CodingErrorAction.IGNORE)
decoder.decode(bb, cb, true)
decoder.flush(cb)
return String(cb.array(), 0, cb.position())
}
/**
@@ -139,6 +200,8 @@ object DiskUtil {
const val NOMEDIA_FILE = ".nomedia"
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8)
const val MAX_FILE_NAME_BYTES = 250
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8).
// To allow for writing to ext4 through a FUSE layer in the future, also subtract 15
// reserved characters.
const val MAX_FILE_NAME_BYTES = 240
}

View File

@@ -83,6 +83,9 @@ fun WebView.setDefaultSettings() {
loadWithOverviewMode = true
cacheMode = WebSettings.LOAD_DEFAULT
// Handle popups properly
setSupportMultipleWindows(true)
// Allow zooming
setSupportZoom(true)
builtInZoomControls = true

View File

@@ -52,6 +52,7 @@ class UpdatesRepositoryImpl(
chapterId: Long,
chapterName: String,
scanlator: String?,
chapterUrl: String,
read: Boolean,
bookmark: Boolean,
lastPageRead: Long,
@@ -67,6 +68,7 @@ class UpdatesRepositoryImpl(
chapterId = chapterId,
chapterName = chapterName,
scanlator = scanlator,
chapterUrl = chapterUrl,
read = read,
bookmark = bookmark,
lastPageRead = lastPageRead,

View File

@@ -0,0 +1,24 @@
-- Add chapter urls to updates view
DROP VIEW IF EXISTS updatesView;
CREATE VIEW updatesView AS
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.url AS chapterUrl,
chapters.read,
chapters.bookmark,
chapters.last_page_read,
mangas.source,
mangas.favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch
FROM mangas JOIN chapters
ON mangas._id = chapters.manga_id
WHERE favorite = 1
AND date_fetch > date_added
ORDER BY date_fetch DESC;

View File

@@ -0,0 +1,7 @@
-- Save the current remote_id as library_id, since old Kitsu tracker did not use this correctly
UPDATE manga_sync SET library_id = remote_id WHERE sync_id = 3;
-- Kitsu and Suwayomi aren't using the remote_id field properly, but for both the ID is present in the URL
-- This parses a url and gets the ID from the trailing path part, e.g. https://kitsu.app/manga/<id>
-- Based on https://stackoverflow.com/a/38330814
UPDATE manga_sync SET remote_id = replace(remote_url, rtrim(remote_url, replace(remote_url, '/', '')), '') WHERE sync_id IN (3, 9);

View File

@@ -5,6 +5,7 @@ SELECT
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.url AS chapterUrl,
chapters.read,
chapters.bookmark,
chapters.last_page_read,
@@ -31,4 +32,4 @@ SELECT *
FROM updatesView
WHERE read = :read
AND dateUpload > :after
LIMIT :limit;
LIMIT :limit;

View File

@@ -31,7 +31,7 @@ dependencies {
api(libs.sqldelight.android.paging)
compileOnly(libs.compose.stablemarker)
compileOnly(compose.runtime.annotation)
testImplementation(libs.bundles.test)
testImplementation(kotlinx.coroutines.test)

View File

@@ -37,6 +37,10 @@ class DownloadPreferences(
fun downloadNewUnreadChaptersOnly() = preferenceStore.getBoolean("download_new_unread_chapters_only", false)
fun parallelSourceLimit() = preferenceStore.getInt("download_parallel_source_limit", 5)
fun parallelPageLimit() = preferenceStore.getInt("download_parallel_page_limit", 5)
companion object {
private const val REMOVE_EXCLUDE_CATEGORIES_PREF_KEY = "remove_exclude_categories"
private const val DOWNLOAD_NEW_CATEGORIES_PREF_KEY = "download_new_categories"

View File

@@ -192,6 +192,8 @@ class LibraryPreferences(
fun updateMangaTitles() = preferenceStore.getBoolean("pref_update_library_manga_titles", false)
fun disallowNonAsciiFilenames() = preferenceStore.getBoolean("disallow_non_ascii_filenames", false)
// endregion
enum class ChapterSwipeAction {

View File

@@ -8,6 +8,7 @@ data class UpdatesWithRelations(
val chapterId: Long,
val chapterName: String,
val scanlator: String?,
val chapterUrl: String,
val read: Boolean,
val bookmark: Boolean,
val lastPageRead: Long,

View File

@@ -1,6 +1,6 @@
[versions]
agp_version = "8.12.0"
lifecycle_version = "2.9.2"
agp_version = "8.13.0"
lifecycle_version = "2.9.4"
paging_version = "3.3.6"
interpolator_version = "1.0.0"
@@ -11,8 +11,8 @@ annotation = "androidx.annotation:annotation:1.9.1"
appcompat = "androidx.appcompat:appcompat:1.7.1"
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
constraintlayout = "androidx.constraintlayout:constraintlayout:2.2.1"
corektx = "androidx.core:core-ktx:1.16.0"
splashscreen = "androidx.core:core-splashscreen:1.0.1"
corektx = "androidx.core:core-ktx:1.17.0"
splashscreen = "androidx.core:core-splashscreen:1.2.0"
recyclerview = "androidx.recyclerview:recyclerview:1.4.0"
viewpager = "androidx.viewpager:viewpager:1.1.0"
profileinstaller = "androidx.profileinstaller:profileinstaller:1.4.1"
@@ -21,14 +21,14 @@ lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref
lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle_version" }
lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" }
workmanager = "androidx.work:work-runtime:2.10.3"
workmanager = "androidx.work:work-runtime:2.11.0"
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
interpolator = { group = "androidx.interpolator", name = "interpolator", version.ref = "interpolator_version" }
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.4.0"
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.4.1"
test-ext = "androidx.test.ext:junit-ktx:1.3.0"
test-espresso-core = "androidx.test.espresso:espresso-core:3.7.0"
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0"

View File

@@ -1,13 +1,14 @@
[versions]
compose-bom = "2025.07.00"
compose-bom = "2025.09.00"
[libraries]
activity = "androidx.activity:activity-compose:1.10.1"
activity = "androidx.activity:activity-compose:1.11.0"
bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
foundation = { module = "androidx.compose.foundation:foundation" }
animation = { module = "androidx.compose.animation:animation" }
animation-graphics = { module = "androidx.compose.animation:animation-graphics" }
runtime = { module = "androidx.compose.runtime:runtime" }
runtime-annotation = { module = "androidx.compose.runtime:runtime-annotation" }
ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
ui-util = { module = "androidx.compose.ui:ui-util" }

View File

@@ -1,7 +1,7 @@
[versions]
kotlin_version = "2.2.0"
kotlin_version = "2.2.21"
serialization_version = "1.9.0"
xml_serialization_version = "0.91.2"
xml_serialization_version = "0.91.3"
[libraries]
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }

View File

@@ -1,17 +1,17 @@
[versions]
aboutlib_version = "12.2.4"
aboutlib_version = "13.1.0"
leakcanary = "2.14"
moko = "0.25.0"
okhttp_version = "5.1.0"
shizuku_version = "13.1.0"
moko = "0.25.1"
okhttp_version = "5.3.0"
shizuku_version = "13.1.5"
sqldelight = "2.1.0"
sqlite = "2.5.2"
sqlite = "2.6.1"
voyager = "1.1.0-beta03"
spotless = "7.2.1"
spotless = "8.0.0"
ktlint-core = "1.7.1"
firebase-bom = "34.0.0"
markdown = "0.35.0"
junit = "5.13.4"
firebase-bom = "34.5.0"
markdown = "0.38.1"
junit = "6.0.1"
[libraries]
desugar = "com.android.tools:desugar_jdk_libs:2.1.5"
@@ -23,13 +23,13 @@ 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.16.0"
okio = "com.squareup.okio:okio:3.16.2"
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.3"
quickjs-android = "app.cash.quickjs:quickjs-android:0.9.2"
quickjs-android = { group = "com.github.zhanghai.quickjs-java", name = "quickjs-android", version = "547f5b1597" }
jsoup = "org.jsoup:jsoup:1.21.1"
jsoup = "org.jsoup:jsoup:1.21.2"
disklrucache = "com.jakewharton:disklrucache:2.0.2"
unifile = "com.github.tachiyomiorg:unifile:e0def6b3dc"
@@ -60,12 +60,10 @@ material = "com.google.android.material:material:1.12.0"
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
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:2.0.1"
compose-webview = "io.github.kevinnzou:compose-webview:0.33.6"
compose-grid = "io.woong.compose.grid:grid:1.2.2"
compose-stablemarker = "com.github.skydoves:compose-stable-marker:1.0.5"
reorderable = { module = "sh.calvin.reorderable:reorderable", version = "2.5.1" }
reorderable = { module = "sh.calvin.reorderable:reorderable", version = "3.0.0" }
swipe = "me.saket.swipe:swipe:1.3.0"
@@ -92,8 +90,8 @@ sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect",
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
kotest-assertions = "io.kotest:kotest-assertions-core:5.9.1"
mockk = "io.mockk:mockk:1.14.5"
kotest-assertions = "io.kotest:kotest-assertions-core:6.0.4"
mockk = "io.mockk:mockk:1.14.6"
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
@@ -109,11 +107,11 @@ markdown-coil = { module = "com.mikepenz:multiplatform-markdown-renderer-coil3",
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version = "0.1.0" }
[plugins]
google-services = { id = "com.google.gms.google-services", version = "4.4.3" }
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlib_version" }
google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlib_version" }
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.5" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.6" }
[bundles]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"]

View File

@@ -105,7 +105,7 @@
<item quantity="other">%d فصل تالٍ لم يُقرؤوا</item>
</plurals>
<plurals name="download_amount">
<item quantity="zero">الفصل التالي</item>
<item quantity="zero">لا فصل تالي</item>
<item quantity="one">الفصل التالي</item>
<item quantity="two">%d فصول تالية</item>
<item quantity="few">%d فصول تالية</item>
@@ -152,4 +152,28 @@
<item quantity="many">بعد %1$d أيام</item>
<item quantity="other">بعد %1$d أيام</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.migrateTitle">
<item quantity="zero">نقل %1$d مدخل</item>
<item quantity="one">نقل %1$d مدخل</item>
<item quantity="two">نقل %1$d مدخل</item>
<item quantity="few">نقل %1$d مدخل</item>
<item quantity="many">نقل %1$d مدخل</item>
<item quantity="other">نقل %1$d مدخل</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.copyTitle">
<item quantity="zero">نسخ %1$d مدخل؟</item>
<item quantity="one">نسخ %1$d مدخل؟</item>
<item quantity="two">نسخ %1$d مدخل؟</item>
<item quantity="few">نسخ %1$d مدخل؟</item>
<item quantity="many">نسخ %1$d مدخل؟</item>
<item quantity="other">نسخ %1$d مدخل؟</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.skipText">
<item quantity="zero">لا مدخل ثم تجاوزه</item>
<item quantity="one">مدخل ثم تجاوزه</item>
<item quantity="two">مدخلان ثم تجاوزه</item>
<item quantity="few">%1$d مدخل ثم تجاوزه</item>
<item quantity="many">%1$d مدخل ثم تجاوزه</item>
<item quantity="other">%1$d مدخل ثم تجاوزه</item>
</plurals>
</resources>

View File

@@ -555,7 +555,7 @@
<string name="clear_database_source_item_count">%1$d مدخلةً في قاعدة البيانات وليست في المكتبة</string>
<string name="extension_api_error">فشل الحصول على قائمة الملحقات</string>
<string name="ext_installer_shizuku_unavailable_dialog">ثبِّت «شيزوكو» وشغِّله لتستخدمه مثبِّت إضافات.</string>
<string name="download_queue_size_warning">تحذير: يمكن أن تؤدِّي التنزيلات كبيرة الحجم والعدد إلى إبطاء المصادر، وقد يُحظر Mihon منها بسبب ذلك. اضغط لمعرفة المزيد۔</string>
<string name="download_queue_size_warning">%sتحذير: يمكن أن تؤدِّي التنزيلات كبيرة الحجم والعدد إلى إبطاء المصادر، وقد يُحظر %s منها بسبب ذلك. اضغط لمعرفة المزيد۔</string>
<string name="action_show_manga">إظهار الدخول</string>
<string name="action_display_cover_only_grid">شبكة بالاغلفة</string>
<string name="skipped_reason_not_started">تُخُطِّيت بسبب عدم وجود فصول قُرئت</string>
@@ -737,7 +737,7 @@
<string name="action_menu_overflow_description">خيارات أكثر</string>
<string name="selected">محدَّد</string>
<string name="not_selected">غير مُحدَّد</string>
<string name="action_bar_up_description">إصعد</string>
<string name="action_bar_up_description">تصفح الاعلى</string>
<string name="pref_storage_location">مكان التخزين</string>
<string name="pref_storage_location_info">يُستخدَم في الاحتياط وتنزيل الفصول والمصدر المحليِّ.</string>
<string name="onboarding_storage_action_select">حدِّد مجلَّدًا</string>
@@ -847,9 +847,60 @@
<string name="library_list">قائمة المكتبات</string>
<string name="non_library_settings">جميع الإدخالات المقروئة</string>
<string name="pref_hardware_bitmap_threshold">عتبة خريطة الصورة النقطية للأجهزة المخصصة</string>
<string name="pref_hardware_bitmap_threshold_default">اساس(%d)</string>
<string name="pref_hardware_bitmap_threshold_default">افتراضي (%d)</string>
<string name="possible_duplicates_summary">لديك مداخلات متشابهة في مكتبك بنفس الاسم.\nحدد واحدة للنقل أو الإضافة على أي حال</string>
<string name="possible_duplicates_title">التكرارات المحتملة</string>
<string name="confirm_tracker_update">تحديث المتابعات إلى الفصل %d؟</string>
<string name="pref_incognito_mode_extension_summary">إيقاف قراءة التاريخ مؤقتًا للتمديد</string>
<string name="label_donate">تبرع</string>
<string name="theme_catppuccin">كاتبوتشين</string>
<string name="pref_display_images_description">­تقديم الصور في أوصاف المانجا</string>
<string name="pref_hide_missing_chapter_indicators">إخفاء مؤشرات الفصل المفقودة</string>
<string name="pref_always_decode_long_strip_with_ssiv_summary">يؤثر على الأداء. فعّله فقط إذا لم يُحل تقليل عتبة الخريطة النقطية مشاكل الصورة الفارغة.</string>
<string name="pref_download_new_unread_chapters_only">تخطي تنزيل الفصول المقروءة المكررة</string>
<string name="pref_auto_update_manga_on_mark_read">تحديث التقدم عند وضع علامة عليه كمقروء</string>
<string name="export">يصدّر</string>
<string name="library_exported">تم تصدير المكتبة</string>
<string name="clear_database_text">أنت على وشك إزالة الإدخالات من قاعدة البيانات</string>
<string name="clear_database_history_warning">سيتم فقدان قراءة الفصول وتقدم الإدخالات غير الموجودة في المكتبة</string>
<string name="clear_db_exclude_read">الاحتفاظ بالإدخالات مع الفصول المقروءة</string>
<string name="tracked_privately">تم تعقبها بشكل خاص</string>
<string name="migrationConfigScreen.selectedHeader">مُختار</string>
<string name="migrationConfigScreen.availableHeader">متاح</string>
<string name="migrationConfigScreen.selectAllLabel">تحديد الكل</string>
<string name="migrationConfigScreen.selectNoneLabel">لا تحدد</string>
<string name="migrationConfigScreen.selectEnabledLabel">­حدد المصادر الممكنة</string>
<string name="migrationConfigScreen.selectPinnedLabel">تحديد المصادر المثبتة</string>
<string name="migrationConfigScreen.continueButtonText">اكمل</string>
<string name="migrationConfigScreen.dataToMigrateHeader">البيانات المراد نقلها</string>
<string name="migrationConfigScreen.removeDownloadsTitle">حذف التنزيلات الخاصة بالإدخال الحالي بعد الترحيل</string>
<string name="migrationConfigScreen.additionalSearchQueryLabel">­كلمات رئيسية إضافية (اختياري)</string>
<string name="migrationConfigScreen.additionalSearchQuerySupportingText">يساعد في تضييق نتائج البحث عن طريق إضافة كلمات رئيسية إضافية</string>
<string name="migrationConfigScreen.hideUnmatchedTitle">إخفاء الإدخالات التي لا تحتوي على تطابق</string>
<string name="migrationConfigScreen.hideWithoutUpdatesTitle">إخفاء الإدخالات بدون فصول أحدث</string>
<string name="migrationConfigScreen.hideWithoutUpdatesSubtitle">إظهار الإدخال فقط إذا كانت المباراة تحتوي على فصول إضافية</string>
<string name="migrationConfigScreen.enhancedOptionsWarning">هذه الخيارات بطيئة وخطيرة وقد تؤدي إلى فرض قيود من المصادر</string>
<string name="migrationConfigScreen.deepSearchModeTitle">وضع البحث المتقدم</string>
<string name="migrationConfigScreen.deepSearchModeSubtitle">يقسم العنوان إلى كلمات رئيسية لإجراء بحث أوسع</string>
<string name="migrationConfigScreen.prioritizeByChaptersTitle">المطابقة على أساس رقم الفصل</string>
<string name="migrationConfigScreen.prioritizeByChaptersSubtitle">في حال تفعيله، يتم اختيار المطابقة الأبعد. وإلا، يتم اختيار المطابقة الأولى حسب أولوية المصدر.</string>
<string name="migrationListScreenTitle">‎نقل</string>
<string name="migrationListScreenTitleWithProgress">نقل (%1$d/%2$d)</string>
<string name="migrationListScreen.copyActionLabel">قطع</string>
<string name="migrationListScreen.migrateActionLabel">نقل</string>
<string name="migrationListScreen.noMatchFoundText">لم يتم العثور على بدائل</string>
<string name="migrationListScreen.latestChapterLabel">الأحدث: %1$s</string>
<string name="migrationListScreen.unknownLatestChapter">مجهول</string>
<string name="migrationListScreen.searchManuallyActionLabel">البحث اليدوي</string>
<string name="migrationListScreen.skipActionLabel">لا تنقل</string>
<string name="migrationListScreen.migrateNowActionLabel">نقل الآن</string>
<string name="migrationListScreen.copyNowActionLabel">نسخ الآن</string>
<string name="migrationListScreen.exitDialogTitle">وقف النقل؟</string>
<string name="migrationListScreen.exitDialog.stopLabel">إيقاف</string>
<string name="migrationListScreen.exitDialog.cancelLabel">إلغاء</string>
<string name="migrationListScreen.migrateDialog.copyLabel">نسخ</string>
<string name="migrationListScreen.migrateDialog.migrateLabel">نقل</string>
<string name="migrationListScreen.migrateDialog.cancelLabel">إلغاء</string>
<string name="migrationListScreen.progressDialog.cancelLabel">إلغاء</string>
<string name="migrationListScreen.matchWithoutChapterToast">لم يتم العثور على فصول، لا يمكن استخدام هذا الإدخال للنقل</string>
</resources>

View File

@@ -164,6 +164,7 @@
<string name="action_webview_back">Back</string>
<string name="action_webview_forward">Forward</string>
<string name="action_webview_refresh">Refresh</string>
<string name="action_webview_close_tab">Close tab</string>
<string name="action_start_downloading_now">Start downloading now</string>
<string name="action_not_now">Not now</string>
<string name="action_add_anyway">Add anyway</string>
@@ -320,6 +321,8 @@
<string name="pref_mark_duplicate_read_chapter_read_new">After fetching new chapter</string>
<string name="pref_hide_missing_chapter_indicators">Hide missing chapter indicators</string>
<string name="pref_disallow_non_ascii_filenames">Disallow non-ASCII filenames</string>
<string name="pref_disallow_non_ascii_filenames_details">Ensures compatibility with certain storage media that don't support Unicode. When this is enabled, you'll need to manually rename source and manga folders by replacing non-ASCII characters with their lowercase UTF-8 hexadecimal representations. Chapter files don't need to be renamed.</string>
<!-- Extension section -->
<string name="multi_lang">Multi</string>
@@ -364,7 +367,7 @@
<string name="information_empty_repos">You have no repos set.</string>
<string name="action_add_repo">Add repo</string>
<string name="label_add_repo_input">Repo URL</string>
<string name="action_add_repo_message">Add additional repos to Mihon. This should be a URL that ends with \"index.min.json\".</string>
<string name="action_add_repo_message">Add additional repos to %s. This should be a URL that ends with \"index.min.json\".</string>
<string name="error_repo_exists">This repo already exists!</string>
<string name="action_delete_repo">Delete repo</string>
<string name="invalid_repo_name">Invalid repo URL</string>
@@ -521,6 +524,9 @@
<string name="save_chapter_as_cbz">Save as CBZ archive</string>
<string name="split_tall_images">Split tall images</string>
<string name="split_tall_images_summary">Improves reader performance</string>
<string name="pref_download_concurrent_sources">Concurrent source downloads</string>
<string name="pref_download_concurrent_pages">Concurrent page downloads</string>
<string name="pref_download_concurrent_pages_summary">Pages downloaded simultaneously per source</string>
<!-- Tracking section -->
<string name="tracking_guide">Tracking guide</string>
@@ -904,7 +910,7 @@
<!-- Downloads activity and service -->
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>
<string name="download_insufficient_space">Couldn\'t download chapters due to low storage space</string>
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking Mihon. Tap to learn more.</string>
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking %s. Tap to learn more.</string>
<!-- Library update service notifications -->
<string name="notification_updating_progress">Updating library… (%s)</string>

View File

@@ -95,4 +95,22 @@
<item quantity="few">%1$s stránky</item>
<item quantity="other">%1$s stránek</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.migrateTitle">
<item quantity="one">Migrovat %1$d položku?</item>
<item quantity="few">Migrovat %1$d položky?</item>
<item quantity="many">Migrovat %1$d položek?</item>
<item quantity="other">Migrovat %1$d položek?</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.copyTitle">
<item quantity="one">Zkopírovat %1$d položku?</item>
<item quantity="few">Zkopírovat %1$d položky?</item>
<item quantity="many">Zkopírovat %1$d položkek?</item>
<item quantity="other">Zkopírovat %1$d položkek?</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.skipText">
<item quantity="one">Položka byla přeskočena</item>
<item quantity="few">%1$d položky byly přeskočeny</item>
<item quantity="many">%1$d položek bylo přeskočeno</item>
<item quantity="other">%1$d položek bylo přeskočeno</item>
</plurals>
</resources>

View File

@@ -540,7 +540,7 @@
<string name="update_72hour">Každé 3 dny</string>
<string name="connected_to_wifi">Jen na Wi-Fi</string>
<string name="pref_verbose_logging">Podrobné protokolování</string>
<string name="download_queue_size_warning">Varování: hromadné stahování může vést k tomu, že zdroje zpomalí a/nebo zablokují Mihon. Klepnutím se dozvíte více.</string>
<string name="download_queue_size_warning">Varování: hromadné stahování může vést k tomu, že zdroje zpomalí a/nebo zablokují %s. Klepnutím se dozvíte více.</string>
<string name="ext_update_all">Aktualizovat vše</string>
<string name="pref_verbose_logging_summary">Vypisovat podrobné informace do systémového protokolu (sníží výkon aplikace)</string>
<string name="channel_app_updates">Aktualizace aplikace</string>
@@ -687,8 +687,8 @@
<string name="pref_page_rotate_invert">Překlopení orientace otočených širokých stránek</string>
<string name="pref_page_rotate">Otočení širokých stránek tak, aby se vešly</string>
<string name="pref_debug_info">Ladící informace</string>
<string name="pref_chapter_swipe_end">Přejetí prstem doprava</string>
<string name="pref_chapter_swipe_start">Přejetí prstem doleva</string>
<string name="pref_chapter_swipe_end">Kapitola po přejetí doprava</string>
<string name="pref_chapter_swipe_start">Kapitola po přejetí doleva</string>
<string name="pref_double_tap_zoom">Přiblížení dvojitým klepnutím</string>
<string name="action_filter_interval_custom">Přizpůsobený interval aktualizace</string>
<string name="action_sort_next_updated">Další očekávaná aktualizace</string>
@@ -768,7 +768,7 @@
\nVybraná složka: %2$s</string>
<string name="invalid_backup_file_error">Úplná chyba:</string>
<string name="pref_library_update_smart_update">Chytrá aktualizace</string>
<string name="action_add_repo_message">Přidat další repozitáře do Mihon. Měli by to být URL končící \"index.min.json\".</string>
<string name="action_add_repo_message">Přidat další repozitáře do %s. Měli by to být URL končící \"index.min.json\".</string>
<string name="error_repo_exists">Tento repozitář již existuje!</string>
<string name="action_delete_repo">Odstranit repozitář</string>
<string name="invalid_repo_name">Neplatná URL repozitáře</string>
@@ -833,7 +833,7 @@
<string name="upcoming_calendar_next">Následující měsíc</string>
<string name="upcoming_guide">Návod k nadcházejícím kapitolám</string>
<string name="label_auto">Automaticky</string>
<string name="pref_mark_duplicate_read_chapter_read">Označit duplicitní přečtené kapitoly jako přečtené</string>
<string name="pref_mark_duplicate_read_chapter_read">Označit duplicitní přečtenou kapitolu jako přečtenou</string>
<string name="theme_monochrome">Černobílý</string>
<string name="author">Autor</string>
<string name="artist">Umělec</string>
@@ -860,4 +860,11 @@
<string name="storage_failed_to_create_directory">Nepodařilo se vytvořit adresář: %s</string>
<string name="clear_database_text">Chystáte se odstranit položky z databáze</string>
<string name="clear_db_exclude_read">Ponechat položky s přečtenými kapitolami</string>
<string name="label_donate">Přispěj</string>
<string name="theme_catppuccin">Catppuccin</string>
<string name="pref_hide_missing_chapter_indicators">Skrýt indikátory chybějících kapitol</string>
<string name="pref_disallow_non_ascii_filenames">Nepovolovat soubory s non-ASCII znaky</string>
<string name="pref_disallow_non_ascii_filenames_details">Zajišťuje kompatibilitu s určitými úložnými médii, která nepodporují Unicode. Pokud je tato funkce povolena, budete muset ručně přejmenovat názvy složkek source a manga tak, že nahradíte non-ASCII znaky jejich malými hexadecimálními znaky v UTF-8. Názvy souborů kapitol není nutné přejmenovávat.</string>
<string name="pref_download_concurrent_sources">Souběžné stahování zdrojů</string>
<string name="pref_download_concurrent_pages">Souběžné stahování stránek</string>
</resources>

View File

@@ -204,7 +204,7 @@
<string name="file_select_cover">Wähle ein Vorschaubild</string>
<string name="file_select_backup">Wähle eine Sicherungsdatei</string>
<string name="update_check_confirm">Herunterladen</string>
<string name="update_check_no_new_updates">Keine neues Update verfügbar</string>
<string name="update_check_no_new_updates">Kein neues Update verfügbar</string>
<string name="update_check_notification_download_in_progress">Herunterladen…</string>
<string name="update_check_notification_download_complete">Tippe, um das Update zu installieren</string>
<string name="update_check_notification_download_error">Fehler beim Herunterladen</string>
@@ -542,7 +542,7 @@
<string name="backup_info">Du solltest Kopien der Datensicherungen auch an anderen Orten aufbewahren. Datensicherungen beinhalten möglicherweise sensible Daten, einschließlich gespeicherter Passwörter. Sei vorsichtig beim Teilen.</string>
<string name="connected_to_wifi">Nur über WLAN</string>
<string name="update_72hour">Alle 3 Tage</string>
<string name="download_queue_size_warning">Achtung: Große Downloads könnten dazu führen, dass Quellen langsamer werden und/oder Mihon blockieren. Tippe, um mehr zu erfahren.</string>
<string name="download_queue_size_warning">Achtung: Große Downloads könnten dazu führen, dass Quellen langsamer werden und/oder %s blockieren. Tippe um mehr zu erfahren.</string>
<string name="ext_update_all">Alle aktualisieren</string>
<string name="channel_app_updates">App-Updates</string>
<string name="pref_auto_clear_chapter_cache">Kapitel-Zwischenspeicher beim Öffnen der App löschen</string>
@@ -778,7 +778,7 @@
<string name="onboarding_storage_help_action">Speicherleitfaden</string>
<string name="pref_library_update_smart_update">Intelligentes Aktualisieren</string>
<string name="label_add_repo_input">Repository-URL</string>
<string name="action_add_repo_message">Füge zusätzliche Repositorys zu Mihon hinzu. Deren URLs sollten mit „index.min.json“ enden.</string>
<string name="action_add_repo_message">Füge zusätzliche Repositorys zu %s hinzu. Deren URLs sollten mit „index.min.json“ enden.</string>
<string name="invalid_repo_name">Ungültige Repository-URL</string>
<string name="manga_interval_expected_update">Ca. %1$s bis zur Veröffentlichung neuer Kapitel, wird ca. alle %2$s überprüft.</string>
<string name="theme_nord">Nord</string>
@@ -903,4 +903,9 @@
<string name="migrationListScreen.matchWithoutChapterToast">Keine Kapitel gefunden, dieser Eintrag konnte nicht für eine Migration verwendet werden</string>
<string name="label_donate">Spenden</string>
<string name="pref_display_images_description">Bilder in Mangabeschreibungen anzeigen</string>
<string name="pref_disallow_non_ascii_filenames">Nicht-ASCII-Dateinamen nicht zulassen</string>
<string name="pref_disallow_non_ascii_filenames_details">Versichert Kompatibilität mit bestimmten Speichermedien, die Unicode nicht unterstützen. Ist diese Option aktiviert, müssen Quellen- und Manga-Ordner manuell umbenannt werden, indem Nicht-ASCII-Zeichen durch ihre UTF-8-Hexadezimaldarstellungen in Kleinbuchstaben ersetzt werden. Kapiteldateien müssen nicht umbenannt werden.</string>
<string name="pref_download_concurrent_sources">Gleichzeitige Quellendownloads</string>
<string name="pref_download_concurrent_pages">Gleichzeitige Seitendownloads</string>
<string name="pref_download_concurrent_pages_summary">Pro Quelle gleichzeitig heruntergeladene Seiten</string>
</resources>

View File

@@ -542,7 +542,7 @@
<string name="notification_size_warning">Οι μεγάλες ενημερώσεις βλάπτουν τις πηγές και μπορεί να οδηγήσουν σε πιο αργές ενημερώσεις και σε αυξημένη χρήση της μπαταρίας. Πατήστε για να μάθετε περισσότερα.</string>
<string name="connected_to_wifi">Μόνο σε Wi-Fi</string>
<string name="update_72hour">Κάθε 3 ημέρες</string>
<string name="download_queue_size_warning">Προειδοποίηση: οι μαζικές λήψεις ενδέχεται να οδηγήσουν σε επιβράδυνση των πηγών ή/και αποκλεισμό του Mihon. Πατήστε για να μάθετε περισσότερα.</string>
<string name="download_queue_size_warning">Προειδοποίηση: οι μαζικές λήψεις ενδέχεται να οδηγήσουν σε επιβράδυνση των πηγών ή/και αποκλεισμό του %s. Πατήστε για να μάθετε περισσότερα.</string>
<string name="ext_update_all">Ενημέρωση όλων</string>
<string name="channel_app_updates">Ενημερώσεις εφαρμογής</string>
<string name="pref_auto_clear_chapter_cache">Εκκαθάριση της προσωρινής μνήμης κεφαλαίων κατά την εκκίνηση της εφαρμογής</string>
@@ -779,7 +779,7 @@
<string name="theme_nord">Nord</string>
<string name="pref_library_update_smart_update">Έξυπνη ενημέρωση</string>
<string name="label_add_repo_input">URL αποθετηρίου</string>
<string name="action_add_repo_message">Προσθέστε επιπλέον αποθετήρια στο Mihon. Αυτό θα πρέπει να είναι ένα URL που τελειώνει με \"index.min.json\".</string>
<string name="action_add_repo_message">Προσθέστε επιπλέον αποθετήρια στο %s. Αυτό θα πρέπει να είναι ένα URL που τελειώνει με \"index.min.json\".</string>
<string name="delete_repo_confirmation">Θέλετε να διαγράψετε το αποθετήριο \"%s\";</string>
<string name="error_repo_exists">Αυτό το αποθετήριο υπάρχει ήδη!</string>
<string name="manga_interval_expected_update_soon">Σύντομα</string>
@@ -901,4 +901,8 @@
<string name="clear_db_exclude_read">Κρατήστε καταχωρήσεις με αναγνωσμένα κεφάλαια</string>
<string name="storage_failed_to_create_download_directory">Αποτυχία δημιουργίας καταλόγου λήψης</string>
<string name="storage_failed_to_create_directory">Αποτυχία δημιουργίας καταλόγου: %s</string>
<string name="label_donate">Δωρεά</string>
<string name="pref_display_images_description">Αναπαράσταση εικόνων σε περιγραφές manga</string>
<string name="pref_disallow_non_ascii_filenames">Απαγόρευση μη ASCII ονομάτων αρχείων</string>
<string name="pref_disallow_non_ascii_filenames_details">Εξασφαλίζει συμβατότητα με ορισμένα μέσα αποθήκευσης που δεν υποστηρίζουν Unicode. Όταν αυτή η επιλογή είναι ενεργοποιημένη, θα πρέπει να μετονομάσετε χειροκίνητα τους φακέλους πηγής και manga, αντικαθιστώντας τους χαρακτήρες που δεν είναι ASCII με τις μικρές κεφαλαίες δεκαεξαδικές αναπαραστάσεις UTF-8. Τα αρχεία κεφαλαίων δε χρειάζεται να μετονομάζονται.</string>
</resources>

View File

@@ -372,7 +372,7 @@
<string name="local_invalid_format">Nevalida ĉapitra formato</string>
<string name="chapter_not_found">Ĉapitro netrovita</string>
<string name="local_source">Loka fonto</string>
<string name="updating_category">Ĝisdatigi kategorion</string>
<string name="updating_category">Ĝisdatigado de kategorio</string>
<string name="check_for_updates">Kontroli ĝisdatigojn</string>
<string name="help_translate">Helpu traduki</string>
<string name="restoring_backup">Savkopia restaŭro</string>
@@ -592,7 +592,7 @@
<string name="pref_hardware_bitmap_threshold_default">Defaŭlta (%d)</string>
<string name="pref_hardware_bitmap_threshold_summary">Se legilo ŝargas malplenan bildon alkremente redukti la sojlon.\nElektita: %s</string>
<string name="label_add_repo_input">Deponeja URL</string>
<string name="action_add_repo_message">Aldoni aldonajn deponejojn al Mihon. Ĉi tio estu URL kiu finas per \"index.min.json\".</string>
<string name="action_add_repo_message">Aldoni aldonajn deponejojn al %s. Ĉi tio estu URL kiu finas per \"index.min.json\".</string>
<string name="error_repo_exists">Ĉi tiu deponejo jam ekzistas!</string>
<string name="delete_repo_confirmation">Ĉu vi volas forigi la deponejon \"%s\"?</string>
<string name="ext_installer_shizuku_unavailable_dialog">Instali kaj lanĉi Shizuku-n por uzi Shizuku-n kiel etendaĵa instalilo.</string>
@@ -812,7 +812,7 @@
<string name="hour_short">%dh</string>
<string name="minute_short">%dm</string>
<string name="seconds_short">%ds</string>
<string name="download_queue_size_warning">Averto: grandaj amasaj elŝutoj povas igi fontojn fariĝi pli malrapidaj kaj/aŭ bloki Mihon. Tuŝetu por ekscii pli.</string>
<string name="download_queue_size_warning">Averto: grandaj amasaj elŝutoj povas igi fontojn fariĝi pli malrapidaj kaj/aŭ bloki %s. Tuŝetu por ekscii pli.</string>
<string name="notification_updating_progress">Ĝisdatigado de biblioteko… (%s)</string>
<string name="notification_size_warning">Grandaj ĝisdatigoj povas damaĝi fontojn kaj konduki al pli malrapidaj ĝisdatigoj kaj pliigan baterian uzadon. Tuŝetu por ekscii pli.</string>
<string name="notification_update_error">%1$d ĝisdatigo(j) fiaskis</string>
@@ -894,4 +894,6 @@
<string name="pref_display_images_description">Montri bildojn en mangaaj priskriboj</string>
<string name="pref_hide_missing_chapter_indicators">Kaŝi mankantajn ĉapitrajn indikilojn</string>
<string name="tracked_privately">Sekvata private</string>
<string name="pref_disallow_non_ascii_filenames">Malpermesi ne-ASCII-dosiernomojn</string>
<string name="pref_disallow_non_ascii_filenames_details">Certigas kongruon kun certaj konservejaj dosiersistemoj kiuj ne subtenas Unikodon. Kiam tio estas ŝaltita, vi bezonos permane renomi dosierujojn de fontoj kaj mangaoj anstataŭigante ne-ASCII-signojn per iliaj minusklaj UTF-8-deksesumaj prezentoj. Ĉapitraj dosieroj ne necesas esti renomitaj.</string>
</resources>

View File

@@ -540,7 +540,7 @@
<string name="backup_info">Es una buena idea tener copias de respaldo fuera de tu dispositivo. Ten en cuenta que incluyen contraseñas y otros datos privados que seguramente no quieras compartir.</string>
<string name="connected_to_wifi">Solo con Wi-Fi</string>
<string name="update_72hour">Cada 3 días</string>
<string name="download_queue_size_warning">Advertencia: Las descargas grandes pueden llevar a que las fuentes se vuelvan cada vez más lentas y en casos extremos que los servidores limiten o impidan el acceso a Mihon. Toca aquí para más información.</string>
<string name="download_queue_size_warning">Advertencia: Las descargas grandes pueden llevar a que las fuentes se vuelvan cada vez más lentas y en casos extremos que los servidores limiten o impidan el acceso a %s. Toca aquí para más información.</string>
<string name="ext_update_all">Actualizar todas</string>
<string name="channel_app_updates">Actualizaciones de la aplicación</string>
<string name="pref_auto_clear_chapter_cache">Limpiar la caché de capítulos al abrir la aplicación</string>
@@ -766,7 +766,7 @@
<string name="error_repo_exists">¡Este repositorio ya existe!</string>
<string name="pref_library_update_smart_update">Actualizaciones inteligentes</string>
<string name="invalid_repo_name">La dirección URL del repositorio no parece ser correcta</string>
<string name="action_add_repo_message">Añade más repositorios a Mihon; la dirección URL tiene que terminar en «index.min.json».</string>
<string name="action_add_repo_message">Añade más repositorios a %s; la dirección URL tiene que terminar en «index.min.json».</string>
<string name="delete_repo_confirmation">¿Seguro que quieres borrar el repositorio «%s»?</string>
<string name="action_delete_repo">Borrar repositorio</string>
<string name="action_add_repo">Añadir un repositorio</string>
@@ -896,4 +896,9 @@
<string name="pref_hide_missing_chapter_indicators">Ocultar los indicadores de capítulos que falten</string>
<string name="label_donate">Donar</string>
<string name="pref_display_images_description">Ver imágenes en las descripciones de manga</string>
<string name="pref_disallow_non_ascii_filenames">Prohibir los nombres de archivo que no sean ASCII</string>
<string name="pref_disallow_non_ascii_filenames_details">Permite guardar tus datos en ciertos tipos de almacenamiento que no admitan Unicode. Al activarlo tendrás que renombrar las carpetas de tus fuentes y manga, pasándolas a representaciones UTF-8 en hexadecimal en minúscula. No tienes que hacer lo mismo con las carpetas de capítulos.</string>
<string name="pref_download_concurrent_sources">Descarga simultánea desde fuentes</string>
<string name="pref_download_concurrent_pages">Descarga simultánea de páginas</string>
<string name="pref_download_concurrent_pages_summary">Cantidad de páginas a descargar simultáneamente desde cada fuente remota</string>
</resources>

View File

@@ -426,7 +426,7 @@
<string name="clear_history_completed">Nabura ang kasaysayan</string>
<string name="clear_history_confirmation">Sigurado ka ba talaga? Mawawala ang buong kasaysayan.</string>
<string name="migration_help_guide">Gabay sa Paglipat ng source</string>
<string name="spen_next_page">Abante</string>
<string name="spen_next_page">Susunod na pahina</string>
<string name="spen_previous_page">Balik</string>
<string name="file_picker_error">Walang nakitang file picker app</string>
<string name="pref_show_nsfw_source">Ipakita sa mga listahan ng source at extension</string>
@@ -542,7 +542,7 @@
<string name="backup_info">Dapat nagtatabi rin kayo ng mga kopya ng backup sa ibang mga lugar. Ang mga backup ay naglalaman ng sensitibong data tulad ng nakaimbak na password; mag-ingat kung ibahagi ito.</string>
<string name="connected_to_wifi">Sa Wi-Fi lang</string>
<string name="update_72hour">Kada 3 araw</string>
<string name="download_queue_size_warning">Babala: maaaring humantong sa pagbagal at/o pagharang ng mga source sa Mihon ang maramihang pag-download. I-tap para matuto pa.</string>
<string name="download_queue_size_warning">Babala: maaaring humantong sa pagbagal at/o pagharang ng mga source sa %s ang maramihang pag-download. I-tap para matuto pa.</string>
<string name="channel_app_updates">Mga update sa app</string>
<string name="ext_update_all">I-update lahat</string>
<string name="clear_database_source_item_count">%1$d na entry sa database na wala sa aklatan</string>
@@ -777,7 +777,7 @@
<string name="error_repo_exists">Umiiral na ang repo na ito!</string>
<string name="action_delete_repo">Tanggalin ang repo</string>
<string name="label_add_repo_input">URL ng repo</string>
<string name="action_add_repo_message">Magdagdag ng mga karagdagang repo sa Mihon. Dapat ito ay isang URL na nagtatapos sa \"index.min.json\".</string>
<string name="action_add_repo_message">Magdagdag ng mga karagdagang repo sa %s. Dapat ito ay isang URL na nagtatapos sa \"index.min.json\".</string>
<string name="invalid_repo_name">Di-wastong URL ng repo</string>
<string name="delete_repo_confirmation">Gusto mo bang tanggalin ang repo na \"%s\"?</string>
<string name="manga_interval_custom_amount">Custom na frequency sa pag-update:</string>
@@ -901,4 +901,9 @@
<string name="pref_hide_missing_chapter_indicators">Itago ang mga indikasyon ng nawawalang kabanata</string>
<string name="pref_display_images_description">I-render ang mga imahe sa mga paglalarawan ng manga</string>
<string name="label_donate">Mag-donate</string>
<string name="pref_disallow_non_ascii_filenames">Di-payagan ang mga non-ASCII na filename</string>
<string name="pref_disallow_non_ascii_filenames_details">Tinitiyak ang pagiging tugma sa ilang partikular na storage media na hindi sumusuporta sa Unicode. Kapag napagana ito, kakailanganin mong manu-manong palitan ang pangalan ng source at manga folder sa pamamagitan ng pagpapalit ng mga hindi ASCII na character ng kanilang lowercase na UTF-8 hexadecimal na representasyon. Hindi kailangang palitan ng pangalan ang mga file ng kabanata.</string>
<string name="pref_download_concurrent_sources">Kasabay na pag-download ng source</string>
<string name="pref_download_concurrent_pages">Kasabay na pag-download ng pahina</string>
<string name="pref_download_concurrent_pages_summary">Mga pahina na nai-download nang sabay-sabay kada source</string>
</resources>

View File

@@ -452,7 +452,7 @@
<string name="pref_dual_page_invert">Inverser le placement des pages divisées</string>
<string name="backup_restore_content_full">Vous devrez installer les extensions manquantes et vous connecter ensuite aux services de suivi pour les utiliser.</string>
<string name="nav_zone_prev">Précédent</string>
<string name="pref_dns_over_https">DNS over HTTPS (DoH)</string>
<string name="pref_dns_over_https">DNS sur HTTPS (DoH)</string>
<string name="nav_zone_right">Droite</string>
<string name="nav_zone_left">Gauche</string>
<string name="nav_zone_next">Suivant</string>
@@ -542,7 +542,7 @@
<string name="backup_info">Vous devez également conserver des copies des sauvegardes à d\'autres endroits.</string>
<string name="connected_to_wifi">Uniquement en Wi-Fi</string>
<string name="update_72hour">Tous les 3 jours</string>
<string name="download_queue_size_warning">Attention : les téléchargements massifs peuvent entraîner un ralentissement des sources ou le blocage de Mihon. Appuyez pour en savoir plus.</string>
<string name="download_queue_size_warning">Attention : les téléchargements massifs peuvent entraîner un ralentissement des sources ou le blocage de %s. Appuyez pour en savoir plus.</string>
<string name="ext_update_all">Tout mettre à jour</string>
<string name="channel_app_updates">Mises à jour de l\'application</string>
<string name="pref_auto_clear_chapter_cache">Vider le cache de chapitre au lancement de l\'application</string>
@@ -717,7 +717,7 @@
<string name="action_menu_overflow_description">Plus d\'options</string>
<string name="selected">Sélectionné</string>
<string name="not_selected">Pas sélectionné(e)</string>
<string name="scanlator">Scanlator</string>
<string name="scanlator">Scanlateur</string>
<string name="pref_flash_page">Flash lors du changement de page</string>
<string name="action_bar_up_description">Naviguer vers le haut</string>
<string name="action_sort_tracker_score">Score du service de suivi</string>
@@ -769,7 +769,7 @@
<string name="label_extension_repos">Répertoire d\'extension</string>
<string name="ext_revoke_trust">Révoquer les extensions provenant d\'un répertoire additionnel</string>
<string name="label_add_repo_input">URL du répertoire</string>
<string name="action_add_repo_message">Ajouter un répertoire additionnel à Mihon. L\'URL devrait se terminer par « index.min.json ».</string>
<string name="action_add_repo_message">Ajouter un répertoire additionnel à %s. L\'URL devrait se terminer par « index.min.json ».</string>
<string name="error_repo_exists">Ce répertoire existe déjà!</string>
<string name="invalid_repo_name">L\'URL du répertoire est invalide</string>
<string name="manga_interval_expected_update_soon">Bientôt</string>
@@ -796,7 +796,7 @@
<string name="upcoming_calendar_prev">Le mois précédent</string>
<string name="action_copy_link">Copier le lien</string>
<string name="action_replace_repo_title">L\'empreinte digitale de la clé de signature existe déjà</string>
<string name="add_repo_confirmation">Souhaitez-vous ajouter le répertoire \"%s\"?</string>
<string name="add_repo_confirmation">Souhaitez-vous ajouter le répertoire \"%s\"?</string>
<string name="pref_flash_with">Flash avec</string>
<string name="action_replace_repo_message">Le répertoire %1$s a la même empreinte digitale de la clé de signature que %2$s.
\nSi cela est attendu, %2$s sera remplacé, sinon contactez votre mainteneur du répertoire.</string>
@@ -902,4 +902,5 @@
<string name="migrationListScreen.migrateDialog.cancelLabel">Annuler</string>
<string name="migrationListScreen.progressDialog.cancelLabel">Annuler</string>
<string name="label_donate">Donation</string>
<string name="pref_display_images_description">Rendre les images dans les descriptions de mangas</string>
</resources>

View File

@@ -29,8 +29,8 @@
<item quantity="other">%d श्रेणियाँ</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="manga_num_chapters">
<item quantity="one">%1$s अध्याय</item>
@@ -76,4 +76,16 @@
<item quantity="one">%1$s अध्याय गायब है</item>
<item quantity="other">%1$s अध्याय गायब हैं</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.migrateTitle">
<item quantity="one">%1$d एंट्री ट्रांसफ़र करें?</item>
<item quantity="other">%1$d एंट्रियाँ ट्रांसफ़र करें?</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.copyTitle">
<item quantity="one">%1$d एंट्री कॉपी करें?</item>
<item quantity="other">%1$d एंट्रियाँ कॉपी करें?</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.skipText">
<item quantity="one">एक प्रविष्टि त्यागी गई</item>
<item quantity="other">%1$d प्रविष्टियाँ त्यागी गईं</item>
</plurals>
</resources>

View File

@@ -67,7 +67,7 @@
<string name="pref_category_tracking">पदचिह्न</string>
<string name="pref_category_advanced">विकसित</string>
<string name="pref_category_about">संबंध में</string>
<string name="pref_library_columns">्रिड आकार</string>
<string name="pref_library_columns">्रति पंक्ति आइटम्स</string>
<string name="portrait">चित्र</string>
<string name="landscape">लैंडस्केप</string>
<string name="pref_library_update_interval">स्वचालित अद्यतन</string>
@@ -118,10 +118,10 @@
<string name="rotation_free">मुक्त</string>
<string name="rotation_force_portrait">सीधे बंद</string>
<string name="rotation_force_landscape">मजबूर लैंडस्केप</string>
<string name="color_filter_r_value">R</string>
<string name="color_filter_g_value">G</string>
<string name="color_filter_b_value">B</string>
<string name="color_filter_a_value">A</string>
<string name="color_filter_r_value">लाल</string>
<string name="color_filter_g_value">हरा</string>
<string name="color_filter_b_value">नीला</string>
<string name="color_filter_a_value">पारदर्शिता</string>
<string name="pref_remove_after_marked_as_read">\'पढ़ें\' के रूप में खुद से चिह्नित करने के बाद</string>
<string name="pref_remove_after_read">पढ़ने के बाद स्वचालित रूप से हटाएं</string>
<string name="disabled">बंद करें</string>
@@ -542,7 +542,7 @@
<string name="label_warning">चेतावनी</string>
<string name="action_display_language_badge">भाषा</string>
<string name="backup_info">आपको अन्य स्थानों पर भी बैकअप की प्रतियाँ रखनी चाहिए।</string>
<string name="download_queue_size_warning">चेतावनी: बड़े बल्क डाउनलोड कारण स्रोत धीमे हो सकते हैं और/या ताचियोमी को ब्लॉक कर सकते हैं। अधिक जानने के लिए यह टैप करें </string>
<string name="download_queue_size_warning">चेतावनी: बड़े पैमाने पर डाउनलोड े स्रोत धीमे हो सकते हैं और/या %s को ब्लॉक कर सकते हैं। अधिक जानने के लिए टैप करें।</string>
<string name="notification_size_warning">बड़े अपडेट स्रोतों को नुकसान पहुंचाते हैं और इससे धीमे अपडेट हो सकते हैं, और बैटरी का उपयोग भी बढ़ सकता है। अधिक जानने के लिए टैप करें ।</string>
<string name="channel_app_updates">ऐप अपडेट</string>
<string name="update_72hour">हर 3 दिन</string>
@@ -648,16 +648,16 @@
<string name="action_set_interval">अंतराल निर्धारित करें</string>
<string name="action_filter_interval_custom">अनुकूलित लाने का अंतराल</string>
<string name="intervals_header">अंतराल</string>
<string name="pref_chapter_swipe_end">दाईं ओर स्वाइप करने पर</string>
<string name="pref_chapter_swipe_end">अध्यााय: दाएँ स्वाइप करें</string>
<string name="action_sort_next_updated">अगला अपेक्षित अपडेट</string>
<string name="pref_debug_info">डीबग जानकारी</string>
<string name="pref_advanced_summary">डंप क्रैश लॉग, बैटरी अनुकूलन</string>
<string name="pref_update_only_in_release_period">अपेक्षित रिलीज़ अवधि से बाहर</string>
<string name="pref_chapter_swipe_start">बाईं ओर स्वाइप करने पर</string>
<string name="pref_chapter_swipe_start">अध्यााय: बाएँ स्वाइप करें</string>
<string name="library_sync_complete">लाइब्रेरी सिंक पूरा</string>
<string name="download_cache_invalidated">डाउनलोड अनुक्रमणिका अमान्य</string>
<string name="action_ok">ठीक है</string>
<string name="pref_invalidate_download_cache">डाउनलोड अनुक्रमणिका अमान्य करें</string>
<string name="pref_invalidate_download_cache">डाउनलोड फिर से अनुक्रमित करें</string>
<string name="copied_to_clipboard_plain">क्लिपबोर्ड पर कॉपी हो गया है</string>
<string name="track_delete_remote_text">%s से भी हटा दें</string>
<string name="label_auto">स्वत:</string>
@@ -700,4 +700,205 @@
<string name="possible_duplicates_summary">आपकी पुस्तकालय में एक समान नाम वाली प्रविष्टियाँ हैं।\n\nस्थानांतरित करने के लिए एक प्रविष्टि चुनें या फिर भी जोड़ें।</string>
<string name="onboarding_storage_info">%1$s अध्याय डाउनलोड, बैकअप और अन्य चीज़ों को संग्रहित करने के लिए एक फ़ोल्डर चुनें।\n\nएक समर्पित फ़ोल्डर उपयुक्त रहेगा।\n\nचयनित फ़ोल्डर: %2$s</string>
<string name="ext_permission_install_apps_warning">एक्सटेंशन स्थापित करने के लिए अनुमतियाँ आवश्यक हैं। अनुमति देने के लिए यहाँ टैप करें।</string>
<string name="label_donate">दान करें</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_crashlytics">क्रैश लॉग भेजें</string>
<string name="onboarding_permission_crashlytics_description">डेवलपर्स को गुमनाम क्रैश लॉग भेजें।</string>
<string name="onboarding_permission_analytics">डाटा विश्लेषण की अनुमति दें</string>
<string name="onboarding_permission_analytics_description">ऐप की सुविधाएँ बेहतर करने के लिए गुमनाम उपयोग डेटा भेजें।</string>
<string name="onboarding_permission_action_grant">अनुमति दें</string>
<string name="onboarding_guides_new_user">%s पर नए हैं? हम सुझाव देते हैं कि शुरुआत करने की गाइड देखें।</string>
<string name="onboarding_guides_returning_user">%s को फिर से इंस्टॉल कर रहे हैं?</string>
<string name="pref_reader_summary">पढ़ने का मोड, प्रदर्शन, मार्गदर्शन</string>
<string name="pref_tracking_summary">एक-तरफ़ा प्रगति सिंक, बेहतर सिंक</string>
<string name="theme_catppuccin">कैटपुचिन</string>
<string name="theme_monochrome">एकवर्णी</string>
<string name="theme_nord">नॉर्ड</string>
<string name="pref_relative_format">सापेक्ष समय-चिह्न</string>
<string name="pref_relative_format_summary">\"%2$s\" के बजाय \"%1$s\"</string>
<string name="pref_display_images_description">मंगा विवरण में चित्र दिखाएँ</string>
<string name="pref_security">सुरक्षा</string>
<string name="pref_firebase">एनालिटिक्स और क्रैश लॉग्स</string>
<string name="firebase_summary">क्रैश लॉग्स और एनालिटिक्स भेजने से हम समस्याएँ पहचानकर ठीक कर पाएँगे, प्रदर्शन सुधार पाएँगे, और भविष्य के अपडेट आपकी ज़रूरतों के अनुसार बना पाएँगे</string>
<string name="pref_library_update_smart_update">स्मार्ट अपडेट</string>
<string name="pref_hide_missing_chapter_indicators">गुम हुए अध्याय के संकेत छुपाएँ</string>
<string name="ext_revoke_trust">विश्वसनीय अज्ञात एक्सटेंशन्स रद्द करें</string>
<string name="label_extension_repos">एक्सटेंशन रिपॉजिटरीज</string>
<string name="information_empty_repos">आपने कोई रिपॉ सेट नहीं किया है।</string>
<string name="action_add_repo">रिपॉ जोड़ें</string>
<string name="label_add_repo_input">रिपॉ URL</string>
<string name="action_add_repo_message">%s में अतिरिक्त रिपॉज़ जोड़ें। यह एक URL होना चाहिए जो \"index.min.json\" पर समाप्त होता हो।</string>
<string name="error_repo_exists">यह रिपॉ पहले से मौजूद है!</string>
<string name="action_delete_repo">रिपॉ हटाएँ</string>
<string name="invalid_repo_name">अमान्य रिपॉ URL</string>
<string name="delete_repo_confirmation">क्या आप रिपॉ \"%s\" हटाना चाहते हैं?</string>
<string name="add_repo_confirmation">क्या आप रिपॉ \"%s\" जोड़ना चाहते हैं?</string>
<string name="action_open_repo">ओपन सोर्स रिपॉ</string>
<string name="action_replace_repo">बदलें</string>
<string name="action_replace_repo_title">साइनिंग की फिंगरप्रिंट पहले से मौजूद है</string>
<string name="action_replace_repo_message">रिपॉजिटरी %1$s का साइनिंग की फिंगरप्रिंट %2$s के समान है।\nयदि यह अपेक्षित है, तो %2$s को बदला जाएगा, अन्यथा अपने रिपॉ मेंटेनर से संपर्क करें।</string>
<string name="pref_page_rotate_invert">घुमाए गए चौड़े पृष्ठों की ओरिएंटेशन उलटें</string>
<string name="pref_double_tap_zoom">दुबारा टैप करके ज़ूम करें</string>
<string name="pref_flash_page">पृष्ठ बदलने पर फ्लैश करें</string>
<string name="pref_flash_page_summ">ई-इंक डिस्प्ले पर घोस्टिंग कम करता है</string>
<string name="pref_flash_duration">फ्लैश की अवधि</string>
<string name="pref_flash_duration_summary">%1$s मिलीसेक</string>
<string name="pref_flash_page_interval">हर बार फ्लैश करें</string>
<string name="pref_flash_with">के साथ फ्लैश करें</string>
<string name="pref_flash_style_black">काला</string>
<string name="pref_flash_style_white">सफ़ेद</string>
<string name="pref_flash_style_white_black">सफ़ेद और काला</string>
<string name="pref_hardware_bitmap_threshold">कस्टम हार्डवेयर बिटमैप सीमा</string>
<string name="pref_hardware_bitmap_threshold_default">डिफ़ॉल्ट (%d)</string>
<string name="pref_hardware_bitmap_threshold_summary">यदि रीडर खाली छवि लोड करता है तो सीमा धीरे-धीरे कम करें।\nचयनित: %s</string>
<string name="pref_always_decode_long_strip_with_ssiv_2">लॉन्ग स्ट्रिप रीडर के लिए पुराना डिकोडर उपयोग करें</string>
<string name="pref_always_decode_long_strip_with_ssiv_summary">प्रदर्शन को प्रभावित करता है। केवल तभी सक्षम करें जब बिटमैप सीमा कम करने से खाली छवि की समस्याएं ठीक न हों</string>
<string name="pref_display_profile">कस्टम डिस्प्ले प्रोफ़ाइल</string>
<string name="pref_webtoon_disable_zoom_out">ज़ूम आउट अक्षम करें</string>
<string name="no_location_set">कोई स्टोरेज स्थान सेट नहीं किया गया है</string>
<string name="storage_failed_to_create_download_directory">डाउनलोड डायरेक्टरी बनाने में विफल</string>
<string name="storage_failed_to_create_directory">डायरेक्टरी बनाने में विफल: %s</string>
<string name="pref_download_new_unread_chapters_only">दोहरे पढ़े गए अध्याय डाउनलोड करना छोड़ें</string>
<string name="split_tall_images">लंबी छवियों को विभाजित करें</string>
<string name="pref_auto_update_manga_on_mark_read">पढ़े गए के रूप में चिह्नित करते समय प्रगति अपडेट करें</string>
<string name="track_activity_name">ट्रैकर लॉगिन</string>
<string name="pref_storage_location">स्टोरेज स्थान</string>
<string name="pref_storage_location_info">स्वचालित बैकअप, अध्याय डाउनलोड और स्थानीय स्रोत के लिए उपयोग किया जाता है।</string>
<string name="action_create">बनाएँ</string>
<string name="invalid_backup_file_error">पूर्ण त्रुटि:</string>
<string name="invalid_backup_file_json">JSON बैकअप समर्थित नहीं है</string>
<string name="invalid_backup_file_unknown">बैकअप फ़ाइल भ्रष्ट हो गई है</string>
<string name="app_settings">ऐप सेटिंग्स</string>
<string name="source_settings">स्रोत सेटिंग्स</string>
<string name="extensionRepo_settings">एक्सटेंशन रिपॉजिटरीज</string>
<string name="private_settings">संवेदनशील सेटिंग्स शामिल करें (जैसे, ट्रैकर लॉगिन टोकन)</string>
<string name="non_library_settings">सभी पढ़ी गई प्रविष्टियाँ</string>
<string name="missing_storage_permission">स्टोरेज अनुमति नहीं मिली है</string>
<string name="create_backup_file_error">बैकअप फ़ाइल बनाने में असमर्थ</string>
<string name="last_auto_backup_info">अंतिम स्वचालित बैकअप: %s</string>
<string name="pref_storage_usage">स्टोरेज उपयोग</string>
<string name="available_disk_space_info">उपलब्ध: %1$s / कुल: %2$s</string>
<string name="export">निर्यात करें</string>
<string name="library_list">पुस्तकालय सूची</string>
<string name="library_exported">पुस्तकालय निर्यातित हो गया</string>
<string name="syncing_library">पुस्तकालय सिंक हो रहा है</string>
<string name="error_user_agent_string_invalid">अमान्य यूज़र एजेंट स्ट्रिंग</string>
<string name="pref_invalidate_download_cache_summary">ऐप को डाउनलोड किए गए अध्याय फिर से जांचने के लिए मजबूर करें</string>
<string name="clear_database_text">आप डेटाबेस से प्रविष्टियाँ हटाने वाले हैं</string>
<string name="clear_database_history_warning">पढ़े गए अध्याय और गैर-पुस्तकालय प्रविष्टियों की प्रगति खो जाएगी</string>
<string name="clear_db_exclude_read">पढ़े गए अध्याय वाली प्रविष्टियाँ रखें</string>
<string name="pref_update_library_manga_titles">पुस्तकालय मंगा शीर्षक स्रोत से मेल खाने के लिए अपडेट करें</string>
<string name="pref_update_library_manga_titles_summary">चेतावनी: यदि किसी मंगा का नाम बदला जाता है, तो वह डाउनलोड कतार से हटा दिया जाएगा (यदि मौजूद हो)।</string>
<string name="fdroid_warning">F-Droid बिल्ड आधिकारिक तौर पर समर्थित नहीं हैं।\nअधिक जानने के लिए टैप करें।</string>
<string name="pref_incognito_mode_extension_summary">एक्सटेंशन के लिए पढ़ने के इतिहास को रोकें</string>
<string name="logging_in">लॉगिन हो रहा है…</string>
<string name="overlay_header">ओवरले</string>
<string name="has_results">परिणाम हैं</string>
<string name="author">लेखक</string>
<string name="artist">कलाकार</string>
<string name="unknown_title">अज्ञात शीर्षक</string>
<string name="possible_duplicates_title">संभावित डुप्लिकेट्स</string>
<string name="manga_display_interval_title">हर बार अनुमान लगाएं</string>
<string name="manga_display_modified_interval_title">हर बार अपडेट करने के लिए सेट करें</string>
<string name="manga_interval_expected_update">नए अध्याय लगभग %1$s में रिलीज़ होने की संभावना है, लगभग हर %2$s में जांच की जा रही है।</string>
<string name="manga_interval_expected_update_null">यह मंगा या तो पूरा हो चुका है, या कोई अनुमानित रिलीज़ तारीख उपलब्ध नहीं है।</string>
<string name="manga_interval_expected_update_soon">जल्द ही</string>
<string name="manga_interval_custom_amount">कस्टम अपडेट आवृत्ति:</string>
<string name="exclude_scanlators">स्कैनलेटर को बाहर करें</string>
<string name="no_scanlators_found">कोई स्कैनलेटर नहीं मिला</string>
<string name="confirm_tracker_update">ट्रैकर्स को अध्याय %d तक अपडेट करें?</string>
<string name="trackers_updated_summary">ट्रैकर्स को अध्याय %d तक अपडेट किया गया</string>
<string name="tracked_privately">निजी तौर पर ट्रैक किया गया</string>
<string name="action_toggle_private_on">निजी रूप से ट्रैक करें</string>
<string name="action_toggle_private_off">सार्वजनिक रूप से ट्रैक करें</string>
<string name="track_error">%1$s त्रुटि: %2$s</string>
<string name="track_remove_date_conf_title">तारीख हटाएं?</string>
<string name="track_remove_start_date_conf_text">यह आपके पहले चुने गए प्रारंभ तिथि %s को हटा देगा</string>
<string name="track_remove_finish_date_conf_text">यह आपके पहले चुने गए समाप्ति तिथि %s को हटा देगा</string>
<string name="track_delete_title">%s ट्रैकिंग हटाएं?</string>
<string name="track_delete_text">यह ट्रैकिंग को स्थानीय रूप से हटा देगा।</string>
<string name="updates_last_update_info_just_now">अभी अभी</string>
<string name="relative_time_span_never">कभी नहीं</string>
<string name="action_view_upcoming">आगामी अपडेट देखें</string>
<string name="upcoming_guide">आगामी मार्गदर्शिका</string>
<string name="upcoming_calendar_next">अगला महीना</string>
<string name="upcoming_calendar_prev">पिछला महीना</string>
<string name="crash_screen_title">अरे!</string>
<string name="crash_screen_description">%s को एक अप्रत्याशित त्रुटि का सामना करना पड़ा। हम सुझाव देते हैं कि आप क्रैश लॉग्स हमारे Discord सपोर्ट चैनल में साझा करें।</string>
<string name="crash_screen_restart_application">एप्लिकेशन पुनः प्रारंभ करें</string>
<string name="label_overview_section">समीक्षा</string>
<string name="label_completed_titles">पूर्ण किए गए प्रविष्टियाँ</string>
<string name="label_read_duration">पढ़ने का समय</string>
<string name="label_titles_section">प्रविष्टियाँ</string>
<string name="label_titles_in_global_update">वैश्विक अपडेट में</string>
<string name="label_total_chapters">कुल</string>
<string name="label_read_chapters">पढ़ा</string>
<string name="label_tracker_section">ट्रैकर्स</string>
<string name="label_tracked_titles">ट्रैक की गई प्रविष्टियाँ</string>
<string name="label_mean_score">औसत स्कोर</string>
<string name="label_used">प्रयुक्त</string>
<string name="not_applicable">लागू नहीं</string>
<string name="day_short">%dd</string>
<string name="hour_short">%dh</string>
<string name="minute_short">%dm</string>
<string name="seconds_short">%ds</string>
<string name="notification_updating_progress">लाइब्रेरी अपडेट हो रही है… (%s)</string>
<string name="skipped_reason_not_always_update">छोड़ दिया गया क्योंकि सीरीज़ को अपडेट की आवश्यकता नहीं है</string>
<string name="skipped_reason_not_in_release_period">छोड़ दिया गया क्योंकि आज कोई रिलीज़ अपेक्षित नहीं थी</string>
<string name="file_picker_uri_permission_unsupported">स्थायी फ़ोल्डर एक्सेस प्राप्त करने में विफल। ऐप असामान्य रूप से काम कर सकता है।</string>
<string name="file_null_uri_error">कोई फ़ाइल चयनित नहीं</string>
<string name="information_no_manga_category">श्रेणी खाली है</string>
<string name="information_no_entries_found">इस श्रेणी में कोई प्रविष्टियाँ नहीं मिलीं</string>
<string name="information_cloudflare_help">Cloudflare सहायता के लिए यहां टैप करें</string>
<string name="information_required_plain">*आवश्यक</string>
<string name="download_notifier_cache_renewal">डाउनलोड जांच रहे हैं</string>
<string name="exception_http">HTTP %d, वेबसाइट WebView में जांचें</string>
<string name="exception_offline">इंटरनेट कनेक्शन नहीं है</string>
<string name="exception_unknown_host">%s तक पहुँच नहीं पाया</string>
<string name="notes_placeholder">उस भाग का आनंद लिया जहाँ…</string>
<string name="migrationConfigScreen.selectedHeader">चयनित</string>
<string name="migrationConfigScreen.availableHeader">उपलब्ध</string>
<string name="migrationConfigScreen.selectAllLabel">सभी चुनें</string>
<string name="migrationConfigScreen.selectNoneLabel">कोई नहीं चुनें</string>
<string name="migrationConfigScreen.selectEnabledLabel">सक्षम स्रोत चुनें</string>
<string name="migrationConfigScreen.selectPinnedLabel">पिन किए गए स्रोत चुनें</string>
<string name="migrationConfigScreen.continueButtonText">जारी रखें</string>
<string name="migrationConfigScreen.dataToMigrateHeader">स्थानांतरित करने के लिए डेटा</string>
<string name="migrationConfigScreen.removeDownloadsTitle">स्थानांतरण के बाद वर्तमान प्रविष्टि के डाउनलोड हटाएं</string>
<string name="migrationConfigScreen.additionalSearchQueryLabel">अतिरिक्त कीवर्ड (वैकल्पिक)</string>
<string name="migrationConfigScreen.additionalSearchQuerySupportingText">अतिरिक्त कीवर्ड जोड़कर खोज परिणामों को सीमित करने में मदद करता है</string>
<string name="migrationConfigScreen.hideUnmatchedTitle">मेल न खाने वाली प्रविष्टियाँ छुपाएं</string>
<string name="migrationConfigScreen.hideWithoutUpdatesTitle">नए अध्याय नहीं वाले प्रविष्टियाँ छुपाएं</string>
<string name="migrationConfigScreen.hideWithoutUpdatesSubtitle">केवल तब प्रविष्टि दिखाएं जब मैच में अतिरिक्त अध्याय हों</string>
<string name="migrationConfigScreen.enhancedOptionsWarning">ये विकल्प धीमे और जोखिम भरे हैं और स्रोतों से प्रतिबंधों का कारण बन सकते हैं</string>
<string name="migrationConfigScreen.deepSearchModeTitle">उन्नत खोज मोड</string>
<string name="migrationConfigScreen.deepSearchModeSubtitle">विस्तृत खोज के लिए शीर्षक को कीवर्ड में विभाजित करता है</string>
<string name="migrationConfigScreen.prioritizeByChaptersTitle">अध्याय संख्या के आधार पर मिलान करें</string>
<string name="migrationConfigScreen.prioritizeByChaptersSubtitle">यदि सक्षम है, तो सबसे आगे वाले मिलान को चुनता है। अन्यथा, स्रोत प्राथमिकता के अनुसार पहला मिलान चुनता है।</string>
<string name="migrationListScreenTitle">स्थानांतरण</string>
<string name="migrationListScreenTitleWithProgress">स्थानांतरण (%1$d/%2$d)</string>
<string name="migrationListScreen.copyActionLabel">कॉपी</string>
<string name="migrationListScreen.migrateActionLabel">स्थानांतरित</string>
<string name="migrationListScreen.noMatchFoundText">कोई विकल्प नहीं मिला</string>
<string name="migrationListScreen.latestChapterLabel">नवीनतम: %1$s</string>
<string name="migrationListScreen.unknownLatestChapter">अज्ञात</string>
<string name="migrationListScreen.searchManuallyActionLabel">मैन्युअल रूप से खोजें</string>
<string name="migrationListScreen.skipActionLabel">स्थानांतरित न करें</string>
<string name="migrationListScreen.migrateNowActionLabel">अभी स्थानांतरित करें</string>
<string name="migrationListScreen.copyNowActionLabel">अभी कॉपी करें</string>
<string name="migrationListScreen.exitDialogTitle">स्थानांतरण रोकें?</string>
<string name="migrationListScreen.exitDialog.stopLabel">रोकें</string>
<string name="migrationListScreen.exitDialog.cancelLabel">रद्द करें</string>
<string name="migrationListScreen.migrateDialog.copyLabel">कॉपी</string>
<string name="migrationListScreen.migrateDialog.migrateLabel">स्थानांतरण</string>
<string name="migrationListScreen.migrateDialog.cancelLabel">रद्द</string>
<string name="migrationListScreen.progressDialog.cancelLabel">रद्द</string>
<string name="migrationListScreen.matchWithoutChapterToast">कोई अध्याय नहीं मिला, इस प्रविष्टि का उपयोग स्थानांतरण के लिए नहीं किया जा सकता</string>
<string name="pref_disallow_non_ascii_filenames">ग़ैर-ASCII फ़ाइल नामों की अनुमति न दें</string>
<string name="pref_disallow_non_ascii_filenames_details">यह उन स्टोरेज मीडिया के साथ संगतता सुनिश्चित करता है जो यूनिकोड का समर्थन नहीं करते, और सक्षम होने पर स्रोत व मंगा फ़ोल्डरों के गैर-ASCII अक्षरों को उनके छोटे अक्षरों वाले UTF-8 हेक्साडेसिमल रूप में मैन्युअली बदलना आवश्यक होता है, जबकि चैप्टर फ़ाइलों के नाम बदलने की आवश्यकता नहीं होती।</string>
</resources>

View File

@@ -33,8 +33,8 @@
<string name="update_never">Isključeno</string>
<string name="pref_library_update_interval">Automatska aktualiziranja</string>
<string name="pref_category_library_update">Globalno aktualiziranje</string>
<string name="landscape">Polegnuto</string>
<string name="portrait">Uspravno</string>
<string name="landscape">Polegnuti format</string>
<string name="portrait">Uspravni format</string>
<string name="pref_library_columns">Broj stavki po retku</string>
<string name="pref_category_display">Prikaz</string>
<string name="hide_notification_content">Sakrij sadržaj obavijesti</string>
@@ -168,8 +168,8 @@
<string name="color_filter_b_value">Plava</string>
<string name="color_filter_g_value">Zelena</string>
<string name="color_filter_r_value">Crvena</string>
<string name="rotation_force_landscape">Prisili polegnuto</string>
<string name="rotation_force_portrait">Prisili uspravno</string>
<string name="rotation_force_landscape">Zaključan polegnuti format</string>
<string name="rotation_force_portrait">Zaključan uspravni format</string>
<string name="rotation_free">Slobodno</string>
<string name="pref_rotation_type">Standardno okretanje</string>
<string name="double_tap_anim_speed_fast">Brzo</string>
@@ -188,8 +188,8 @@
<string name="scale_type_fit_screen">Prilagodi ekranu</string>
<string name="pref_image_scale_type">Vrsta skaliranja</string>
<string name="pager_viewer">Stranice</string>
<string name="vertical_plus_viewer">Duga traka s razmacima</string>
<string name="webtoon_viewer">Duga traka</string>
<string name="vertical_plus_viewer">Kontinuirano listanje s razmacima</string>
<string name="webtoon_viewer">Kontinuirano listanje</string>
<string name="vertical_viewer">Stranica (okomito)</string>
<string name="right_to_left_viewer">Stranica (s desna na lijevo)</string>
<string name="left_to_right_viewer">Stranica (s lijeva na desno)</string>
@@ -302,7 +302,7 @@
<string name="unknown">Nepoznato</string>
<string name="ongoing">Nastavljajući</string>
<string name="local_source_help_guide">Vodič za lokalni izvor</string>
<string name="browse">Pretraži</string>
<string name="browse">Pregledaj</string>
<string name="latest">Najnoviji</string>
<string name="action_global_search_hint">Globalna pretraga …</string>
<string name="pinned_sources">Označeni</string>
@@ -468,8 +468,8 @@
<string name="action_show_errors">Dodirni za prikaz detalja</string>
<string name="update_check_eol">Ove se Android verzija više ne podržava</string>
<string name="clipboard_copy_error">Kopiranje nije uspješno</string>
<string name="rotation_landscape">Polegnuto</string>
<string name="rotation_portrait">Uspravno</string>
<string name="rotation_landscape">Polegnuti format</string>
<string name="rotation_portrait">Uspravni format</string>
<string name="pref_grayscale">Sive nijanse</string>
<string name="notification_incognito_text">Deaktiviraj anonimni modus</string>
<string name="rotation_type">Okretanje</string>
@@ -512,7 +512,7 @@
<string name="pref_category_appearance">Izgled</string>
<string name="getting_started_guide">Vodič za pokretanje</string>
<string name="confirm_lock_change">Ovjeri za potvrditi promjenu</string>
<string name="label_default">Zadano</string>
<string name="label_default">Standardno</string>
<string name="restore_miui_warning">Spremanje sigurnosne kopije i obnavljanje možda neće ispravno raditi, ako MIUI optimizacija nije aktivirana.</string>
<string name="help_translate">Pomogni prevoditi</string>
<string name="action_sort_count">Ukupan broj unosa</string>
@@ -525,7 +525,7 @@
<string name="ext_install_service_notif">Instaliranje proširenja …</string>
<string name="ext_app_info">Podaci aplikacije</string>
<string name="connected_to_wifi">Samo putem Wi-Fi veze</string>
<string name="download_queue_size_warning">Upozorenje: velika skupna preuzimanja mogu dovesti do usporavanja izvora i/ili blokiranja Mihonja. Za daljnje informacije dodirni.</string>
<string name="download_queue_size_warning">Upozorenje: velika skupna preuzimanja mogu dovesti do usporavanja izvora i/ili blokiranja %s. Za daljnje informacije dodirni.</string>
<string name="theme_tealturquoise">Plavozelena i tirkiz</string>
<string name="clear_database_source_item_count">Broj unosa u bazi odataka koje nisu u zbirci: %1$d</string>
<string name="pref_verbose_logging">Opširno zapisivanje</string>
@@ -535,7 +535,7 @@
<string name="notification_size_warning">Velika aktualiziranja štete izvorima i mogu usporiti aktualiziranja i povećati potrošnju baterije. Dodirni i saznaj više.</string>
<string name="pref_low">Niska</string>
<string name="label_background_activity">Aktivnost u pozadini</string>
<string name="pref_hide_threshold">Osjetljivost za skrivanje izbornika pri pomicanju</string>
<string name="pref_hide_threshold">Osjetljivost za skrivanje izbornika pri listanju</string>
<string name="pref_auto_clear_chapter_cache">Izbriši predmemoriju poglavlja tijekom pokretanja aplikacije</string>
<string name="channel_app_updates">Aktualiziranja aplikacije</string>
<string name="database_clean">Nema se što raščistiti</string>
@@ -566,7 +566,7 @@
<string name="skipped_reason_not_caught_up">Preskočeno, jer postoje nepročitana poglavlja</string>
<string name="notification_update_error">Nauspjela aktualiziranja: %1$d</string>
<string name="learn_more">Dodirni za daljnje informacije</string>
<string name="rotation_reverse_portrait">Preokrenuto uspravno</string>
<string name="rotation_reverse_portrait">Preokreni uspravni format</string>
<string name="action_move_to_top_all_for_series">Premjesti seriju na vrh</string>
<string name="disabled_nav">Deaktivirano</string>
<string name="empty_backup_error">Nema unosa u biblioteci za spremanje u sigurnosnu kopiju</string>
@@ -619,7 +619,7 @@
<string name="pref_reset_user_agent_string">Obnovi standardni izraz korisničkog agenta</string>
<string name="error_user_agent_string_invalid">Nevažeći niz korisničkog agenta</string>
<string name="pref_invalidate_download_cache_summary">Prisili aplikaciju da ponovno provjeri preuzeta poglavlja</string>
<string name="pref_user_agent_string">Zadani niz korisničkog agenta</string>
<string name="pref_user_agent_string">Standardni niz korisničkog agenta</string>
<string name="error_user_agent_string_blank">Niz korisničkog agenta ne može biti prazan</string>
<string name="pref_invalidate_download_cache">Ponovo indeksiraj preuzimanja</string>
<string name="pref_reset_viewer_flags">Obnovi postavke čitača serija</string>
@@ -776,12 +776,12 @@
<string name="action_add_repo">Dodaj repozitorij</string>
<string name="action_delete_repo">Izbriši repozitorij</string>
<string name="label_add_repo_input">URL repozitorija</string>
<string name="action_add_repo_message">Dodaj dodatne repozitorije u Mihon. To bi trebao biti URL koji završava s „index.min.json”.</string>
<string name="action_add_repo_message">Dodaj dodatne repozitorije u %s. To bi trebao biti URL koji završava s „index.min.json”.</string>
<string name="invalid_repo_name">Neispravan URL repozitorija</string>
<string name="ext_revoke_trust">Opozovi pouzdana nepoznata proširenja</string>
<string name="delete_repo_confirmation">Želiš li izbrisati repozitorij „%s”?</string>
<string name="action_open_repo">Otvori repozitorij izvora</string>
<string name="private_settings">Omogući osjetljive postavke (kao što su tokeni za prijavu za usluge praćenja)</string>
<string name="private_settings">Uključi osjetljive postavke (kao što su tokeni za prijavu za usluge praćenja)</string>
<string name="manga_interval_expected_update">Predviđa se da će nova poglavlja biti izdana za oko %1$s, provjera se svakih %2$s.</string>
<string name="manga_interval_expected_update_soon">Uskoro</string>
<string name="available_disk_space_info">Dostupno: %1$s / Ukupno: %2$s</string>
@@ -847,7 +847,7 @@
<string name="pref_mark_duplicate_read_chapter_read_existing">Nakon čitanja poglavlja</string>
<string name="pref_mark_duplicate_read_chapter_read_new">Nakon dohvaćanja novog poglavlja</string>
<string name="pref_hide_missing_chapter_indicators">Sakrij indikatore nedostajućih poglavlja</string>
<string name="pref_always_decode_long_strip_with_ssiv_2">Koristi zastarjeli dekoder za čitač bezprekidnog prikaza mange</string>
<string name="pref_always_decode_long_strip_with_ssiv_2">Koristi zastarjeli dekoder za čitač kontinuiranog listanja</string>
<string name="clear_database_text">Uklonit ćeš unose iz baze podataka</string>
<string name="clear_database_history_warning">Izgubit će se pročitana poglavlja i napredak unosa koji se ne nalaze u biblioteci</string>
<string name="clear_db_exclude_read">Zadrži unose s pročitanim poglavljima</string>
@@ -901,4 +901,11 @@
<string name="migrationListScreen.latestChapterLabel">Najnovije: %1$s</string>
<string name="migrationListScreen.unknownLatestChapter">Nepoznato</string>
<string name="migrationListScreen.matchWithoutChapterToast">Nije pronađeno nijedno poglavlje. Ovaj se unos ne može koristiti za premještanje</string>
<string name="label_donate">Doniraj</string>
<string name="pref_display_images_description">Iscrtaj slike u opisima manga</string>
<string name="pref_disallow_non_ascii_filenames">Zabrani imena datoteka koji nisu u ASCII formatu</string>
<string name="pref_disallow_non_ascii_filenames_details">Osigurava kompatibilnost s određenim medijima za spremanje podataka koji ne podržavaju Unicode. Kada je ova opcija aktivirana, morat ćeš ručno preimenovati izvor i mape s mangama zamjenjivanjem znakova koji nisu ASCII s njihovim UTF-8 heksadecimalnim vrijednostima (u malim slovima). Datoteke poglavlja se ne moraju preimenovati.</string>
<string name="pref_download_concurrent_sources">Istovremena preuzimanja izvora</string>
<string name="pref_download_concurrent_pages">Istovremena preuzimanja stranica</string>
<string name="pref_download_concurrent_pages_summary">Istovremeno preuzete stranice po izvoru</string>
</resources>

View File

@@ -34,7 +34,7 @@
<string name="action_add_category">Tambah kategori</string>
<string name="action_edit_categories">Ubah kategori</string>
<string name="action_rename_category">Ubah nama kategori</string>
<string name="action_move_category">"Tentukan kategori"</string>
<string name="action_move_category">Tentukan kategori</string>
<string name="action_edit_cover">Ubah gambar sampul</string>
<string name="action_pause">Hentikan sementara</string>
<string name="action_previous_chapter">Bab sebelumnya</string>
@@ -533,7 +533,7 @@
<string name="ext_installer_pref">Pemasang</string>
<string name="action_sort_count">Total entri</string>
<string name="notification_size_warning">Pembaruan berskala besar membahayakan sumber, dapat membuat pembaruan lambat dan meningkatkan penggunaan baterai. Ketuk untuk mempelajari lebih lanjut.</string>
<string name="download_queue_size_warning">Peringatan: mengunduh dalam jumlah besar bisa menyebabkan sumber menjadi lambat dan/atau memblokir Mihon. Ketuk untuk mempelajari lebih lanjut.</string>
<string name="download_queue_size_warning">Peringatan: mengunduh dalam jumlah besar bisa menyebabkan sumber menjadi lambat dan/atau memblokir %s. Ketuk untuk mempelajari lebih lanjut.</string>
<string name="label_warning">Peringatan</string>
<string name="pref_verbose_logging_summary">Cetak catatan berlebih ke catatan sistem (mengurangi kinerja aplikasi)</string>
<string name="backup_info">Anda juga harus menyimpan salinan cadangan di tempat lain. Cadangan mungkin berisi data sensitif termasuk kata sandi yang tersimpan; berhati-hatilah jika berbagi.</string>
@@ -767,7 +767,7 @@
<string name="pref_library_update_smart_update">Pembauan pintar</string>
<string name="onboarding_storage_help_info">Memperbarui dari versi lama dan tak yakin harus pilih mana? lihat panduan penyimpanan untuk informasi lebih lanjut.</string>
<string name="action_add_repo">Tambahkan repo</string>
<string name="action_add_repo_message">Tambahkan repo lain ke Mihon. Seharusnya URL yang memiliki akhiran \"index.min.json\".</string>
<string name="action_add_repo_message">Tambahkan repo lain ke %s. Seharusnya URL yang memiliki akhiran \"index.min.json\".</string>
<string name="label_extension_repos">Repositori ekstensi</string>
<string name="information_empty_repos">Anda tidak memiliki repositori yang ditetapkan.</string>
<string name="invalid_backup_file_error">Kesalahan penuh:</string>
@@ -853,4 +853,59 @@
<string name="pref_mark_duplicate_read_chapter_read_new">Setelah mengambil bab baru</string>
<string name="pref_hide_missing_chapter_indicators">Sembunyikan indikator bab yang hilang</string>
<string name="storage_failed_to_create_download_directory">Gagal membuat direktori unduhan</string>
<string name="label_donate">Donasi</string>
<string name="pref_display_images_description">Render gambar dalam deskripsi manga</string>
<string name="storage_failed_to_create_directory">Gagal membuat direktori: %s</string>
<string name="clear_database_text">Anda akan menghapus entri dari database</string>
<string name="clear_database_history_warning">Membaca bab dan kemajuan entri non-perpustakaan akan hilang</string>
<string name="clear_db_exclude_read">Simpan entri dengan bab yang sudah dibaca</string>
<string name="pref_update_library_manga_titles">Perbarui judul manga perpustakaan agar sesuai dengan sumbernya</string>
<string name="pref_update_library_manga_titles_summary">Peringatan: Jika sebuah manga diganti namanya, maka manga tersebut akan dihapus dari antrean unduhan (jika ada).</string>
<string name="logging_in">Sedang masuk…</string>
<string name="possible_duplicates_title">Duplikat yang mungkin</string>
<string name="possible_duplicates_summary">Anda memiliki entri di perpustakaan Anda dengan nama yang serupa.\n\nPilih entri yang ingin Anda pindahkan atau tambahkan tetap.</string>
<string name="notes_placeholder">Suka bagian di mana…</string>
<string name="migrationConfigScreen.selectedHeader">Dipilih</string>
<string name="migrationConfigScreen.availableHeader">Tersedia</string>
<string name="migrationConfigScreen.selectAllLabel">Pilih semua</string>
<string name="migrationConfigScreen.selectNoneLabel">Tidak memilh</string>
<string name="migrationConfigScreen.selectEnabledLabel">Pilih sumber yang diaktifkan</string>
<string name="migrationConfigScreen.selectPinnedLabel">Pilih sumber yang disematkan</string>
<string name="migrationConfigScreen.continueButtonText">Lanjutkan</string>
<string name="migrationConfigScreen.dataToMigrateHeader">Data yang akan dipindahkan</string>
<string name="migrationConfigScreen.removeDownloadsTitle">Hapus unduhan entri saat ini setelah migrasi</string>
<string name="migrationConfigScreen.additionalSearchQueryLabel">Kata kunci tambahan (opsional)</string>
<string name="migrationConfigScreen.additionalSearchQuerySupportingText">Membantu mempersempit hasil pencarian dengan menambahkan kata kunci tambahan</string>
<string name="migrationConfigScreen.hideUnmatchedTitle">Sembunyikan entri yang tidak cocok</string>
<string name="migrationConfigScreen.hideWithoutUpdatesTitle">Sembunyikan entri yang tidak memiliki bab baru</string>
<string name="migrationConfigScreen.hideWithoutUpdatesSubtitle">Hanya tampilkan entri jika pertandingan memiliki bab tambahan</string>
<string name="migrationConfigScreen.enhancedOptionsWarning">Opsi-opsi ini lambat dan berbahaya, dan dapat menyebabkan pembatasan dari sumber-sumber</string>
<string name="migrationConfigScreen.deepSearchModeTitle">Mode pencarian lanjutan</string>
<string name="migrationConfigScreen.deepSearchModeSubtitle">Memecah judul menjadi kata kunci untuk pencarian yang lebih luas</string>
<string name="migrationConfigScreen.prioritizeByChaptersTitle">Cocokkan berdasarkan nomor bab</string>
<string name="migrationConfigScreen.prioritizeByChaptersSubtitle">Jika diaktifkan, memilih pertandingan yang paling jauh di depan. Jika tidak, memilih pertandingan pertama berdasarkan prioritas sumber.</string>
<string name="migrationListScreenTitle">Migrasi</string>
<string name="migrationListScreenTitleWithProgress">Migrasi (%1$d/%2$d)</string>
<string name="migrationListScreen.copyActionLabel">Salin</string>
<string name="migrationListScreen.migrateActionLabel">Migrasi</string>
<string name="migrationListScreen.noMatchFoundText">Alternatif tidak ditemukan</string>
<string name="migrationListScreen.latestChapterLabel">Terbaru: %1$s</string>
<string name="migrationListScreen.unknownLatestChapter">Tidak Diketahui</string>
<string name="migrationListScreen.searchManuallyActionLabel">Cari secara manual</string>
<string name="migrationListScreen.skipActionLabel">Jangan migrasi</string>
<string name="migrationListScreen.migrateNowActionLabel">Migrasi sekarang</string>
<string name="migrationListScreen.copyNowActionLabel">Salin sekarang</string>
<string name="migrationListScreen.exitDialogTitle">Berhenti bermigrasi?</string>
<string name="migrationListScreen.exitDialog.stopLabel">Berhenti</string>
<string name="migrationListScreen.exitDialog.cancelLabel">Batal</string>
<string name="migrationListScreen.migrateDialog.copyLabel">Salin</string>
<string name="migrationListScreen.migrateDialog.migrateLabel">Migrasi</string>
<string name="migrationListScreen.migrateDialog.cancelLabel">Batal</string>
<string name="migrationListScreen.progressDialog.cancelLabel">Batal</string>
<string name="migrationListScreen.matchWithoutChapterToast">Tidak ditemukan bab, entri ini tidak dapat digunakan untuk migrasi</string>
<string name="pref_disallow_non_ascii_filenames">Jangan izinkan nama file non-ASCII</string>
<string name="pref_disallow_non_ascii_filenames_details">Memastikan kompatibilitas dengan beberapa media penyimpanan yang tidak mendukung Unicode. Jika opsi ini diaktifkan, Anda harus mengganti nama folder sumber dan manga secara manual dengan mengganti karakter non-ASCII menjadi representasi heksadesimal UTF-8 huruf kecil. File chapter tidak perlu diganti namanya.</string>
<string name="pref_download_concurrent_sources">Ambil berkas sumber secara bersamaan</string>
<string name="pref_download_concurrent_pages">Ambil berkas halaman secara bersamaan</string>
<string name="pref_download_concurrent_pages_summary">Halaman yang diunduh secara bersamaan per sumber</string>
</resources>

View File

@@ -121,10 +121,10 @@
<string name="pref_rotation_type">既定の画面向き</string>
<string name="rotation_free">自動回転</string>
<string name="rotation_force_portrait">縦向き画面を強制</string>
<string name="color_filter_r_value">R</string>
<string name="color_filter_g_value">G</string>
<string name="color_filter_b_value">B</string>
<string name="color_filter_a_value">A</string>
<string name="color_filter_r_value"></string>
<string name="color_filter_g_value"></string>
<string name="color_filter_b_value"></string>
<string name="color_filter_a_value">アルファ</string>
<string name="pref_remove_after_marked_as_read">手動で既読にした後</string>
<string name="pref_remove_after_read">読んだ後自動で削除</string>
<string name="disabled">無効</string>
@@ -342,7 +342,7 @@
<string name="ext_updates_pending">更新あり</string>
<string name="pref_library_update_refresh_metadata_summary">ライブラリを更新時、新しい表紙と情報を確認します</string>
<string name="pref_library_update_refresh_metadata">メタデータを自動で更新</string>
<string name="pref_library_columns">グリッドの項目</string>
<string name="pref_library_columns">行あたりのアイテム</string>
<string name="pref_category_display">画面</string>
<string name="hide_notification_content">通知内容を非表示</string>
<string name="secure_screen_summary">セキュア画面はアプリを切り替える時アプリの内容を非表示し、画面キャプチャを無効化します</string>
@@ -540,7 +540,7 @@
<string name="action_display_language_badge">言語</string>
<string name="label_warning">警告</string>
<string name="pref_verbose_logging">Verboseログ出力</string>
<string name="download_queue_size_warning">警告: 大量の一括ダウンロードにより、ソースは遅くなったり、Mihonを接続禁止したりする恐れがあります。詳しくはタップでご覧ください。</string>
<string name="download_queue_size_warning">警告大量のダウンロード、ソースの速度低下や %s のブロックにつながる可能性があります。詳細を確認するにはタップしてください。</string>
<string name="update_72hour">3日ごと</string>
<string name="connected_to_wifi">Wi-Fi接続時のみ</string>
<string name="ext_update_all">全て更新</string>
@@ -687,8 +687,8 @@
<string name="pref_page_rotate">画面に合わせるように幅広いページを回転</string>
<string name="pref_page_rotate_invert">回転した幅広いページの向きを反転</string>
<string name="pref_debug_info">デバッグ情報</string>
<string name="pref_chapter_swipe_start">スワイプ時の操作</string>
<string name="pref_chapter_swipe_end">スワイプ時の操作</string>
<string name="pref_chapter_swipe_start">スワイプする章</string>
<string name="pref_chapter_swipe_end">スワイプする章</string>
<string name="pref_double_tap_zoom">ダブルタップでズーム</string>
<string name="action_set_interval">間隔を設定</string>
<string name="action_filter_interval_custom">カスタマイズした更新頻度</string>
@@ -777,7 +777,7 @@
<string name="ext_revoke_trust">不明な拡張機能の信頼を取り消す</string>
<string name="label_extension_repos">拡張機能リポジトリ</string>
<string name="invalid_repo_name">リポジトリURLが無効です</string>
<string name="action_add_repo_message">Mihonにリポジトリを追加します。「index.min.json」で終わるURLを入力してください。</string>
<string name="action_add_repo_message">%sにリポジトリを追加します。「index.min.json」で終わるURLを入力してください。</string>
<string name="action_add_repo">リポジトリを追加</string>
<string name="action_open_repo">ソース リポジトリを開く</string>
<string name="onboarding_storage_help_info">古いバージョンからバージョンアップしたばかりで、選択に悩んでいますか?ストレージ ガイドにご参照ください。</string>
@@ -835,21 +835,38 @@
<string name="action_notes">ノート</string>
<string name="action_edit_notes">ノートを編集</string>
<string name="export">バックアップ</string>
<string name="pref_behavior">作設定</string>
<string name="pref_behavior"></string>
<string name="pref_mark_duplicate_read_chapter_read_existing">章の読了後</string>
<string name="pref_update_library_manga_titles">ライブラリーのマンガのタイトルをソースに合わせて更新</string>
<string name="pref_incognito_mode_extension_summary">拡張機能の既読章履歴を一時停止</string>
<string name="pref_incognito_mode_extension_summary">拡張機能の閲覧履歴を一時停止</string>
<string name="logging_in">ログイン中…</string>
<string name="pref_mark_duplicate_read_chapter_read">重複した既読済みの章を既読扱い</string>
<string name="pref_mark_duplicate_read_chapter_read">重複する既読章を既読としてマークする</string>
<string name="pref_mark_duplicate_read_chapter_read_new">新章取得後</string>
<string name="possible_duplicates_summary">ライブラリに名前が似ている作品があります。\n\n移行作品の選択、またはそのまま追加</string>
<string name="possible_duplicates_summary">ライブラリに類似した名前のエントリが存在します。\n\n移行するエントリを選択するか、そのまま追加してください</string>
<string name="author">著者</string>
<string name="artist">アーティスト</string>
<string name="action_display_unread_badge">未読の章</string>
<string name="storage_failed_to_create_download_directory">ダウンロードディレクトリの作成に失敗しました</string>
<string name="storage_failed_to_create_directory">ディレクトリの作成に失敗しました: %s</string>
<string name="clear_database_text">データベースから作品が削除されます</string>
<string name="clear_database_history_warning">ライブラリ外作品の既読の章と進捗は失われます</string>
<string name="clear_db_exclude_read">既読の章がある作品は保持</string>
<string name="possible_duplicates_title">重複の可能性あり</string>
<string name="clear_database_history_warning">非図書館エントリの章の読み取りと進捗は失われます</string>
<string name="clear_db_exclude_read">読み終えた章を含むエントリーを保持する</string>
<string name="possible_duplicates_title">重複の可能性</string>
<string name="label_donate">寄付する</string>
<string name="label_auto">自動車</string>
<string name="theme_catppuccin">キャットプッチン</string>
<string name="theme_monochrome">モノクロ</string>
<string name="pref_display_images_description">マンガの描写で画像をレンダリングする</string>
<string name="pref_hide_missing_chapter_indicators">欠落している章のインジケーターを非表示にする</string>
<string name="pref_always_decode_long_strip_with_ssiv_2">レガシーデコーダーを長尺ストリップリーダーに使用する</string>
<string name="library_list">ライブラリーリスト</string>
<string name="pref_disallow_non_ascii_filenames">非ASCIIファイル名を許可しない</string>
<string name="pref_disallow_non_ascii_filenames_details">特定のUnicode非対応ストレージメディアとの互換性を確保します。有効にした場合、ソースフォルダとマンガフォルダは手動で名前を変更する必要があります。非ASCII文字を小文字のUTF-8 16進数表記に置き換えてください。チャプターファイルの名前変更は不要です。</string>
<string name="pref_download_concurrent_sources">同時ソースダウンロード</string>
<string name="pref_download_concurrent_pages">同時ページダウンロード</string>
<string name="pref_download_concurrent_pages_summary">ソースごとの同時ダウンロードページ数</string>
<string name="pref_update_library_manga_titles_summary">警告:マンガのタイトルが変更された場合、ダウンロードキューから削除されます(存在する場合)。</string>
<string name="tracked_privately">非公開で追跡</string>
<string name="action_toggle_private_on">非公開で追跡する</string>
<string name="action_toggle_private_off">公開で追跡する</string>
</resources>

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