mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-18 15:07:30 +01:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
412815af06 | ||
|
|
f7fb68692a | ||
|
|
aa300cb53e | ||
|
|
855eea2ada | ||
|
|
f4703ed83a | ||
|
|
506d51a007 | ||
|
|
9f9155121c | ||
|
|
282110ef21 | ||
|
|
ace387f8bf | ||
|
|
5e428071c9 | ||
|
|
0acd80dd95 | ||
|
|
bdb0ce4779 | ||
|
|
e8bdf58530 | ||
|
|
e36b4ce60b | ||
|
|
6d543024a3 | ||
|
|
0e0b6d9283 | ||
|
|
38b1bd7383 | ||
|
|
8609553896 | ||
|
|
5f0c460668 | ||
|
|
ac28b6c80c | ||
|
|
cc28776735 | ||
|
|
6ab87c7931 | ||
|
|
8662f80fbf | ||
|
|
6508766ccd | ||
|
|
09ec9fc8c5 | ||
|
|
87c6f34a55 | ||
|
|
643762f913 | ||
|
|
7e880014b0 | ||
|
|
f36c259c1f | ||
|
|
aef3beb15f | ||
|
|
e9469451ac | ||
|
|
f9793d3323 | ||
|
|
93ba6acea5 | ||
|
|
5e7fecc2c1 | ||
|
|
343074da5f | ||
|
|
7c08b75555 | ||
|
|
cbf72f4c60 | ||
|
|
0b6de39f2f | ||
|
|
72c4d1fdee | ||
|
|
fa96366b55 | ||
|
|
3ff25bc984 | ||
|
|
e9224bc2ba | ||
|
|
3c731c2cf5 | ||
|
|
5ac58d01b8 | ||
|
|
eefaf028ce | ||
|
|
582ccca1ab | ||
|
|
8f972115a8 | ||
|
|
6f6c033811 | ||
|
|
57a0ab6711 | ||
|
|
58b25d697f | ||
|
|
1a31c7c7ee | ||
|
|
ad6b651b37 | ||
|
|
96e5131358 | ||
|
|
1d5bc8d2c2 | ||
|
|
6cee911239 | ||
|
|
96347e3f76 | ||
|
|
9a45d248b1 | ||
|
|
04168ecec8 | ||
|
|
607f0ea9cd | ||
|
|
27a4f6f45c | ||
|
|
5236d003d2 | ||
|
|
d61a41e819 | ||
|
|
5637860dd2 | ||
|
|
d4d18d0898 | ||
|
|
065147472e | ||
|
|
6f635782c2 | ||
|
|
86d85f74c0 | ||
|
|
29e6a2c4a6 | ||
|
|
60c66bbd3a | ||
|
|
060e5b2e2e | ||
|
|
4ac9fcd4d3 | ||
|
|
d3b7f7e55f | ||
|
|
0d926626a1 | ||
|
|
6495a2ea43 | ||
|
|
94f711ba2a | ||
|
|
9f5c4e03b2 | ||
|
|
49562e1915 | ||
|
|
57c82b30ba | ||
|
|
e573f72cfd | ||
|
|
95357a8625 | ||
|
|
bd90307df9 | ||
|
|
16b5317b90 | ||
|
|
83f4b48629 | ||
|
|
4665dc50f6 | ||
|
|
85f5e5019e | ||
|
|
4bc3b9f3b6 | ||
|
|
2c0d3678d9 | ||
|
|
feda410152 | ||
|
|
200c2df5ba | ||
|
|
be09cddde2 | ||
|
|
498317de52 | ||
|
|
33b876edc6 | ||
|
|
e7251f2034 | ||
|
|
3d3c36078a | ||
|
|
c6a96b3970 | ||
|
|
fb3dc1c984 |
@@ -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
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/1_request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/1_request_feature.yml
vendored
@@ -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
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/2_report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/2_report_issue.yml
vendored
@@ -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
|
||||
|
||||
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -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 }}
|
||||
|
||||
56
CHANGELOG.md
56
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@ Discover and read manga, webtoons, comics, and more – easier than ever on your
|
||||
[](https://discord.gg/mihon)
|
||||
[](https://mihon.app/download)
|
||||
|
||||
[](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
|
||||
[](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
|
||||
[](/LICENSE)
|
||||
[](https://hosted.weblate.org/engage/mihon/)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
7
app/src/main/aidl/mihon/app/shizuku/IShellInterface.aidl
Normal file
7
app/src/main/aidl/mihon/app/shizuku/IShellInterface.aidl
Normal file
@@ -0,0 +1,7 @@
|
||||
package mihon.app.shizuku;
|
||||
|
||||
interface IShellInterface {
|
||||
void install(in AssetFileDescriptor apk) = 1;
|
||||
|
||||
void destroy() = 16777114;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -71,7 +71,7 @@ fun PreferenceScreen(
|
||||
}
|
||||
|
||||
// Create Preference Item
|
||||
is Preference.PreferenceItem<*> -> item {
|
||||
is Preference.PreferenceItem<*, *> -> item {
|
||||
PreferenceItem(
|
||||
item = preference,
|
||||
highlightKey = highlightKey,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
@@ -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(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ?: ""
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -169,6 +169,7 @@ class ExtensionsScreenModel(
|
||||
|
||||
fun cancelInstallUpdateExtension(extension: Extension) {
|
||||
extensionManager.cancelInstallUpdateExtension(extension)
|
||||
removeDownloadState(extension)
|
||||
}
|
||||
|
||||
private fun addDownloadState(extension: Extension, installStep: InstallStep) {
|
||||
|
||||
@@ -484,6 +484,7 @@ class LibraryScreenModel(
|
||||
downloadManager.isChapterDownloaded(
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.url,
|
||||
manga.title,
|
||||
manga.source,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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].
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ class ChapterLoader(
|
||||
val isDownloaded = downloadManager.isChapterDownloaded(
|
||||
dbChapter.name,
|
||||
dbChapter.scanlator,
|
||||
dbChapter.url,
|
||||
manga.title,
|
||||
manga.source,
|
||||
skipCache = true,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -107,6 +107,7 @@ class UpdatesScreenModel(
|
||||
val downloaded = downloadManager.isChapterDownloaded(
|
||||
update.chapterName,
|
||||
update.scanlator,
|
||||
update.chapterUrl,
|
||||
update.mangaTitle,
|
||||
update.sourceId,
|
||||
)
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
176
app/src/main/java/mihon/app/shizuku/ShellInterface.kt
Normal file
176
app/src/main/java/mihon/app/shizuku/ShellInterface.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -251,6 +251,7 @@ class MigrationListScreenModel(
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
migratingManga.searchResult.value = result.toSuccessSearchResult()
|
||||
updateMigrationProgress()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
10
app/src/main/res/drawable/ic_tab_close_24px.xml
Normal file
10
app/src/main/res/drawable/ic_tab_close_24px.xml
Normal 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>
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ fun WebView.setDefaultSettings() {
|
||||
loadWithOverviewMode = true
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
|
||||
// Handle popups properly
|
||||
setSupportMultipleWindows(true)
|
||||
|
||||
// Allow zooming
|
||||
setSupportZoom(true)
|
||||
builtInZoomControls = true
|
||||
|
||||
@@ -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,
|
||||
|
||||
24
data/src/main/sqldelight/tachiyomi/migrations/7.sqm
Normal file
24
data/src/main/sqldelight/tachiyomi/migrations/7.sqm
Normal 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;
|
||||
7
data/src/main/sqldelight/tachiyomi/migrations/8.sqm
Normal file
7
data/src/main/sqldelight/tachiyomi/migrations/8.sqm
Normal 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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user