Compare commits

..

61 Commits

Author SHA1 Message Date
222e111806 Release v0.16.2 2024-01-28 00:28:28 +06:00
8489b0dd8b [skip ci] Translations update from Hosted Weblate (#190)
* Translated using Weblate (Nepali)

Currently translated at 94.1% (747 of 793 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Nepali)

Currently translated at 98.3% (780 of 793 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Nepali)

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Finnish)

Currently translated at 80.9% (642 of 793 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Nepali)

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (793 of 793 strings)

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

* Translated using Weblate (Persian)

Currently translated at 82.7% (656 of 793 strings)

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

* Translated using Weblate (Finnish)

Currently translated at 80.9% (642 of 793 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (17 of 17 strings)

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

* Translated using Weblate (Chuvash)

Currently translated at 88.2% (15 of 17 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (17 of 17 strings)

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

---------

Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Federico Pierantoni <federico.pieranton@gmail.com>
Co-authored-by: Zero O <godarms2010@live.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
Co-authored-by: Paweł Waresiak <pwaresia@redhat.com>
Co-authored-by: kret <cihanbeykoroglu@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: dan-malprod <diabolic0240@proton.me>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: A <ogloppi@mailbox.org>
Co-authored-by: Christian Elbrianno <crse@protonmail.ch>
Co-authored-by: abdelbasset jabrane <ribago9317@cubene.com>
Co-authored-by: Arash <ara.khoram95@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
2024-01-28 00:25:43 +06:00
88ed634978 Lint 2024-01-28 00:15:17 +06:00
32188f9f65 Refactor MAL code to not spam refresh token when it fails 2024-01-28 00:12:31 +06:00
05efc4ebeb Update types of legacy tracker model to match to domain one (#245)
* `score` to Double

* `tracker_id` to Long

* `last_chapter_read` to Double

* `total_chapters` to Long

* `status` to Long
2024-01-27 23:17:09 +06:00
65bfa083f2 Replace "tachiyomi" with "mihon" in crash log name (#234)
Closes #223.
2024-01-26 01:00:23 +06:00
b8a9998bbd [skip ci] Remove official extensions check from issue templates (#233)
* Update report_issue.yml

There are no official extensions anymore and the URL was for the tachiyomi repo anyway

* Update request_feature.yml

No more official extensions
2024-01-25 23:27:23 +06:00
d736bec003 Translations update from Hosted Weblate (#225)
* Translated using Weblate (Swedish)

Currently translated at 100.0% (17 of 17 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (17 of 17 strings)

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

---------

Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
2024-01-24 19:23:39 +06:00
348b23a9fd Fix refreshing from enhanced tracker (#219)
fix refreshing from enhanced tracker
2024-01-24 19:16:28 +06:00
121b2ec829 [skip ci] Removing extensions from config issues (#224) 2024-01-24 19:05:40 +06:00
1dd130df9e Fix #126: Inconsistent button height with some languages in "Data and storage" (#202)
* replace the windowInsetsPadding for navigationBarsPadding + statusBarsPadding

* Fixing bug in the MultiChoiceSegmentedButtonRow

* Rollback file
2024-01-23 18:47:05 +06:00
e17d87f357 Adding Type-safe project accessors (#194)
* replace the windowInsetsPadding for navigationBarsPadding + statusBarsPadding

* Enabling TYPESAFE_PROJECT_ACCESSORS

* Adding typesafe project accessors in the app module

* Adding typesafe project accessors in the core module

* Adding typesafe project accessors in the core-metadata module

* Adding typesafe project accessors in the data module

* Adding typesafe project accessors in the domain module

* Adding typesafe project accessors in the presentation-core module

* Adding typesafe project accessors in the presentation-widget module

* Adding typesafe project accessors in the source-local module

* Adding typesafe project accessors in the source-api module

* Rolling back

* Changing TYPESAFE_PROJECT_ACCESSORS line

* Removing extra spaces
2024-01-23 18:35:58 +06:00
de75561402 Change README to Markdown (#208)
* Change README to Markdown

Also fix license and add disclaimer

* Change some links to markdown as well
2024-01-23 14:14:05 +06:00
58085336a5 Lint 2024-01-22 20:20:11 +06:00
89ea0a271b Add translation widget (#195) 2024-01-22 20:12:39 +06:00
e3f33e24f5 Use own client for trackers + custom user agents
Closes #114
Closes #143

Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
2024-01-22 16:18:30 +06:00
9fd1419142 Translations (#189)
* Small fix on french translation. (#104)

Update fr/strings.xml

Remove mentions of "official" extensions repos. (On 18+ extensions warning)

Fixed a setting label who had the first letter in lowercase.

* Updated Turkish suffixes (#125)

Update strings.xml

* Fix zh-rTW Translation (#118)

fix zh-tw translate

* Update Filipino Plurals (#112)

Updated some Filipino Plurals to make sense grammatically

* Update Filipino Strings (#111)

Changed/updated a few grammatical strings for the Filipino Translation

---------

Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: NukeSource <123626751+NukeSource@users.noreply.github.com>
Co-authored-by: ɴᴇᴋᴏ <111511925+NeKoOuO@users.noreply.github.com>
Co-authored-by: InfinityDouki56 <31158494+infyProductions@users.noreply.github.com>
2024-01-22 15:45:37 +06:00
cb06898430 Fix issues when updating extensions 2024-01-22 02:27:45 +06:00
39407407f2 Remove usage of .not() where possible 2024-01-21 19:40:42 +06:00
a024218410 Fix faulty MangaUpdates score in db
Closes #117
2024-01-21 12:21:30 +06:00
26815c7356 Tweak app icon scaling 2024-01-21 11:55:25 +06:00
e0deeb8008 Backup and Restore Excluded scanlators (#166)
* Backup and Restore Excluded scanlators

* Improve performance

* This looks better
2024-01-21 11:38:36 +06:00
38d6ab80ce Fix "Flash on page change" gives black screen on page change
Fixes #108
2024-01-20 16:33:50 +06:00
78e66fd8d3 Tweak README (#154)
* update README.md

* Update README.md

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-01-20 15:59:18 +06:00
26aa126ecb Modernize README (#139)
* Prepare new Readme

* Modernize README

* Tweak wording
2024-01-20 01:33:42 +06:00
e4a65656e7 refactor: db changes for syncing. (#113)
fix: sync marking chapter unread when we do library update before syncing.

So this should have been 0 on insert instead of the current time on insert. Essentially this issue arises: https://discord.com/channels/1099009852791083058/1099009853864812708/1190022356060614756

Signed-off-by: KaiserBh <kaiserbh@proton.me>
2024-01-18 10:37:41 +06:00
6018aa99e2 Release v0.16.1 2024-01-18 01:30:03 +06:00
99fd2731f5 Fix score issue with MangaUpdates
Also add custom user agent

Potentially fix #17
2024-01-18 01:28:54 +06:00
e34043f1fe Fixed Serbian translation (#75) 2024-01-18 01:27:08 +06:00
3c3a1cd448 [skip ci] Refer to the preview build as beta 2024-01-17 16:01:26 +06:00
0a2df21c5b Fix Indonesian translation (#68)
Fix minor Indonesian translation
2024-01-17 14:05:06 +06:00
277be02682 Update project icon 2024-01-17 13:29:42 +06:00
1849715418 Fix icons not filled
Closes #3
2024-01-17 13:19:49 +06:00
dc6d4f9917 Fix minor grammatical errors in Finnish strings (#64) 2024-01-17 10:48:30 +06:00
a9d98e5048 [skip ci] Updated Issue and Feature Request templates (#41)
* Update version in issue template

* Update request_feature.yml
2024-01-16 23:20:37 +06:00
653940613d Replace some more Tachiyomi reference 2024-01-16 19:55:56 +06:00
23a2d816e4 [skip ci] Replaced mentions of Tachiyomi with Mihon in Issue Templates (#22)
Tachiyomi => Mihon in Issue Templates
2024-01-16 17:53:52 +06:00
8a3a9146db [skip ci] Remove inorichi's Funding.yml, and replace app-icon 2024-01-16 12:53:28 +06:00
a605a4ec75 Release v0.16.0 2024-01-16 11:27:18 +06:00
c83037eeab Fix update downloader borked 2024-01-16 05:43:26 +06:00
25c76f5612 Update icon
Huge thanks to LinkCable for making it
2024-01-16 05:30:38 +06:00
0d449a9b1d That env variable was needed 2024-01-16 04:31:51 +06:00
62cb12a3f1 Add back "Check for Updates" and "What's new" on about 2024-01-16 03:52:35 +06:00
9ec4dc5758 Remove unneeded env from build_push.yml 2024-01-16 03:27:22 +06:00
f594f1994b Lint 2024-01-16 03:04:08 +06:00
96b85962e3 Change application id 2024-01-16 02:55:29 +06:00
f77e0e2d00 Remove weblate from readme 2024-01-16 02:46:08 +06:00
ce60ac150b Rename master branch to main 2024-01-16 01:36:08 +06:00
19afd8c9ca Rename more references 2024-01-16 01:31:22 +06:00
5067160132 Add a temporary icon 2024-01-16 01:17:18 +06:00
c9906491fb Make version code 1 2024-01-16 01:07:32 +06:00
e51013d2a4 Remove unnecessary migrations 2024-01-16 01:07:32 +06:00
1aa75f22d0 Replace all Tachi links to Mihon 2024-01-16 01:07:32 +06:00
8c910f2a2c Make tracker use Mihon's client 2024-01-16 01:07:32 +06:00
dfb3091e38 Make the app Android 8+ 2024-01-16 01:07:32 +06:00
98bdef230a Replace several reference of Tachiyomi to Mihon 2024-01-15 23:53:13 +06:00
4b594fc11f Add back update checker 2024-01-15 21:03:10 +06:00
71931cf697 Add back analytics 2024-01-15 20:55:26 +06:00
87e3525f88 Add back reference to Discord
Partially reverts commit 33c62ab711.
2024-01-15 20:44:08 +06:00
a9c7cbf2c4 Install build-tools 29.0.3 2024-01-13 14:01:49 -05:00
e63a52b8e3 Use newer build tools 2024-01-13 13:52:50 -05:00
257 changed files with 1626 additions and 2205 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
ko_fi: inorichi

View File

@ -5,7 +5,7 @@ I acknowledge that:
- I have updated: - I have updated:
- To the latest version of the app (stable is v0.15.3) - To the latest version of the app (stable is v0.15.3)
- All extensions - All extensions
- I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/) - I have gone through the FAQ (https://mihon.app/docs/faq/general) and troubleshooting guide (https://mihon.app/docs/guides/troubleshooting/)
- If this is an issue with an official extension, that I should be opening an issue in https://github.com/tachiyomiorg/extensions - If this is an issue with an official extension, that I should be opening an issue in https://github.com/tachiyomiorg/extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue - I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
- I will fill out the title and the information in this template - I will fill out the title and the information in this template

View File

@ -1,11 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Extension/source issue - name: 🖥 Mihon website
url: https://github.com/tachiyomiorg/extensions/issues/new/choose url: https://mihon.app/
about: Issues and requests for official extensions and sources should be opened in the extensions repository instead
- name: 📦 Tachiyomi extensions
url: https://tachiyomi.org/extensions/
about: List of all available extensions with download links
- name: 🖥️ Tachiyomi website
url: https://tachiyomi.org/
about: Guides, troubleshooting, and answers to common questions about: Guides, troubleshooting, and answers to common questions

View File

@ -1,5 +1,5 @@
name: 🐞 Issue report name: 🐞 Issue report
description: Report an issue in Tachiyomi description: Report an issue in Mihon
labels: [Bug] labels: [Bug]
body: body:
@ -48,12 +48,12 @@ body:
You can paste the crash logs in plain text or upload it as an attachment. You can paste the crash logs in plain text or upload it as an attachment.
- type: input - type: input
id: tachiyomi-version id: mihon-version
attributes: attributes:
label: Tachiyomi version label: Mihon version
description: You can find your Tachiyomi version in **More → About**. description: You can find your Mihon version in **More → About**.
placeholder: | placeholder: |
Example: "0.15.3" Example: "0.16.1"
validations: validations:
required: true required: true
@ -94,11 +94,9 @@ body:
required: true required: true
- label: I have written a short but informative title. - label: I have written a short but informative title.
required: true required: true
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/extensions/issues/new/choose). - label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true required: true
- label: I have gone through the [FAQ](https://tachiyomi.org/docs/faq/general) and [troubleshooting guide](https://tachiyomi.org/docs/guides/troubleshooting/). - label: I have updated the app to version **[0.16.1](https://github.com/mihonapp/mihon/releases/latest)**.
required: true
- label: I have updated the app to version **[0.15.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true

View File

@ -1,5 +1,5 @@
name: ⭐ Feature request name: ⭐ Feature request
description: Suggest a feature to improve Tachiyomi description: Suggest a feature to improve Mihon
labels: [Feature request] labels: [Feature request]
body: body:
@ -7,7 +7,7 @@ body:
id: feature-description id: feature-description
attributes: attributes:
label: Describe your suggested feature label: Describe your suggested feature
description: How can Tachiyomi be improved? description: How can Mihon be improved?
placeholder: | placeholder: |
Example: Example:
"It should work like this..." "It should work like this..."
@ -31,9 +31,7 @@ body:
required: true required: true
- label: I have written a short but informative title. - label: I have written a short but informative title.
required: true required: true
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/extensions/issues/new/choose). - label: I have updated the app to version **[0.16.1](https://github.com/mihonapp/mihon/releases/latest)**.
required: true
- label: I have updated the app to version **[0.15.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

BIN
.github/assets/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -2,7 +2,7 @@ name: CI
on: on:
push: push:
branches: branches:
- master - main
tags: tags:
- v* - v*
@ -22,6 +22,10 @@ jobs:
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
- name: Setup Android SDK
run: |
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
@ -36,13 +40,13 @@ jobs:
# Sign APK and create release for tags # Sign APK and create release for tags
- name: Get tag name - name: Get tag name
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi' if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
run: | run: |
set -x set -x
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Sign APK - name: Sign APK
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi' if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
uses: r0adkll/sign-android-release@v1 uses: r0adkll/sign-android-release@v1
with: with:
releaseDirectory: app/build/outputs/apk/standard/release releaseDirectory: app/build/outputs/apk/standard/release
@ -52,36 +56,36 @@ jobs:
keyPassword: ${{ secrets.KEY_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Clean up build artifacts - name: Clean up build artifacts
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi' if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
run: | run: |
set -e set -e
mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk tachiyomi-${{ env.VERSION_TAG }}.apk mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk mihon-${{ env.VERSION_TAG }}.apk
sha=`sha256sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` sha=`sha256sum mihon-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
sha=`sha256sum tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` sha=`sha256sum mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk
sha=`sha256sum tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` sha=`sha256sum mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk tachiyomi-x86-${{ env.VERSION_TAG }}.apk cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk mihon-x86-${{ env.VERSION_TAG }}.apk
sha=`sha256sum tachiyomi-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` sha=`sha256sum mihon-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk tachiyomi-x86_64-${{ env.VERSION_TAG }}.apk cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk
sha=`sha256sum tachiyomi-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` sha=`sha256sum mihon-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
- name: Create Release - name: Create Release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi' if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
tag_name: ${{ env.VERSION_TAG }} tag_name: ${{ env.VERSION_TAG }}
name: Tachiyomi ${{ env.VERSION_TAG }} name: Mihon ${{ env.VERSION_TAG }}
body: | body: |
--- ---
@ -94,23 +98,15 @@ jobs:
| armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }} | armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }}
| x86 | ${{ env.APK_X86_SHA }} | | x86 | ${{ env.APK_X86_SHA }} |
| x86_64 | ${{ env.APK_X86_64_SHA }} | | x86_64 | ${{ env.APK_X86_64_SHA }} |
If you are unsure which version to choose then go with mihon-${{ env.VERSION_TAG }}.apk
files: | files: |
tachiyomi-${{ env.VERSION_TAG }}.apk mihon-${{ env.VERSION_TAG }}.apk
tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk
tachiyomi-x86-${{ env.VERSION_TAG }}.apk mihon-x86-${{ env.VERSION_TAG }}.apk
tachiyomi-x86_64-${{ env.VERSION_TAG }}.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk
draft: true draft: true
prerelease: false prerelease: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.PAT }}
update-website:
needs: [build]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
steps:
- name: Trigger Netlify build hook
run: curl -s -X POST -d {} "https://api.netlify.com/build_hooks/${TOKEN}"
env:
TOKEN: ${{ secrets.NETLIFY_HOOK_RELEASE }}

View File

@ -39,7 +39,7 @@ jobs:
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*", "regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
"ignoreCase": true, "ignoreCase": true,
"labels": ["Cloudflare protected"], "labels": ["Cloudflare protected"],
"message": "Refer to the **Solving Cloudflare issues** section at https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection." "message": "Refer to the **Solving Cloudflare issues** section at https://mihon.app/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
} }
] ]
auto-close-ignore-label: do-not-autoclose auto-close-ignore-label: do-not-autoclose

BIN
.idea/icon.png generated

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -59,7 +59,8 @@ representative at an online or offline event.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community moderators via issues. reported to the community moderators responsible for enforcement at
the [Mihon Discord server](https://discord.gg/mihon).
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community moderators are obligated to respect the privacy and security of the All community moderators are obligated to respect the privacy and security of the

View File

@ -1,15 +1,15 @@
Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/tachiyomiorg/tachiyomi#issues-feature-requests-and-contributing). Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/mihonapp/mihon#issues-feature-requests-and-contributing).
--- ---
Thanks for your interest in contributing to Tachiyomi! Thanks for your interest in contributing to Mihon!
# Code contributions # Code contributions
Pull requests are welcome! Pull requests are welcome!
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware. If you're interested in taking on [an open issue](https://github.com/mihonapp/mihon/issues), please comment on it so others are aware.
You do not need to ask for permission nor an assignment. You do not need to ask for permission nor an assignment.
## Prerequisites ## Prerequisites
@ -30,25 +30,24 @@ To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
## Getting help ## Getting help
No support is currently provided. - Join [the Discord server](https://discord.gg/mihon) for online help and to ask questions while developing.
# Translations # Translations
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/docs/contribute#translation) for more details. Translations are done externally via Weblate. See [our website](https://mihon.app/docs/contribute#translation) for more details.
# Forks # Forks
Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/tachiyomiorg/tachiyomi/blob/master/LICENSE). Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/mihonapp/mihon/blob/main/LICENSE).
When creating a fork, remember to: When creating a fork, remember to:
- To avoid confusion with the main app: - To avoid confusion with the main app:
- Change the app name - Change the app name
- Change the app icon - Change the app icon
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt) - Change or disable the [app update checker](https://github.com/mihonapp/mihon/blob/main/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt)
- To avoid installation conflicts: - To avoid installation conflicts:
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) - Change the `applicationId` in [`build.gradle.kts`](https://github.com/mihonapp/mihon/blob/main/app/build.gradle.kts)
- To avoid having your data polluting the main app's analytics and crash report services: - To avoid having your data polluting the main app's analytics and crash report services:
- If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/standard/google-services.json) with your own - If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/mihonapp/mihon/blob/main/app/src/standard/google-services.json) with your own
- If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) with your own

138
README.md
View File

@ -1,91 +1,113 @@
| Build | Stable | Weekly Preview | Contribute | <div align="center">
|-------|--------|----------------|------------|
| [![CI](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest preview build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) |
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi <a href="https://mihon.app">
Tachiyomi is a free and open source manga reader for Android 6.0 and above. <img src="./.github/assets/logo.png" alt="Mihon logo" title="Mihon logo" width="80"/>
</a>
# Mihon [App](#)
### Full-featured reader
Discover and read manga, webtoons, comics, and more easier than ever on your Android device.
[![Discord server](https://img.shields.io/discord/1195734228319617024.svg?label=&labelColor=6A7EC2&color=7389D8&logo=discord&logoColor=FFFFFF)](https://discord.gg/mihon)
[![GitHub downloads](https://img.shields.io/github/downloads/mihonapp/mihon/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://github.com/mihonapp/mihon/releases)
[![CI](https://img.shields.io/github/actions/workflow/status/mihonapp/mihon/build_push.yml?labelColor=27303D)](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
[![License: Apache-2.0](https://img.shields.io/github/license/mihonapp/mihon?labelColor=27303D&color=0877d2)](/LICENSE)
[![Translation status](https://img.shields.io/weblate/progress/mihon?labelColor=27303D&color=946300)](https://hosted.weblate.org/engage/mihon/)
## Download
[![Mihon Stable](https://img.shields.io/github/release/mihonapp/mihon.svg?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://github.com/mihonapp/mihon/releases)
[![Mihon Beta](https://img.shields.io/github/v/release/mihonapp/mihon-preview.svg?maxAge=3600&label=Beta&labelColor=2c2c47&color=1c1c39)](https://github.com/mihonapp/mihon-preview/releases)
*Requires Android 8.0 or higher.*
## Features ## Features
Features include: <div align="left">
* Online reading from a variety of sources
* Local reading of downloaded content * Local reading of content.
* A configurable reader with multiple viewers, reading directions and other settings. * A configurable reader with multiple viewers, reading directions and other settings.
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support * Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support.
* Categories to organize your library * Categories to organize your library.
* Light and dark themes * Light and dark themes.
* Schedule updating your library for new chapters * Schedule updating your library for new chapters.
* Create backups locally to read offline or to your desired cloud service * Create backups locally to read offline or to your desired cloud service.
* Plus much more...
## Download </div>
Get the app from our [releases page](https://github.com/tachiyomiorg/tachiyomi/releases).
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/tachiyomiorg/tachiyomi-preview/releases). ## Contributing
## Issues, Feature Requests and Contributing [Code of conduct](./CODE_OF_CONDUCT.md) · [Contributing guide](./CONTRIBUTING.md)
Please make sure to read the full guidelines. Your issue may be closed without warning if you do not. Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
<details><summary>Issues</summary> If you got any questions, [join our Discord server](https://discord.gg/mihon).
**Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://tachiyomi.org/changelogs/) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).** <details align="center"><summary>Issues</summary><div align="left">
</details> Before reporting a new issue, take a look at the [FAQ](https://mihon.app/docs/faq/general), the [changelog](https://mihon.app/changelogs/) and the already opened [issues](https://github.com/mihonapp/mihon/issues).
<details><summary>Bugs</summary> </div></details>
* Include version (More → About → Version) <details align="center"><summary>Bugs</summary><div align="left">
* If not latest, try updating, it may have already been solved
* Preview version is equal to the number of commits as seen on the main page * Include version (**More → About → Version**).
* Include steps to reproduce (if not obvious from description) * If not latest, try updating, it may have already been solved.
* Include screenshot (if needed) * Beta version is equal to the number of commits as seen on the main page.
* If it could be device-dependent, try reproducing on another device (if possible) * Include steps to reproduce (if not obvious from description).
* Include screenshot (if needed).
* If it could be device-dependent, try reproducing on another device (if possible).
* Don't group unrelated requests into one issue * Don't group unrelated requests into one issue
- **DO:** [#24](https://git.mihon.dev/tachiyomi/tachiyomi/issues/24), [#71](https://git.mihon.dev/tachiyomi/tachiyomi/issues/71)
- **DON'T:** [#75](https://git.mihon.dev/tachiyomi/tachiyomi/issues/75)
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71 </div></details>
DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75 <details align="center"><summary>Feature requests</summary><div align="left">
</details> * Write a detailed issue, explaining what it should do or how.
* Avoid writing just "like X app does";
<details><summary>Feature Requests</summary>
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed) * Include screenshot (if needed)
* Source requests are not accepted.
Source requests are not accepted. </div></details>
</details>
<details><summary>Contributing</summary> ### Repositories
See [CONTRIBUTING.md](./CONTRIBUTING.md). [![mihonapp/website - GitHub](https://github-readme-stats.vercel.app/api/pin/?username=mihonapp&repo=website&bg_color=161B22&text_color=c9d1d9&title_color=0877d2&icon_color=0877d2&border_radius=8&hide_border=true)](https://github.com/mihonapp/website/)
</details>
<details><summary>Code of Conduct</summary> ### Credits
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). Thank you to all the people who have contributed!
</details>
## FAQ <a href="https://github.com/mihonapp/mihon/graphs/contributors">
<img src="https://contrib.rocks/image?repo=mihonapp/mihon" alt="Mihon app contributors" title="Mihon app contributors" width="800"/>
</a>
[See our website.](https://tachiyomi.org/) ### Disclaimer
## License The developer(s) of this application does not have any affiliation with the content providers available, and this application hosts zero content.
Copyright 2015 Javier Tomás ### License
Licensed under the Apache License, Version 2.0 (the "License"); ```
you may not use this file except in compliance with the License. Copyright © 2015 Javier Tomás
You may obtain a copy of the License at 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 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
## Disclaimer Modifications Copyright © 2024 The Mihon Open Source Project
```
The developer of this application does not have any affiliation with the content providers available. </div>

View File

@ -8,6 +8,10 @@ plugins {
id("com.github.zellius.shortcut-helper") id("com.github.zellius.shortcut-helper")
} }
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
}
shortcutHelper.setFilePath("./shortcuts.xml") shortcutHelper.setFilePath("./shortcuts.xml")
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
@ -16,10 +20,10 @@ android {
namespace = "eu.kanade.tachiyomi" namespace = "eu.kanade.tachiyomi"
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi" applicationId = "app.mihon"
versionCode = 119 versionCode = 3
versionName = "0.15.3" versionName = "0.16.2"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -135,15 +139,15 @@ android {
} }
dependencies { dependencies {
implementation(project(":i18n")) implementation(projects.i18n)
implementation(project(":core")) implementation(projects.core)
implementation(project(":core-metadata")) implementation(projects.coreMetadata)
implementation(project(":source-api")) implementation(projects.sourceApi)
implementation(project(":source-local")) implementation(projects.sourceLocal)
implementation(project(":data")) implementation(projects.data)
implementation(project(":domain")) implementation(projects.domain)
implementation(project(":presentation-core")) implementation(projects.presentationCore)
implementation(project(":presentation-widget")) implementation(projects.presentationWidget)
// Compose // Compose
implementation(platform(compose.bom)) implementation(platform(compose.bom))
@ -238,6 +242,9 @@ dependencies {
// Logging // Logging
implementation(libs.logcat) implementation(libs.logcat)
// Crash reports/analytics
"standardImplementation"(libs.firebase.analytics)
// Shizuku // Shizuku
implementation(libs.bundles.shizuku) implementation(libs.bundles.shizuku)

View File

@ -71,3 +71,7 @@
# XmlUtil # XmlUtil
-keep public enum nl.adaptivity.xmlutil.EventType { *; } -keep public enum nl.adaptivity.xmlutil.EventType { *; }
# Firebase
-keep class com.google.firebase.installations.** { *; }
-keep interface com.google.firebase.installations.** { *; }

View File

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="432"
android:viewportHeight="432">
<group>
<clip-path
android:pathData="M0,0h432v432h-432z"/>
<path
android:pathData="M0,0h432v432h-432z"
android:fillColor="#FAFAFA"/>
<path
android:pathData="M0,0h432v432h-432z"
android:fillColor="#2E3943"/>
<path
android:pathData="M322.13,215.5C322.13,272.66 274.64,319 216.07,319C157.49,319 110,272.66 110,215.5C110,158.34 157.49,112 216.07,112C274.64,112 322.13,158.34 322.13,215.5Z"
android:fillColor="#F2FAFF"/>
<path
android:pathData="M216.07,299.59C263.66,299.59 302.24,261.94 302.24,215.5C302.24,169.06 263.66,131.41 216.07,131.41C168.47,131.41 129.89,169.06 129.89,215.5C129.89,261.94 168.47,299.59 216.07,299.59ZM216.07,319C274.64,319 322.13,272.66 322.13,215.5C322.13,158.34 274.64,112 216.07,112C157.49,112 110,158.34 110,215.5C110,272.66 157.49,319 216.07,319Z"
android:fillColor="#7EBBED"
android:fillType="evenOdd"/>
</group>
</vector>

View File

@ -1,27 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="108.0" android:viewportWidth="432"
android:viewportHeight="108.0"> android:viewportHeight="432">
<path <path
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z" android:pathData="M182.03,188.7L181.33,172.69C183.42,173.09 185.91,173.19 191.57,173.19C198.44,173.19 207.49,172.79 212.16,172.19C214.15,171.99 214.95,171.7 216.24,171L226.98,180.15C225.98,181.54 225.68,182.14 224.59,184.92C223.7,187.11 219.62,199.74 218.03,205.11C225.39,206.6 229.46,207.7 235.03,209.98C235.73,205.11 235.83,202.52 235.83,193.67C235.83,191.39 235.73,190.09 235.43,188.01L252.74,188.6C252.24,190.99 252.14,191.98 252.04,195.86C251.64,205.21 251.24,209.68 250.25,216.45C257.11,219.93 257.11,219.93 260.59,221.82C262.38,222.81 262.78,223.01 263.97,223.41L258.2,242.01C255.42,239.52 251.54,236.83 245.87,233.65C240.9,245.49 232.65,254.14 220.12,261C215.94,255.43 212.76,252.05 207.68,248.07C215.04,244.59 218.43,242.4 222.3,238.72C226.08,235.04 228.57,231.46 230.96,226.09C224.59,223.21 220.51,221.92 213.45,220.43C209.38,232.56 206.09,240.32 203.21,244.99C199.33,251.25 194.06,254.54 187.99,254.54C183.32,254.54 178.55,252.45 175.07,248.87C171.09,244.79 169,239.12 169,232.56C169,222.81 173.67,214.36 181.83,209.09C187.1,205.71 192.67,204.21 201.52,203.72C203.31,197.85 204.8,192.78 206.19,187.11C201.82,187.51 196.35,187.81 189.68,188.1C186.1,188.2 184.91,188.3 182.03,188.7ZM197.14,218.93C192.47,219.73 189.68,221.22 187.2,224.4C185.31,226.59 184.41,229.18 184.41,231.96C184.41,235.04 185.91,237.33 187.8,237.33C190.08,237.33 192.67,232.16 197.14,218.93Z"
android:fillColor="#000"/> android:fillColor="#031019"/>
<path
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
android:fillColor="#455A64"/>
<path
android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
android:fillColor="#607D8B"/>
<path
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
android:fillColor="#000"/>
<path
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
android:fillColor="#CE2828"/>
<path
android:pathData="M54,54.5m-19.94,0a19.94,19.94 0,1 1,39.87 0a19.94,19.94 0,1 1,-39.87 0"
android:fillColor="#FFF"/>
<path
android:pathData="M52.04,46.3L47.42,46.3C46.14,46.3 44.93,46.23 44.2,46.14L44.2,49.76C45,49.65 46.16,49.6 47.42,49.6L60.58,49.6C61.86,49.6 63.02,49.65 63.82,49.76L63.82,46.14C63.09,46.23 61.86,46.3 60.58,46.3L55.69,46.3L55.69,45.07C55.69,44.43 55.73,43.95 55.82,43.45L51.9,43.45C51.99,44 52.04,44.43 52.04,45.07L52.04,46.3ZM46.78,60.68C45.46,60.68 44.29,60.63 43.45,60.52L43.45,64.14C44.34,64.03 45.46,63.98 46.78,63.98L61.29,63.98C62.57,63.98 63.71,64.03 64.57,64.14L64.57,60.52C63.73,60.63 62.57,60.68 61.29,60.68L58.24,60.68C59.33,58.06 59.99,56.23 60.7,53.91C61.34,51.81 61.34,51.81 61.56,51.13L57.58,50.06C57.51,50.93 57.37,51.52 56.89,53.41C56.19,56.14 55.32,58.74 54.5,60.68L46.78,60.68ZM46.48,51.36C47.55,54.02 48.28,56.53 49.03,60.15L52.66,58.9C51.65,54.98 50.92,52.66 49.94,50.11L46.48,51.36Z"
android:fillColor="#000"/>
</vector> </vector>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/transparent"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
</adaptive-icon>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/transparent"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -33,6 +33,11 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Remove permission from Firebase dependency -->
<uses-permission
android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" />
<application <application
android:name=".App" android:name=".App"
android:allowBackup="false" android:allowBackup="false"
@ -45,7 +50,7 @@
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:preserveLegacyExternalStorage="true" android:preserveLegacyExternalStorage="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Tachiyomi"> android:theme="@style/Theme.Tachiyomi">
@ -174,7 +179,7 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tachiyomi" /> <data android:scheme="mihon" />
<data android:host="anilist-auth" /> <data android:host="anilist-auth" />
<data android:host="bangumi-auth" /> <data android:host="bangumi-auth" />
@ -231,6 +236,11 @@
android:name="android.webkit.WebView.MetricsOptOut" android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" /> android:value="true" />
<!-- Disable advertising ID collection for Firebase -->
<meta-data
android:name="google_analytics_adid_collection_enabled"
android:value="false" />
</application> </application>
</manifest> </manifest>

View File

@ -163,7 +163,7 @@ class SyncChaptersWithSource(
var updatedToAdd = newChapters.map { toAddItem -> var updatedToAdd = newChapters.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--) var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
chapter = chapter.copy( chapter = chapter.copy(
read = chapter.chapterNumber in deletedReadChapterNumbers, read = chapter.chapterNumber in deletedReadChapterNumbers,

View File

@ -23,7 +23,7 @@ class GetExtensionSources(
ExtensionSourceItem( ExtensionSourceItem(
source = source, source = source,
enabled = source.isEnabled(), enabled = source.isEnabled(),
labelAsName = isMultiSource && isMultiLangSingleSource.not(), labelAsName = isMultiSource && !isMultiLangSingleSource,
) )
} }
} }

View File

@ -22,9 +22,9 @@ class GetExtensionsByType(
extensionManager.availableExtensionsFlow, extensionManager.availableExtensionsFlow,
) { _activeLanguages, _installed, _untrusted, _available -> ) { _activeLanguages, _installed, _untrusted, _available ->
val (updates, installed) = _installed val (updates, installed) = _installed
.filter { (showNsfwSources || it.isNsfw.not()) } .filter { (showNsfwSources || !it.isNsfw) }
.sortedWith( .sortedWith(
compareBy<Extension.Installed> { it.isObsolete.not() } compareBy<Extension.Installed> { !it.isObsolete }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
) )
.partition { it.hasUpdate } .partition { it.hasUpdate }
@ -36,7 +36,7 @@ class GetExtensionsByType(
.filter { extension -> .filter { extension ->
_installed.none { it.pkgName == extension.pkgName } && _installed.none { it.pkgName == extension.pkgName } &&
_untrusted.none { it.pkgName == extension.pkgName } && _untrusted.none { it.pkgName == extension.pkgName } &&
(showNsfwSources || extension.isNsfw.not()) (showNsfwSources || !extension.isNsfw)
} }
.flatMap { ext -> .flatMap { ext ->
if (ext.sources.isEmpty()) { if (ext.sources.isEmpty()) {

View File

@ -34,15 +34,15 @@ class GetSourcesWithFavoriteCount(
when (sorting) { when (sorting) {
SetMigrateSorting.Mode.ALPHABETICAL -> { SetMigrateSorting.Mode.ALPHABETICAL -> {
when { when {
a.first.isStub && b.first.isStub.not() -> -1 a.first.isStub && !b.first.isStub -> -1
b.first.isStub && a.first.isStub.not() -> 1 b.first.isStub && !a.first.isStub -> 1
else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase()) else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase())
} }
} }
SetMigrateSorting.Mode.TOTAL -> { SetMigrateSorting.Mode.TOTAL -> {
when { when {
a.first.isStub && b.first.isStub.not() -> -1 a.first.isStub && !b.first.isStub -> -1
b.first.isStub && a.first.isStub.not() -> 1 b.first.isStub && !a.first.isStub -> 1
else -> a.second.compareTo(b.second) else -> a.second.compareTo(b.second)
} }
} }

View File

@ -30,9 +30,9 @@ class RefreshTracks(
.map { (track, service) -> .map { (track, service) ->
async { async {
return@async try { return@async try {
val updatedTrack = service!!.refresh(track.toDbTrack()) val updatedTrack = service!!.refresh(track.toDbTrack()).toDomainTrack()!!
insertTrack.await(updatedTrack.toDomainTrack()!!) insertTrack.await(updatedTrack)
syncChapterProgressWithTrack.await(mangaId, track, service) syncChapterProgressWithTrack.await(mangaId, updatedTrack, service)
null null
} catch (e: Throwable) { } catch (e: Throwable) {
service to e service to e

View File

@ -19,30 +19,28 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
it.remote_id = remoteId it.remote_id = remoteId
it.library_id = libraryId it.library_id = libraryId
it.title = title it.title = title
it.last_chapter_read = lastChapterRead.toFloat() it.last_chapter_read = lastChapterRead
it.total_chapters = totalChapters.toInt() it.total_chapters = totalChapters
it.status = status.toInt() it.status = status
it.score = score.toFloat() it.score = score
it.tracking_url = remoteUrl it.tracking_url = remoteUrl
it.started_reading_date = startDate it.started_reading_date = startDate
it.finished_reading_date = finishDate it.finished_reading_date = finishDate
} }
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? { fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
val trackId = id ?: if (idRequired.not()) -1 else return null val trackId = id ?: if (!idRequired) -1 else return null
return Track( return Track(
id = trackId, id = trackId,
mangaId = manga_id, mangaId = manga_id,
trackerId = tracker_id.toLong(), trackerId = tracker_id,
remoteId = remote_id, remoteId = remote_id,
libraryId = library_id, libraryId = library_id,
title = title, title = title,
lastChapterRead = last_chapter_read.toDouble(), lastChapterRead = last_chapter_read,
totalChapters = total_chapters.toLong(), totalChapters = total_chapters,
status = status.toLong(), status = status,
// Jank workaround due to precision issues while converting score = score,
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
score = score.toString().toDouble(),
remoteUrl = tracking_url, remoteUrl = tracking_url,
startDate = started_reading_date, startDate = started_reading_date,
finishDate = finished_reading_date, finishDate = finished_reading_date,

View File

@ -19,9 +19,15 @@ class TrackPreferences(
"", "",
) )
fun trackAuthExpired(tracker: Tracker) = preferenceStore.getBoolean(
Preference.privateKey("pref_tracker_auth_expired_${tracker.id}"),
false,
)
fun setCredentials(tracker: Tracker, username: String, password: String) { fun setCredentials(tracker: Tracker, username: String, password: String) {
trackUsername(tracker).set(username) trackUsername(tracker).set(username)
trackPassword(tracker).set(password) trackPassword(tracker).set(password)
trackAuthExpired(tracker).set(false)
} }
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "") fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")

View File

@ -1,6 +1,5 @@
package eu.kanade.domain.ui package eu.kanade.domain.ui
import android.os.Build
import eu.kanade.domain.ui.model.AppTheme import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.domain.ui.model.TabletUiMode import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode import eu.kanade.domain.ui.model.ThemeMode
@ -16,10 +15,7 @@ class UiPreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun themeMode() = preferenceStore.getEnum( fun themeMode() = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM)
"pref_theme_mode_key",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ThemeMode.SYSTEM } else { ThemeMode.LIGHT },
)
fun appTheme() = preferenceStore.getEnum( fun appTheme() = preferenceStore.getEnum(
"pref_app_theme", "pref_app_theme",

View File

@ -203,7 +203,13 @@ private fun ExtensionContent(
items( items(
items = items, items = items,
contentType = { "item" }, contentType = { "item" },
key = { "extension-${it.hashCode()}" }, key = { item ->
when (item.extension) {
is Extension.Untrusted -> "extension-untrusted-${item.hashCode()}"
is Extension.Installed -> "extension-installed-${item.hashCode()}"
is Extension.Available -> "extension-available-${item.hashCode()}"
}
},
) { item -> ) { item ->
ExtensionItem( ExtensionItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),

View File

@ -2,7 +2,6 @@ package eu.kanade.presentation.manga.components
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.os.Build
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -173,14 +172,9 @@ fun MangaCoverDialog(
// Copy bitmap in case it came from memory cache // Copy bitmap in case it came from memory cache
// Because SSIV needs to thoroughly read the image // Because SSIV needs to thoroughly read the image
val copy = (drawable as? BitmapDrawable)?.let { val copy = (drawable as? BitmapDrawable)?.let {
val config = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Bitmap.Config.HARDWARE
} else {
Bitmap.Config.ARGB_8888
}
BitmapDrawable( BitmapDrawable(
view.context.resources, view.context.resources,
it.bitmap.copy(config, false), it.bitmap.copy(Bitmap.Config.HARDWARE, false),
) )
} ?: drawable } ?: drawable
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500)) view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))

View File

@ -21,7 +21,7 @@ fun LogoHeader() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Icon( Icon(
painter = painterResource(R.drawable.ic_tachi), painter = painterResource(R.drawable.ic_mihon),
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier modifier = Modifier

View File

@ -56,7 +56,7 @@ internal class GuidesStep(
} }
} }
const val GETTING_STARTED_URL = "https://tachiyomi.org/docs/guides/getting-started" const val GETTING_STARTED_URL = "https://mihon.app/docs/guides/getting-started"
@PreviewLightDark @PreviewLightDark
@Composable @Composable

View File

@ -3,7 +3,6 @@ package eu.kanade.presentation.more.settings.screen
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.webkit.WebStorage import android.webkit.WebStorage
import android.webkit.WebView import android.webkit.WebView
@ -84,60 +83,48 @@ object SettingsAdvancedScreen : SearchableSettings {
val basePreferences = remember { Injekt.get<BasePreferences>() } val basePreferences = remember { Injekt.get<BasePreferences>() }
val networkPreferences = remember { Injekt.get<NetworkPreferences>() } val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
return buildList { return listOf(
addAll( Preference.PreferenceItem.TextPreference(
listOf( title = stringResource(MR.strings.pref_dump_crash_logs),
Preference.PreferenceItem.TextPreference( subtitle = stringResource(MR.strings.pref_dump_crash_logs_summary),
title = stringResource(MR.strings.pref_dump_crash_logs), onClick = {
subtitle = stringResource(MR.strings.pref_dump_crash_logs_summary), scope.launch {
onClick = { CrashLogUtil(context).dumpLogs()
scope.launch { }
CrashLogUtil(context).dumpLogs() },
} ),
}, Preference.PreferenceItem.SwitchPreference(
), pref = networkPreferences.verboseLogging(),
Preference.PreferenceItem.SwitchPreference( title = stringResource(MR.strings.pref_verbose_logging),
pref = networkPreferences.verboseLogging(), subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
title = stringResource(MR.strings.pref_verbose_logging), onValueChanged = {
subtitle = stringResource(MR.strings.pref_verbose_logging_summary), context.toast(MR.strings.requires_app_restart)
onValueChanged = { true
context.toast(MR.strings.requires_app_restart) },
true ),
}, Preference.PreferenceItem.TextPreference(
), title = stringResource(MR.strings.pref_debug_info),
Preference.PreferenceItem.TextPreference( onClick = { navigator.push(DebugInfoScreen()) },
title = stringResource(MR.strings.pref_debug_info), ),
onClick = { navigator.push(DebugInfoScreen()) }, Preference.PreferenceItem.TextPreference(
), title = stringResource(MR.strings.pref_onboarding_guide),
Preference.PreferenceItem.TextPreference( onClick = { navigator.push(OnboardingScreen()) },
title = stringResource(MR.strings.pref_onboarding_guide), ),
onClick = { navigator.push(OnboardingScreen()) }, Preference.PreferenceItem.TextPreference(
), title = stringResource(MR.strings.pref_manage_notifications),
), onClick = {
) val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
add( }
Preference.PreferenceItem.TextPreference( context.startActivity(intent)
title = stringResource(MR.strings.pref_manage_notifications), },
onClick = { ),
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { getBackgroundActivityGroup(),
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) getDataGroup(),
} getNetworkGroup(networkPreferences = networkPreferences),
context.startActivity(intent) getLibraryGroup(),
}, getExtensionsGroup(basePreferences = basePreferences),
), )
)
}
addAll(
listOf(
getBackgroundActivityGroup(),
getDataGroup(),
getNetworkGroup(networkPreferences = networkPreferences),
getLibraryGroup(),
getExtensionsGroup(basePreferences = basePreferences),
),
)
}
} }
@Composable @Composable

View File

@ -7,8 +7,11 @@ import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
@ -64,7 +67,7 @@ import uy.kohesive.injekt.api.get
object SettingsDataScreen : SearchableSettings { object SettingsDataScreen : SearchableSettings {
val restorePreferenceKeyString = MR.strings.label_backup val restorePreferenceKeyString = MR.strings.label_backup
const val HELP_URL = "https://tachiyomi.org/docs/faq/storage" const val HELP_URL = "https://mihon.app/docs/faq/storage"
@ReadOnlyComposable @ReadOnlyComposable
@Composable @Composable
@ -189,9 +192,11 @@ object SettingsDataScreen : SearchableSettings {
MultiChoiceSegmentedButtonRow( MultiChoiceSegmentedButtonRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(intrinsicSize = IntrinsicSize.Min)
.padding(horizontal = PrefsHorizontalPadding), .padding(horizontal = PrefsHorizontalPadding),
) { ) {
SegmentedButton( SegmentedButton(
modifier = Modifier.fillMaxHeight(),
checked = false, checked = false,
onCheckedChange = { navigator.push(CreateBackupScreen()) }, onCheckedChange = { navigator.push(CreateBackupScreen()) },
shape = SegmentedButtonDefaults.itemShape(0, 2), shape = SegmentedButtonDefaults.itemShape(0, 2),
@ -199,6 +204,7 @@ object SettingsDataScreen : SearchableSettings {
Text(stringResource(MR.strings.pref_create_backup)) Text(stringResource(MR.strings.pref_create_backup))
} }
SegmentedButton( SegmentedButton(
modifier = Modifier.fillMaxHeight(),
checked = false, checked = false,
onCheckedChange = { onCheckedChange = {
if (!BackupRestoreJob.isRunning(context)) { if (!BackupRestoreJob.isRunning(context)) {

View File

@ -60,7 +60,6 @@ object SettingsReaderScreen : SearchableSettings {
pref = readerPref.trueColor(), pref = readerPref.trueColor(),
title = stringResource(MR.strings.pref_true_color), title = stringResource(MR.strings.pref_true_color),
subtitle = stringResource(MR.strings.pref_true_color_summary), subtitle = stringResource(MR.strings.pref_true_color_summary),
enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPref.pageTransitions(), pref = readerPref.pageTransitions(),

View File

@ -71,7 +71,7 @@ object SettingsTrackingScreen : SearchableSettings {
@Composable @Composable
override fun RowScope.AppBarAction() { override fun RowScope.AppBarAction() {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/tracking") }) { IconButton(onClick = { uriHandler.openUri("https://mihon.app/docs/guides/tracking") }) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Outlined.HelpOutline, imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
contentDescription = stringResource(MR.strings.tracking_guide), contentDescription = stringResource(MR.strings.tracking_guide),

View File

@ -1,12 +1,15 @@
package eu.kanade.presentation.more.settings.screen.about package eu.kanade.presentation.more.settings.screen.about
import android.content.Context import android.content.Context
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Public
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -27,10 +30,13 @@ import eu.kanade.presentation.util.LocalBackPress
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.toDateTimestampString import eu.kanade.tachiyomi.util.lang.toDateTimestampString
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
@ -42,6 +48,7 @@ import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.icons.CustomIcons import tachiyomi.presentation.core.icons.CustomIcons
import tachiyomi.presentation.core.icons.Discord
import tachiyomi.presentation.core.icons.Facebook import tachiyomi.presentation.core.icons.Facebook
import tachiyomi.presentation.core.icons.Github import tachiyomi.presentation.core.icons.Github
import tachiyomi.presentation.core.icons.Reddit import tachiyomi.presentation.core.icons.Reddit
@ -91,6 +98,54 @@ object AboutScreen : Screen() {
) )
} }
if (BuildConfig.INCLUDE_UPDATER) {
item {
TextPreferenceWidget(
title = stringResource(MR.strings.check_for_updates),
widget = {
AnimatedVisibility(visible = isCheckingUpdates) {
CircularProgressIndicator(
modifier = Modifier.size(28.dp),
strokeWidth = 3.dp,
)
}
},
onPreferenceClick = {
if (!isCheckingUpdates) {
scope.launch {
isCheckingUpdates = true
checkVersion(
context = context,
onAvailableUpdate = { result ->
val updateScreen = NewUpdateScreen(
versionName = result.release.version,
changelogInfo = result.release.info,
releaseLink = result.release.releaseLink,
downloadLink = result.release.getDownloadLink(),
)
navigator.push(updateScreen)
},
onFinish = {
isCheckingUpdates = false
},
)
}
}
},
)
}
}
if (!BuildConfig.DEBUG) {
item {
TextPreferenceWidget(
title = stringResource(MR.strings.whats_new),
onPreferenceClick = { uriHandler.openUri(RELEASE_URL) },
)
}
}
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(MR.strings.licenses), title = stringResource(MR.strings.licenses),
@ -101,7 +156,7 @@ object AboutScreen : Screen() {
item { item {
TextPreferenceWidget( TextPreferenceWidget(
title = stringResource(MR.strings.privacy_policy), title = stringResource(MR.strings.privacy_policy),
onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/privacy/") }, onPreferenceClick = { uriHandler.openUri("https://mihon.app/privacy/") },
) )
} }
@ -115,27 +170,32 @@ object AboutScreen : Screen() {
LinkIcon( LinkIcon(
label = stringResource(MR.strings.website), label = stringResource(MR.strings.website),
icon = Icons.Outlined.Public, icon = Icons.Outlined.Public,
url = "https://tachiyomi.org", url = "https://mihon.app",
)
LinkIcon(
label = "Discord",
icon = CustomIcons.Discord,
url = "https://discord.gg/mihon",
) )
LinkIcon( LinkIcon(
label = "X", label = "X",
icon = CustomIcons.X, icon = CustomIcons.X,
url = "https://x.com/tachiyomiorg", url = "https://x.com/mihonapp",
) )
LinkIcon( LinkIcon(
label = "Facebook", label = "Facebook",
icon = CustomIcons.Facebook, icon = CustomIcons.Facebook,
url = "https://facebook.com/tachiyomiorg", url = "https://facebook.com/mihonapp",
) )
LinkIcon( LinkIcon(
label = "Reddit", label = "Reddit",
icon = CustomIcons.Reddit, icon = CustomIcons.Reddit,
url = "https://www.reddit.com/r/Tachiyomi", url = "https://www.reddit.com/r/mihonapp",
) )
LinkIcon( LinkIcon(
label = "GitHub", label = "GitHub",
icon = CustomIcons.Github, icon = CustomIcons.Github,
url = "https://github.com/tachiyomiorg", url = "https://github.com/mihonapp",
) )
} }
} }
@ -187,7 +247,7 @@ object AboutScreen : Screen() {
} }
} }
BuildConfig.PREVIEW -> { BuildConfig.PREVIEW -> {
"Preview r${BuildConfig.COMMIT_COUNT}".let { "Beta r${BuildConfig.COMMIT_COUNT}".let {
if (withBuildDate) { if (withBuildDate) {
"$it (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})" "$it (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})"
} else { } else {

View File

@ -1,6 +1,5 @@
package eu.kanade.presentation.more.settings.widget package eu.kanade.presentation.more.settings.widget
import android.os.Build
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MultiChoiceSegmentedButtonRow import androidx.compose.material3.MultiChoiceSegmentedButtonRow
@ -13,18 +12,11 @@ import eu.kanade.domain.ui.model.ThemeMode
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
private val options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { private val options = mapOf(
mapOf( ThemeMode.SYSTEM to MR.strings.theme_system,
ThemeMode.SYSTEM to MR.strings.theme_system, ThemeMode.LIGHT to MR.strings.theme_light,
ThemeMode.LIGHT to MR.strings.theme_light, ThemeMode.DARK to MR.strings.theme_dark,
ThemeMode.DARK to MR.strings.theme_dark, )
)
} else {
mapOf(
ThemeMode.LIGHT to MR.strings.theme_light,
ThemeMode.DARK to MR.strings.theme_dark,
)
}
@Composable @Composable
internal fun AppThemeModePreferenceWidget( internal fun AppThemeModePreferenceWidget(

View File

@ -11,6 +11,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
@Stable @Stable
class DisplayRefreshHost { class DisplayRefreshHost {
@ -30,15 +31,15 @@ fun DisplayRefreshHost(
val currentDisplayRefresh = hostState.currentDisplayRefresh val currentDisplayRefresh = hostState.currentDisplayRefresh
LaunchedEffect(currentDisplayRefresh) { LaunchedEffect(currentDisplayRefresh) {
if (currentDisplayRefresh) { if (currentDisplayRefresh) {
delay(1500) delay(1.5.seconds)
hostState.currentDisplayRefresh = false hostState.currentDisplayRefresh = false
} }
} }
if (currentDisplayRefresh) { Canvas(
Canvas( modifier = modifier.fillMaxSize(),
modifier = modifier.fillMaxSize(), ) {
) { if (currentDisplayRefresh) {
drawRect(Color.Black) drawRect(Color.Black)
} }
} }

View File

@ -88,7 +88,7 @@ fun TrackInfoDialogHome(
TrackInfoItem( TrackInfoItem(
title = item.track.title, title = item.track.title,
tracker = item.tracker, tracker = item.tracker,
status = item.tracker.getStatus(item.track.status.toInt()), status = item.tracker.getStatus(item.track.status),
onStatusClick = { onStatusClick(item) }, onStatusClick = { onStatusClick(item) },
chapters = "${item.track.lastChapterRead.toInt()}".let { chapters = "${item.track.lastChapterRead.toInt()}".let {
val totalChapters = item.track.totalChapters val totalChapters = item.track.totalChapters

View File

@ -48,9 +48,9 @@ import tachiyomi.presentation.core.util.isScrolledToStart
@Composable @Composable
fun TrackStatusSelector( fun TrackStatusSelector(
selection: Int, selection: Long,
onSelectionChange: (Int) -> Unit, onSelectionChange: (Long) -> Unit,
selections: Map<Int, StringResource?>, selections: Map<Long, StringResource?>,
onConfirm: () -> Unit, onConfirm: () -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
@ -236,12 +236,12 @@ private fun TrackStatusSelectorPreviews() {
onSelectionChange = {}, onSelectionChange = {},
selections = persistentMapOf( selections = persistentMapOf(
// Anilist values // Anilist values
1 to MR.strings.reading, 1L to MR.strings.reading,
2 to MR.strings.plan_to_read, 2L to MR.strings.plan_to_read,
3 to MR.strings.completed, 3L to MR.strings.completed,
4 to MR.strings.on_hold, 4L to MR.strings.on_hold,
5 to MR.strings.dropped, 5L to MR.strings.dropped,
6 to MR.strings.repeating, 6L to MR.strings.repeating,
), ),
onConfirm = {}, onConfirm = {},
onDismissRequest = {}, onDismissRequest = {},

View File

@ -301,7 +301,7 @@ private fun SearchResultItem(
text = status, text = status,
) )
} }
if (trackSearch.score != -1f) { if (trackSearch.score != -1.0) {
SearchResultItemDetails( SearchResultItemDetails(
title = stringResource(MR.strings.score), title = stringResource(MR.strings.score),
text = trackSearch.score.toString(), text = trackSearch.score.toString(),

View File

@ -62,14 +62,14 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
private fun randTrackSearch() = TrackSearch().let { private fun randTrackSearch() = TrackSearch().let {
it.id = Random.nextLong() it.id = Random.nextLong()
it.manga_id = Random.nextLong() it.manga_id = Random.nextLong()
it.tracker_id = Random.nextInt() it.tracker_id = Random.nextLong()
it.remote_id = Random.nextLong() it.remote_id = Random.nextLong()
it.library_id = Random.nextLong() it.library_id = Random.nextLong()
it.title = lorem((1..10).random()).joinToString() it.title = lorem((1..10).random()).joinToString()
it.last_chapter_read = (0..100).random().toFloat() it.last_chapter_read = (0..100).random().toDouble()
it.total_chapters = (100..1000).random() it.total_chapters = (100L..1000L).random()
it.score = (0..10).random().toFloat() it.score = (0..10).random().toDouble()
it.status = Random.nextInt() it.status = Random.nextLong()
it.started_reading_date = 0L it.started_reading_date = 0L
it.finished_reading_date = 0L it.finished_reading_date = 0L
it.tracking_url = "https://example.com/tracker-example" it.tracking_url = "https://example.com/tracker-example"

View File

@ -1,7 +1,5 @@
package eu.kanade.presentation.util package eu.kanade.presentation.util
import android.os.Build
import android.provider.Settings
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -23,12 +21,7 @@ fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false)
DisposableEffect(lifecycleOwner.lifecycle) { DisposableEffect(lifecycleOwner.lifecycle) {
val observer = object : DefaultLifecycleObserver { val observer = object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {
installGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { installGranted = context.packageManager.canRequestPackageInstalls()
context.packageManager.canRequestPackageInstalls()
} else {
@Suppress("DEPRECATION")
Settings.Secure.getInt(context.contentResolver, Settings.Secure.INSTALL_NON_MARKET_APPS) != 0
}
} }
} }
lifecycleOwner.lifecycle.addObserver(observer) lifecycleOwner.lifecycle.addObserver(observer)

View File

@ -175,7 +175,7 @@ fun WebViewScreenContent(
.clip(MaterialTheme.shapes.small) .clip(MaterialTheme.shapes.small)
.clickable { .clickable {
uriHandler.openUri( uriHandler.openUri(
"https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare", "https://mihon.app/docs/guides/troubleshooting/#cloudflare",
) )
}, },
) )

View File

@ -169,22 +169,19 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
} }
override fun getPackageName(): String { override fun getPackageName(): String {
// This causes freezes in Android 6/7 for some reason try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Override the value passed as X-Requested-With in WebView requests
try { val stackTrace = Looper.getMainLooper().thread.stackTrace
// Override the value passed as X-Requested-With in WebView requests val chromiumElement = stackTrace.find {
val stackTrace = Looper.getMainLooper().thread.stackTrace it.className.equals(
val chromiumElement = stackTrace.find { "org.chromium.base.BuildInfo",
it.className.equals( ignoreCase = true,
"org.chromium.base.BuildInfo", )
ignoreCase = true,
)
}
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME
}
} catch (_: Exception) {
} }
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME
}
} catch (_: Exception) {
} }
return super.getPackageName() return super.getPackageName()
} }

View File

@ -1,34 +1,10 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import android.content.Context import android.content.Context
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.workManager
import tachiyomi.core.preference.Preference import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.TriState
import tachiyomi.core.preference.getAndSet
import tachiyomi.core.preference.getEnum
import tachiyomi.core.preference.minusAssign
import tachiyomi.core.preference.plusAssign
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
import tachiyomi.i18n.MR
import java.io.File
object Migrations { object Migrations {
@ -37,18 +13,10 @@ object Migrations {
* *
* @return true if a migration is performed, false otherwise. * @return true if a migration is performed, false otherwise.
*/ */
@Suppress("SameReturnValue")
fun upgrade( fun upgrade(
context: Context, context: Context,
preferenceStore: PreferenceStore, preferenceStore: PreferenceStore,
basePreferences: BasePreferences,
uiPreferences: UiPreferences,
networkPreferences: NetworkPreferences,
sourcePreferences: SourcePreferences,
securityPreferences: SecurityPreferences,
libraryPreferences: LibraryPreferences,
readerPreferences: ReaderPreferences,
backupPreferences: BackupPreferences,
trackerManager: TrackerManager,
): Boolean { ): Boolean {
val lastVersionCode = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0) val lastVersionCode = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
val oldVersion = lastVersionCode.get() val oldVersion = lastVersionCode.get()
@ -63,399 +31,8 @@ object Migrations {
if (oldVersion == 0) { if (oldVersion == 0) {
return false return false
} }
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (oldVersion < 15) {
// Delete internal chapter cache dir.
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
}
if (oldVersion < 19) {
// Move covers to external files dir.
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
if (oldDir.exists()) {
val destDir = context.getExternalFilesDir("covers")
if (destDir != null) {
oldDir.listFiles()?.forEach {
it.renameTo(File(destDir, it.name))
}
}
}
}
if (oldVersion < 26) {
// Delete external chapter cache dir.
val extCache = context.externalCacheDir
if (extCache != null) {
val chapterCache = File(extCache, "chapter_disk_cache")
if (chapterCache.exists()) {
chapterCache.deleteRecursively()
}
}
}
if (oldVersion < 44) {
// Reset sorting preference if using removed sort by source
val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0)
if (oldSortingMode == 5) { // SOURCE = 5
prefs.edit {
putInt(libraryPreferences.sortingMode().key(), 0) // ALPHABETICAL = 0
}
}
}
if (oldVersion < 52) {
// Migrate library filters to tri-state versions
fun convertBooleanPrefToTriState(key: String): Int {
val oldPrefValue = prefs.getBoolean(key, false)
return if (oldPrefValue) {
1
} else {
0
}
}
prefs.edit {
putInt(
libraryPreferences.filterDownloaded().key(),
convertBooleanPrefToTriState("pref_filter_downloaded_key"),
)
remove("pref_filter_downloaded_key")
putInt(
libraryPreferences.filterUnread().key(),
convertBooleanPrefToTriState("pref_filter_unread_key"),
)
remove("pref_filter_unread_key")
putInt(
libraryPreferences.filterCompleted().key(),
convertBooleanPrefToTriState("pref_filter_completed_key"),
)
remove("pref_filter_completed_key")
}
}
if (oldVersion < 54) {
// Force MAL log out due to login flow change
// v52: switched from scraping to WebView
// v53: switched from WebView to OAuth
if (trackerManager.myAnimeList.isLoggedIn) {
trackerManager.myAnimeList.logout()
context.toast(MR.strings.myanimelist_relogin)
}
}
if (oldVersion < 57) {
// Migrate DNS over HTTPS setting
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
putInt(networkPreferences.dohProvider().key(), PREF_DOH_CLOUDFLARE)
remove("enable_doh")
}
}
}
if (oldVersion < 59) {
// Reset rotation to Free after replacing Lock
if (prefs.contains("pref_rotation_type_key")) {
prefs.edit {
putInt("pref_rotation_type_key", 1)
}
}
}
if (oldVersion < 60) {
// Migrate Rotation and Viewer values to default values for viewer_flags
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> ReaderOrientation.FREE.flagValue
2 -> ReaderOrientation.PORTRAIT.flagValue
3 -> ReaderOrientation.LANDSCAPE.flagValue
4 -> ReaderOrientation.LOCKED_PORTRAIT.flagValue
5 -> ReaderOrientation.LOCKED_LANDSCAPE.flagValue
else -> ReaderOrientation.FREE.flagValue
}
// Reading mode flag and prefValue is the same value
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
prefs.edit {
putInt("pref_default_orientation_type_key", newOrientation)
remove("pref_rotation_type_key")
putInt("pref_default_reading_mode_key", newReadingMode)
remove("pref_default_viewer_key")
}
}
if (oldVersion < 61) {
// Handle removed every 1 or 2 hour library updates
val updateInterval = libraryPreferences.autoUpdateInterval().get()
if (updateInterval == 1 || updateInterval == 2) {
libraryPreferences.autoUpdateInterval().set(3)
LibraryUpdateJob.setupTask(context, 3)
}
}
if (oldVersion < 64) {
val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0)
val oldSortingDirection = prefs.getBoolean("library_sorting_ascending", true)
val newSortingMode = when (oldSortingMode) {
0 -> "ALPHABETICAL"
1 -> "LAST_READ"
2 -> "LAST_CHECKED"
3 -> "UNREAD"
4 -> "TOTAL_CHAPTERS"
6 -> "LATEST_CHAPTER"
8 -> "DATE_FETCHED"
7 -> "DATE_ADDED"
else -> "ALPHABETICAL"
}
val newSortingDirection = when (oldSortingDirection) {
true -> "ASCENDING"
else -> "DESCENDING"
}
prefs.edit(commit = true) {
remove(libraryPreferences.sortingMode().key())
remove("library_sorting_ascending")
}
prefs.edit {
putString(libraryPreferences.sortingMode().key(), newSortingMode)
putString("library_sorting_ascending", newSortingDirection)
}
}
if (oldVersion < 70) {
if (sourcePreferences.enabledLanguages().isSet()) {
sourcePreferences.enabledLanguages() += "all"
}
}
if (oldVersion < 71) {
// Handle removed every 3, 4, 6, and 8 hour library updates
val updateInterval = libraryPreferences.autoUpdateInterval().get()
if (updateInterval in listOf(3, 4, 6, 8)) {
libraryPreferences.autoUpdateInterval().set(12)
LibraryUpdateJob.setupTask(context, 12)
}
}
if (oldVersion < 72) {
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
if (!oldUpdateOngoingOnly) {
libraryPreferences.autoUpdateMangaRestrictions() -= MANGA_NON_COMPLETED
}
}
if (oldVersion < 75) {
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
if (oldSecureScreen) {
securityPreferences.secureScreen().set(SecurityPreferences.SecureScreenMode.ALWAYS)
}
if (
DeviceUtil.isMiui &&
basePreferences.extensionInstaller().get() == BasePreferences.ExtensionInstaller.PACKAGEINSTALLER
) {
basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY)
}
}
if (oldVersion < 77) {
val oldReaderTap = prefs.getBoolean("reader_tap", false)
if (!oldReaderTap) {
readerPreferences.navigationModePager().set(5)
readerPreferences.navigationModeWebtoon().set(5)
}
}
if (oldVersion < 81) {
// Handle renamed enum values
prefs.edit {
val newSortingMode = when (
val oldSortingMode = prefs.getString(
libraryPreferences.sortingMode().key(),
"ALPHABETICAL",
)
) {
"LAST_CHECKED" -> "LAST_MANGA_UPDATE"
"UNREAD" -> "UNREAD_COUNT"
"DATE_FETCHED" -> "CHAPTER_FETCH_DATE"
else -> oldSortingMode
}
putString(libraryPreferences.sortingMode().key(), newSortingMode)
}
}
if (oldVersion < 82) {
prefs.edit {
val sort = prefs.getString(libraryPreferences.sortingMode().key(), null) ?: return@edit
val direction = prefs.getString("library_sorting_ascending", "ASCENDING")!!
putString(libraryPreferences.sortingMode().key(), "$sort,$direction")
remove("library_sorting_ascending")
}
}
if (oldVersion < 84) {
if (backupPreferences.backupInterval().get() == 0) {
backupPreferences.backupInterval().set(12)
BackupCreateJob.setupTask(context)
}
}
if (oldVersion < 85) {
val preferences = listOf(
libraryPreferences.filterChapterByRead(),
libraryPreferences.filterChapterByDownloaded(),
libraryPreferences.filterChapterByBookmarked(),
libraryPreferences.sortChapterBySourceOrNumber(),
libraryPreferences.displayChapterByNameOrNumber(),
libraryPreferences.sortChapterByAscendingOrDescending(),
)
prefs.edit {
preferences.forEach { preference ->
val key = preference.key()
val value = prefs.getInt(key, Int.MIN_VALUE)
if (value == Int.MIN_VALUE) return@forEach
remove(key)
putLong(key, value.toLong())
}
}
}
if (oldVersion < 86) {
if (uiPreferences.themeMode().isSet()) {
prefs.edit {
val themeMode = prefs.getString(uiPreferences.themeMode().key(), null) ?: return@edit
putString(uiPreferences.themeMode().key(), themeMode.uppercase())
}
}
}
if (oldVersion < 92) {
val trackingQueuePref = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
trackingQueuePref.all.forEach {
val (_, lastChapterRead) = it.value.toString().split(":")
trackingQueuePref.edit {
remove(it.key)
putFloat(it.key, lastChapterRead.toFloat())
}
}
}
if (oldVersion < 96) {
LibraryUpdateJob.cancelAllWorks(context)
LibraryUpdateJob.setupTask(context)
}
if (oldVersion < 97) {
// Removed background jobs
context.workManager.cancelAllWorkByTag("UpdateChecker")
context.workManager.cancelAllWorkByTag("ExtensionUpdate")
prefs.edit {
remove("automatic_ext_updates")
}
}
if (oldVersion < 99) {
val prefKeys = listOf(
"pref_filter_library_downloaded",
"pref_filter_library_unread",
"pref_filter_library_started",
"pref_filter_library_bookmarked",
"pref_filter_library_completed",
) + trackerManager.trackers.map { "pref_filter_library_tracked_${it.id}" }
prefKeys.forEach { key ->
val pref = preferenceStore.getInt(key, 0)
prefs.edit {
remove(key)
val newValue = when (pref.get()) {
1 -> TriState.ENABLED_IS
2 -> TriState.ENABLED_NOT
else -> TriState.DISABLED
}
preferenceStore.getEnum("${key}_v2", TriState.DISABLED).set(newValue)
}
}
}
if (oldVersion < 105) {
val pref = libraryPreferences.autoUpdateDeviceRestrictions()
if (pref.isSet() && "battery_not_low" in pref.get()) {
pref.getAndSet { it - "battery_not_low" }
}
}
if (oldVersion < 106) {
val pref = preferenceStore.getInt("relative_time", 7)
if (pref.get() == 0) {
uiPreferences.relativeTime().set(false)
}
}
if (oldVersion < 113) {
val prefsToReplace = listOf(
"pref_download_only",
"incognito_mode",
"last_catalogue_source",
"trusted_signatures",
"last_app_closed",
"library_update_last_timestamp",
"library_unseen_updates_count",
"last_used_category",
"last_app_check",
"last_ext_check",
"last_version_code",
"storage_dir",
)
replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key in prefsToReplace },
newKey = { Preference.appStateKey(it) },
)
// Deleting old download cache index files, but might as well clear it all out
context.cacheDir.deleteRecursively()
}
if (oldVersion < 114) {
sourcePreferences.extensionRepos().getAndSet {
it.map { repo -> "https://raw.githubusercontent.com/$repo/repo" }.toSet()
}
}
if (oldVersion < 116) {
replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") },
newKey = { Preference.privateKey(it) },
)
}
if (oldVersion < 117) {
prefs.edit {
remove(Preference.appStateKey("trusted_signatures"))
}
}
return true
} }
return false return false
} }
} }
@Suppress("UNCHECKED_CAST")
private fun replacePreferences(
preferenceStore: PreferenceStore,
filterPredicate: (Map.Entry<String, Any?>) -> Boolean,
newKey: (String) -> String,
) {
preferenceStore.getAll()
.filter(filterPredicate)
.forEach { (key, value) ->
when (value) {
is Int -> {
preferenceStore.getInt(newKey(key)).set(value)
preferenceStore.getInt(key).delete()
}
is Long -> {
preferenceStore.getLong(newKey(key)).set(value)
preferenceStore.getLong(key).delete()
}
is Float -> {
preferenceStore.getFloat(newKey(key)).set(value)
preferenceStore.getFloat(key).delete()
}
is String -> {
preferenceStore.getString(newKey(key)).set(value)
preferenceStore.getString(key).delete()
}
is Boolean -> {
preferenceStore.getBoolean(newKey(key)).set(value)
preferenceStore.getBoolean(key).delete()
}
is Set<*> -> (value as? Set<String>)?.let {
preferenceStore.getStringSet(newKey(key)).set(value)
preferenceStore.getStringSet(key).delete()
}
}
}
}

View File

@ -28,7 +28,7 @@ class BackupNotifier(private val context: Context) {
Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS, Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS,
) { ) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_mihon)
setAutoCancel(false) setAutoCancel(false)
setOngoing(true) setOngoing(true)
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
@ -38,7 +38,7 @@ class BackupNotifier(private val context: Context) {
Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE, Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE,
) { ) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_mihon)
setAutoCancel(false) setAutoCancel(false)
} }

View File

@ -30,6 +30,10 @@ class MangaBackupCreator(
// Entry for this manga // Entry for this manga
val mangaObject = manga.toBackupManga() val mangaObject = manga.toBackupManga()
mangaObject.excludedScanlators = handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id)
}
if (options.chapters) { if (options.chapters) {
// Backup all the chapters // Backup all the chapters
handler.awaitList { handler.awaitList {

View File

@ -38,6 +38,7 @@ data class BackupManga(
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE, @ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
@ProtoNumber(106) var lastModifiedAt: Long = 0, @ProtoNumber(106) var lastModifiedAt: Long = 0,
@ProtoNumber(107) var favoriteModifiedAt: Long? = null, @ProtoNumber(107) var favoriteModifiedAt: Long? = null,
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
) { ) {
fun getMangaImpl(): Manga { fun getMangaImpl(): Manga {
return Manga.create().copy( return Manga.create().copy(

View File

@ -73,6 +73,7 @@ class MangaRestorer(
backupCategories = backupCategories, backupCategories = backupCategories,
history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() }, history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() },
tracks = backupManga.tracking, tracks = backupManga.tracking,
excludedScanlators = backupManga.excludedScanlators,
) )
} }
} }
@ -264,11 +265,13 @@ class MangaRestorer(
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
history: List<BackupHistory>, history: List<BackupHistory>,
tracks: List<BackupTracking>, tracks: List<BackupTracking>,
excludedScanlators: List<String>,
): Manga { ): Manga {
restoreCategories(manga, categories, backupCategories) restoreCategories(manga, categories, backupCategories)
restoreChapters(manga, chapters) restoreChapters(manga, chapters)
restoreTracking(manga, tracks) restoreTracking(manga, tracks)
restoreHistory(history) restoreHistory(history)
restoreExcludedScanlators(manga, excludedScanlators)
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow) updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
return manga return manga
} }
@ -401,4 +404,25 @@ class MangaRestorer(
} }
private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L) private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L)
/**
* Restores the excluded scanlators for the manga.
*
* @param manga the manga whose excluded scanlators have to be restored.
* @param excludedScanlators the excluded scanlators to restore.
*/
private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List<String>) {
if (excludedScanlators.isEmpty()) return
val existingExcludedScanlators = handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id)
}
val toInsert = excludedScanlators.filter { it !in existingExcludedScanlators }
if (toInsert.isNotEmpty()) {
handler.await {
toInsert.forEach {
excluded_scanlatorsQueries.insert(manga.id, it)
}
}
}
}
} }

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.coil package eu.kanade.tachiyomi.data.coil
import android.os.Build
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader import coil.ImageLoader
import coil.decode.DecodeResult import coil.decode.DecodeResult
@ -48,8 +47,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
ImageUtil.findImageType(it) ImageUtil.findImageType(it)
} }
return when (type) { return when (type) {
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL, ImageUtil.ImageType.HEIF -> true
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
else -> false else -> false
} }
} }

View File

@ -8,7 +8,7 @@ interface Track : Serializable {
var manga_id: Long var manga_id: Long
var tracker_id: Int var tracker_id: Long
var remote_id: Long var remote_id: Long
@ -16,13 +16,13 @@ interface Track : Serializable {
var title: String var title: String
var last_chapter_read: Float var last_chapter_read: Double
var total_chapters: Int var total_chapters: Long
var score: Float var score: Double
var status: Int var status: Long
var started_reading_date: Long var started_reading_date: Long
@ -40,7 +40,7 @@ interface Track : Serializable {
companion object { companion object {
fun create(serviceId: Long): Track = TrackImpl().apply { fun create(serviceId: Long): Track = TrackImpl().apply {
tracker_id = serviceId.toInt() tracker_id = serviceId
} }
} }
} }

View File

@ -6,7 +6,7 @@ class TrackImpl : Track {
override var manga_id: Long = 0 override var manga_id: Long = 0
override var tracker_id: Int = 0 override var tracker_id: Long = 0
override var remote_id: Long = 0 override var remote_id: Long = 0
@ -14,13 +14,13 @@ class TrackImpl : Track {
override lateinit var title: String override lateinit var title: String
override var last_chapter_read: Float = 0F override var last_chapter_read: Double = 0.0
override var total_chapters: Int = 0 override var total_chapters: Long = 0
override var score: Float = 0f override var score: Double = 0.0
override var status: Int = 0 override var status: Long = 0
override var started_reading_date: Long = 0 override var started_reading_date: Long = 0

View File

@ -315,13 +315,13 @@ class DownloadManager(
val capitalizationChanged = oldFolder.name.equals(newName, ignoreCase = true) val capitalizationChanged = oldFolder.name.equals(newName, ignoreCase = true)
if (capitalizationChanged) { if (capitalizationChanged) {
val tempName = newName + Downloader.TMP_DIR_SUFFIX val tempName = newName + Downloader.TMP_DIR_SUFFIX
if (oldFolder.renameTo(tempName).not()) { if (!oldFolder.renameTo(tempName)) {
logcat(LogPriority.ERROR) { "Failed to rename source download folder: ${oldFolder.name}" } logcat(LogPriority.ERROR) { "Failed to rename source download folder: ${oldFolder.name}" }
return return
} }
} }
if (oldFolder.renameTo(newName).not()) { if (!oldFolder.renameTo(newName)) {
logcat(LogPriority.ERROR) { "Failed to rename source download folder: ${oldFolder.name}" } logcat(LogPriority.ERROR) { "Failed to rename source download folder: ${oldFolder.name}" }
} }
} }

View File

@ -130,7 +130,7 @@ class DownloadProvider(
val newChapterName = sanitizeChapterName(chapterName) val newChapterName = sanitizeChapterName(chapterName)
return DiskUtil.buildValidFilename( return DiskUtil.buildValidFilename(
when { when {
chapterScanlator.isNullOrBlank().not() -> "${chapterScanlator}_$newChapterName" !chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$newChapterName"
else -> newChapterName else -> newChapterName
}, },
) )

View File

@ -413,7 +413,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private const val WORK_NAME_AUTO = "LibraryUpdate-auto" private const val WORK_NAME_AUTO = "LibraryUpdate-auto"
private const val WORK_NAME_MANUAL = "LibraryUpdate-manual" private const val WORK_NAME_MANUAL = "LibraryUpdate-manual"
private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/docs/guides/troubleshooting/" private const val ERROR_LOG_HELP_URL = "https://mihon.app/docs/guides/troubleshooting/"
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60 private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60

View File

@ -153,7 +153,7 @@ class LibraryUpdateNotifier(
) { ) {
setContentTitle(context.stringResource(MR.strings.notification_update_error, failed)) setContentTitle(context.stringResource(MR.strings.notification_update_error, failed))
setContentText(context.stringResource(MR.strings.action_show_errors)) setContentText(context.stringResource(MR.strings.action_show_errors))
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_mihon)
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri)) setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
} }
@ -193,7 +193,7 @@ class LibraryUpdateNotifier(
} }
} }
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_mihon)
setLargeIcon(notificationBitmap) setLargeIcon(notificationBitmap)
setGroup(Notifications.GROUP_NEW_CHAPTERS) setGroup(Notifications.GROUP_NEW_CHAPTERS)
@ -229,7 +229,7 @@ class LibraryUpdateNotifier(
setContentText(description) setContentText(description)
setStyle(NotificationCompat.BigTextStyle().bigText(description)) setStyle(NotificationCompat.BigTextStyle().bigText(description))
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_mihon)
if (icon != null) { if (icon != null) {
setLargeIcon(icon) setLargeIcon(icon)
@ -377,7 +377,7 @@ class LibraryUpdateNotifier(
companion object { companion object {
const val HELP_WARNING_URL = const val HELP_WARNING_URL =
"https://tachiyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads" "https://mihon.app/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads"
} }
} }

View File

@ -5,7 +5,6 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import androidx.core.net.toUri import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
@ -354,20 +353,18 @@ class NotificationReceiver : BroadcastReceiver() {
When programmatically dismissing this notification, the group notification is not automatically dismissed. When programmatically dismissing this notification, the group notification is not automatically dismissed.
*/ */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val groupKey = context.notificationManager.activeNotifications.find {
val groupKey = context.notificationManager.activeNotifications.find { it.id == notificationId
it.id == notificationId }?.groupKey
}?.groupKey
if (groupId != null && groupId != 0 && !groupKey.isNullOrEmpty()) { if (groupId != null && groupId != 0 && !groupKey.isNullOrEmpty()) {
val notifications = context.notificationManager.activeNotifications.filter { val notifications = context.notificationManager.activeNotifications.filter {
it.groupKey == groupKey it.groupKey == groupKey
} }
if (notifications.size == 2) { if (notifications.size == 2) {
context.cancelNotification(groupId) context.cancelNotification(groupId)
return return
}
} }
} }

View File

@ -40,8 +40,8 @@ abstract class BaseTracker(
return track.score return track.score
} }
override fun indexToScore(index: Int): Float { override fun indexToScore(index: Int): Double {
return index.toFloat() return index.toDouble()
} }
@CallSuper @CallSuper
@ -70,24 +70,24 @@ abstract class BaseTracker(
} }
} }
override suspend fun setRemoteStatus(track: Track, status: Int) { override suspend fun setRemoteStatus(track: Track, status: Long) {
track.status = status track.status = status
if (track.status == getCompletionStatus() && track.total_chapters != 0) { if (track.status == getCompletionStatus() && track.total_chapters != 0L) {
track.last_chapter_read = track.total_chapters.toFloat() track.last_chapter_read = track.total_chapters.toDouble()
} }
updateRemote(track) updateRemote(track)
} }
override suspend fun setRemoteLastChapterRead(track: Track, chapterNumber: Int) { override suspend fun setRemoteLastChapterRead(track: Track, chapterNumber: Int) {
if ( if (
track.last_chapter_read == 0f && track.last_chapter_read == 0.0 &&
track.last_chapter_read < chapterNumber && track.last_chapter_read < chapterNumber &&
track.status != getRereadingStatus() track.status != getRereadingStatus()
) { ) {
track.status = getReadingStatus() track.status = getReadingStatus()
} }
track.last_chapter_read = chapterNumber.toFloat() track.last_chapter_read = chapterNumber.toDouble()
if (track.total_chapters != 0 && track.last_chapter_read.toInt() == track.total_chapters) { if (track.total_chapters != 0L && track.last_chapter_read.toLong() == track.total_chapters) {
track.status = getCompletionStatus() track.status = getCompletionStatus()
track.finished_reading_date = System.currentTimeMillis() track.finished_reading_date = System.currentTimeMillis()
} }

View File

@ -27,22 +27,22 @@ interface Tracker {
@DrawableRes @DrawableRes
fun getLogo(): Int fun getLogo(): Int
fun getStatusList(): List<Int> fun getStatusList(): List<Long>
fun getStatus(status: Int): StringResource? fun getStatus(status: Long): StringResource?
fun getReadingStatus(): Int fun getReadingStatus(): Long
fun getRereadingStatus(): Int fun getRereadingStatus(): Long
fun getCompletionStatus(): Int fun getCompletionStatus(): Long
fun getScoreList(): ImmutableList<String> fun getScoreList(): ImmutableList<String>
// TODO: Store all scores as 10 point in the future maybe? // TODO: Store all scores as 10 point in the future maybe?
fun get10PointScore(track: DomainTrack): Double fun get10PointScore(track: DomainTrack): Double
fun indexToScore(index: Int): Float fun indexToScore(index: Int): Double
fun displayScore(track: DomainTrack): String fun displayScore(track: DomainTrack): String
@ -70,7 +70,7 @@ interface Tracker {
// TODO: move this to an interactor, and update all trackers based on common data // TODO: move this to an interactor, and update all trackers based on common data
suspend fun register(item: Track, mangaId: Long) suspend fun register(item: Track, mangaId: Long)
suspend fun setRemoteStatus(track: Track, status: Int) suspend fun setRemoteStatus(track: Track, status: Long)
suspend fun setRemoteLastChapterRead(track: Track, chapterNumber: Int) suspend fun setRemoteLastChapterRead(track: Track, chapterNumber: Int)

View File

@ -20,12 +20,12 @@ import tachiyomi.domain.track.model.Track as DomainTrack
class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
companion object { companion object {
const val READING = 1 const val READING = 1L
const val COMPLETED = 2 const val COMPLETED = 2L
const val ON_HOLD = 3 const val ON_HOLD = 3L
const val DROPPED = 4 const val DROPPED = 4L
const val PLAN_TO_READ = 5 const val PLAN_TO_READ = 5L
const val REREADING = 6 const val REREADING = 6L
const val POINT_100 = "POINT_100" const val POINT_100 = "POINT_100"
const val POINT_10 = "POINT_10" const val POINT_10 = "POINT_10"
@ -58,11 +58,11 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
override fun getLogoColor() = Color.rgb(18, 25, 35) override fun getLogoColor() = Color.rgb(18, 25, 35)
override fun getStatusList(): List<Int> { override fun getStatusList(): List<Long> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING) return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
} }
override fun getStatus(status: Int): StringResource? = when (status) { override fun getStatus(status: Long): StringResource? = when (status) {
READING -> MR.strings.reading READING -> MR.strings.reading
PLAN_TO_READ -> MR.strings.plan_to_read PLAN_TO_READ -> MR.strings.plan_to_read
COMPLETED -> MR.strings.completed COMPLETED -> MR.strings.completed
@ -72,11 +72,11 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
else -> null else -> null
} }
override fun getReadingStatus(): Int = READING override fun getReadingStatus(): Long = READING
override fun getRereadingStatus(): Int = REREADING override fun getRereadingStatus(): Long = REREADING
override fun getCompletionStatus(): Int = COMPLETED override fun getCompletionStatus(): Long = COMPLETED
override fun getScoreList(): ImmutableList<String> { override fun getScoreList(): ImmutableList<String> {
return when (scorePreference.get()) { return when (scorePreference.get()) {
@ -99,24 +99,24 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
return track.score / 10.0 return track.score / 10.0
} }
override fun indexToScore(index: Int): Float { override fun indexToScore(index: Int): Double {
return when (scorePreference.get()) { return when (scorePreference.get()) {
// 10 point // 10 point
POINT_10 -> index * 10f POINT_10 -> index * 10.0
// 100 point // 100 point
POINT_100 -> index.toFloat() POINT_100 -> index.toDouble()
// 5 stars // 5 stars
POINT_5 -> when (index) { POINT_5 -> when (index) {
0 -> 0f 0 -> 0.0
else -> index * 20f - 10f else -> index * 20.0 - 10.0
} }
// Smiley // Smiley
POINT_3 -> when (index) { POINT_3 -> when (index) {
0 -> 0f 0 -> 0.0
else -> index * 25f + 10f else -> index * 25.0 + 10.0
} }
// 10 point decimal // 10 point decimal
POINT_10_DECIMAL -> index.toFloat() POINT_10_DECIMAL -> index.toDouble()
else -> throw Exception("Unknown score type") else -> throw Exception("Unknown score type")
} }
} }
@ -153,12 +153,12 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
if (didReadChapter) { if (didReadChapter) {
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) {
track.status = COMPLETED track.status = COMPLETED
track.finished_reading_date = System.currentTimeMillis() track.finished_reading_date = System.currentTimeMillis()
} else if (track.status != REREADING) { } else if (track.status != REREADING) {
track.status = READING track.status = READING
if (track.last_chapter_read == 1F) { if (track.last_chapter_read == 1.0) {
track.started_reading_date = System.currentTimeMillis() track.started_reading_date = System.currentTimeMillis()
} }
} }
@ -185,14 +185,14 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
val isRereading = track.status == REREADING val isRereading = track.status == REREADING
track.status = if (isRereading.not() && hasReadChapters) READING else track.status track.status = if (!isRereading && hasReadChapters) READING else track.status
} }
update(track) update(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.status = if (hasReadChapters) READING else PLAN_TO_READ track.status = if (hasReadChapters) READING else PLAN_TO_READ
track.score = 0F track.score = 0.0
add(track) add(track)
} }
} }

View File

@ -20,6 +20,7 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long import kotlinx.serialization.json.long
import kotlinx.serialization.json.longOrNull
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject import kotlinx.serialization.json.putJsonObject
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -312,7 +313,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
struct["format"]!!.jsonPrimitive.content.replace("_", "-"), struct["format"]!!.jsonPrimitive.content.replace("_", "-"),
struct["status"]!!.jsonPrimitive.contentOrNull ?: "", struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
parseDate(struct, "startDate"), parseDate(struct, "startDate"),
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0, struct["chapters"]!!.jsonPrimitive.longOrNull ?: 0,
struct["averageScore"]?.jsonPrimitive?.intOrNull ?: -1, struct["averageScore"]?.jsonPrimitive?.intOrNull ?: -1,
) )
} }
@ -363,7 +364,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
companion object { companion object {
private const val clientId = "385" private const val clientId = "16329"
private const val apiUrl = "https://graphql.anilist.co/" private const val apiUrl = "https://graphql.anilist.co/"
private const val baseUrl = "https://anilist.co/api/v2/" private const val baseUrl = "https://anilist.co/api/v2/"
private const val baseMangaUrl = "https://anilist.co/manga/" private const val baseMangaUrl = "https://anilist.co/manga/"

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.anilist package eu.kanade.tachiyomi.data.track.anilist
import eu.kanade.tachiyomi.BuildConfig
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import java.io.IOException import java.io.IOException
@ -40,6 +41,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
// Add the authorization header to the original request. // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)

View File

@ -19,7 +19,7 @@ data class ALManga(
val format: String, val format: String,
val publishing_status: String, val publishing_status: String,
val start_date_fuzzy: Long, val start_date_fuzzy: Long,
val total_chapters: Int, val total_chapters: Long,
val average_score: Int, val average_score: Int,
) { ) {
@ -29,7 +29,7 @@ data class ALManga(
total_chapters = this@ALManga.total_chapters total_chapters = this@ALManga.total_chapters
cover_url = image_url_lge cover_url = image_url_lge
summary = description?.htmlDecode() ?: "" summary = description?.htmlDecode() ?: ""
score = average_score.toFloat() score = average_score.toDouble()
tracking_url = AnilistApi.mangaUrl(remote_id) tracking_url = AnilistApi.mangaUrl(remote_id)
publishing_status = this@ALManga.publishing_status publishing_status = this@ALManga.publishing_status
publishing_type = format publishing_type = format
@ -58,10 +58,10 @@ data class ALUserManga(
remote_id = manga.remote_id remote_id = manga.remote_id
title = manga.title_user_pref title = manga.title_user_pref
status = toTrackStatus() status = toTrackStatus()
score = score_raw.toFloat() score = score_raw.toDouble()
started_reading_date = start_date_fuzzy started_reading_date = start_date_fuzzy
finished_reading_date = completed_date_fuzzy finished_reading_date = completed_date_fuzzy
last_chapter_read = chapters_read.toFloat() last_chapter_read = chapters_read.toDouble()
library_id = this@ALUserManga.library_id library_id = this@ALUserManga.library_id
total_chapters = manga.total_chapters total_chapters = manga.total_chapters
} }

View File

@ -35,7 +35,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
override suspend fun update(track: Track, didReadChapter: Boolean): Track { override suspend fun update(track: Track, didReadChapter: Boolean): Track {
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
if (didReadChapter) { if (didReadChapter) {
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) {
track.status = COMPLETED track.status = COMPLETED
} else { } else {
track.status = READING track.status = READING
@ -64,7 +64,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.status = if (hasReadChapters) READING else PLAN_TO_READ track.status = if (hasReadChapters) READING else PLAN_TO_READ
track.score = 0F track.score = 0.0
add(track) add(track)
update(track) update(track)
} }
@ -87,11 +87,11 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
override fun getLogoColor() = Color.rgb(240, 145, 153) override fun getLogoColor() = Color.rgb(240, 145, 153)
override fun getStatusList(): List<Int> { override fun getStatusList(): List<Long> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
} }
override fun getStatus(status: Int): StringResource? = when (status) { override fun getStatus(status: Long): StringResource? = when (status) {
READING -> MR.strings.reading READING -> MR.strings.reading
PLAN_TO_READ -> MR.strings.plan_to_read PLAN_TO_READ -> MR.strings.plan_to_read
COMPLETED -> MR.strings.completed COMPLETED -> MR.strings.completed
@ -100,11 +100,11 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
else -> null else -> null
} }
override fun getReadingStatus(): Int = READING override fun getReadingStatus(): Long = READING
override fun getRereadingStatus(): Int = -1 override fun getRereadingStatus(): Long = -1
override fun getCompletionStatus(): Int = COMPLETED override fun getCompletionStatus(): Long = COMPLETED
override suspend fun login(username: String, password: String) = login(password) override suspend fun login(username: String, password: String) = login(password)
@ -137,11 +137,11 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
} }
companion object { companion object {
const val READING = 3 const val READING = 3L
const val COMPLETED = 2 const val COMPLETED = 2L
const val ON_HOLD = 4 const val ON_HOLD = 4L
const val DROPPED = 5 const val DROPPED = 5L
const val PLAN_TO_READ = 1 const val PLAN_TO_READ = 1L
private val SCORE_LIST = IntRange(0, 10) private val SCORE_LIST = IntRange(0, 10)
.map(Int::toString) .map(Int::toString)

View File

@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.floatOrNull import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
@ -105,11 +105,11 @@ class BangumiApi(
"" ""
} }
val totalChapters = if (obj["eps_count"] != null) { val totalChapters = if (obj["eps_count"] != null) {
obj["eps_count"]!!.jsonPrimitive.int obj["eps_count"]!!.jsonPrimitive.long
} else { } else {
0 0
} }
val rating = obj["rating"]?.jsonObject?.get("score")?.jsonPrimitive?.floatOrNull ?: -1f val rating = obj["rating"]?.jsonObject?.get("score")?.jsonPrimitive?.doubleOrNull ?: -1.0
return TrackSearch.create(trackId).apply { return TrackSearch.create(trackId).apply {
remote_id = obj["id"]!!.jsonPrimitive.long remote_id = obj["id"]!!.jsonPrimitive.long
title = obj["name_cn"]!!.jsonPrimitive.content title = obj["name_cn"]!!.jsonPrimitive.content
@ -152,7 +152,7 @@ class BangumiApi(
} else { } else {
json.decodeFromString<Collection>(responseBody).let { json.decodeFromString<Collection>(responseBody).let {
track.status = it.status?.id!! track.status = it.status?.id!!
track.last_chapter_read = it.ep_status!!.toFloat() track.last_chapter_read = it.ep_status!!.toDouble()
track.score = it.rating!! track.score = it.rating!!
track track
} }
@ -182,14 +182,14 @@ class BangumiApi(
) )
companion object { companion object {
private const val clientId = "bgm10555cda0762e80ca" private const val clientId = "bgm291665acbd06a4c28"
private const val clientSecret = "8fff394a8627b4c388cbf349ec865775" private const val clientSecret = "43e5ce36b207de16e5d3cfd3e79118db"
private const val apiUrl = "https://api.bgm.tv" private const val apiUrl = "https://api.bgm.tv"
private const val oauthUrl = "https://bgm.tv/oauth/access_token" private const val oauthUrl = "https://bgm.tv/oauth/access_token"
private const val loginUrl = "https://bgm.tv/oauth/authorize" private const val loginUrl = "https://bgm.tv/oauth/authorize"
private const val redirectUrl = "tachiyomi://bangumi-auth" private const val redirectUrl = "mihon://bangumi-auth"
fun authUrl(): Uri = fun authUrl(): Uri =
loginUrl.toUri().buildUpon() loginUrl.toUri().buildUpon()

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.bangumi package eu.kanade.tachiyomi.data.track.bangumi
import eu.kanade.tachiyomi.BuildConfig
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Interceptor import okhttp3.Interceptor
@ -29,22 +30,23 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
} }
} }
val authRequest = if (originalRequest.method == "GET") { return originalRequest.newBuilder()
originalRequest.newBuilder() .header(
.header("User-Agent", "Tachiyomi") "User-Agent",
.url( "antsylich/Mihon/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/mihonapp/mihon)",
originalRequest.url.newBuilder() )
.addQueryParameter("access_token", currAuth.access_token).build(), .apply {
) if (originalRequest.method == "GET") {
.build() val newUrl = originalRequest.url.newBuilder()
} else { .addQueryParameter("access_token", currAuth.access_token)
originalRequest.newBuilder() .build()
.post(addToken(currAuth.access_token, originalRequest.body as FormBody)) url(newUrl)
.header("User-Agent", "Tachiyomi") } else {
.build() post(addToken(currAuth.access_token, originalRequest.body as FormBody))
} }
}
return chain.proceed(authRequest) .build()
.let(chain::proceed)
} }
fun newAuth(oauth: OAuth?) { fun newAuth(oauth: OAuth?) {

View File

@ -16,7 +16,7 @@ data class Collection(
val comment: String? = "", val comment: String? = "",
val ep_status: Int? = 0, val ep_status: Int? = 0,
val lasttouch: Int? = 0, val lasttouch: Int? = 0,
val rating: Float? = 0f, val rating: Double? = 0.0,
val status: Status? = Status(), val status: Status? = Status(),
val tag: List<String?>? = emptyList(), val tag: List<String?>? = emptyList(),
val user: User? = User(), val user: User? = User(),
@ -25,7 +25,7 @@ data class Collection(
@Serializable @Serializable
data class Status( data class Status(
val id: Int? = 0, val id: Long? = 0,
val name: String? = "", val name: String? = "",
val type: String? = "", val type: String? = "",
) )

View File

@ -22,9 +22,9 @@ import tachiyomi.domain.track.model.Track as DomainTrack
class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker { class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
companion object { companion object {
const val UNREAD = 1 const val UNREAD = 1L
const val READING = 2 const val READING = 2L
const val COMPLETED = 3 const val COMPLETED = 3L
} }
var authentications: OAuth? = null var authentications: OAuth? = null
@ -38,20 +38,20 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
override fun getLogoColor() = Color.rgb(74, 198, 148) override fun getLogoColor() = Color.rgb(74, 198, 148)
override fun getStatusList() = listOf(UNREAD, READING, COMPLETED) override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
override fun getStatus(status: Int): StringResource? = when (status) { override fun getStatus(status: Long): StringResource? = when (status) {
UNREAD -> MR.strings.unread UNREAD -> MR.strings.unread
READING -> MR.strings.reading READING -> MR.strings.reading
COMPLETED -> MR.strings.completed COMPLETED -> MR.strings.completed
else -> null else -> null
} }
override fun getReadingStatus(): Int = READING override fun getReadingStatus(): Long = READING
override fun getRereadingStatus(): Int = -1 override fun getRereadingStatus(): Long = -1
override fun getCompletionStatus(): Int = COMPLETED override fun getCompletionStatus(): Long = COMPLETED
override fun getScoreList(): ImmutableList<String> = persistentListOf() override fun getScoreList(): ImmutableList<String> = persistentListOf()
@ -60,7 +60,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
override suspend fun update(track: Track, didReadChapter: Boolean): Track { override suspend fun update(track: Track, didReadChapter: Boolean): Track {
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
if (didReadChapter) { if (didReadChapter) {
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) {
track.status = COMPLETED track.status = COMPLETED
} else { } else {
track.status = READING track.status = READING

View File

@ -93,7 +93,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
* Ignores volumes. * Ignores volumes.
* Volumes consisting of 1 file treated as chapter * Volumes consisting of 1 file treated as chapter
*/ */
private fun getTotalChapters(url: String): Int { private fun getTotalChapters(url: String): Long {
val requestUrl = getApiVolumesUrl(url) val requestUrl = getApiVolumesUrl(url)
try { try {
val listVolumeDto = with(json) { val listVolumeDto = with(json) {
@ -101,13 +101,13 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
.execute() .execute()
.parseAs<List<VolumeDto>>() .parseAs<List<VolumeDto>>()
} }
var volumeNumber = 0 var volumeNumber = 0L
var maxChapterNumber = 0 var maxChapterNumber = 0L
for (volume in listVolumeDto) { for (volume in listVolumeDto) {
if (volume.chapters.maxOf { it.number!!.toFloat() } == 0f) { if (volume.chapters.maxOf { it.number!!.toFloat() } == 0f) {
volumeNumber++ volumeNumber++
} else if (maxChapterNumber < volume.chapters.maxOf { it.number!!.toFloat() }) { } else if (maxChapterNumber < volume.chapters.maxOf { it.number!!.toFloat() }) {
maxChapterNumber = volume.chapters.maxOf { it.number!!.toFloat().toInt() } maxChapterNumber = volume.chapters.maxOf { it.number!!.toFloat().toLong() }
} }
} }
@ -118,17 +118,17 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
} }
} }
private fun getLatestChapterRead(url: String): Float { private fun getLatestChapterRead(url: String): Double {
val seriesId = getIdFromUrl(url) val seriesId = getIdFromUrl(url)
val requestUrl = "${getApiFromUrl(url)}/Tachiyomi/latest-chapter?seriesId=$seriesId" val requestUrl = "${getApiFromUrl(url)}/Tachiyomi/latest-chapter?seriesId=$seriesId"
try { try {
with(json) { with(json) {
authClient.newCall(GET(requestUrl)).execute().use { authClient.newCall(GET(requestUrl)).execute().use {
if (it.code == 200) { if (it.code == 200) {
return it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat() return it.parseAs<ChapterDto>().number!!.replace(",", ".").toDouble()
} }
if (it.code == 204) { if (it.code == 204) {
return 0F return 0.0
} }
} }
} }
@ -139,7 +139,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
) { "Exception getting latest chapter read. Could not get itemRequest: $requestUrl" } ) { "Exception getting latest chapter read. Could not get itemRequest: $requestUrl" }
throw e throw e
} }
return 0F return 0.0
} }
suspend fun getTrackSearch(url: String): TrackSearch = withIOContext { suspend fun getTrackSearch(url: String): TrackSearch = withIOContext {

View File

@ -18,7 +18,7 @@ class KavitaInterceptor(private val kavita: Kavita) : Interceptor {
// Add the authorization header to the original request. // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer $jwtToken") .addHeader("Authorization", "Bearer $jwtToken")
.header("User-Agent", "Tachiyomi Kavita v${BuildConfig.VERSION_NAME}") .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)

View File

@ -19,11 +19,11 @@ import tachiyomi.domain.track.model.Track as DomainTrack
class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
companion object { companion object {
const val READING = 1 const val READING = 1L
const val COMPLETED = 2 const val COMPLETED = 2L
const val ON_HOLD = 3 const val ON_HOLD = 3L
const val DROPPED = 4 const val DROPPED = 4L
const val PLAN_TO_READ = 5 const val PLAN_TO_READ = 5L
} }
override val supportsReadingDates: Boolean = true override val supportsReadingDates: Boolean = true
@ -38,11 +38,11 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
override fun getLogoColor() = Color.rgb(51, 37, 50) override fun getLogoColor() = Color.rgb(51, 37, 50)
override fun getStatusList(): List<Int> { override fun getStatusList(): List<Long> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
} }
override fun getStatus(status: Int): StringResource? = when (status) { override fun getStatus(status: Long): StringResource? = when (status) {
READING -> MR.strings.reading READING -> MR.strings.reading
PLAN_TO_READ -> MR.strings.plan_to_read PLAN_TO_READ -> MR.strings.plan_to_read
COMPLETED -> MR.strings.completed COMPLETED -> MR.strings.completed
@ -51,19 +51,19 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
else -> null else -> null
} }
override fun getReadingStatus(): Int = READING override fun getReadingStatus(): Long = READING
override fun getRereadingStatus(): Int = -1 override fun getRereadingStatus(): Long = -1
override fun getCompletionStatus(): Int = COMPLETED override fun getCompletionStatus(): Long = COMPLETED
override fun getScoreList(): ImmutableList<String> { override fun getScoreList(): ImmutableList<String> {
val df = DecimalFormat("0.#") val df = DecimalFormat("0.#")
return (listOf("0") + IntRange(2, 20).map { df.format(it / 2f) }).toImmutableList() return (listOf("0") + IntRange(2, 20).map { df.format(it / 2f) }).toImmutableList()
} }
override fun indexToScore(index: Int): Float { override fun indexToScore(index: Int): Double {
return if (index > 0) (index + 1) / 2f else 0f return if (index > 0) (index + 1) / 2.0 else 0.0
} }
override fun displayScore(track: DomainTrack): String { override fun displayScore(track: DomainTrack): String {
@ -78,12 +78,12 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
override suspend fun update(track: Track, didReadChapter: Boolean): Track { override suspend fun update(track: Track, didReadChapter: Boolean): Track {
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
if (didReadChapter) { if (didReadChapter) {
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) {
track.status = COMPLETED track.status = COMPLETED
track.finished_reading_date = System.currentTimeMillis() track.finished_reading_date = System.currentTimeMillis()
} else { } else {
track.status = READING track.status = READING
if (track.last_chapter_read == 1F) { if (track.last_chapter_read == 1.0) {
track.started_reading_date = System.currentTimeMillis() track.started_reading_date = System.currentTimeMillis()
} }
} }
@ -110,7 +110,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
update(track) update(track)
} else { } else {
track.status = if (hasReadChapters) READING else PLAN_TO_READ track.status = if (hasReadChapters) READING else PLAN_TO_READ
track.score = 0F track.score = 0.0
add(track) add(track)
} }
} }

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.kitsu package eu.kanade.tachiyomi.data.track.kitsu
import eu.kanade.tachiyomi.BuildConfig
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
@ -34,6 +35,7 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor {
// Add the authorization header to the original request. // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.header("Accept", "application/vnd.api+json") .header("Accept", "application/vnd.api+json")
.header("Content-Type", "application/vnd.api+json") .header("Content-Type", "application/vnd.api+json")
.build() .build()

View File

@ -8,10 +8,10 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long import kotlinx.serialization.json.long
import kotlinx.serialization.json.longOrNull
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -19,7 +19,7 @@ import java.util.Locale
class KitsuSearchManga(obj: JsonObject) { class KitsuSearchManga(obj: JsonObject) {
val id = obj["id"]!!.jsonPrimitive.long val id = obj["id"]!!.jsonPrimitive.long
private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content
private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.intOrNull private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.longOrNull
val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull
val original = try { val original = try {
obj["posterImage"]?.jsonObject?.get("original")?.jsonPrimitive?.content obj["posterImage"]?.jsonObject?.get("original")?.jsonPrimitive?.content
@ -28,7 +28,7 @@ class KitsuSearchManga(obj: JsonObject) {
null null
} }
private val synopsis = obj["synopsis"]?.jsonPrimitive?.contentOrNull private val synopsis = obj["synopsis"]?.jsonPrimitive?.contentOrNull
private val rating = obj["averageRating"]?.jsonPrimitive?.contentOrNull?.toFloatOrNull() private val rating = obj["averageRating"]?.jsonPrimitive?.contentOrNull?.toDoubleOrNull()
private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let { private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(Date(it.toLong() * 1000)) outputDf.format(Date(it.toLong() * 1000))
@ -43,7 +43,7 @@ class KitsuSearchManga(obj: JsonObject) {
cover_url = original ?: "" cover_url = original ?: ""
summary = synopsis ?: "" summary = synopsis ?: ""
tracking_url = KitsuApi.mangaUrl(remote_id) tracking_url = KitsuApi.mangaUrl(remote_id)
score = rating ?: -1f score = rating ?: -1.0
publishing_status = if (endDate == null) { publishing_status = if (endDate == null) {
"Publishing" "Publishing"
} else { } else {
@ -57,7 +57,7 @@ class KitsuSearchManga(obj: JsonObject) {
class KitsuLibManga(obj: JsonObject, manga: JsonObject) { class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
val id = manga["id"]!!.jsonPrimitive.int val id = manga["id"]!!.jsonPrimitive.int
private val canonicalTitle = manga["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content private val canonicalTitle = manga["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content
private val chapterCount = manga["attributes"]!!.jsonObject["chapterCount"]?.jsonPrimitive?.intOrNull private val chapterCount = manga["attributes"]!!.jsonObject["chapterCount"]?.jsonPrimitive?.longOrNull
val type = manga["attributes"]!!.jsonObject["mangaType"]?.jsonPrimitive?.contentOrNull.orEmpty() val type = manga["attributes"]!!.jsonObject["mangaType"]?.jsonPrimitive?.contentOrNull.orEmpty()
val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content
private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
@ -82,8 +82,8 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
started_reading_date = KitsuDateHelper.parse(startedAt) started_reading_date = KitsuDateHelper.parse(startedAt)
finished_reading_date = KitsuDateHelper.parse(finishedAt) finished_reading_date = KitsuDateHelper.parse(finishedAt)
status = toTrackStatus() status = toTrackStatus()
score = ratingTwenty?.let { it.toInt() / 2f } ?: 0f score = ratingTwenty?.let { it.toInt() / 2.0 } ?: 0.0
last_chapter_read = progress.toFloat() last_chapter_read = progress.toDouble()
} }
private fun toTrackStatus() = when (status) { private fun toTrackStatus() = when (status) {

View File

@ -19,9 +19,9 @@ import tachiyomi.domain.track.model.Track as DomainTrack
class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker { class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
companion object { companion object {
const val UNREAD = 1 const val UNREAD = 1L
const val READING = 2 const val READING = 2L
const val COMPLETED = 3 const val COMPLETED = 3L
} }
override val client: OkHttpClient = override val client: OkHttpClient =
@ -35,20 +35,20 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
override fun getLogoColor() = Color.rgb(51, 37, 50) override fun getLogoColor() = Color.rgb(51, 37, 50)
override fun getStatusList() = listOf(UNREAD, READING, COMPLETED) override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
override fun getStatus(status: Int): StringResource? = when (status) { override fun getStatus(status: Long): StringResource? = when (status) {
UNREAD -> MR.strings.unread UNREAD -> MR.strings.unread
READING -> MR.strings.reading READING -> MR.strings.reading
COMPLETED -> MR.strings.completed COMPLETED -> MR.strings.completed
else -> null else -> null
} }
override fun getReadingStatus(): Int = READING override fun getReadingStatus(): Long = READING
override fun getRereadingStatus(): Int = -1 override fun getRereadingStatus(): Long = -1
override fun getCompletionStatus(): Int = COMPLETED override fun getCompletionStatus(): Long = COMPLETED
override fun getScoreList(): ImmutableList<String> = persistentListOf() override fun getScoreList(): ImmutableList<String> = persistentListOf()
@ -57,7 +57,7 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
override suspend fun update(track: Track, didReadChapter: Boolean): Track { override suspend fun update(track: Track, didReadChapter: Boolean): Track {
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
if (didReadChapter) { if (didReadChapter) {
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) {
track.status = COMPLETED track.status = COMPLETED
} else { } else {
track.status = READING track.status = READING

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.komga package eu.kanade.tachiyomi.data.track.komga
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -8,6 +9,7 @@ import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import logcat.LogPriority import logcat.LogPriority
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -23,6 +25,12 @@ class KomgaApi(
private val client: OkHttpClient, private val client: OkHttpClient,
) { ) {
private val headers: Headers by lazy {
Headers.Builder()
.add("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build()
}
private val json: Json by injectLazy() private val json: Json by injectLazy()
suspend fun getTrackSearch(url: String): TrackSearch = suspend fun getTrackSearch(url: String): TrackSearch =
@ -30,12 +38,12 @@ class KomgaApi(
try { try {
val track = with(json) { val track = with(json) {
if (url.contains(READLIST_API)) { if (url.contains(READLIST_API)) {
client.newCall(GET(url)) client.newCall(GET(url, headers))
.awaitSuccess() .awaitSuccess()
.parseAs<ReadListDto>() .parseAs<ReadListDto>()
.toTrack() .toTrack()
} else { } else {
client.newCall(GET(url)) client.newCall(GET(url, headers))
.awaitSuccess() .awaitSuccess()
.parseAs<SeriesDto>() .parseAs<SeriesDto>()
.toTrack() .toTrack()
@ -43,7 +51,9 @@ class KomgaApi(
} }
val progress = client val progress = client
.newCall(GET("${url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi")) .newCall(
GET("${url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi", headers),
)
.awaitSuccess().let { .awaitSuccess().let {
with(json) { with(json) {
if (url.contains("/api/v1/series/")) { if (url.contains("/api/v1/series/")) {
@ -57,7 +67,7 @@ class KomgaApi(
track.apply { track.apply {
cover_url = "$url/thumbnail" cover_url = "$url/thumbnail"
tracking_url = url tracking_url = url
total_chapters = progress.maxNumberSort.toInt() total_chapters = progress.maxNumberSort.toLong()
status = when (progress.booksCount) { status = when (progress.booksCount) {
progress.booksUnreadCount -> Komga.UNREAD progress.booksUnreadCount -> Komga.UNREAD
progress.booksReadCount -> Komga.COMPLETED progress.booksReadCount -> Komga.COMPLETED
@ -80,6 +90,7 @@ class KomgaApi(
client.newCall( client.newCall(
Request.Builder() Request.Builder()
.url("${track.tracking_url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi") .url("${track.tracking_url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi")
.headers(headers)
.put(payload.toRequestBody("application/json".toMediaType())) .put(payload.toRequestBody("application/json".toMediaType()))
.build(), .build(),
) )

View File

@ -65,7 +65,7 @@ data class ReadProgressUpdateDto(
@Serializable @Serializable
data class ReadProgressUpdateV2Dto( data class ReadProgressUpdateV2Dto(
val lastBookNumberSortRead: Float, val lastBookNumberSortRead: Double,
) )
@Serializable @Serializable
@ -91,7 +91,7 @@ data class ReadProgressDto(
booksReadCount, booksReadCount,
booksUnreadCount, booksUnreadCount,
booksInProgressCount, booksInProgressCount,
lastReadContinuousIndex.toFloat(), lastReadContinuousIndex.toDouble(),
booksCount.toFloat(), booksCount.toFloat(),
) )
} }
@ -102,6 +102,6 @@ data class ReadProgressV2Dto(
val booksReadCount: Int, val booksReadCount: Int,
val booksUnreadCount: Int, val booksUnreadCount: Int,
val booksInProgressCount: Int, val booksInProgressCount: Int,
val lastReadContinuousNumberSort: Float, val lastReadContinuousNumberSort: Double,
val maxNumberSort: Float, val maxNumberSort: Float,
) )

View File

@ -6,6 +6,8 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.ListItem
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Rating
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
@ -17,16 +19,22 @@ import tachiyomi.domain.track.model.Track as DomainTrack
class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker { class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker {
companion object { companion object {
const val READING_LIST = 0 const val READING_LIST = 0L
const val WISH_LIST = 1 const val WISH_LIST = 1L
const val COMPLETE_LIST = 2 const val COMPLETE_LIST = 2L
const val UNFINISHED_LIST = 3 const val UNFINISHED_LIST = 3L
const val ON_HOLD_LIST = 4 const val ON_HOLD_LIST = 4L
private val SCORE_LIST = ( private val SCORE_LIST = (0..10)
(0..9) .flatMap { decimal ->
.flatMap { i -> (0..9).map { j -> "$i.$j" } } + listOf("10.0") when (decimal) {
) 0 -> listOf("-")
10 -> listOf("10.0")
else -> (0..9).map { fraction ->
"$decimal.$fraction"
}
}
}
.toImmutableList() .toImmutableList()
} }
@ -38,11 +46,11 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
override fun getLogoColor(): Int = Color.rgb(146, 160, 173) override fun getLogoColor(): Int = Color.rgb(146, 160, 173)
override fun getStatusList(): List<Int> { override fun getStatusList(): List<Long> {
return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST) return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST)
} }
override fun getStatus(status: Int): StringResource? = when (status) { override fun getStatus(status: Long): StringResource? = when (status) {
READING_LIST -> MR.strings.reading_list READING_LIST -> MR.strings.reading_list
WISH_LIST -> MR.strings.wish_list WISH_LIST -> MR.strings.wish_list
COMPLETE_LIST -> MR.strings.complete_list COMPLETE_LIST -> MR.strings.complete_list
@ -51,15 +59,15 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
else -> null else -> null
} }
override fun getReadingStatus(): Int = READING_LIST override fun getReadingStatus(): Long = READING_LIST
override fun getRereadingStatus(): Int = -1 override fun getRereadingStatus(): Long = -1
override fun getCompletionStatus(): Int = COMPLETE_LIST override fun getCompletionStatus(): Long = COMPLETE_LIST
override fun getScoreList(): ImmutableList<String> = SCORE_LIST override fun getScoreList(): ImmutableList<String> = SCORE_LIST
override fun indexToScore(index: Int): Float = SCORE_LIST[index].toFloat() override fun indexToScore(index: Int): Double = if (index == 0) 0.0 else SCORE_LIST[index].toDouble()
override fun displayScore(track: DomainTrack): String = track.score.toString() override fun displayScore(track: DomainTrack): String = track.score.toString()
@ -78,9 +86,9 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
return try { return try {
val (series, rating) = api.getSeriesListItem(track) val (series, rating) = api.getSeriesListItem(track)
series.copyTo(track) track.copyFrom(series, rating)
rating?.copyTo(track) ?: track
} catch (e: Exception) { } catch (e: Exception) {
track.score = 0.0
api.addSeriesToList(track, hasReadChapters) api.addSeriesToList(track, hasReadChapters)
track track
} }
@ -95,8 +103,12 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
override suspend fun refresh(track: Track): Track { override suspend fun refresh(track: Track): Track {
val (series, rating) = api.getSeriesListItem(track) val (series, rating) = api.getSeriesListItem(track)
series.copyTo(track) return track.copyFrom(series, rating)
return rating?.copyTo(track) ?: track }
private fun Track.copyFrom(item: ListItem, rating: Rating?): Track = apply {
item.copyTo(this)
score = rating?.rating ?: 0.0
} }
override suspend fun login(username: String, password: String) { override suspend fun login(username: String, password: String) {
@ -106,6 +118,6 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
} }
fun restoreSession(): String? { fun restoreSession(): String? {
return trackPreferences.trackPassword(this).get() return trackPreferences.trackPassword(this).get().ifBlank { null }
} }
} }

View File

@ -79,7 +79,7 @@ class MangaUpdatesApi(
.let { .let {
if (it.code == 200) { if (it.code == 200) {
track.status = status track.status = status
track.last_chapter_read = 1f track.last_chapter_read = 1.0
} }
} }
} }
@ -133,7 +133,8 @@ class MangaUpdatesApi(
} }
private suspend fun updateSeriesRating(track: Track) { private suspend fun updateSeriesRating(track: Track) {
if (track.score != 0f) { if (track.score < 0.0) return
if (track.score != 0.0) {
val body = buildJsonObject { val body = buildJsonObject {
put("rating", track.score) put("rating", track.score)
} }

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.mangaupdates package eu.kanade.tachiyomi.data.track.mangaupdates
import eu.kanade.tachiyomi.BuildConfig
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import java.io.IOException import java.io.IOException
@ -18,6 +19,7 @@ class MangaUpdatesInterceptor(
// Add the authorization header to the original request. // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer $token") .addHeader("Authorization", "Bearer $token")
.header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)

View File

@ -9,7 +9,7 @@ import kotlinx.serialization.Serializable
data class ListItem( data class ListItem(
val series: Series? = null, val series: Series? = null,
@SerialName("list_id") @SerialName("list_id")
val listId: Int? = null, val listId: Long? = null,
val status: Status? = null, val status: Status? = null,
val priority: Int? = null, val priority: Int? = null,
) )
@ -17,6 +17,6 @@ data class ListItem(
fun ListItem.copyTo(track: Track): Track { fun ListItem.copyTo(track: Track): Track {
return track.apply { return track.apply {
this.status = listId ?: READING_LIST this.status = listId ?: READING_LIST
this.last_chapter_read = this@copyTo.status?.chapter?.toFloat() ?: 0f this.last_chapter_read = this@copyTo.status?.chapter?.toDouble() ?: 0.0
} }
} }

View File

@ -5,11 +5,11 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Rating( data class Rating(
val rating: Float? = null, val rating: Double? = null,
) )
fun Rating.copyTo(track: Track): Track { fun Rating.copyTo(track: Track): Track {
return track.apply { return track.apply {
this.score = rating ?: 0f this.score = rating ?: 0.0
} }
} }

View File

@ -8,7 +8,7 @@ class TrackSearch : Track {
override var manga_id: Long = 0 override var manga_id: Long = 0
override var tracker_id: Int = 0 override var tracker_id: Long = 0
override var remote_id: Long = 0 override var remote_id: Long = 0
@ -16,13 +16,13 @@ class TrackSearch : Track {
override lateinit var title: String override lateinit var title: String
override var last_chapter_read: Float = 0F override var last_chapter_read: Double = 0.0
override var total_chapters: Int = 0 override var total_chapters: Long = 0
override var score: Float = -1f override var score: Double = -1.0
override var status: Int = 0 override var status: Long = 0
override var started_reading_date: Long = 0 override var started_reading_date: Long = 0
@ -55,14 +55,14 @@ class TrackSearch : Track {
override fun hashCode(): Int { override fun hashCode(): Int {
var result = manga_id.hashCode() var result = manga_id.hashCode()
result = 31 * result + tracker_id result = 31 * result + tracker_id.hashCode()
result = 31 * result + remote_id.hashCode() result = 31 * result + remote_id.hashCode()
return result return result
} }
companion object { companion object {
fun create(serviceId: Long): TrackSearch = TrackSearch().apply { fun create(serviceId: Long): TrackSearch = TrackSearch().apply {
tracker_id = serviceId.toInt() tracker_id = serviceId
} }
} }
} }

View File

@ -18,12 +18,12 @@ import tachiyomi.domain.track.model.Track as DomainTrack
class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker { class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
companion object { companion object {
const val READING = 1 const val READING = 1L
const val COMPLETED = 2 const val COMPLETED = 2L
const val ON_HOLD = 3 const val ON_HOLD = 3L
const val DROPPED = 4 const val DROPPED = 4L
const val PLAN_TO_READ = 6 const val PLAN_TO_READ = 6L
const val REREADING = 7 const val REREADING = 7L
private const val SEARCH_ID_PREFIX = "id:" private const val SEARCH_ID_PREFIX = "id:"
private const val SEARCH_LIST_PREFIX = "my:" private const val SEARCH_LIST_PREFIX = "my:"
@ -35,7 +35,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val interceptor by lazy { MyAnimeListInterceptor(this, getPassword()) } private val interceptor by lazy { MyAnimeListInterceptor(this) }
private val api by lazy { MyAnimeListApi(id, client, interceptor) } private val api by lazy { MyAnimeListApi(id, client, interceptor) }
override val supportsReadingDates: Boolean = true override val supportsReadingDates: Boolean = true
@ -44,11 +44,11 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
override fun getLogoColor() = Color.rgb(46, 81, 162) override fun getLogoColor() = Color.rgb(46, 81, 162)
override fun getStatusList(): List<Int> { override fun getStatusList(): List<Long> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING) return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
} }
override fun getStatus(status: Int): StringResource? = when (status) { override fun getStatus(status: Long): StringResource? = when (status) {
READING -> MR.strings.reading READING -> MR.strings.reading
PLAN_TO_READ -> MR.strings.plan_to_read PLAN_TO_READ -> MR.strings.plan_to_read
COMPLETED -> MR.strings.completed COMPLETED -> MR.strings.completed
@ -58,11 +58,11 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
else -> null else -> null
} }
override fun getReadingStatus(): Int = READING override fun getReadingStatus(): Long = READING
override fun getRereadingStatus(): Int = REREADING override fun getRereadingStatus(): Long = REREADING
override fun getCompletionStatus(): Int = COMPLETED override fun getCompletionStatus(): Long = COMPLETED
override fun getScoreList(): ImmutableList<String> = SCORE_LIST override fun getScoreList(): ImmutableList<String> = SCORE_LIST
@ -77,12 +77,12 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
override suspend fun update(track: Track, didReadChapter: Boolean): Track { override suspend fun update(track: Track, didReadChapter: Boolean): Track {
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
if (didReadChapter) { if (didReadChapter) {
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) {
track.status = COMPLETED track.status = COMPLETED
track.finished_reading_date = System.currentTimeMillis() track.finished_reading_date = System.currentTimeMillis()
} else if (track.status != REREADING) { } else if (track.status != REREADING) {
track.status = READING track.status = READING
if (track.last_chapter_read == 1F) { if (track.last_chapter_read == 1.0) {
track.started_reading_date = System.currentTimeMillis() track.started_reading_date = System.currentTimeMillis()
} }
} }
@ -104,14 +104,14 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
val isRereading = track.status == REREADING val isRereading = track.status == REREADING
track.status = if (isRereading.not() && hasReadChapters) READING else track.status track.status = if (!isRereading && hasReadChapters) READING else track.status
} }
update(track) update(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.status = if (hasReadChapters) READING else PLAN_TO_READ track.status = if (hasReadChapters) READING else PLAN_TO_READ
track.score = 0F track.score = 0.0
add(track) add(track)
} }
} }
@ -155,6 +155,14 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
interceptor.setAuth(null) interceptor.setAuth(null)
} }
fun getIfAuthExpired(): Boolean {
return trackPreferences.trackAuthExpired(this).get()
}
fun setAuthExpired() {
trackPreferences.trackAuthExpired(this).set(true)
}
fun saveOAuth(oAuth: OAuth?) { fun saveOAuth(oAuth: OAuth?) {
trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) trackPreferences.trackToken(this).set(json.encodeToString(oAuth))
} }

View File

@ -16,8 +16,8 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.float import kotlinx.serialization.json.double
import kotlinx.serialization.json.floatOrNull import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
@ -47,13 +47,13 @@ class MyAnimeListApi(
suspend fun getAccessToken(authCode: String): OAuth { suspend fun getAccessToken(authCode: String): OAuth {
return withIOContext { return withIOContext {
val formBody: RequestBody = FormBody.Builder() val formBody: RequestBody = FormBody.Builder()
.add("client_id", clientId) .add("client_id", CLIENT_ID)
.add("code", authCode) .add("code", authCode)
.add("code_verifier", codeVerifier) .add("code_verifier", codeVerifier)
.add("grant_type", "authorization_code") .add("grant_type", "authorization_code")
.build() .build()
with(json) { with(json) {
client.newCall(POST("$baseOAuthUrl/token", body = formBody)) client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
@ -63,7 +63,7 @@ class MyAnimeListApi(
suspend fun getCurrentUser(): String { suspend fun getCurrentUser(): String {
return withIOContext { return withIOContext {
val request = Request.Builder() val request = Request.Builder()
.url("$baseApiUrl/users/@me") .url("$BASE_API_URL/users/@me")
.get() .get()
.build() .build()
with(json) { with(json) {
@ -77,7 +77,7 @@ class MyAnimeListApi(
suspend fun search(query: String): List<TrackSearch> { suspend fun search(query: String): List<TrackSearch> {
return withIOContext { return withIOContext {
val url = "$baseApiUrl/manga".toUri().buildUpon() val url = "$BASE_API_URL/manga".toUri().buildUpon()
// MAL API throws a 400 when the query is over 64 characters... // MAL API throws a 400 when the query is over 64 characters...
.appendQueryParameter("q", query.take(64)) .appendQueryParameter("q", query.take(64))
.appendQueryParameter("nsfw", "true") .appendQueryParameter("nsfw", "true")
@ -102,7 +102,7 @@ class MyAnimeListApi(
suspend fun getMangaDetails(id: Int): TrackSearch { suspend fun getMangaDetails(id: Int): TrackSearch {
return withIOContext { return withIOContext {
val url = "$baseApiUrl/manga".toUri().buildUpon() val url = "$BASE_API_URL/manga".toUri().buildUpon()
.appendPath(id.toString()) .appendPath(id.toString())
.appendQueryParameter( .appendQueryParameter(
"fields", "fields",
@ -119,8 +119,8 @@ class MyAnimeListApi(
remote_id = obj["id"]!!.jsonPrimitive.long remote_id = obj["id"]!!.jsonPrimitive.long
title = obj["title"]!!.jsonPrimitive.content title = obj["title"]!!.jsonPrimitive.content
summary = obj["synopsis"]?.jsonPrimitive?.content ?: "" summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int total_chapters = obj["num_chapters"]!!.jsonPrimitive.long
score = obj["mean"]?.jsonPrimitive?.floatOrNull ?: -1f score = obj["mean"]?.jsonPrimitive?.doubleOrNull ?: -1.0
cover_url = cover_url =
obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content
?: "" ?: ""
@ -178,7 +178,7 @@ class MyAnimeListApi(
suspend fun findListItem(track: Track): Track? { suspend fun findListItem(track: Track): Track? {
return withIOContext { return withIOContext {
val uri = "$baseApiUrl/manga".toUri().buildUpon() val uri = "$BASE_API_URL/manga".toUri().buildUpon()
.appendPath(track.remote_id.toString()) .appendPath(track.remote_id.toString())
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}") .appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
.build() .build()
@ -187,7 +187,7 @@ class MyAnimeListApi(
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
.let { obj -> .let { obj ->
track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.int track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.long
obj.jsonObject["my_list_status"]?.jsonObject?.let { obj.jsonObject["my_list_status"]?.jsonObject?.let {
parseMangaItem(it, track) parseMangaItem(it, track)
} }
@ -216,7 +216,7 @@ class MyAnimeListApi(
// Check next page if there's more // Check next page if there's more
if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) { if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) {
matches + findListItems(query, offset + listPaginationAmount) matches + findListItems(query, offset + LIST_PAGINATION_AMOUNT)
} else { } else {
matches matches
} }
@ -225,9 +225,9 @@ class MyAnimeListApi(
private suspend fun getListPage(offset: Int): JsonObject { private suspend fun getListPage(offset: Int): JsonObject {
return withIOContext { return withIOContext {
val urlBuilder = "$baseApiUrl/users/@me/mangalist".toUri().buildUpon() val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
.appendQueryParameter("fields", "list_status{start_date,finish_date}") .appendQueryParameter("fields", "list_status{start_date,finish_date}")
.appendQueryParameter("limit", listPaginationAmount.toString()) .appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString())
if (offset > 0) { if (offset > 0) {
urlBuilder.appendQueryParameter("offset", offset.toString()) urlBuilder.appendQueryParameter("offset", offset.toString())
} }
@ -249,8 +249,8 @@ class MyAnimeListApi(
return track.apply { return track.apply {
val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean
status = if (isRereading) MyAnimeList.REREADING else getStatus(obj["status"]?.jsonPrimitive?.content) status = if (isRereading) MyAnimeList.REREADING else getStatus(obj["status"]?.jsonPrimitive?.content)
last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.float last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.double
score = obj["score"]!!.jsonPrimitive.int.toFloat() score = obj["score"]!!.jsonPrimitive.int.toDouble()
obj["start_date"]?.let { obj["start_date"]?.let {
started_reading_date = parseDate(it.jsonPrimitive.content) started_reading_date = parseDate(it.jsonPrimitive.content)
} }
@ -277,30 +277,29 @@ class MyAnimeListApi(
} }
companion object { companion object {
// Registered under arkon's MAL account private const val CLIENT_ID = "c46c9e24640a64dad5be5ca7a1a53a0f"
private const val clientId = "8fd3313bc138e8b890551aa1de1a2589"
private const val baseOAuthUrl = "https://myanimelist.net/v1/oauth2" private const val BASE_OAUTH_URL = "https://myanimelist.net/v1/oauth2"
private const val baseApiUrl = "https://api.myanimelist.net/v2" private const val BASE_API_URL = "https://api.myanimelist.net/v2"
private const val listPaginationAmount = 250 private const val LIST_PAGINATION_AMOUNT = 250
private var codeVerifier: String = "" private var codeVerifier: String = ""
fun authUrl(): Uri = "$baseOAuthUrl/authorize".toUri().buildUpon() fun authUrl(): Uri = "$BASE_OAUTH_URL/authorize".toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", CLIENT_ID)
.appendQueryParameter("code_challenge", getPkceChallengeCode()) .appendQueryParameter("code_challenge", getPkceChallengeCode())
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
.build() .build()
fun mangaUrl(id: Long): Uri = "$baseApiUrl/manga".toUri().buildUpon() fun mangaUrl(id: Long): Uri = "$BASE_API_URL/manga".toUri().buildUpon()
.appendPath(id.toString()) .appendPath(id.toString())
.appendPath("my_list_status") .appendPath("my_list_status")
.build() .build()
fun refreshTokenRequest(oauth: OAuth): Request { fun refreshTokenRequest(oauth: OAuth): Request {
val formBody: RequestBody = FormBody.Builder() val formBody: RequestBody = FormBody.Builder()
.add("client_id", clientId) .add("client_id", CLIENT_ID)
.add("refresh_token", oauth.refresh_token) .add("refresh_token", oauth.refresh_token)
.add("grant_type", "refresh_token") .add("grant_type", "refresh_token")
.build() .build()
@ -312,7 +311,7 @@ class MyAnimeListApi(
.add("Authorization", "Bearer ${oauth.access_token}") .add("Authorization", "Bearer ${oauth.access_token}")
.build() .build()
return POST("$baseOAuthUrl/token", body = formBody, headers = headers) return POST("$BASE_OAUTH_URL/token", body = formBody, headers = headers)
} }
private fun getPkceChallengeCode(): String { private fun getPkceChallengeCode(): String {

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.myanimelist package eu.kanade.tachiyomi.data.track.myanimelist
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Interceptor import okhttp3.Interceptor
@ -7,33 +8,32 @@ import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor { class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private var oauth: OAuth? = null private var oauth: OAuth? = myanimelist.loadOAuth()
private val tokenExpired get() = myanimelist.getIfAuthExpired()
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
if (tokenExpired) {
throw MALTokenExpired()
}
val originalRequest = chain.request() val originalRequest = chain.request()
if (token.isNullOrEmpty()) {
throw IOException("Not authenticated with MyAnimeList")
}
if (oauth == null) {
oauth = myanimelist.loadOAuth()
}
// Refresh access token if expired // Refresh access token if expired
if (oauth != null && oauth!!.isExpired()) { if (oauth != null && oauth!!.isExpired()) {
setAuth(refreshToken(chain)) setAuth(refreshToken(chain))
} }
if (oauth == null) { if (oauth == null) {
throw IOException("No authentication token") throw IOException("MAL: User is not authenticated")
} }
// Add the authorization header to the original request // Add the authorization header to the original request
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build() .build()
val response = chain.proceed(authRequest) val response = chain.proceed(authRequest)
@ -50,6 +50,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
val newRequest = originalRequest.newBuilder() val newRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${newToken.access_token}") .addHeader("Authorization", "Bearer ${newToken.access_token}")
.header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build() .build()
return chain.proceed(newRequest) return chain.proceed(newRequest)
@ -63,15 +64,16 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
* and the oauth object. * and the oauth object.
*/ */
fun setAuth(oauth: OAuth?) { fun setAuth(oauth: OAuth?) {
token = oauth?.access_token
this.oauth = oauth this.oauth = oauth
myanimelist.saveOAuth(oauth) myanimelist.saveOAuth(oauth)
} }
private fun refreshToken(chain: Interceptor.Chain): OAuth { private fun refreshToken(chain: Interceptor.Chain): OAuth {
val newOauth = runCatching { return runCatching {
val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!)) val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
if (oauthResponse.code == 401) {
myanimelist.setAuthExpired()
}
if (oauthResponse.isSuccessful) { if (oauthResponse.isSuccessful) {
with(json) { oauthResponse.parseAs<OAuth>() } with(json) { oauthResponse.parseAs<OAuth>() }
} else { } else {
@ -79,11 +81,9 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
null null
} }
} }
.getOrNull()
if (newOauth.getOrNull() == null) { ?: throw MALTokenExpired()
throw IOException("Failed to refresh the access token")
}
return newOauth.getOrNull()!!
} }
} }
class MALTokenExpired : IOException("MAL: Login has expired")

View File

@ -18,12 +18,12 @@ import tachiyomi.domain.track.model.Track as DomainTrack
class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker { class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
companion object { companion object {
const val READING = 1 const val READING = 1L
const val COMPLETED = 2 const val COMPLETED = 2L
const val ON_HOLD = 3 const val ON_HOLD = 3L
const val DROPPED = 4 const val DROPPED = 4L
const val PLAN_TO_READ = 5 const val PLAN_TO_READ = 5L
const val REREADING = 6 const val REREADING = 6L
private val SCORE_LIST = IntRange(0, 10) private val SCORE_LIST = IntRange(0, 10)
.map(Int::toString) .map(Int::toString)
@ -49,7 +49,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
override suspend fun update(track: Track, didReadChapter: Boolean): Track { override suspend fun update(track: Track, didReadChapter: Boolean): Track {
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
if (didReadChapter) { if (didReadChapter) {
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) {
track.status = COMPLETED track.status = COMPLETED
} else if (track.status != REREADING) { } else if (track.status != REREADING) {
track.status = READING track.status = READING
@ -72,14 +72,14 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
if (track.status != COMPLETED) { if (track.status != COMPLETED) {
val isRereading = track.status == REREADING val isRereading = track.status == REREADING
track.status = if (isRereading.not() && hasReadChapters) READING else track.status track.status = if (!isRereading && hasReadChapters) READING else track.status
} }
update(track) update(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
track.status = if (hasReadChapters) READING else PLAN_TO_READ track.status = if (hasReadChapters) READING else PLAN_TO_READ
track.score = 0F track.score = 0.0
add(track) add(track)
} }
} }
@ -101,11 +101,11 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
override fun getLogoColor() = Color.rgb(40, 40, 40) override fun getLogoColor() = Color.rgb(40, 40, 40)
override fun getStatusList(): List<Int> { override fun getStatusList(): List<Long> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING) return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
} }
override fun getStatus(status: Int): StringResource? = when (status) { override fun getStatus(status: Long): StringResource? = when (status) {
READING -> MR.strings.reading READING -> MR.strings.reading
PLAN_TO_READ -> MR.strings.plan_to_read PLAN_TO_READ -> MR.strings.plan_to_read
COMPLETED -> MR.strings.completed COMPLETED -> MR.strings.completed
@ -115,11 +115,11 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
else -> null else -> null
} }
override fun getReadingStatus(): Int = READING override fun getReadingStatus(): Long = READING
override fun getRereadingStatus(): Int = REREADING override fun getRereadingStatus(): Long = REREADING
override fun getCompletionStatus(): Int = COMPLETED override fun getCompletionStatus(): Long = COMPLETED
override suspend fun login(username: String, password: String) = login(password) override suspend fun login(username: String, password: String) = login(password)

View File

@ -15,7 +15,7 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.float import kotlinx.serialization.json.double
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
@ -102,10 +102,10 @@ class ShikimoriApi(
return TrackSearch.create(trackId).apply { return TrackSearch.create(trackId).apply {
remote_id = obj["id"]!!.jsonPrimitive.long remote_id = obj["id"]!!.jsonPrimitive.long
title = obj["name"]!!.jsonPrimitive.content title = obj["name"]!!.jsonPrimitive.content
total_chapters = obj["chapters"]!!.jsonPrimitive.int total_chapters = obj["chapters"]!!.jsonPrimitive.long
cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content
summary = "" summary = ""
score = obj["score"]!!.jsonPrimitive.float score = obj["score"]!!.jsonPrimitive.double
tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content
publishing_status = obj["status"]!!.jsonPrimitive.content publishing_status = obj["status"]!!.jsonPrimitive.content
publishing_type = obj["kind"]!!.jsonPrimitive.content publishing_type = obj["kind"]!!.jsonPrimitive.content
@ -117,10 +117,10 @@ class ShikimoriApi(
return Track.create(trackId).apply { return Track.create(trackId).apply {
title = mangas["name"]!!.jsonPrimitive.content title = mangas["name"]!!.jsonPrimitive.content
remote_id = obj["id"]!!.jsonPrimitive.long remote_id = obj["id"]!!.jsonPrimitive.long
total_chapters = mangas["chapters"]!!.jsonPrimitive.int total_chapters = mangas["chapters"]!!.jsonPrimitive.long
library_id = obj["id"]!!.jsonPrimitive.long library_id = obj["id"]!!.jsonPrimitive.long
last_chapter_read = obj["chapters"]!!.jsonPrimitive.float last_chapter_read = obj["chapters"]!!.jsonPrimitive.double
score = (obj["score"]!!.jsonPrimitive.int).toFloat() score = obj["score"]!!.jsonPrimitive.int.toDouble()
status = toTrackStatus(obj["status"]!!.jsonPrimitive.content) status = toTrackStatus(obj["status"]!!.jsonPrimitive.content)
tracking_url = baseUrl + mangas["url"]!!.jsonPrimitive.content tracking_url = baseUrl + mangas["url"]!!.jsonPrimitive.content
} }
@ -192,15 +192,15 @@ class ShikimoriApi(
) )
companion object { companion object {
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc" private const val clientId = "PB9dq8DzI405s7wdtwTdirYqHiyVMh--djnP7lBUqSA"
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0" private const val clientSecret = "NajpZcOBKB9sJtgNcejf8OB9jBN1OYYoo-k4h2WWZus"
private const val baseUrl = "https://shikimori.one" private const val baseUrl = "https://shikimori.one"
private const val apiUrl = "$baseUrl/api" private const val apiUrl = "$baseUrl/api"
private const val oauthUrl = "$baseUrl/oauth/token" private const val oauthUrl = "$baseUrl/oauth/token"
private const val loginUrl = "$baseUrl/oauth/authorize" private const val loginUrl = "$baseUrl/oauth/authorize"
private const val redirectUrl = "tachiyomi://shikimori-auth" private const val redirectUrl = "mihon://shikimori-auth"
fun authUrl(): Uri = loginUrl.toUri().buildUpon() fun authUrl(): Uri = loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.shikimori package eu.kanade.tachiyomi.data.track.shikimori
import eu.kanade.tachiyomi.BuildConfig
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
@ -33,7 +34,7 @@ class ShikimoriInterceptor(private val shikimori: Shikimori) : Interceptor {
// Add the authorization header to the original request. // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.header("User-Agent", "Tachiyomi") .header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)

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