├── .editorconfig
├── .github
├── ISSUE_TEMPLATE
│ ├── 01_report_issue.yml
│ ├── 02_request_source.yml
│ ├── 03_report_url_change.yml
│ ├── 04_report_dead_source.yml
│ ├── 05_request_feature.yml
│ ├── 06_request_meta.yml
│ └── config.yml
├── pull_request_template.md
├── readme-images
│ └── app-icon.png
├── renovate.json
├── scripts
│ ├── commit-repo.sh
│ ├── create-repo.sh
│ └── move-apks.sh
└── workflows
│ ├── batch_close_issues.yml
│ ├── build_pull_request.yml
│ ├── build_push.yml
│ ├── issue_moderator.yml
│ └── lock.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle.kts
├── buildSrc
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── AndroidConfig.kt
├── common.gradle
├── core
├── AndroidManifest.xml
├── build.gradle.kts
└── res
│ ├── mipmap-hdpi
│ └── ic_launcher.png
│ ├── mipmap-mdpi
│ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ └── mipmap-xxxhdpi
│ └── ic_launcher.png
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── ktlintCodeStyle.xml
├── lib
├── cryptoaes
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── eu
│ │ └── kanade
│ │ └── tachiyomi
│ │ └── lib
│ │ └── cryptoaes
│ │ ├── CryptoAES.kt
│ │ └── Deobfuscator.kt
├── dataimage
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── eu
│ │ └── kanade
│ │ └── tachiyomi
│ │ └── lib
│ │ └── dataimage
│ │ └── DataImageInterceptor.kt
├── i18n
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── eu
│ │ └── kanade
│ │ └── tachiyomi
│ │ └── lib
│ │ └── i18n
│ │ └── Intl.kt
├── randomua
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── eu
│ │ └── kanade
│ │ └── tachiyomi
│ │ └── lib
│ │ └── randomua
│ │ ├── RandomUserAgentInterceptor.kt
│ │ └── RandomUserAgentPreference.kt
├── synchrony
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── assets
│ │ └── synchrony-v2.4.2.1.js
│ │ └── java
│ │ └── eu
│ │ └── kanade
│ │ └── tachiyomi
│ │ └── lib
│ │ └── synchrony
│ │ └── Deobfuscator.kt
├── textinterceptor
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── eu
│ │ └── kanade
│ │ └── tachiyomi
│ │ └── lib
│ │ └── textinterceptor
│ │ └── TextInterceptor.kt
└── unpacker
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── eu
│ └── kanade
│ └── tachiyomi
│ └── lib
│ └── unpacker
│ ├── SubstringExtractor.kt
│ └── Unpacker.kt
├── multisrc
├── build.gradle.kts
├── overrides
│ └── madara
│ │ ├── default
│ │ ├── AndroidManifest.xml
│ │ ├── additional.gradle
│ │ └── res
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ └── web_hi_res_512.png
│ │ └── s2manga
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ └── web_hi_res_512.png
│ │ └── src
│ │ └── S2Manga.kt
└── src
│ └── main
│ └── java
│ ├── eu
│ └── kanade
│ │ └── tachiyomi
│ │ └── multisrc
│ │ └── madara
│ │ ├── Madara.kt
│ │ ├── MadaraGenerator.kt
│ │ └── MadaraUrlActivity.kt
│ └── generator
│ ├── GeneratorMain.kt
│ ├── IntelijConfigurationGeneratorMain.kt
│ └── ThemeSourceGenerator.kt
├── settings.gradle.kts
└── src
├── all
├── batoto
│ ├── AndroidManifest.xml
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── build.gradle
│ ├── res
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ └── web_hi_res_512.png
│ └── src
│ │ └── eu
│ │ └── kanade
│ │ └── tachiyomi
│ │ └── extension
│ │ └── all
│ │ └── batoto
│ │ ├── BatoTo.kt
│ │ ├── BatoToFactory.kt
│ │ └── BatoToUrlActivity.kt
└── mangadex
│ ├── AndroidManifest.xml
│ ├── README.md
│ ├── assets
│ └── i18n
│ │ ├── messages_en.properties
│ │ ├── messages_es.properties
│ │ ├── messages_pt_br.properties
│ │ └── messages_ru.properties
│ ├── build.gradle
│ ├── res
│ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ └── web_hi_res_512.png
│ └── src
│ └── eu
│ └── kanade
│ └── tachiyomi
│ └── extension
│ └── all
│ └── mangadex
│ ├── MDConstants.kt
│ ├── MangaDex.kt
│ ├── MangaDexFactory.kt
│ ├── MangaDexFilters.kt
│ ├── MangaDexHelper.kt
│ ├── MangaDexIntl.kt
│ ├── MangadexUrlActivity.kt
│ ├── MdAtHomeReportInterceptor.kt
│ ├── MdUserAgentInterceptor.kt
│ └── dto
│ ├── AggregateDto.kt
│ ├── AtHomeDto.kt
│ ├── AuthorDto.kt
│ ├── ChapterDto.kt
│ ├── CoverArtDto.kt
│ ├── EntityDto.kt
│ ├── ListDto.kt
│ ├── MangaDto.kt
│ ├── ResponseDto.kt
│ ├── ScanlationGroupDto.kt
│ └── UserDto.kt
├── en
└── reaperscans
│ ├── AndroidManifest.xml
│ ├── build.gradle
│ ├── res
│ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ └── web_hi_res_512.png
│ └── src
│ └── eu
│ └── kanade
│ └── tachiyomi
│ └── extension
│ └── en
│ └── reaperscans
│ ├── ReaperScans.kt
│ ├── ReaperScansDto.kt
│ └── ReaperScansUrlActivity.kt
└── ko
└── newtoki
├── AndroidManifest.xml
├── build.gradle
├── res
├── mipmap-hdpi
│ └── ic_launcher.png
├── mipmap-mdpi
│ └── ic_launcher.png
├── mipmap-xhdpi
│ └── ic_launcher.png
├── mipmap-xxhdpi
│ └── ic_launcher.png
├── mipmap-xxxhdpi
│ └── ic_launcher.png
└── web_hi_res_512.png
└── src
└── eu
└── kanade
└── tachiyomi
└── extension
└── ko
└── newtoki
├── DomainNumber.kt
├── FallbackDomainNumber.kt
├── ManaToki.kt
├── NewToki.kt
├── NewTokiWebtoon.kt
├── Preferences.kt
├── Strings.kt
└── TokiFactory.kt
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*.kt]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 4
8 | indent_style = space
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 | ij_kotlin_allow_trailing_comma = true
12 | ij_kotlin_allow_trailing_comma_on_call_site = true
13 | ij_kotlin_name_count_to_use_star_import = 2147483647
14 | ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
15 |
16 | [*.properties]
17 | charset = utf-8
18 | end_of_line = lf
19 | insert_final_newline = true
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/01_report_issue.yml:
--------------------------------------------------------------------------------
1 | name: 🐞 Issue report
2 | description: Report a source issue in Tachiyomi
3 | labels: [Bug]
4 | body:
5 |
6 | - type: input
7 | id: source
8 | attributes:
9 | label: Source information
10 | description: |
11 | You can find the extension name and version in **Browse → Extensions**.
12 | placeholder: |
13 | Example: "Mangahere 1.3.18"
14 | validations:
15 | required: true
16 |
17 | - type: input
18 | id: language
19 | attributes:
20 | label: Source language
21 | placeholder: |
22 | Example: "English"
23 | validations:
24 | required: true
25 |
26 | - type: textarea
27 | id: reproduce-steps
28 | attributes:
29 | label: Steps to reproduce
30 | description: Provide an example of the issue.
31 | placeholder: |
32 | Example:
33 | 1. First step
34 | 2. Second step
35 | 3. Issue here
36 | validations:
37 | required: true
38 |
39 | - type: textarea
40 | id: expected-behavior
41 | attributes:
42 | label: Expected behavior
43 | placeholder: |
44 | Example:
45 | "This should happen..."
46 | validations:
47 | required: true
48 |
49 | - type: textarea
50 | id: actual-behavior
51 | attributes:
52 | label: Actual behavior
53 | placeholder: |
54 | Example:
55 | "This happened instead..."
56 | validations:
57 | required: true
58 |
59 | - type: input
60 | id: tachiyomi-version
61 | attributes:
62 | label: Tachiyomi version
63 | description: |
64 | You can find your Tachiyomi version in **More → About**.
65 | placeholder: |
66 | Example: "0.15.0"
67 | validations:
68 | required: true
69 |
70 | - type: input
71 | id: android-version
72 | attributes:
73 | label: Android version
74 | description: |
75 | You can find this somewhere in your Android settings.
76 | placeholder: |
77 | Example: "Android 11"
78 | validations:
79 | required: true
80 |
81 | - type: textarea
82 | id: other-details
83 | attributes:
84 | label: Other details
85 | placeholder: |
86 | Additional details and attachments.
87 |
88 | - type: checkboxes
89 | id: acknowledgements
90 | attributes:
91 | label: Acknowledgements
92 | description: Your issue will be closed if you haven't done these steps.
93 | options:
94 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
95 | required: true
96 | - label: I have written a short but informative title.
97 | required: true
98 | - label: I have updated the app to version **[0.15.0](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
99 | required: true
100 | - label: I have updated all installed extensions.
101 | required: true
102 | - label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
103 | required: true
104 | - label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
105 | required: true
106 | - label: I will fill out all of the requested information in this form.
107 | required: true
108 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/02_request_source.yml:
--------------------------------------------------------------------------------
1 | name: 🌐 Source request
2 | description: Suggest a new source for Tachiyomi
3 | labels: [Source request]
4 | body:
5 |
6 | - type: input
7 | id: name
8 | attributes:
9 | label: Source name
10 | placeholder: |
11 | Example: "Not Real Scans"
12 | validations:
13 | required: true
14 |
15 | - type: input
16 | id: link
17 | attributes:
18 | label: Source link
19 | placeholder: |
20 | Example: "https://notrealscans.org"
21 | validations:
22 | required: true
23 |
24 | - type: input
25 | id: language
26 | attributes:
27 | label: Source language
28 | placeholder: |
29 | Example: "English"
30 | validations:
31 | required: true
32 |
33 | - type: textarea
34 | id: other-details
35 | attributes:
36 | label: Other details
37 | placeholder: |
38 | Additional details and attachments.
39 | Example:
40 | "18+/NSFW = yes"
41 |
42 | - type: checkboxes
43 | id: acknowledgements
44 | attributes:
45 | label: Acknowledgements
46 | description: Your issue will be closed if you haven't done these steps.
47 | options:
48 | - label: I have checked that the extension does not already exist by searching the [GitHub repository](https://github.com/keiyoushi/tachiyomi-extensions-source/) and verified it does not appear in the code base.
49 | required: true
50 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
51 | required: true
52 | - label: I have written a meaningful title with the source name.
53 | required: true
54 | - label: I will fill out all of the requested information in this form.
55 | required: true
56 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/03_report_url_change.yml:
--------------------------------------------------------------------------------
1 | name: 🔗 URL change report
2 | description: Report URL change of an existing source
3 | labels: [Bug,Domain changed]
4 | body:
5 |
6 | - type: input
7 | id: source
8 | attributes:
9 | label: Source information
10 | description: |
11 | You can find the extension name and version in **Browse → Extensions**.
12 | placeholder: |
13 | Example: "NotRealScans 1.3.1"
14 | validations:
15 | required: true
16 |
17 | - type: input
18 | id: language
19 | attributes:
20 | label: Source language
21 | placeholder: |
22 | Example: "English"
23 | validations:
24 | required: true
25 |
26 | - type: input
27 | id: link
28 | attributes:
29 | label: Source new URL
30 | placeholder: |
31 | Example: "https://notrealscans.org"
32 | validations:
33 | required: true
34 |
35 | - type: textarea
36 | id: other-details
37 | attributes:
38 | label: Other details
39 | placeholder: |
40 | Additional details and attachments.
41 |
42 | - type: checkboxes
43 | id: acknowledgements
44 | attributes:
45 | label: Acknowledgements
46 | description: Your issue will be closed if you haven't done these steps.
47 | options:
48 | - label: I have updated all installed extensions.
49 | required: true
50 | - label: I have opened WebView and checked that the source URL is not updated yet.
51 | required: true
52 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
53 | required: true
54 | - label: I have written a short but informative title.
55 | required: true
56 | - label: I will fill out all of the requested information in this form.
57 | required: true
58 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/04_report_dead_source.yml:
--------------------------------------------------------------------------------
1 | name: ❌ Dead source report
2 | description: Source is down and website is closed
3 | labels: [Source is down]
4 | body:
5 |
6 | - type: markdown
7 | attributes:
8 | value: |
9 | ### Notice
10 | If you have a lot of dead sources to report, please go back and submit a single meta request.
11 |
12 | - type: input
13 | id: source
14 | attributes:
15 | label: Source name
16 | description: |
17 | You can find the extension name in **Browse → Extensions**.
18 | placeholder: |
19 | Example: "NotRealScans"
20 | validations:
21 | required: true
22 |
23 | - type: input
24 | id: language
25 | attributes:
26 | label: Source language
27 | placeholder: |
28 | Example: "English"
29 | validations:
30 | required: true
31 |
32 | - type: input
33 | id: link
34 | attributes:
35 | label: Source link
36 | placeholder: |
37 | Example: "https://notrealscans.org"
38 | validations:
39 | required: true
40 |
41 | - type: textarea
42 | id: other-details
43 | attributes:
44 | label: Other details
45 | placeholder: |
46 | Additional details and attachments.
47 |
48 | - type: checkboxes
49 | id: acknowledgements
50 | attributes:
51 | label: Acknowledgements
52 | description: Your issue will be closed if you haven't done these steps.
53 | options:
54 | - label: I have updated all installed extensions.
55 | required: true
56 | - label: I have opened WebView and checked that the source website is down.
57 | required: true
58 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
59 | required: true
60 | - label: I have written a meaningful title with the source name.
61 | required: true
62 | - label: I will fill out all of the requested information in this form.
63 | required: true
64 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/05_request_feature.yml:
--------------------------------------------------------------------------------
1 | name: ⭐ Feature request
2 | description: Suggest a feature to improve an existing source
3 | labels: [Feature request]
4 | body:
5 |
6 | - type: input
7 | id: source
8 | attributes:
9 | label: Source name
10 | description: |
11 | You can find the extension name in **Browse → Extensions**.
12 | placeholder: |
13 | Example: "Mangahere"
14 | validations:
15 | required: true
16 |
17 | - type: input
18 | id: language
19 | attributes:
20 | label: Source language
21 | placeholder: |
22 | Example: "English"
23 | validations:
24 | required: true
25 |
26 | - type: textarea
27 | id: feature-description
28 | attributes:
29 | label: Describe your suggested feature
30 | description: How can an existing extension be improved?
31 | placeholder: |
32 | Example:
33 | "It should work like this..."
34 | validations:
35 | required: true
36 |
37 | - type: textarea
38 | id: other-details
39 | attributes:
40 | label: Other details
41 | placeholder: |
42 | Additional details and attachments.
43 |
44 | - type: checkboxes
45 | id: acknowledgements
46 | attributes:
47 | label: Acknowledgements
48 | description: Your issue will be closed if you haven't done these steps.
49 | options:
50 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
51 | required: true
52 | - label: I have written a short but informative title.
53 | required: true
54 | - label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
55 | required: true
56 | - label: I have updated the app to version **[0.15.0](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
57 | required: true
58 | - label: I will fill out all of the requested information in this form.
59 | required: true
60 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/06_request_meta.yml:
--------------------------------------------------------------------------------
1 | name: 🧠 Meta request
2 | description: Suggest improvements to the project
3 | labels: [Meta request]
4 | body:
5 |
6 | - type: textarea
7 | id: feature-description
8 | attributes:
9 | label: Describe why this should be added
10 | description: How can the project be improved?
11 | placeholder: |
12 | Example:
13 | "It should work like this..."
14 | validations:
15 | required: true
16 |
17 | - type: textarea
18 | id: other-details
19 | attributes:
20 | label: Other details
21 | placeholder: |
22 | Additional details and attachments.
23 |
24 | - type: checkboxes
25 | id: acknowledgements
26 | attributes:
27 | label: Acknowledgements
28 | description: Your issue will be closed if you haven't done these steps.
29 | options:
30 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
31 | required: true
32 | - label: I have written a short but informative title.
33 | required: true
34 | - label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
35 | required: true
36 | - label: I have updated the app to version **[0.15.0](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
37 | required: true
38 | - label: I have updated all installed extensions.
39 | required: true
40 | - label: I will fill out all of the requested information in this form.
41 | required: true
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: ⚠️ Application issue
4 | url: https://github.com/tachiyomiorg/tachiyomi/issues/new/choose
5 | about: Issues and requests about the app itself should be opened in the tachiyomi repository instead
6 | - name: 📦 Tachiyomi official extensions
7 | url: https://tachiyomi.org/extensions
8 | about: List of all available official extensions with download links
9 | - name: 🖥️ Tachiyomi website
10 | url: https://tachiyomi.org/help/
11 | about: Guides, troubleshooting, and answers to common questions
12 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | Checklist:
2 |
3 | - [ ] Updated `extVersionCode` value in `build.gradle` for individual extensions
4 | - [ ] Updated `overrideVersionCode` or `baseVersionCode` as needed for all multisrc extensions
5 | - [ ] Referenced all related issues in the PR body (e.g. "Closes #xyz")
6 | - [ ] Added the `isNsfw = true` flag in `build.gradle` when appropriate
7 | - [ ] Have not changed source names
8 | - [ ] Have explicitly kept the `id` if a source's name or language were changed
9 | - [ ] Have tested the modifications by compiling and running the extension through Android Studio
10 |
--------------------------------------------------------------------------------
/.github/readme-images/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/.github/readme-images/app-icon.png
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "includePaths": [
6 | ".github/**"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.github/scripts/commit-repo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | rsync -a --delete --exclude .git --exclude .gitignore --exclude README.md ../main/repo/ .
5 | git config --global user.email "github-actions[bot]@users.noreply.github.com"
6 | git config --global user.name "github-actions[bot]"
7 | git status
8 | if [ -n "$(git status --porcelain)" ]; then
9 | git add .
10 | git commit -m "Update extensions repo"
11 | git push
12 | else
13 | echo "No changes to commit"
14 | fi
15 |
--------------------------------------------------------------------------------
/.github/scripts/create-repo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | TOOLS="$(ls -d ${ANDROID_HOME}/build-tools/* | tail -1)"
5 |
6 | mkdir -p repo/apk
7 | mkdir -p repo/icon
8 |
9 | cp -f apk/* repo/apk
10 |
11 | cd repo
12 |
13 | APKS=( ../apk/*".apk" )
14 |
15 | for APK in ${APKS[@]}; do
16 | FILENAME=$(basename ${APK})
17 | BADGING="$(${TOOLS}/aapt dump --include-meta-data badging $APK)"
18 |
19 | PACKAGE=$(echo "$BADGING" | grep package:)
20 | PKGNAME=$(echo $PACKAGE | grep -Po "package: name='\K[^']+")
21 | VCODE=$(echo $PACKAGE | grep -Po "versionCode='\K[^']+")
22 | VNAME=$(echo $PACKAGE | grep -Po "versionName='\K[^']+")
23 | NSFW=$(echo $BADGING | grep -Po "tachiyomi.extension.nsfw' value='\K[^']+")
24 | HASREADME=$(echo $BADGING | grep -Po "tachiyomi.extension.hasReadme' value='\K[^']+")
25 | HASCHANGELOG=$(echo $BADGING | grep -Po "tachiyomi.extension.hasChangelog' value='\K[^']+")
26 |
27 | APPLICATION=$(echo "$BADGING" | grep application:)
28 | LABEL=$(echo $APPLICATION | grep -Po "label='\K[^']+")
29 |
30 | LANG=$(echo $APK | grep -Po "tachiyomi-\K[^\.]+")
31 |
32 | ICON=$(echo "$BADGING" | grep -Po "application-icon-320.*'\K[^']+")
33 | unzip -p $APK $ICON > icon/${PKGNAME}.png
34 |
35 | # TODO: legacy icons; remove after a while
36 | cp icon/${PKGNAME}.png icon/${FILENAME%.*}.png
37 |
38 | SOURCE_INFO=$(jq ".[\"$PKGNAME\"]" < ../output.json)
39 |
40 | # Fixes the language code without needing to update the packages.
41 | SOURCE_LEN=$(echo $SOURCE_INFO | jq length)
42 |
43 | if [ $SOURCE_LEN = "1" ]; then
44 | SOURCE_LANG=$(echo $SOURCE_INFO | jq -r '.[0].lang')
45 |
46 | if [ $SOURCE_LANG != $LANG ] && [ $SOURCE_LANG != "all" ] && [ $SOURCE_LANG != "other" ] && [ $LANG != "all" ] && [ $LANG != "other" ]; then
47 | LANG=$SOURCE_LANG
48 | fi
49 | fi
50 |
51 | jq -n \
52 | --arg name "$LABEL" \
53 | --arg pkg "$PKGNAME" \
54 | --arg apk "$FILENAME" \
55 | --arg lang "$LANG" \
56 | --argjson code $VCODE \
57 | --arg version "$VNAME" \
58 | --argjson nsfw $NSFW \
59 | --argjson hasReadme $HASREADME \
60 | --argjson hasChangelog $HASCHANGELOG \
61 | --argjson sources "$SOURCE_INFO" \
62 | '{name:$name, pkg:$pkg, apk:$apk, lang:$lang, code:$code, version:$version, nsfw:$nsfw, hasReadme:$hasReadme, hasChangelog:$hasChangelog, sources:$sources}'
63 |
64 | done | jq -sr '[.[]]' > index.json
65 |
66 | # Alternate minified copy
67 | jq -c '.' < index.json > index.min.json
68 |
69 | cat index.json
70 |
--------------------------------------------------------------------------------
/.github/scripts/move-apks.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | shopt -s globstar nullglob extglob
4 |
5 | # Get APKs from previous jobs' artifacts
6 | cp -R ~/apk-artifacts/ $PWD
7 | APKS=( **/*".apk" )
8 |
9 | # Fail if too little extensions seem to have been built
10 | #if [ "${#APKS[@]}" -le "100" ]; then
11 | # echo "Insufficient amount of APKs found. Please check the project configuration."
12 | # exit 1
13 | #else
14 | # echo "Moving ${#APKS[@]} APKs"
15 | #fi
16 |
17 | DEST=$PWD/apk
18 | rm -rf $DEST && mkdir -p $DEST
19 |
20 | for APK in ${APKS[@]}; do
21 | BASENAME=$(basename $APK)
22 | APKNAME="${BASENAME%%+(-release*)}.apk"
23 | APKDEST="$DEST/$APKNAME"
24 |
25 | cp $APK $APKDEST
26 | done
27 |
--------------------------------------------------------------------------------
/.github/workflows/batch_close_issues.yml:
--------------------------------------------------------------------------------
1 | name: "Batch close stale issues"
2 |
3 | on:
4 | # Monthly
5 | schedule:
6 | - cron: '0 0 1 * *'
7 | # Manual trigger
8 | workflow_dispatch:
9 | inputs:
10 |
11 | jobs:
12 | stale:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/stale@v9
16 | with:
17 | repo-token: ${{ secrets.GITHUB_TOKEN }}
18 | # Close everything older than a year
19 | days-before-issue-stale: 365
20 | days-before-issue-close: 0
21 | exempt-issue-labels: "do-not-autoclose,Meta request"
22 | close-issue-message: "In an effort to have a more manageable issue backlog, we're closing older requests that weren't addressed since there's a low chance of it being addressed if it hasn't already. If your request is still relevant, please [open a new request](https://github.com/keiyoushi/tachiyomi-extensions-source/issues/new/choose)."
23 | close-issue-reason: not_planned
24 | ascending: true
25 | operations-per-run: 250
26 |
--------------------------------------------------------------------------------
/.github/workflows/build_pull_request.yml:
--------------------------------------------------------------------------------
1 | name: PR build check
2 |
3 | on:
4 | pull_request:
5 | paths-ignore:
6 | - '**.md'
7 | - '.github/workflows/issue_moderator.yml'
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
11 | cancel-in-progress: true
12 |
13 | env:
14 | CI_CHUNK_SIZE: 65
15 |
16 | jobs:
17 | prepare:
18 | name: Prepare job
19 | runs-on: ubuntu-latest
20 | outputs:
21 | individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
22 | multisrcMatrix: ${{ steps.generate-matrices.outputs.multisrcMatrix }}
23 | isIndividualChanged: ${{ steps.parse-changed-files.outputs.isIndividualChanged }}
24 | isMultisrcChanged: ${{ steps.parse-changed-files.outputs.isMultisrcChanged }}
25 | env:
26 | CI_MODULE_GEN: true
27 | steps:
28 | - name: Clone repo
29 | uses: actions/checkout@v4
30 |
31 | - name: Validate Gradle Wrapper
32 | uses: gradle/wrapper-validation-action@v1
33 |
34 | - name: Set up JDK
35 | uses: actions/setup-java@v4
36 | with:
37 | java-version: 11
38 | distribution: adopt
39 |
40 | - id: get-changed-files
41 | name: Get changed files
42 | uses: Ana06/get-changed-files@v2.2.0
43 |
44 | - id: parse-changed-files
45 | name: Parse changed files
46 | run: |
47 | isIndividualChanged=0
48 | isMultisrcChanged=0
49 | for changedFile in ${{ steps.get-changed-files.outputs.all }}; do
50 | if [[ ${changedFile} == src/* ]]; then
51 | isIndividualChanged=1
52 | elif [[ ${changedFile} == multisrc/* ]]; then
53 | isMultisrcChanged=1
54 | elif [[ ${changedFile} == .github/workflows/issue_moderator.yml ]]; then
55 | true
56 | elif [[ ${changedFile} == *.md ]]; then
57 | true
58 | else
59 | isIndividualChanged=1
60 | isMultisrcChanged=1
61 | break
62 | fi
63 | done
64 | echo "isIndividualChanged=$isIndividualChanged" >> $GITHUB_OUTPUT
65 | echo "isMultisrcChanged=$isMultisrcChanged" >> $GITHUB_OUTPUT
66 |
67 | - name: Generate multisrc sources
68 | if: ${{ steps.parse-changed-files.outputs.isMultisrcChanged == '1' }}
69 | uses: gradle/gradle-build-action@v2
70 | with:
71 | arguments: :multisrc:generateExtensions
72 |
73 | - name: Get number of modules
74 | run: |
75 | set -x
76 | ./gradlew -q projects | grep '.*extensions\:\(individual\|multisrc\)\:.*\:.*' > projects.txt
77 |
78 | echo "NUM_INDIVIDUAL_MODULES=$(cat projects.txt | grep '.*\:individual\:.*' | wc -l)" >> $GITHUB_ENV
79 | echo "NUM_MULTISRC_MODULES=$(cat projects.txt | grep '.*\:multisrc\:.*' | wc -l)" >> $GITHUB_ENV
80 |
81 | - id: generate-matrices
82 | name: Create output matrices
83 | uses: actions/github-script@v7
84 | with:
85 | script: |
86 | const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
87 | const numMultisrcModules = process.env.NUM_MULTISRC_MODULES;
88 | const chunkSize = process.env.CI_CHUNK_SIZE;
89 |
90 | const numIndividualChunks = Math.ceil(numIndividualModules / chunkSize);
91 | const numMultisrcChunks = Math.ceil(numMultisrcModules / chunkSize);
92 |
93 | console.log(`Individual modules: ${numIndividualModules} (${numIndividualChunks} chunks of ${chunkSize})`);
94 | console.log(`Multi-source modules: ${numMultisrcModules} (${numMultisrcChunks} chunks of ${chunkSize})`);
95 |
96 | core.setOutput('individualMatrix', { 'chunk': [...Array(numIndividualChunks).keys()] });
97 | core.setOutput('multisrcMatrix', { 'chunk': [...Array(numMultisrcChunks).keys()] });
98 |
99 | build_multisrc:
100 | name: Build multisrc modules
101 | needs: prepare
102 | if: ${{ needs.prepare.outputs.isMultisrcChanged == '1' }}
103 | runs-on: ubuntu-latest
104 | strategy:
105 | matrix: ${{ fromJSON(needs.prepare.outputs.multisrcMatrix) }}
106 | steps:
107 | - name: Checkout PR
108 | uses: actions/checkout@v4
109 |
110 | - name: Set up JDK
111 | uses: actions/setup-java@v4
112 | with:
113 | java-version: 11
114 | distribution: adopt
115 |
116 | - name: Generate sources from the multi-source library
117 | uses: gradle/gradle-build-action@v2
118 | env:
119 | CI_MODULE_GEN: "true"
120 | with:
121 | arguments: :multisrc:generateExtensions
122 | cache-read-only: true
123 |
124 | - name: Build extensions (chunk ${{ matrix.chunk }})
125 | uses: gradle/gradle-build-action@v2
126 | env:
127 | CI_MULTISRC: "true"
128 | CI_CHUNK_NUM: ${{ matrix.chunk }}
129 | with:
130 | arguments: assembleDebug
131 | cache-read-only: true
132 |
133 | build_individual:
134 | name: Build individual modules
135 | needs: prepare
136 | if: ${{ needs.prepare.outputs.isIndividualChanged == '1' }}
137 | runs-on: ubuntu-latest
138 | strategy:
139 | matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
140 | steps:
141 | - name: Checkout PR
142 | uses: actions/checkout@v4
143 |
144 | - name: Set up JDK
145 | uses: actions/setup-java@v4
146 | with:
147 | java-version: 11
148 | distribution: adopt
149 |
150 | - name: Build extensions (chunk ${{ matrix.chunk }})
151 | uses: gradle/gradle-build-action@v2
152 | env:
153 | CI_MULTISRC: "false"
154 | CI_CHUNK_NUM: ${{ matrix.chunk }}
155 | with:
156 | arguments: assembleDebug
157 | cache-read-only: true
158 |
--------------------------------------------------------------------------------
/.github/workflows/build_push.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - '**.md'
9 | - '.github/workflows/issue_moderator.yml'
10 |
11 | concurrency:
12 | group: ${{ github.workflow }}
13 | cancel-in-progress: true
14 |
15 | env:
16 | CI_CHUNK_SIZE: 65
17 |
18 | jobs:
19 | prepare:
20 | name: Prepare job
21 | runs-on: ubuntu-latest
22 | outputs:
23 | individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
24 | multisrcMatrix: ${{ steps.generate-matrices.outputs.multisrcMatrix }}
25 | env:
26 | CI_MODULE_GEN: true
27 | steps:
28 | - name: Clone repo
29 | uses: actions/checkout@v4
30 |
31 | - name: Validate Gradle Wrapper
32 | uses: gradle/wrapper-validation-action@v1
33 |
34 | - name: Set up JDK
35 | uses: actions/setup-java@v4
36 | with:
37 | java-version: 11
38 | distribution: adopt
39 |
40 | - name: Generate multisrc sources
41 | uses: gradle/gradle-build-action@v2
42 | with:
43 | arguments: :multisrc:generateExtensions
44 |
45 | - name: Get number of modules
46 | run: |
47 | set -x
48 | ./gradlew -q projects | grep '.*extensions\:\(individual\|multisrc\)\:.*\:.*' > projects.txt
49 |
50 | echo "NUM_INDIVIDUAL_MODULES=$(cat projects.txt | grep '.*\:individual\:.*' | wc -l)" >> $GITHUB_ENV
51 | echo "NUM_MULTISRC_MODULES=$(cat projects.txt | grep '.*\:multisrc\:.*' | wc -l)" >> $GITHUB_ENV
52 |
53 | - id: generate-matrices
54 | name: Create output matrices
55 | uses: actions/github-script@v7
56 | with:
57 | script: |
58 | const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
59 | const numMultisrcModules = process.env.NUM_MULTISRC_MODULES;
60 | const chunkSize = process.env.CI_CHUNK_SIZE;
61 |
62 | const numIndividualChunks = Math.ceil(numIndividualModules / chunkSize);
63 | const numMultisrcChunks = Math.ceil(numMultisrcModules / chunkSize);
64 |
65 | console.log(`Individual modules: ${numIndividualModules} (${numIndividualChunks} chunks of ${chunkSize})`);
66 | console.log(`Multi-source modules: ${numMultisrcModules} (${numMultisrcChunks} chunks of ${chunkSize})`);
67 |
68 | core.setOutput('individualMatrix', { 'chunk': [...Array(numIndividualChunks).keys()] });
69 | core.setOutput('multisrcMatrix', { 'chunk': [...Array(numMultisrcChunks).keys()] });
70 |
71 | build_multisrc:
72 | name: Build multisrc modules
73 | needs: prepare
74 | runs-on: ubuntu-latest
75 | strategy:
76 | matrix: ${{ fromJSON(needs.prepare.outputs.multisrcMatrix) }}
77 | steps:
78 | - name: Checkout main branch
79 | uses: actions/checkout@v4
80 |
81 | - name: Set up JDK
82 | uses: actions/setup-java@v4
83 | with:
84 | java-version: 11
85 | distribution: adopt
86 |
87 | - name: Prepare signing key
88 | run: |
89 | echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
90 |
91 | - name: Generate sources from the multi-source library
92 | uses: gradle/gradle-build-action@v2
93 | env:
94 | CI_MODULE_GEN: "true"
95 | with:
96 | arguments: :multisrc:generateExtensions
97 |
98 | - name: Build extensions (chunk ${{ matrix.chunk }})
99 | uses: gradle/gradle-build-action@v2
100 | env:
101 | CI_MULTISRC: "true"
102 | CI_CHUNK_NUM: ${{ matrix.chunk }}
103 | ALIAS: ${{ secrets.ALIAS }}
104 | KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
105 | KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
106 | with:
107 | arguments: assembleRelease
108 |
109 | - name: Upload APKs (chunk ${{ matrix.chunk }})
110 | uses: actions/upload-artifact@v4
111 | if: "github.repository == 'keiyoushi/tachiyomi-extensions-source'"
112 | with:
113 | name: "multisrc-apks-${{ matrix.chunk }}"
114 | path: "**/*.apk"
115 | retention-days: 1
116 |
117 | - name: Clean up CI files
118 | run: rm signingkey.jks
119 |
120 | build_individual:
121 | name: Build individual modules
122 | needs: prepare
123 | runs-on: ubuntu-latest
124 | strategy:
125 | matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
126 | steps:
127 | - name: Checkout main branch
128 | uses: actions/checkout@v4
129 |
130 | - name: Set up JDK
131 | uses: actions/setup-java@v4
132 | with:
133 | java-version: 11
134 | distribution: adopt
135 |
136 | - name: Prepare signing key
137 | run: |
138 | echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
139 |
140 | - name: Build extensions (chunk ${{ matrix.chunk }})
141 | uses: gradle/gradle-build-action@v2
142 | env:
143 | CI_MULTISRC: "false"
144 | CI_CHUNK_NUM: ${{ matrix.chunk }}
145 | ALIAS: ${{ secrets.ALIAS }}
146 | KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
147 | KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
148 | with:
149 | arguments: assembleRelease
150 |
151 | - name: Upload APKs (chunk ${{ matrix.chunk }})
152 | uses: actions/upload-artifact@v4
153 | if: "github.repository == 'keiyoushi/tachiyomi-extensions-source'"
154 | with:
155 | name: "individual-apks-${{ matrix.chunk }}"
156 | path: "**/*.apk"
157 | retention-days: 1
158 |
159 | - name: Clean up CI files
160 | run: rm signingkey.jks
161 |
162 | publish_repo:
163 | name: Publish repo
164 | needs:
165 | - build_multisrc
166 | - build_individual
167 | if: "github.repository == 'keiyoushi/tachiyomi-extensions-source'"
168 | runs-on: ubuntu-latest
169 | steps:
170 | - name: Download APK artifacts
171 | uses: actions/download-artifact@v4
172 | with:
173 | path: ~/apk-artifacts
174 |
175 | - name: Set up JDK
176 | uses: actions/setup-java@v4
177 | with:
178 | java-version: 17
179 | distribution: adopt
180 |
181 | - name: Checkout main branch
182 | uses: actions/checkout@v4
183 | with:
184 | ref: main
185 | path: main
186 |
187 | - name: Create repo artifacts
188 | run: |
189 | cd main
190 | ./.github/scripts/move-apks.sh
191 | INSPECTOR_LINK="$(curl -s "https://api.github.com/repos/tachiyomiorg/tachiyomi-extensions-inspector/releases/latest" | jq -r '.assets[0].browser_download_url')"
192 | curl -L "$INSPECTOR_LINK" -o ./Inspector.jar
193 | java -jar ./Inspector.jar "apk" "output.json" "tmp"
194 | ./.github/scripts/create-repo.sh
195 |
196 | - name: Checkout repo branch
197 | uses: actions/checkout@v4
198 | with:
199 | repository: keiyoushi/tachiyomi-extensions
200 | token: ${{ secrets.BOT_PAT }}
201 | ref: repo
202 | path: repo
203 |
204 | - name: Deploy repo
205 | run: |
206 | cd repo
207 | ../main/.github/scripts/commit-repo.sh
208 |
--------------------------------------------------------------------------------
/.github/workflows/issue_moderator.yml:
--------------------------------------------------------------------------------
1 | name: Issue moderator
2 |
3 | on:
4 | issues:
5 | types: [opened, edited, reopened]
6 | issue_comment:
7 | types: [created]
8 |
9 | jobs:
10 | autoclose:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Moderate issues
14 | uses: tachiyomiorg/issue-moderator-action@v2
15 | with:
16 | repo-token: ${{ secrets.GITHUB_TOKEN }}
17 | duplicate-label: Duplicate
18 |
19 | duplicate-check-enabled: true
20 | duplicate-check-labels: |
21 | ["Source request", "Domain changed"]
22 |
23 | existing-check-enabled: true
24 | existing-check-labels: |
25 | ["Source request", "Domain changed"]
26 |
27 | auto-close-rules: |
28 | [
29 | {
30 | "type": "body",
31 | "regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
32 | "message": "Requested information in the template was not filled out."
33 | },
34 | {
35 | "type": "title",
36 | "regex": ".*(Source name|Short description).*",
37 | "message": "You did not fill out the description in the title."
38 | },
39 | {
40 | "type": "both",
41 | "regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?("clean") {
24 | delete(rootProject.layout.buildDirectory.asFile.get())
25 | }
26 |
--------------------------------------------------------------------------------
/buildSrc/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle/
2 | build/
3 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | mavenCentral()
7 | }
8 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/AndroidConfig.kt:
--------------------------------------------------------------------------------
1 | object AndroidConfig {
2 | const val compileSdk = 34
3 | const val minSdk = 21
4 | @Suppress("UNUSED")
5 | const val targetSdk = 34
6 | }
7 |
--------------------------------------------------------------------------------
/common.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'org.jmailen.kotlinter'
2 |
3 | android {
4 | compileSdkVersion AndroidConfig.compileSdk
5 |
6 | namespace "eu.kanade.tachiyomi.extension"
7 | sourceSets {
8 | main {
9 | manifest.srcFile "AndroidManifest.xml"
10 | java.srcDirs = ['src']
11 | res.srcDirs = ['res']
12 | assets.srcDirs = ['assets']
13 | }
14 | release {
15 | manifest.srcFile "AndroidManifest.xml"
16 | }
17 | debug {
18 | manifest.srcFile "AndroidManifest.xml"
19 | }
20 | }
21 |
22 | defaultConfig {
23 | minSdkVersion AndroidConfig.minSdk
24 | targetSdkVersion AndroidConfig.targetSdk
25 | applicationIdSuffix pkgNameSuffix
26 | versionCode extVersionCode
27 | versionName project.ext.properties.getOrDefault("libVersion", "1.4") + ".$extVersionCode"
28 | setProperty("archivesBaseName", "tachiyomi-$pkgNameSuffix-v$versionName")
29 | def readmes = project.projectDir.listFiles({ File file ->
30 | file.name == "README.md" || file.name == "CHANGELOG.md"
31 | } as FileFilter)
32 | def hasReadme = readmes != null && readmes.any { File file ->
33 | file.name.startsWith("README")
34 | }
35 | def hasChangelog = readmes != null && readmes.any { File file ->
36 | file.name.startsWith("CHANGELOG")
37 | }
38 | manifestPlaceholders = [
39 | appName : "Tachiyomi: $extName",
40 | extClass: extClass,
41 | extFactory: project.ext.properties.getOrDefault("extFactory", ""),
42 | nsfw: project.ext.properties.getOrDefault("isNsfw", false) ? 1 : 0,
43 | hasReadme: hasReadme ? 1 : 0,
44 | hasChangelog: hasChangelog ? 1 : 0,
45 | ]
46 | }
47 |
48 | signingConfigs {
49 | release {
50 | storeFile rootProject.file("signingkey.jks")
51 | storePassword System.getenv("KEY_STORE_PASSWORD")
52 | keyAlias System.getenv("ALIAS")
53 | keyPassword System.getenv("KEY_PASSWORD")
54 | }
55 | }
56 |
57 | buildTypes {
58 | release {
59 | signingConfig signingConfigs.release
60 | minifyEnabled false
61 | }
62 | }
63 |
64 | dependenciesInfo {
65 | includeInApk = false
66 | }
67 |
68 | buildFeatures {
69 | // Disable unused AGP features
70 | aidl false
71 | renderScript false
72 | resValues false
73 | shaders false
74 | }
75 |
76 | compileOptions {
77 | sourceCompatibility = JavaVersion.VERSION_1_8
78 | targetCompatibility = JavaVersion.VERSION_1_8
79 | }
80 |
81 | kotlinOptions {
82 | jvmTarget = JavaVersion.VERSION_1_8.toString()
83 | freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
84 | }
85 |
86 | kotlinter {
87 | experimentalRules = true
88 | disabledRules = [
89 | "experimental:argument-list-wrapping", // Doesn't play well with Android Studio
90 | "experimental:comment-wrapping",
91 | ]
92 | }
93 | }
94 |
95 | repositories {
96 | mavenCentral()
97 | }
98 |
99 | dependencies {
100 | implementation(project(":core"))
101 | compileOnly(libs.bundles.common)
102 | }
103 |
104 | preBuild.dependsOn(lintKotlin)
105 | lintKotlin.dependsOn(formatKotlin)
106 |
--------------------------------------------------------------------------------
/core/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | }
4 |
5 | android {
6 | compileSdk = AndroidConfig.compileSdk
7 |
8 | defaultConfig {
9 | minSdk = AndroidConfig.minSdk
10 | }
11 |
12 | namespace = "eu.kanade.tachiyomi.extension"
13 |
14 | @Suppress("UnstableApiUsage")
15 | sourceSets {
16 | named("main") {
17 | manifest.srcFile("AndroidManifest.xml")
18 | res.setSrcDirs(listOf("res"))
19 | }
20 | }
21 |
22 | libraryVariants.all {
23 | generateBuildConfigProvider?.configure {
24 | enabled = false
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/core/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/core/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/core/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/core/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/core/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/core/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/core/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/core/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/core/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/core/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx6144m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | org.gradle.parallel=true
18 | org.gradle.workers.max=5
19 |
20 | org.gradle.caching=true
21 |
22 | # Enable AndroidX dependencies
23 | android.useAndroidX=true
24 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin_version = "1.7.21"
3 | coroutines_version = "1.6.4"
4 | serialization_version = "1.4.0"
5 |
6 | [libraries]
7 | gradle-agp = { module = "com.android.tools.build:gradle", version = "7.4.2" }
8 | gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
9 | gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
10 | gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
11 |
12 | tachiyomi-lib = { module = "com.github.tachiyomiorg:extensions-lib", version = "1.4.2" }
13 |
14 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin_version" }
15 | kotlin-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" }
16 | kotlin-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" }
17 |
18 | coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines_version" }
19 | coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines_version" }
20 |
21 | injekt-core = { module = "com.github.inorichi.injekt:injekt-core", version = "65b0440" }
22 | rxjava = { module = "io.reactivex:rxjava", version = "1.3.8" }
23 | jsoup = { module = "org.jsoup:jsoup", version = "1.15.1" }
24 | okhttp = { module = "com.squareup.okhttp3:okhttp", version = "5.0.0-alpha.11" }
25 | quickjs = { module = "app.cash.quickjs:quickjs-android", version = "0.9.2" }
26 |
27 | [bundles]
28 | common = ["kotlin-stdlib", "coroutines-core", "coroutines-android", "injekt-core", "rxjava", "kotlin-protobuf", "kotlin-json", "jsoup", "okhttp", "tachiyomi-lib", "quickjs"]
29 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/ktlintCodeStyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | xmlns:android
23 |
24 | ^$
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | xmlns:.*
34 |
35 | ^$
36 |
37 |
38 | BY_NAME
39 |
40 |
41 |
42 |
43 |
44 |
45 | .*:id
46 |
47 | http://schemas.android.com/apk/res/android
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | .*:name
57 |
58 | http://schemas.android.com/apk/res/android
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | name
68 |
69 | ^$
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | style
79 |
80 | ^$
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | .*
90 |
91 | ^$
92 |
93 |
94 | BY_NAME
95 |
96 |
97 |
98 |
99 |
100 |
101 | .*
102 |
103 | http://schemas.android.com/apk/res/android
104 |
105 |
106 | ANDROID_ATTRIBUTE_ORDER
107 |
108 |
109 |
110 |
111 |
112 |
113 | .*
114 |
115 | .*
116 |
117 |
118 | BY_NAME
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/lib/cryptoaes/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | }
5 |
6 | android {
7 | compileSdk = AndroidConfig.compileSdk
8 |
9 | defaultConfig {
10 | minSdk = AndroidConfig.minSdk
11 | }
12 |
13 | namespace = "eu.kanade.tachiyomi.lib.cryptoaes"
14 | }
15 |
16 | repositories {
17 | mavenCentral()
18 | }
19 |
20 | dependencies {
21 | compileOnly(libs.kotlin.stdlib)
22 | }
23 |
--------------------------------------------------------------------------------
/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/CryptoAES.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.lib.cryptoaes
2 | // Thanks to Vlad on Stackoverflow: https://stackoverflow.com/a/63701411
3 |
4 | import android.util.Base64
5 | import java.security.MessageDigest
6 | import java.util.Arrays
7 | import javax.crypto.Cipher
8 | import javax.crypto.spec.IvParameterSpec
9 | import javax.crypto.spec.SecretKeySpec
10 |
11 | /**
12 | * Conforming with CryptoJS AES method
13 | */
14 | object CryptoAES {
15 |
16 | private const val KEY_SIZE = 256
17 | private const val IV_SIZE = 128
18 | private const val HASH_CIPHER = "AES/CBC/PKCS7PADDING"
19 | private const val AES = "AES"
20 | private const val KDF_DIGEST = "MD5"
21 |
22 | /**
23 | * Decrypt using CryptoJS defaults compatible method.
24 | * Uses KDF equivalent to OpenSSL's EVP_BytesToKey function
25 | *
26 | * http://stackoverflow.com/a/29152379/4405051
27 | * @param cipherText base64 encoded ciphertext
28 | * @param password passphrase
29 | */
30 | fun decrypt(cipherText: String, password: String): String {
31 | return try {
32 | val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
33 | val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
34 | val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
35 | val md5: MessageDigest = MessageDigest.getInstance("MD5")
36 | val keyAndIV = generateKeyAndIV(32, 16, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5)
37 | decryptAES(cipherTextBytes,
38 | keyAndIV?.get(0) ?: ByteArray(32),
39 | keyAndIV?.get(1) ?: ByteArray(16))
40 | } catch (e: Exception) {
41 | ""
42 | }
43 | }
44 |
45 | /**
46 | * Decrypt using CryptoJS defaults compatible method.
47 | *
48 | * @param cipherText base64 encoded ciphertext
49 | * @param keyBytes key as a bytearray
50 | * @param ivBytes iv as a bytearray
51 | */
52 | fun decrypt(cipherText: String, keyBytes: ByteArray, ivBytes: ByteArray): String {
53 | return try {
54 | val cipherTextBytes = Base64.decode(cipherText, Base64.DEFAULT)
55 | decryptAES(cipherTextBytes, keyBytes, ivBytes)
56 | } catch (e: Exception) {
57 | ""
58 | }
59 | }
60 |
61 | /**
62 | * Decrypt using CryptoJS defaults compatible method.
63 | *
64 | * @param cipherTextBytes encrypted text as a bytearray
65 | * @param keyBytes key as a bytearray
66 | * @param ivBytes iv as a bytearray
67 | */
68 | private fun decryptAES(cipherTextBytes: ByteArray, keyBytes: ByteArray, ivBytes: ByteArray): String {
69 | return try {
70 | val cipher = Cipher.getInstance(HASH_CIPHER)
71 | val keyS = SecretKeySpec(keyBytes, AES)
72 | cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(ivBytes))
73 | cipher.doFinal(cipherTextBytes).toString(Charsets.UTF_8)
74 | } catch (e: Exception) {
75 | ""
76 | }
77 | }
78 |
79 | /**
80 | * Generates a key and an initialization vector (IV) with the given salt and password.
81 | *
82 | * https://stackoverflow.com/a/41434590
83 | * This method is equivalent to OpenSSL's EVP_BytesToKey function
84 | * (see https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c).
85 | * By default, OpenSSL uses a single iteration, MD5 as the algorithm and UTF-8 encoded password data.
86 | *
87 | * @param keyLength the length of the generated key (in bytes)
88 | * @param ivLength the length of the generated IV (in bytes)
89 | * @param iterations the number of digestion rounds
90 | * @param salt the salt data (8 bytes of data or `null`)
91 | * @param password the password data (optional)
92 | * @param md the message digest algorithm to use
93 | * @return an two-element array with the generated key and IV
94 | */
95 | @Suppress("SameParameterValue")
96 | private fun generateKeyAndIV(keyLength: Int, ivLength: Int, iterations: Int, salt: ByteArray, password: ByteArray, md: MessageDigest): Array? {
97 | val digestLength = md.digestLength
98 | val requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength
99 | val generatedData = ByteArray(requiredLength)
100 | var generatedLength = 0
101 | return try {
102 | md.reset()
103 |
104 | // Repeat process until sufficient data has been generated
105 | while (generatedLength < keyLength + ivLength) {
106 |
107 | // Digest data (last digest if available, password data, salt if available)
108 | if (generatedLength > 0) md.update(generatedData, generatedLength - digestLength, digestLength)
109 | md.update(password)
110 | md.update(salt, 0, 8)
111 | md.digest(generatedData, generatedLength, digestLength)
112 |
113 | // additional rounds
114 | for (i in 1 until iterations) {
115 | md.update(generatedData, generatedLength, digestLength)
116 | md.digest(generatedData, generatedLength, digestLength)
117 | }
118 | generatedLength += digestLength
119 | }
120 |
121 | // Copy key and IV into separate byte arrays
122 | val result = arrayOfNulls(2)
123 | result[0] = generatedData.copyOfRange(0, keyLength)
124 | if (ivLength > 0) result[1] = generatedData.copyOfRange(keyLength, keyLength + ivLength)
125 | result
126 | } catch (e: Exception) {
127 | throw e
128 | } finally {
129 | // Clean out temporary data
130 | Arrays.fill(generatedData, 0.toByte())
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/Deobfuscator.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.lib.cryptoaes
2 | /**
3 | * Helper class to deobfuscate JavaScript strings encoded in JSFuck style.
4 | *
5 | * More info on JSFuck found [here](https://en.wikipedia.org/wiki/JSFuck).
6 | *
7 | * Currently only supports Numeric and decimal ('.') characters
8 | */
9 | object Deobfuscator {
10 | fun deobfuscateJsPassword(inputString: String): String {
11 | var idx = 0
12 | val brackets = listOf('[', '(')
13 | val evaluatedString = StringBuilder()
14 | while (idx < inputString.length) {
15 | val chr = inputString[idx]
16 | if (chr !in brackets) {
17 | idx++
18 | continue
19 | }
20 | val closingIndex = getMatchingBracketIndex(idx, inputString)
21 | if (chr == '[') {
22 | val digit = calculateDigit(inputString.substring(idx, closingIndex))
23 | evaluatedString.append(digit)
24 | } else {
25 | evaluatedString.append('.')
26 | if (inputString.getOrNull(closingIndex + 1) == '[') {
27 | val skippingIndex = getMatchingBracketIndex(closingIndex + 1, inputString)
28 | idx = skippingIndex + 1
29 | continue
30 | }
31 | }
32 | idx = closingIndex + 1
33 | }
34 | return evaluatedString.toString()
35 | }
36 |
37 | private fun getMatchingBracketIndex(openingIndex: Int, inputString: String): Int {
38 | val openingBracket = inputString[openingIndex]
39 | val closingBracket = when (openingBracket) {
40 | '[' -> ']'
41 | else -> ')'
42 | }
43 | var counter = 0
44 | for (idx in openingIndex until inputString.length) {
45 | if (inputString[idx] == openingBracket) counter++
46 | if (inputString[idx] == closingBracket) counter--
47 |
48 | if (counter == 0) return idx // found matching bracket
49 | if (counter < 0) return -1 // unbalanced brackets
50 | }
51 | return -1 // matching bracket not found
52 | }
53 |
54 | private fun calculateDigit(inputSubString: String): Char {
55 | /* 0 == '+[]'
56 | 1 == '+!+[]'
57 | 2 == '!+[]+!+[]'
58 | 3 == '!+[]+!+[]+!+[]'
59 | ...
60 | therefore '!+[]' count equals the digit
61 | if count equals 0, check for '+[]' just to be sure
62 | */
63 | val digit = "!\\+\\[]".toRegex().findAll(inputSubString).count() // matches '!+[]'
64 | if (digit == 0) {
65 | if ("\\+\\[]".toRegex().findAll(inputSubString).count() == 1) { // matches '+[]'
66 | return '0'
67 | }
68 | } else if (digit in 1..9) {
69 | return digit.digitToChar()
70 | }
71 | return '-' // Illegal digit
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/dataimage/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | }
5 |
6 | android {
7 | compileSdk = AndroidConfig.compileSdk
8 |
9 | defaultConfig {
10 | minSdk = AndroidConfig.minSdk
11 | }
12 |
13 | namespace = "eu.kanade.tachiyomi.lib.dataimage"
14 | }
15 |
16 | repositories {
17 | mavenCentral()
18 | }
19 |
20 | dependencies {
21 | compileOnly(libs.kotlin.stdlib)
22 | compileOnly(libs.okhttp)
23 | compileOnly(libs.jsoup)
24 | }
25 |
--------------------------------------------------------------------------------
/lib/dataimage/src/main/java/eu/kanade/tachiyomi/lib/dataimage/DataImageInterceptor.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.lib.dataimage
2 |
3 | import android.util.Base64
4 | import okhttp3.Interceptor
5 | import okhttp3.MediaType.Companion.toMediaTypeOrNull
6 | import okhttp3.Protocol
7 | import okhttp3.Response
8 | import okhttp3.ResponseBody.Companion.toResponseBody
9 | import org.jsoup.nodes.Element
10 |
11 | /**
12 | * If a source provides images via a data:image string instead of a URL, use these functions and interceptor
13 | */
14 |
15 | /**
16 | * Use if the attribute tag could have a data:image string or URL
17 | * Transforms data:image in to a fake URL that OkHttp won't die on
18 | */
19 | fun Element.dataImageAsUrl(attr: String): String {
20 | return if (this.attr(attr).startsWith("data")) {
21 | "https://127.0.0.1/?" + this.attr(attr).substringAfter(":")
22 | } else {
23 | this.attr("abs:$attr")
24 | }
25 | }
26 |
27 | /**
28 | * Use if the attribute tag has a data:image string but real URLs are on a different attribute
29 | */
30 | fun Element.dataImageAsUrlOrNull(attr: String): String? {
31 | return if (this.attr(attr).startsWith("data")) {
32 | "https://127.0.0.1/?" + this.attr(attr).substringAfter(":")
33 | } else {
34 | null
35 | }
36 | }
37 |
38 | /**
39 | * Interceptor that detects the URLs we created with the above functions, base64 decodes the data if necessary,
40 | * and builds a response with a valid image that Tachiyomi can display
41 | */
42 | class DataImageInterceptor : Interceptor {
43 | private val mediaTypePattern = Regex("""(^[^;,]*)[;,]""")
44 |
45 | override fun intercept(chain: Interceptor.Chain): Response {
46 | val url = chain.request().url.toString()
47 | return if (url.startsWith("https://127.0.0.1/?image")) {
48 | val dataString = url.substringAfter("?")
49 | val byteArray = if (dataString.contains("base64")) {
50 | Base64.decode(dataString.substringAfter("base64,"), Base64.DEFAULT)
51 | } else {
52 | dataString.substringAfter(",").toByteArray()
53 | }
54 | val mediaType = mediaTypePattern.find(dataString)!!.value.toMediaTypeOrNull()
55 | Response.Builder().body(byteArray.toResponseBody(mediaType))
56 | .request(chain.request())
57 | .protocol(Protocol.HTTP_1_0)
58 | .code(200)
59 | .message("")
60 | .build()
61 | } else {
62 | chain.proceed(chain.request())
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/lib/i18n/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | }
5 |
6 | android {
7 | compileSdk = AndroidConfig.compileSdk
8 |
9 | defaultConfig {
10 | minSdk = AndroidConfig.minSdk
11 | }
12 |
13 | namespace = "eu.kanade.tachiyomi.lib.i18n"
14 | }
15 |
16 | repositories {
17 | mavenCentral()
18 | }
19 |
20 | dependencies {
21 | compileOnly(libs.kotlin.stdlib)
22 | }
23 |
--------------------------------------------------------------------------------
/lib/i18n/src/main/java/eu/kanade/tachiyomi/lib/i18n/Intl.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.lib.i18n
2 |
3 | import org.jetbrains.annotations.PropertyKey
4 | import java.io.InputStreamReader
5 | import java.text.Collator
6 | import java.util.Locale
7 | import java.util.PropertyResourceBundle
8 |
9 | /**
10 | * A simple wrapper to make internationalization easier to use in sources.
11 | *
12 | * Message files should be put in the `assets/i18n` folder, with the name
13 | * `messages_{iso_639_1}.properties`, where `iso_639_1` should be using
14 | * snake case and be in lowercase.
15 | *
16 | * To edit the strings, use the official JetBrain's
17 | * [Resource Bundle Editor plugin](https://plugins.jetbrains.com/plugin/17035-resource-bundle-editor).
18 | *
19 | * Make sure to configure Android Studio to save Properties files as UTF-8 as well.
20 | * You can refer to this [documentation](https://www.jetbrains.com/help/idea/properties-files.html#1cbc434e)
21 | * on how to do so.
22 | */
23 | class Intl(
24 | language: String,
25 | availableLanguages: Set,
26 | private val baseLanguage: String,
27 | private val classLoader: ClassLoader,
28 | private val createMessageFileName: (String) -> String = { createDefaultMessageFileName(it) }
29 | ) {
30 |
31 | val chosenLanguage: String = when (language) {
32 | in availableLanguages -> language
33 | else -> baseLanguage
34 | }
35 |
36 | private val locale: Locale = Locale.forLanguageTag(chosenLanguage)
37 |
38 | val collator: Collator = Collator.getInstance(locale)
39 |
40 | private val baseBundle: PropertyResourceBundle by lazy { createBundle(baseLanguage) }
41 |
42 | private val bundle: PropertyResourceBundle by lazy {
43 | if (chosenLanguage == baseLanguage) baseBundle else createBundle(chosenLanguage)
44 | }
45 |
46 | /**
47 | * Returns the string from the message file. If the [key] is not present
48 | * in the current language, the English value will be returned. If the [key]
49 | * is also not present in English, the [key] surrounded by brackets will be returned.
50 | */
51 | @Suppress("InvalidBundleOrProperty")
52 | operator fun get(@PropertyKey(resourceBundle = "i18n.messages") key: String): String = when {
53 | bundle.containsKey(key) -> bundle.getString(key)
54 | baseBundle.containsKey(key) -> baseBundle.getString(key)
55 | else -> "[$key]"
56 | }
57 |
58 | /**
59 | * Uses the string as a format string and returns a string obtained by
60 | * substituting the specified arguments, using the instance locale.
61 | */
62 | @Suppress("InvalidBundleOrProperty")
63 | fun format(@PropertyKey(resourceBundle = "i18n.messages") key: String, vararg args: Any?) =
64 | get(key).format(locale, *args)
65 |
66 | fun languageDisplayName(localeCode: String): String =
67 | Locale.forLanguageTag(localeCode)
68 | .getDisplayName(locale)
69 | .replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() }
70 |
71 | /**
72 | * Creates a [PropertyResourceBundle] instance from the language specified.
73 | * The expected message file will be loaded from the `res/raw`.
74 | *
75 | * The [PropertyResourceBundle] is used directly instead of [java.util.ResourceBundle]
76 | * because the later has issues with UTF-8 files in Java 8, which would need
77 | * the message files to be saved in ISO-8859-1, making the file readability bad.
78 | */
79 | private fun createBundle(lang: String): PropertyResourceBundle {
80 | val fileName = createMessageFileName(lang)
81 | val fileContent = classLoader.getResourceAsStream(fileName)
82 |
83 | return PropertyResourceBundle(InputStreamReader(fileContent, "UTF-8"))
84 | }
85 |
86 | companion object {
87 | fun createDefaultMessageFileName(lang: String): String {
88 | val langSnakeCase = lang.replace("-", "_").lowercase()
89 |
90 | return "assets/i18n/messages_$langSnakeCase.properties"
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/lib/randomua/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | id("kotlinx-serialization")
5 | }
6 |
7 | android {
8 | compileSdk = AndroidConfig.compileSdk
9 |
10 | defaultConfig {
11 | minSdk = AndroidConfig.minSdk
12 | }
13 |
14 | namespace = "eu.kanade.tachiyomi.lib.randomua"
15 | }
16 |
17 | repositories {
18 | mavenCentral()
19 | }
20 |
21 | dependencies {
22 | compileOnly(libs.bundles.common)
23 | }
24 |
--------------------------------------------------------------------------------
/lib/randomua/src/main/java/eu/kanade/tachiyomi/lib/randomua/RandomUserAgentInterceptor.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.lib.randomua
2 |
3 | import eu.kanade.tachiyomi.network.GET
4 | import eu.kanade.tachiyomi.network.NetworkHelper
5 | import kotlinx.serialization.Serializable
6 | import kotlinx.serialization.decodeFromString
7 | import kotlinx.serialization.json.Json
8 | import okhttp3.Interceptor
9 | import okhttp3.OkHttpClient
10 | import okhttp3.Response
11 | import uy.kohesive.injekt.injectLazy
12 | import java.io.IOException
13 |
14 | private class RandomUserAgentInterceptor(
15 | private val userAgentType: UserAgentType,
16 | private val customUA: String?,
17 | private val filterInclude: List,
18 | private val filterExclude: List,
19 | ) : Interceptor {
20 |
21 | private var userAgent: String? = null
22 |
23 | private val json: Json by injectLazy()
24 |
25 | private val network: NetworkHelper by injectLazy()
26 |
27 | private val client = network.client
28 |
29 | override fun intercept(chain: Interceptor.Chain): Response {
30 | try {
31 | val originalRequest = chain.request()
32 |
33 | val newUserAgent = getUserAgent()
34 | ?: return chain.proceed(originalRequest)
35 |
36 | val originalHeaders = originalRequest.headers
37 |
38 | val modifiedHeaders = originalHeaders.newBuilder()
39 | .set("User-Agent", newUserAgent)
40 | .build()
41 |
42 | return chain.proceed(
43 | originalRequest.newBuilder()
44 | .headers(modifiedHeaders)
45 | .build()
46 | )
47 | } catch (e: Exception) {
48 | throw IOException(e.message)
49 | }
50 | }
51 |
52 | private fun getUserAgent(): String? {
53 | if (userAgentType == UserAgentType.OFF) {
54 | return customUA?.ifBlank { null }
55 | }
56 |
57 | if (!userAgent.isNullOrEmpty()) return userAgent
58 |
59 | val uaResponse = client.newCall(GET(UA_DB_URL)).execute()
60 |
61 | if (!uaResponse.isSuccessful) {
62 | uaResponse.close()
63 | return null
64 | }
65 |
66 | val userAgentList = uaResponse.use { json.decodeFromString(it.body.string()) }
67 |
68 | return when (userAgentType) {
69 | UserAgentType.DESKTOP -> userAgentList.desktop
70 | UserAgentType.MOBILE -> userAgentList.mobile
71 | else -> error("Expected UserAgentType.DESKTOP or UserAgentType.MOBILE but got UserAgentType.${userAgentType.name} instead")
72 | }
73 | .filter {
74 | filterInclude.isEmpty() || filterInclude.any { filter ->
75 | it.contains(filter, ignoreCase = true)
76 | }
77 | }
78 | .filterNot {
79 | filterExclude.any { filter ->
80 | it.contains(filter, ignoreCase = true)
81 | }
82 | }
83 | .randomOrNull()
84 | .also { userAgent = it }
85 | }
86 |
87 | companion object {
88 | private const val UA_DB_URL = "https://tachiyomiorg.github.io/user-agents/user-agents.json"
89 | }
90 | }
91 |
92 | /**
93 | * Helper function to add a latest random user agent interceptor.
94 | * The interceptor will added at the first position in the chain,
95 | * so the CloudflareInterceptor in the app will be able to make usage of it.
96 | *
97 | * @param userAgentType User Agent type one of (DESKTOP, MOBILE, OFF)
98 | * @param customUA Optional custom user agent used when userAgentType is OFF
99 | * @param filterInclude Filter to only include User Agents containing these strings
100 | * @param filterExclude Filter to exclude User Agents containing these strings
101 | */
102 | fun OkHttpClient.Builder.setRandomUserAgent(
103 | userAgentType: UserAgentType,
104 | customUA: String? = null,
105 | filterInclude: List = emptyList(),
106 | filterExclude: List = emptyList(),
107 | ) = apply {
108 | interceptors().add(0, RandomUserAgentInterceptor(userAgentType, customUA, filterInclude, filterExclude))
109 | }
110 |
111 | enum class UserAgentType {
112 | MOBILE,
113 | DESKTOP,
114 | OFF
115 | }
116 |
117 | @Serializable
118 | private data class UserAgentList(
119 | val desktop: List,
120 | val mobile: List
121 | )
122 |
--------------------------------------------------------------------------------
/lib/randomua/src/main/java/eu/kanade/tachiyomi/lib/randomua/RandomUserAgentPreference.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.lib.randomua
2 |
3 | import android.content.SharedPreferences
4 | import android.widget.Toast
5 | import androidx.preference.EditTextPreference
6 | import androidx.preference.ListPreference
7 | import androidx.preference.PreferenceScreen
8 | import okhttp3.Headers
9 |
10 |
11 | /**
12 | * Helper function to return UserAgentType based on SharedPreference value
13 | */
14 | fun SharedPreferences.getPrefUAType(): UserAgentType {
15 | return when (getString(PREF_KEY_RANDOM_UA, "off")) {
16 | "mobile" -> UserAgentType.MOBILE
17 | "desktop" -> UserAgentType.DESKTOP
18 | else -> UserAgentType.OFF
19 | }
20 | }
21 |
22 | /**
23 | * Helper function to return custom UserAgent from SharedPreference
24 | */
25 | fun SharedPreferences.getPrefCustomUA(): String? {
26 | return getString(PREF_KEY_CUSTOM_UA, null)
27 | }
28 |
29 | /**
30 | * Helper function to add Random User-Agent settings to SharedPreference
31 | *
32 | * @param screen, PreferenceScreen from `setupPreferenceScreen`
33 | */
34 | fun addRandomUAPreferenceToScreen(
35 | screen: PreferenceScreen,
36 | ) {
37 | ListPreference(screen.context).apply {
38 | key = PREF_KEY_RANDOM_UA
39 | title = TITLE_RANDOM_UA
40 | entries = RANDOM_UA_ENTRIES
41 | entryValues = RANDOM_UA_VALUES
42 | summary = "%s"
43 | setDefaultValue("off")
44 | }.also(screen::addPreference)
45 |
46 | EditTextPreference(screen.context).apply {
47 | key = PREF_KEY_CUSTOM_UA
48 | title = TITLE_CUSTOM_UA
49 | summary = CUSTOM_UA_SUMMARY
50 | setOnPreferenceChangeListener { _, newValue ->
51 | try {
52 | Headers.Builder().add("User-Agent", newValue as String).build()
53 | true
54 | } catch (e: IllegalArgumentException) {
55 | Toast.makeText(screen.context, "User Agent invalid:${e.message}", Toast.LENGTH_LONG).show()
56 | false
57 | }
58 | }
59 | }.also(screen::addPreference)
60 | }
61 |
62 | const val TITLE_RANDOM_UA = "Random User-Agent (Requires Restart)"
63 | const val PREF_KEY_RANDOM_UA = "pref_key_random_ua_"
64 | val RANDOM_UA_ENTRIES = arrayOf("OFF", "Desktop", "Mobile")
65 | val RANDOM_UA_VALUES = arrayOf("off", "desktop", "mobile")
66 |
67 | const val TITLE_CUSTOM_UA = "Custom User-Agent (Requires Restart)"
68 | const val PREF_KEY_CUSTOM_UA = "pref_key_custom_ua_"
69 | const val CUSTOM_UA_SUMMARY = "Leave blank to use application default user-agent (IGNORED if Random User-Agent is enabled)"
70 |
71 |
--------------------------------------------------------------------------------
/lib/synchrony/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | }
5 |
6 | android {
7 | compileSdk = AndroidConfig.compileSdk
8 |
9 | defaultConfig {
10 | minSdk = AndroidConfig.minSdk
11 | }
12 |
13 | namespace = "eu.kanade.tachiyomi.lib.synchrony"
14 | }
15 |
16 | repositories {
17 | mavenCentral()
18 | }
19 |
20 | dependencies {
21 | compileOnly(libs.bundles.common)
22 | }
23 |
--------------------------------------------------------------------------------
/lib/synchrony/src/main/java/eu/kanade/tachiyomi/lib/synchrony/Deobfuscator.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.lib.synchrony
2 |
3 | import app.cash.quickjs.QuickJs
4 |
5 | /**
6 | * Helper class to deobfuscate JavaScript strings with synchrony.
7 | */
8 | object Deobfuscator {
9 | fun deobfuscateScript(source: String): String? {
10 | val originalScript = javaClass.getResource("/assets/$SCRIPT_NAME")
11 | ?.readText() ?: return null
12 |
13 | // Sadly needed until QuickJS properly supports module imports:
14 | // Regex for finding one and two in "export{one as Deobfuscator,two as Transformer};"
15 | val regex = """export\{(.*) as Deobfuscator,(.*) as Transformer\};""".toRegex()
16 | val synchronyScript = regex.find(originalScript)?.let { match ->
17 | val (deob, trans) = match.destructured
18 | val replacement = "const Deobfuscator = $deob, Transformer = $trans;"
19 | originalScript.replace(match.value, replacement)
20 | } ?: return null
21 |
22 | return QuickJs.create().use { engine ->
23 | engine.evaluate("globalThis.console = { log: () => {}, warn: () => {}, error: () => {}, trace: () => {} };")
24 | engine.evaluate(synchronyScript)
25 |
26 | engine.set(
27 | "source", TestInterface::class.java,
28 | object : TestInterface {
29 | override fun getValue() = source
30 | },
31 | )
32 | engine.evaluate("new Deobfuscator().deobfuscateSource(source.getValue())") as? String
33 | }
34 | }
35 |
36 | private interface TestInterface {
37 | fun getValue(): String
38 | }
39 | }
40 |
41 | // Update this when the script is updated!
42 | private const val SCRIPT_NAME = "synchrony-v2.4.2.1.js"
43 |
--------------------------------------------------------------------------------
/lib/textinterceptor/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | }
5 |
6 | android {
7 | compileSdk = AndroidConfig.compileSdk
8 |
9 | defaultConfig {
10 | minSdk = AndroidConfig.minSdk
11 | }
12 |
13 | namespace = "eu.kanade.tachiyomi.lib.textinterceptor"
14 | }
15 |
16 | repositories {
17 | mavenCentral()
18 | }
19 |
20 | dependencies {
21 | compileOnly(libs.kotlin.stdlib)
22 | compileOnly(libs.okhttp)
23 | }
24 |
--------------------------------------------------------------------------------
/lib/textinterceptor/src/main/java/eu/kanade/tachiyomi/lib/textinterceptor/TextInterceptor.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.lib.textinterceptor
2 |
3 | import android.annotation.SuppressLint
4 | import android.graphics.Bitmap
5 | import android.graphics.Canvas
6 | import android.graphics.Color
7 | import android.graphics.Typeface
8 | import android.net.Uri
9 | import android.os.Build
10 | import android.text.Html
11 | import android.text.Layout
12 | import android.text.StaticLayout
13 | import android.text.TextPaint
14 | import okhttp3.Interceptor
15 | import okhttp3.MediaType.Companion.toMediaType
16 | import okhttp3.Protocol
17 | import okhttp3.Response
18 | import okhttp3.ResponseBody.Companion.toResponseBody
19 | import java.io.ByteArrayOutputStream
20 |
21 | class TextInterceptor : Interceptor {
22 | // With help from:
23 | // https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13304#issuecomment-1234532897
24 | // https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a
25 |
26 | companion object {
27 | // Designer values:
28 | private const val WIDTH: Int = 1000
29 | private const val X_PADDING: Float = 50f
30 | private const val Y_PADDING: Float = 25f
31 | private const val HEADING_FONT_SIZE: Float = 36f
32 | private const val BODY_FONT_SIZE: Float = 30f
33 | private const val SPACING_MULT: Float = 1.1f
34 | private const val SPACING_ADD: Float = 2f
35 |
36 | // No need to touch this one:
37 | private const val HOST = TextInterceptorHelper.HOST
38 | }
39 |
40 | override fun intercept(chain: Interceptor.Chain): Response {
41 | val request = chain.request()
42 | val url = request.url
43 | if (url.host != HOST) return chain.proceed(request)
44 |
45 | val creator = textFixer("Author's Notes from ${url.pathSegments[0]}")
46 | val story = textFixer(url.pathSegments[1])
47 |
48 | // Heading
49 | val paintHeading = TextPaint().apply {
50 | color = Color.BLACK
51 | textSize = HEADING_FONT_SIZE
52 | typeface = Typeface.DEFAULT_BOLD
53 | isAntiAlias = true
54 | }
55 |
56 | @Suppress("DEPRECATION")
57 | val heading = StaticLayout(
58 | creator, paintHeading, (WIDTH - 2 * X_PADDING).toInt(),
59 | Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
60 | )
61 |
62 | // Body
63 | val paintBody = TextPaint().apply {
64 | color = Color.BLACK
65 | textSize = BODY_FONT_SIZE
66 | typeface = Typeface.DEFAULT
67 | isAntiAlias = true
68 | }
69 |
70 | @Suppress("DEPRECATION")
71 | val body = StaticLayout(
72 | story, paintBody, (WIDTH - 2 * X_PADDING).toInt(),
73 | Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
74 | )
75 |
76 | // Image building
77 | val imgHeight: Int = (heading.height + body.height + 2 * Y_PADDING).toInt()
78 | val bitmap: Bitmap = Bitmap.createBitmap(WIDTH, imgHeight, Bitmap.Config.ARGB_8888)
79 |
80 | Canvas(bitmap).apply {
81 | drawColor(Color.WHITE)
82 | heading.draw(this, X_PADDING, Y_PADDING)
83 | body.draw(this, X_PADDING, Y_PADDING + heading.height.toFloat())
84 | }
85 |
86 | // Image converting & returning
87 | val stream = ByteArrayOutputStream()
88 | bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)
89 | val responseBody = stream.toByteArray().toResponseBody("image/png".toMediaType())
90 | return Response.Builder()
91 | .request(request)
92 | .protocol(Protocol.HTTP_1_1)
93 | .code(200)
94 | .message("OK")
95 | .body(responseBody)
96 | .build()
97 | }
98 |
99 | @SuppressLint("ObsoleteSdkInt")
100 | private fun textFixer(htmlString: String): String {
101 | return if (Build.VERSION.SDK_INT >= 24) {
102 | Html.fromHtml(htmlString , Html.FROM_HTML_MODE_LEGACY).toString()
103 | } else {
104 | @Suppress("DEPRECATION")
105 | Html.fromHtml(htmlString).toString()
106 | }
107 | }
108 |
109 | @Suppress("SameParameterValue")
110 | private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
111 | canvas.save()
112 | canvas.translate(x, y)
113 | this.draw(canvas)
114 | canvas.restore()
115 | }
116 | }
117 |
118 | object TextInterceptorHelper {
119 |
120 | const val HOST = "tachiyomi-lib-textinterceptor"
121 |
122 | fun createUrl(creator: String, text: String): String {
123 | return "http://$HOST/" + Uri.encode(creator) + "/" + Uri.encode(text)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/lib/unpacker/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `java-library`
3 | kotlin("jvm")
4 | }
5 |
6 | repositories {
7 | mavenCentral()
8 | }
9 |
10 | dependencies {
11 | compileOnly(libs.kotlin.stdlib)
12 | }
13 |
--------------------------------------------------------------------------------
/lib/unpacker/src/main/java/eu/kanade/tachiyomi/lib/unpacker/SubstringExtractor.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.lib.unpacker
2 |
3 | /**
4 | * A helper class to extract substrings efficiently.
5 | *
6 | * Note that all methods move [startIndex] over the ending delimiter.
7 | */
8 | class SubstringExtractor(private val text: String) {
9 | private var startIndex = 0
10 |
11 | fun skipOver(str: String) {
12 | val index = text.indexOf(str, startIndex)
13 | if (index == -1) return
14 | startIndex = index + str.length
15 | }
16 |
17 | fun substringBefore(str: String): String {
18 | val index = text.indexOf(str, startIndex)
19 | if (index == -1) return ""
20 | val result = text.substring(startIndex, index)
21 | startIndex = index + str.length
22 | return result
23 | }
24 |
25 | fun substringBetween(left: String, right: String): String {
26 | val index = text.indexOf(left, startIndex)
27 | if (index == -1) return ""
28 | val leftIndex = index + left.length
29 | val rightIndex = text.indexOf(right, leftIndex)
30 | if (rightIndex == -1) return ""
31 | startIndex = rightIndex + right.length
32 | return text.substring(leftIndex, rightIndex)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/unpacker/src/main/java/eu/kanade/tachiyomi/lib/unpacker/Unpacker.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.lib.unpacker
2 |
3 | /**
4 | * Helper class to unpack JavaScript code compressed by [packer](http://dean.edwards.name/packer/).
5 | *
6 | * Source code of packer can be found [here](https://github.com/evanw/packer/blob/master/packer.js).
7 | */
8 | object Unpacker {
9 |
10 | /**
11 | * Unpacks JavaScript code compressed by packer.
12 | *
13 | * Specify [left] and [right] to unpack only the data between them.
14 | *
15 | * Note: single quotes `\'` in the data will be replaced with double quotes `"`.
16 | */
17 | fun unpack(script: String, left: String? = null, right: String? = null): String =
18 | unpack(SubstringExtractor(script), left, right)
19 |
20 | /**
21 | * Unpacks JavaScript code compressed by packer.
22 | *
23 | * Specify [left] and [right] to unpack only the data between them.
24 | *
25 | * Note: single quotes `\'` in the data will be replaced with double quotes `"`.
26 | */
27 | fun unpack(script: SubstringExtractor, left: String? = null, right: String? = null): String {
28 | val packed = script
29 | .substringBetween("}('", ".split('|'),0,{}))")
30 | .replace("\\'", "\"")
31 |
32 | val parser = SubstringExtractor(packed)
33 | val data: String
34 | if (left != null && right != null) {
35 | data = parser.substringBetween(left, right)
36 | parser.skipOver("',")
37 | } else {
38 | data = parser.substringBefore("',")
39 | }
40 | if (data.isEmpty()) return ""
41 |
42 | val dictionary = parser.substringBetween("'", "'").split("|")
43 | val size = dictionary.size
44 |
45 | return wordRegex.replace(data) {
46 | val key = it.value
47 | val index = parseRadix62(key)
48 | if (index >= size) return@replace key
49 | dictionary[index].ifEmpty { key }
50 | }
51 | }
52 |
53 | private val wordRegex by lazy { Regex("""\w+""") }
54 |
55 | private fun parseRadix62(str: String): Int {
56 | var result = 0
57 | for (ch in str.toCharArray()) {
58 | result = result * 62 + when {
59 | ch.code <= '9'.code -> { // 0-9
60 | ch.code - '0'.code
61 | }
62 |
63 | ch.code >= 'a'.code -> { // a-z
64 | // ch - 'a' + 10
65 | ch.code - ('a'.code - 10)
66 | }
67 |
68 | else -> { // A-Z
69 | // ch - 'A' + 36
70 | ch.code - ('A'.code - 36)
71 | }
72 | }
73 | }
74 | return result
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/multisrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | id("kotlinx-serialization")
5 | }
6 |
7 | android {
8 | compileSdk = AndroidConfig.compileSdk
9 |
10 | defaultConfig {
11 | minSdk = 29
12 | }
13 |
14 | namespace = "eu.kanade.tachiyomi.lib.themesources"
15 |
16 | kotlinOptions {
17 | freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
18 | }
19 | }
20 |
21 | repositories {
22 | mavenCentral()
23 | }
24 |
25 | configurations {
26 | compileOnly {
27 | isCanBeResolved = true
28 | }
29 | }
30 |
31 | dependencies {
32 | compileOnly(libs.bundles.common)
33 |
34 | // Implements all :lib libraries on the multisrc generator
35 | // Note that this does not mean that generated sources are going to
36 | // implement them too; this is just to be able to compile and generate sources.
37 | rootProject.subprojects
38 | .filter { it.path.startsWith(":lib") }
39 | .forEach(::implementation)
40 | }
41 |
42 | tasks {
43 | register("generateExtensions") {
44 | val buildDir = layout.buildDirectory.asFile.get()
45 | classpath = configurations.compileOnly.get() +
46 | configurations.androidApis.get() + // android.jar path
47 | files("$buildDir/intermediates/aar_main_jar/debug/classes.jar") // jar made from this module
48 | mainClass.set("generator.GeneratorMainKt")
49 |
50 | workingDir = workingDir.parentFile // project root
51 |
52 | errorOutput = System.out // for GitHub workflow commands
53 |
54 | if (!logger.isInfoEnabled) {
55 | standardOutput = org.gradle.internal.io.NullOutputStream.INSTANCE
56 | }
57 |
58 | dependsOn("ktLint", "assembleDebug")
59 | }
60 |
61 | register("ktLint") {
62 | if (project.hasProperty("theme")) {
63 | val theme = project.property("theme")
64 | source(files("src/main/java/eu/kanade/tachiyomi/multisrc/$theme", "overrides/$theme"))
65 | return@register
66 | }
67 | source(files("src", "overrides"))
68 | }
69 |
70 | register("ktFormat") {
71 | if (project.hasProperty("theme")) {
72 | val theme = project.property("theme")
73 | source(files("src/main/java/eu/kanade/tachiyomi/multisrc/$theme", "overrides/$theme"))
74 | return@register
75 | }
76 | source(files("src", "overrides"))
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/multisrc/overrides/madara/default/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/multisrc/overrides/madara/default/additional.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation(project(":lib-cryptoaes"))
3 | implementation(project(":lib-randomua"))
4 | }
5 |
--------------------------------------------------------------------------------
/multisrc/overrides/madara/default/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/default/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/default/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/default/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/default/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/default/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/default/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/default/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/default/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/default/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/default/res/web_hi_res_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/default/res/web_hi_res_512.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/s2manga/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/s2manga/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/s2manga/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/s2manga/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/s2manga/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/s2manga/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/s2manga/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/s2manga/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/s2manga/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/s2manga/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/s2manga/res/web_hi_res_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/multisrc/overrides/madara/s2manga/res/web_hi_res_512.png
--------------------------------------------------------------------------------
/multisrc/overrides/madara/s2manga/src/S2Manga.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.en.s2manga
2 |
3 | import eu.kanade.tachiyomi.multisrc.madara.Madara
4 |
5 | class S2Manga : Madara("S2Manga", "https://www.s2manga.com", "en") {
6 |
7 | override fun headersBuilder() = super.headersBuilder()
8 | .add("Referer", "$baseUrl/")
9 |
10 | override val pageListParseSelector = "div.page-break img[src*=\"https\"]"
11 | }
12 |
--------------------------------------------------------------------------------
/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.multisrc.madara
2 |
3 | import generator.ThemeSourceData.SingleLang
4 | import generator.ThemeSourceGenerator
5 |
6 | class MadaraGenerator : ThemeSourceGenerator {
7 |
8 | override val themePkg = "madara"
9 |
10 | override val themeClass = "Madara"
11 |
12 | override val baseVersionCode: Int = 32
13 |
14 | override val sources = listOf(
15 | SingleLang("S2Manga", "https://www.s2manga.com", "en", overrideVersionCode = 2),
16 | )
17 |
18 | companion object {
19 | @JvmStatic
20 | fun main(args: Array) {
21 | MadaraGenerator().createAll()
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraUrlActivity.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.multisrc.madara
2 |
3 | import android.app.Activity
4 | import android.content.ActivityNotFoundException
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import android.util.Log
8 | import kotlin.system.exitProcess
9 |
10 | class MadaraUrlActivity : Activity() {
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | val pathSegments = intent?.data?.pathSegments
14 |
15 | if (pathSegments != null && pathSegments.size >= 2) {
16 | val mainIntent = Intent().apply {
17 | action = "eu.kanade.tachiyomi.SEARCH"
18 | putExtra("query", "${getSLUG(pathSegments)}")
19 | putExtra("filter", packageName)
20 | }
21 | try {
22 | startActivity(mainIntent)
23 | } catch (e: ActivityNotFoundException) {
24 | Log.e("MadaraUrl", e.toString())
25 | }
26 | } else {
27 | Log.e("MadaraUrl", "could not parse uri from intent $intent")
28 | }
29 |
30 | finish()
31 | exitProcess(0)
32 | }
33 |
34 | private fun getSLUG(pathSegments: MutableList): String? {
35 | return if (pathSegments.size >= 2) {
36 | val slug = pathSegments[1]
37 | "${Madara.URL_SEARCH_PREFIX}$slug"
38 | } else {
39 | null
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/multisrc/src/main/java/generator/GeneratorMain.kt:
--------------------------------------------------------------------------------
1 | package generator
2 |
3 | import java.io.File
4 |
5 | /**
6 | * Finds and calls all `ThemeSourceGenerator`s
7 | */
8 | fun main(args: Array) {
9 | val userDir = System.getProperty("user.dir")!!
10 | val sourcesDirPath = "$userDir/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc"
11 | val sourcesDir = File(sourcesDirPath)
12 |
13 | // find all theme packages
14 | sourcesDir.list()!!
15 | .filter { File(sourcesDir, it).isDirectory }
16 | .forEach { themeSource ->
17 | // Find all XxxGenerator.kt files and invoke main from them
18 | File("$sourcesDirPath/$themeSource").list()!!
19 | .filter { it.endsWith("Generator.kt") }
20 | .mapNotNull { generatorClass ->
21 | // Find Java class and extract method lists
22 | Class.forName("eu/kanade/tachiyomi/multisrc/$themeSource/$generatorClass".replace("/", ".").substringBefore(".kt"))
23 | .methods
24 | .find { it.name == "main" }
25 | }
26 | .forEach { it.invoke(null, emptyArray()) }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/multisrc/src/main/java/generator/IntelijConfigurationGeneratorMain.kt:
--------------------------------------------------------------------------------
1 | package generator
2 |
3 | import java.io.File
4 |
5 | /**
6 | * Finds all themes and creates an Intellij Idea run configuration for their generators
7 | * Should be run after creation/deletion of each theme
8 | */
9 | fun main(args: Array) {
10 | val userDir = System.getProperty("user.dir")!!
11 | val sourcesDirPath = "$userDir/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc"
12 | val sourcesDir = File(sourcesDirPath)
13 |
14 | // cleanup from past runs
15 | File("$userDir/.run").apply {
16 | if (exists()) {
17 | deleteRecursively()
18 | }
19 | mkdirs()
20 | }
21 |
22 | // find all theme packages
23 | sourcesDir.list()!!
24 | .filter { File(sourcesDir, it).isDirectory }
25 | .forEach { themeSource ->
26 | // Find all XxxGenerator.kt files
27 | File("$sourcesDirPath/$themeSource").list()!!
28 | .filter { it.endsWith("Generator.kt") }
29 | .map { it.substringBefore(".kt") }
30 | .forEach { generatorClass ->
31 | val file = File("$userDir/.run/$generatorClass.run.xml")
32 | val intellijConfStr = """
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | """.trimIndent()
45 | file.writeText(intellijConfStr)
46 | file.appendText("\n")
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | include(":core")
2 |
3 | // Load all modules under /lib
4 | File(rootDir, "lib").eachDir {
5 | val libName = it.name
6 | include(":lib-$libName")
7 | project(":lib-$libName").projectDir = File("lib/$libName")
8 | }
9 |
10 | if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") {
11 | // Local development (full project build)
12 |
13 | include(":multisrc")
14 | project(":multisrc").projectDir = File("multisrc")
15 |
16 | /**
17 | * Add or remove modules to load as needed for local development here.
18 | * To generate multisrc extensions first, run the `:multisrc:generateExtensions` task first.
19 | */
20 | loadAllIndividualExtensions()
21 | loadAllGeneratedMultisrcExtensions()
22 | // loadIndividualExtension("all", "mangadex")
23 | // loadGeneratedMultisrcExtension("en", "guya")
24 | } else {
25 | // Running in CI (GitHub Actions)
26 |
27 | val isMultisrc = System.getenv("CI_MULTISRC") == "true"
28 | val chunkSize = System.getenv("CI_CHUNK_SIZE").toInt()
29 | val chunk = System.getenv("CI_CHUNK_NUM").toInt()
30 |
31 | if (isMultisrc) {
32 | include(":multisrc")
33 | project(":multisrc").projectDir = File("multisrc")
34 |
35 | // Loads generated extensions from multisrc
36 | File(rootDir, "generated-src").getChunk(chunk, chunkSize)?.forEach {
37 | val name = ":extensions:multisrc:${it.parentFile.name}:${it.name}"
38 | println(name)
39 | include(name)
40 | project(name).projectDir = File("generated-src/${it.parentFile.name}/${it.name}")
41 | }
42 | } else {
43 | // Loads individual extensions
44 | File(rootDir, "src").getChunk(chunk, chunkSize)?.forEach {
45 | val name = ":extensions:individual:${it.parentFile.name}:${it.name}"
46 | println(name)
47 | include(name)
48 | project(name).projectDir = File("src/${it.parentFile.name}/${it.name}")
49 | }
50 | }
51 | }
52 |
53 | fun loadAllIndividualExtensions() {
54 | File(rootDir, "src").eachDir { dir ->
55 | dir.eachDir { subdir ->
56 | val name = ":extensions:individual:${dir.name}:${subdir.name}"
57 | include(name)
58 | project(name).projectDir = File("src/${dir.name}/${subdir.name}")
59 | }
60 | }
61 | }
62 | fun loadAllGeneratedMultisrcExtensions() {
63 | File(rootDir, "generated-src").eachDir { dir ->
64 | dir.eachDir { subdir ->
65 | val name = ":extensions:multisrc:${dir.name}:${subdir.name}"
66 | include(name)
67 | project(name).projectDir = File("generated-src/${dir.name}/${subdir.name}")
68 | }
69 | }
70 | }
71 | fun loadIndividualExtension(lang: String, name: String) {
72 | val projectName = ":extensions:individual:$lang:$name"
73 | include(projectName)
74 | project(projectName).projectDir = File("src/${lang}/${name}")
75 | }
76 | fun loadGeneratedMultisrcExtension(lang: String, name: String) {
77 | val projectName = ":extensions:multisrc:$lang:$name"
78 | include(projectName)
79 | project(projectName).projectDir = File("generated-src/${lang}/${name}")
80 | }
81 |
82 | fun File.getChunk(chunk: Int, chunkSize: Int): List? {
83 | return listFiles()
84 | // Lang folder
85 | ?.filter { it.isDirectory }
86 | // Extension subfolders
87 | ?.mapNotNull { dir -> dir.listFiles()?.filter { it.isDirectory } }
88 | ?.flatten()
89 | ?.sortedBy { it.name }
90 | ?.chunked(chunkSize)
91 | ?.get(chunk)
92 | }
93 |
94 | fun File.eachDir(block: (File) -> Unit) {
95 | listFiles()?.filter { it.isDirectory }?.forEach { block(it) }
96 | }
97 |
--------------------------------------------------------------------------------
/src/all/batoto/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/all/batoto/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 1.3.30
2 |
3 | ### Refactor
4 |
5 | * Replace CryptoJS with Native Kotlin Functions
6 | * Remove QuickJS dependency
7 |
8 | ## 1.3.29
9 |
10 | ### Refactor
11 |
12 | * Cleanup pageListParse function
13 | * Replace Duktape with QuickJS
14 |
15 | ## 1.3.28
16 |
17 | ### Features
18 |
19 | * Add mirror `batocc.com`
20 | * Add mirror `batotwo.com`
21 | * Add mirror `mangatoto.net`
22 | * Add mirror `mangatoto.org`
23 | * Add mirror `mycordant.co.uk`
24 | * Add mirror `dto.to`
25 | * Add mirror `hto.to`
26 | * Add mirror `mto.to`
27 | * Add mirror `wto.to`
28 | * Remove mirror `mycdhands.com`
29 |
30 | ## 1.3.27
31 |
32 | ### Features
33 |
34 | * Change default popular sort by `Most Views Totally`
35 |
36 | ## 1.3.26
37 |
38 | ### Fix
39 |
40 | * Update author and artist parsing
41 |
42 | ## 1.3.25
43 |
44 | ### Fix
45 |
46 | * Status parsing
47 | * Artist name parsing
48 |
49 | ## 1.3.24
50 |
51 | ### Fix
52 |
53 | * Bump versions for individual extension with URL handler activities
54 |
55 | ## 1.2.23
56 |
57 | ### Fix
58 |
59 | * Update pageListParse logic to handle website changes
60 |
61 | ## 1.2.22
62 |
63 | ### Features
64 |
65 | * Add `CHANGELOG.md` & `README.md`
66 |
67 | ## 1.2.21
68 |
69 | ### Fix
70 |
71 | * Update lang codes
72 |
73 | ## 1.2.20
74 |
75 | ### Features
76 |
77 | * Rework of search
78 |
79 | ## 1.2.19
80 |
81 | ### Features
82 |
83 | * Support for alternative chapter list
84 | * Personal lists filter
85 |
86 | ## 1.2.18
87 |
88 | ### Features
89 |
90 | * Utils lists filter
91 | * Letter matching filter
92 |
93 | ## 1.2.17
94 |
95 | ### Features
96 |
97 | * Add mirror `mycdhands.com`
98 |
99 | ## 1.2.16
100 |
101 | ### Features
102 |
103 | * Mirror support
104 | * URL intent updates
105 |
106 | ## 1.2.15
107 |
108 | ### Fix
109 |
110 | * Manga description
111 |
112 | ## 1.2.14
113 |
114 | ### Features
115 |
116 | * Escape entities
117 |
118 | ## 1.2.13
119 |
120 | ### Refactor
121 |
122 | * Replace Gson with kotlinx.serialization
123 |
124 | ## 1.2.12
125 |
126 | ### Fix
127 |
128 | * Infinity search
129 |
130 | ## 1.2.11
131 |
132 | ### Fix
133 |
134 | * No search result
135 |
136 | ## 1.2.10
137 |
138 | ### Features
139 |
140 | * Support for URL intent
141 | * Updated filters
142 |
143 | ## 1.2.9
144 |
145 | ### Fix
146 |
147 | * Chapter parsing
148 |
149 | ## 1.2.8
150 |
151 | ### Features
152 |
153 | * More chapter filtering
154 |
155 | ## 1.2.7
156 |
157 | ### Fix
158 |
159 | * Language filtering in latest
160 | * Parsing of seconds
161 |
162 | ## 1.2.6
163 |
164 | ### Features
165 |
166 | * Scanlator support
167 |
168 | ### Fix
169 |
170 | * Date parsing
171 |
172 | ## 1.2.5
173 |
174 | ### Features
175 |
176 | * Update supported Language list
177 |
178 | ## 1.2.4
179 |
180 | ### Features
181 |
182 | * Support for excluding genres
183 |
184 | ## 1.2.3
185 |
186 | ### Fix
187 |
188 | * Typo in some genres
189 |
190 | ## 1.2.2
191 |
192 | ### Features
193 |
194 | * Reworked filter option
195 |
196 | ## 1.2.1
197 |
198 | ### Features
199 |
200 | * Conversion from Emerald to Bato.to
201 | * First version
202 |
--------------------------------------------------------------------------------
/src/all/batoto/README.md:
--------------------------------------------------------------------------------
1 | # Bato.to
2 |
3 | Table of Content
4 | - [FAQ](#FAQ)
5 | - [Why are there Manga of diffrent languge than the selected one in Personal & Utils lists?](#why-are-there-manga-of-diffrent-languge-than-the-selected-one-in-personal--utils-lists)
6 | - [Bato.to is not loading anything?](#batoto-is-not-loading-anything)
7 |
8 | [Uncomment this if needed; and replace ( and ) with ( and )]: <> (- [Guides](#Guides))
9 |
10 | Don't find the question you are look for go check out our general FAQs and Guides over at [Extension FAQ](https://tachiyomi.org/help/faq/#extensions) or [Getting Started](https://tachiyomi.org/help/guides/getting-started/#installation)
11 |
12 | ## FAQ
13 |
14 | ### Why are there Manga of diffrent languge than the selected one in Personal & Utils lists?
15 | Personol & Utils lists have no way to difritiate between langueges.
16 |
17 | ### Bato.to is not loading anything?
18 | Bato.to get blocked by some ISPs, try using a diffrent mirror of Bato.to from the settings.
19 |
20 | [Uncomment this if needed]: <> (## Guides)
21 |
--------------------------------------------------------------------------------
/src/all/batoto/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlinx-serialization'
4 |
5 | ext {
6 | extName = 'Bato.to'
7 | pkgNameSuffix = 'all.batoto'
8 | extClass = '.BatoToFactory'
9 | extVersionCode = 32
10 | isNsfw = true
11 | }
12 |
13 | apply from: "$rootDir/common.gradle"
14 |
15 | dependencies {
16 | implementation(project(':lib-cryptoaes'))
17 | }
18 |
--------------------------------------------------------------------------------
/src/all/batoto/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/batoto/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/all/batoto/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/batoto/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/all/batoto/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/batoto/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/all/batoto/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/batoto/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/all/batoto/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/batoto/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/all/batoto/res/web_hi_res_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/batoto/res/web_hi_res_512.png
--------------------------------------------------------------------------------
/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToFactory.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.batoto
2 |
3 | import eu.kanade.tachiyomi.source.Source
4 | import eu.kanade.tachiyomi.source.SourceFactory
5 |
6 | class BatoToFactory : SourceFactory {
7 | override fun createSources(): List = languages.map { BatoTo(it.lang, it.siteLang) }
8 | }
9 |
10 | class LanguageOption(val lang: String, val siteLang: String = lang)
11 | private val languages = listOf(
12 | LanguageOption("all", ""),
13 | // Lang options from publish.bato.to
14 | LanguageOption("en"),
15 | LanguageOption("ar"),
16 | LanguageOption("bg"),
17 | LanguageOption("zh"),
18 | LanguageOption("cs"),
19 | LanguageOption("da"),
20 | LanguageOption("nl"),
21 | LanguageOption("fil"),
22 | LanguageOption("fi"),
23 | LanguageOption("fr"),
24 | LanguageOption("de"),
25 | LanguageOption("el"),
26 | LanguageOption("he"),
27 | LanguageOption("hi"),
28 | LanguageOption("hu"),
29 | LanguageOption("id"),
30 | LanguageOption("it"),
31 | LanguageOption("ja"),
32 | LanguageOption("ko"),
33 | LanguageOption("ms"),
34 | LanguageOption("pl"),
35 | LanguageOption("pt"),
36 | LanguageOption("pt-BR", "pt_br"),
37 | LanguageOption("ro"),
38 | LanguageOption("ru"),
39 | LanguageOption("es"),
40 | LanguageOption("es-419", "es_419"),
41 | LanguageOption("sv"),
42 | LanguageOption("th"),
43 | LanguageOption("tr"),
44 | LanguageOption("uk"),
45 | LanguageOption("vi"),
46 | LanguageOption("af"),
47 | LanguageOption("sq"),
48 | LanguageOption("am"),
49 | LanguageOption("hy"),
50 | LanguageOption("az"),
51 | LanguageOption("be"),
52 | LanguageOption("bn"),
53 | LanguageOption("bs"),
54 | LanguageOption("my"),
55 | LanguageOption("km"),
56 | LanguageOption("ca"),
57 | LanguageOption("ceb"),
58 | LanguageOption("zh-Hans", "zh_hk"),
59 | LanguageOption("zh-Hant", "zh_tw"),
60 | LanguageOption("hr"),
61 | LanguageOption("en-US", "en_us"),
62 | LanguageOption("eo"),
63 | LanguageOption("et"),
64 | LanguageOption("fo"),
65 | LanguageOption("ka"),
66 | LanguageOption("gn"),
67 | LanguageOption("gu"),
68 | LanguageOption("ht"),
69 | LanguageOption("ha"),
70 | LanguageOption("is"),
71 | LanguageOption("ig"),
72 | LanguageOption("ga"),
73 | LanguageOption("jv"),
74 | LanguageOption("kn"),
75 | LanguageOption("kk"),
76 | LanguageOption("ku"),
77 | LanguageOption("ky"),
78 | LanguageOption("lo"),
79 | LanguageOption("lv"),
80 | LanguageOption("lt"),
81 | LanguageOption("lb"),
82 | LanguageOption("mk"),
83 | LanguageOption("mg"),
84 | LanguageOption("ml"),
85 | LanguageOption("mt"),
86 | LanguageOption("mi"),
87 | LanguageOption("mr"),
88 | LanguageOption("mo", "ro-MD"),
89 | LanguageOption("mn"),
90 | LanguageOption("ne"),
91 | LanguageOption("no"),
92 | LanguageOption("ny"),
93 | LanguageOption("ps"),
94 | LanguageOption("fa"),
95 | LanguageOption("rm"),
96 | LanguageOption("sm"),
97 | LanguageOption("sr"),
98 | LanguageOption("sh"),
99 | LanguageOption("st"),
100 | LanguageOption("sn"),
101 | LanguageOption("sd"),
102 | LanguageOption("si"),
103 | LanguageOption("sk"),
104 | LanguageOption("sl"),
105 | LanguageOption("so"),
106 | LanguageOption("sw"),
107 | LanguageOption("tg"),
108 | LanguageOption("ta"),
109 | LanguageOption("ti"),
110 | LanguageOption("to"),
111 | LanguageOption("tk"),
112 | LanguageOption("ur"),
113 | LanguageOption("uz"),
114 | LanguageOption("yo"),
115 | LanguageOption("zu"),
116 | LanguageOption("other", "_t"),
117 | // Lang options from bato.to brows not in publish.bato.to
118 | LanguageOption("eu"),
119 | LanguageOption("pt-PT", "pt_pt"),
120 | // Lang options that got removed
121 | // Pair("xh", "xh"),
122 | )
123 |
--------------------------------------------------------------------------------
/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToUrlActivity.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.batoto
2 |
3 | import android.app.Activity
4 | import android.content.ActivityNotFoundException
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import android.util.Log
8 | import kotlin.system.exitProcess
9 |
10 | class BatoToUrlActivity : Activity() {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | val host = intent?.data?.host
15 | val pathSegments = intent?.data?.pathSegments
16 |
17 | if (host != null && pathSegments != null) {
18 | val query = fromBatoTo(pathSegments)
19 |
20 | if (query == null) {
21 | Log.e("BatoToUrlActivity", "Unable to parse URI from intent $intent")
22 | finish()
23 | exitProcess(1)
24 | }
25 |
26 | val mainIntent = Intent().apply {
27 | action = "eu.kanade.tachiyomi.SEARCH"
28 | putExtra("query", query)
29 | putExtra("filter", packageName)
30 | }
31 |
32 | try {
33 | startActivity(mainIntent)
34 | } catch (e: ActivityNotFoundException) {
35 | Log.e("BatoToUrlActivity", e.toString())
36 | }
37 | }
38 |
39 | finish()
40 | exitProcess(0)
41 | }
42 |
43 | private fun fromBatoTo(pathSegments: MutableList): String? {
44 | return if (pathSegments.size >= 2) {
45 | val id = pathSegments[1]
46 | "ID:$id"
47 | } else {
48 | null
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/all/mangadex/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/all/mangadex/README.md:
--------------------------------------------------------------------------------
1 | # MangaDex
2 |
3 | Table of Content
4 | - [FAQ](#FAQ)
5 | - [Version 5 API Rewrite](#version-5-api-rewrite)
6 | - [Guides](#Guides)
7 | - [How can I block particular Scanlator Groups?](#how-can-i-block-particular-scanlator-groups)
8 |
9 | Don't find the question you are look for go check out our general FAQs and Guides over at [Extension FAQ](https://tachiyomi.org/help/faq/#extensions) or [Getting Started](https://tachiyomi.org/help/guides/getting-started/#installation)
10 |
11 | ## FAQ
12 |
13 | ### Version 5 API Rewrite
14 |
15 | #### Why are all my manga saying "Manga ID format has changed, migrate from MangaDex to MangaDex to continue reading"?
16 | You need to [migrate](https://tachiyomi.org/help/guides/source-migration/) all your MangaDex manga from MangaDex to MangaDex as MangaDex has changed their manga ID system from IDs to UUIDs.
17 |
18 | #### Why can I not restore from a JSON backup?
19 | JSON backups are now unusable due to the ID change. You will have to manually re-add your manga.
20 |
21 | ## Guides
22 |
23 | ### What does the Status of a Manga in Tachiyomi mean?
24 |
25 | Please refer to the following table
26 |
27 | | Status in Tachiyomi | in MangaDex | Remarks |
28 | |---------------------|------------------------|---------|
29 | | Ongoing | Publication: Ongoing | |
30 | | Cancelled | Publication: Cancelled | This title was abruptly stopped and will not resume |
31 | | Publishing Finished | Publication: Completed | The title is finished in its original language. However, Translations remain |
32 | | On_Hiatus | Publication: Hiatus | The title is not currently receiving any new chapters |
33 | | Completed | Completed/Cancelled | All chapters are translated and available |
34 | | Unknown | Unknown | There is no info about the Status of this Entry |
35 |
36 | ### How can I block particular Scanlator Groups?
37 |
38 | The **MangaDex** extension allows blocking **Scanlator Groups**. Chapters uploaded by a **Blocked Scanlator Group** will not show up in **Latest** or in **Manga feed** (chapters list). For now, you can only block Groups by entering their UUIDs manually.
39 |
40 | Follow the following steps to easily block a group from the Tachiyomi MangaDex extension:
41 |
42 | A. Finding the **UUIDs**:
43 | - Go to [https://mangadex.org](https://mangadex.org) and **Search** for the Scanlation Group that you wish to block and view their Group Details
44 | - Using the URL of this page, get the 16-digit alphanumeric string which will be the UUID for that scanlation group
45 | - For Example:
46 | * The Group *Tristan's test scans* has the URL
47 | - [https://mangadex.org/group/6410209a-0f39-4f51-a139-bc559ad61a4f/tristan-s-test-scans](https://mangadex.org/group/6410209a-0f39-4f51-a139-bc559ad61a4f/tristan-s-test-scans)
48 | - Therefore, their UUID will be `6410209a-0f39-4f51-a139-bc559ad61a4f`
49 | * Other Examples include:
50 | + Azuki Manga | `5fed0576-8b94-4f9a-b6a7-08eecd69800d`
51 | + Bilibili Comics | `06a9fecb-b608-4f19-b93c-7caab06b7f44`
52 | + Comikey | `8d8ecf83-8d42-4f8c-add8-60963f9f28d9`
53 | + INKR | `caa63201-4a17-4b7f-95ff-ed884a2b7e60`
54 | + MangaHot | `319c1b10-cbd0-4f55-a46e-c4ee17e65139`
55 | + MangaPlus | `4f1de6a2-f0c5-4ac5-bce5-02c7dbb67deb`
56 |
57 | B. Blocking a group using their UUID in Tachiyomi MangaDex extension `v1.2.150+`:
58 | 1. Go to **Browse** → **Extensions**.
59 | 1. Click on **MangaDex** extension and then **Settings** under your Language of choice.
60 | 1. Tap on the option **Block Groups by UUID** and enter the UUIDs.
61 | - By Default, the following groups are blocked:
62 | ```
63 | Azuki Manga, Bilibili Comics, Comikey, INKR, MangaHot & MangaPlus
64 | ```
65 | - Which are entered as:
66 | ```
67 | 5fed0576-8b94-4f9a-b6a7-08eecd69800d, 06a9fecb-b608-4f19-b93c-7caab06b7f44,
68 | 8d8ecf83-8d42-4f8c-add8-60963f9f28d9, caa63201-4a17-4b7f-95ff-ed884a2b7e60,
69 | 319c1b10-cbd0-4f55-a46e-c4ee17e65139, 4f1de6a2-f0c5-4ac5-bce5-02c7dbb67deb
70 | ```
71 |
--------------------------------------------------------------------------------
/src/all/mangadex/assets/i18n/messages_en.properties:
--------------------------------------------------------------------------------
1 | alternative_titles=Alternative titles:
2 | alternative_titles_in_description=Alternative titles in description
3 | alternative_titles_in_description_summary=Include a manga's alternative titles at the end of its description
4 | block_group_by_uuid=Block groups by UUID
5 | block_group_by_uuid_summary=Chapters from blocked groups will not show up in Latest or Manga feed. Enter as a Comma-separated list of group UUIDs
6 | block_uploader_by_uuid=Block uploader by UUID
7 | block_uploader_by_uuid_summary=Chapters from blocked uploaders will not show up in Latest or Manga feed. Enter as a Comma-separated list of uploader UUIDs
8 | content=Content
9 | content_gore=Gore
10 | content_rating=Content rating
11 | content_rating_erotica=Erotica
12 | content_rating_genre=Content rating: %s
13 | content_rating_pornographic=Pornographic
14 | content_rating_safe=Safe
15 | content_rating_suggestive=Suggestive
16 | content_sexual_violence=Sexual violence
17 | cover_quality=Cover quality
18 | cover_quality_low=Low
19 | cover_quality_medium=Medium
20 | cover_quality_original=Original
21 | data_saver=Data saver
22 | data_saver_summary=Enables smaller, more compressed images
23 | excluded_tags_mode=Excluded tags mode
24 | filter_original_languages=Filter original languages
25 | filter_original_languages_summary=Only show content that was originally published in the selected languages in both latest and browse
26 | format=Format
27 | format_adaptation=Adaptation
28 | format_anthology=Anthology
29 | format_award_winning=Award Winning
30 | format_doujinshi=Doujinshi
31 | format_fan_colored=Fan Colored
32 | format_full_color=Full Color
33 | format_long_strip=Long Strip
34 | format_official_colored=Official Colored
35 | format_oneshot=Oneshot
36 | format_user_created=User Created
37 | format_web_comic=Web Comic
38 | format_yonkoma=4-Koma
39 | genre=Genre
40 | genre_action=Action
41 | genre_adventure=Adventure
42 | genre_boys_love=Boy's Love
43 | genre_comedy=Comedy
44 | genre_crime=Crime
45 | genre_drama=Drama
46 | genre_fantasy=Fantasy
47 | genre_girls_love=Girl's Love
48 | genre_historical=Historical
49 | genre_horror=Horror
50 | genre_isekai=Isekai
51 | genre_magical_girls=Magical Girls
52 | genre_mecha=Mecha
53 | genre_medical=Medical
54 | genre_mystery=Mystery
55 | genre_philosophical=Philosophical
56 | genre_romance=Romance
57 | genre_sci_fi=Sci-Fi
58 | genre_slice_of_life=Slice of Life
59 | genre_sports=Sports
60 | genre_superhero=Superhero
61 | genre_thriller=Thriller
62 | genre_tragedy=Tragedy
63 | genre_wuxia=Wuxia
64 | has_available_chapters=Has available chapters
65 | included_tags_mode=Included tags mode
66 | invalid_author_id=Not a valid author ID
67 | invalid_manga_id=Not a valid manga ID
68 | invalid_group_id=Not a valid group ID
69 | invalid_uuids=The text contains invalid UUIDs
70 | migrate_warning=Migrate this entry from MangaDex to MangaDex to update it
71 | mode_and=And
72 | mode_or=Or
73 | no_group=No Group
74 | no_series_in_list=No series in the list
75 | original_language=Original language
76 | original_language_filter_chinese=%s (Manhua)
77 | original_language_filter_japanese=%s (Manga)
78 | original_language_filter_korean=%s (Manhwa)
79 | publication_demographic=Publication demographic
80 | publication_demographic_josei=Josei
81 | publication_demographic_none=None
82 | publication_demographic_seinen=Seinen
83 | publication_demographic_shoujo=Shoujo
84 | publication_demographic_shounen=Shounen
85 | sort=Sort
86 | sort_alphabetic=Alphabetic
87 | sort_chapter_uploaded_at=Chapter uploaded at
88 | sort_content_created_at=Content created at
89 | sort_content_info_updated_at=Content info updated at
90 | sort_number_of_follows=Number of follows
91 | sort_rating=Rating
92 | sort_relevance=Relevance
93 | sort_year=Year
94 | standard_content_rating=Default content rating
95 | standard_content_rating_summary=Show content with the selected ratings by default
96 | standard_https_port=Use HTTPS port 443 only
97 | standard_https_port_summary=Enable to only request image servers that use port 443. This allows users with stricter firewall restrictions to access MangaDex images
98 | status=Status
99 | status_cancelled=Cancelled
100 | status_completed=Completed
101 | status_hiatus=Hiatus
102 | status_ongoing=Ongoing
103 | tags_mode=Tags mode
104 | theme=Theme
105 | theme_aliens=Aliens
106 | theme_animals=Animals
107 | theme_cooking=Cooking
108 | theme_crossdressing=Crossdressing
109 | theme_delinquents=Delinquents
110 | theme_demons=Demons
111 | theme_gender_swap=Genderswap
112 | theme_ghosts=Ghosts
113 | theme_gyaru=Gyaru
114 | theme_harem=Harem
115 | theme_incest=Incest
116 | theme_loli=Loli
117 | theme_mafia=Mafia
118 | theme_magic=Magic
119 | theme_martial_arts=Martial Arts
120 | theme_military=Military
121 | theme_monster_girls=Monster Girls
122 | theme_monsters=Monsters
123 | theme_music=Music
124 | theme_ninja=Ninja
125 | theme_office_workers=Office Workers
126 | theme_police=Police
127 | theme_post_apocalyptic=Post-Apocalyptic
128 | theme_psychological=Psychological
129 | theme_reincarnation=Reincarnation
130 | theme_reverse_harem=Reverse Harem
131 | theme_samurai=Samurai
132 | theme_school_life=School Life
133 | theme_shota=Shota
134 | theme_supernatural=Supernatural
135 | theme_survival=Survival
136 | theme_time_travel=Time Travel
137 | theme_traditional_games=Traditional Games
138 | theme_vampires=Vampires
139 | theme_video_games=Video Games
140 | theme_villainess=Villainess
141 | theme_virtual_reality=Virtual Reality
142 | theme_zombies=Zombies
143 | try_using_first_volume_cover=Attempt to use the first volume cover as cover
144 | try_using_first_volume_cover_summary=May need to manually refresh entries already in library. Otherwise, clear database to have new covers to show up.
145 | unable_to_process_chapter_request=Unable to process Chapter request. HTTP code: %d
146 | uploaded_by=Uploaded by %s
147 | set_custom_useragent=Set custom User-Agent
148 | set_custom_useragent_summary=Keep it as default
149 | set_custom_useragent_dialog=\n\nSpecify a custom user agent\n After each modification, the application needs to be restarted.\n\nDefault value:\n%s
150 | set_custom_useragent_error_invalid=Invalid User-Agent: %s
151 |
--------------------------------------------------------------------------------
/src/all/mangadex/assets/i18n/messages_es.properties:
--------------------------------------------------------------------------------
1 | block_group_by_uuid=Bloquear grupos por UUID
2 | block_group_by_uuid_summary=Los capítulos de los grupos bloqueados no aparecerán en Recientes o en el Feed de mangas. Introduce una coma para separar la lista de UUIDs
3 | block_uploader_by_uuid=Bloquear uploader por UUID
4 | block_uploader_by_uuid_summary=Los capítulos de los uploaders bloqueados no aparecerán en Recientes o en el Feed de mangas. Introduce una coma para separar la lista de UUIDs
5 | content=Contenido
6 | content_rating=Clasificación de contenido
7 | content_rating_erotica=Erótico
8 | content_rating_genre=Clasificación: %s
9 | content_rating_pornographic=Pornográfico
10 | content_rating_safe=Seguro
11 | content_rating_suggestive=Sugestivo
12 | content_sexual_violence=Violencia sexual
13 | cover_quality=Calidad de la portada
14 | cover_quality_low=Bajo
15 | cover_quality_medium=Medio
16 | data_saver=Ahorro de datos
17 | data_saver_summary=Utiliza imágenes más pequeñas y más comprimidas
18 | excluded_tags_mode=Modo de etiquetas excluidas
19 | filter_original_languages=Filtrar por lenguajes
20 | filter_original_languages_summary=Muestra solo el contenido publicado en los idiomas seleccionados en recientes y en la búsqueda
21 | format=Formato
22 | format_adaptation=Adaptación
23 | format_anthology=Antología
24 | format_award_winning=Ganador de premio
25 | format_fan_colored=Coloreado por fans
26 | format_full_color=Todo a color
27 | format_long_strip=Tira larga
28 | format_official_colored=Coloreo oficial
29 | format_user_created=Creado por usuario
30 | genre=Genero
31 | genre_action=Acción
32 | genre_adventure=Aventura
33 | genre_comedy=Comedia
34 | genre_crime=Crimen
35 | genre_fantasy=Fantasia
36 | genre_historical=Histórico
37 | genre_magical_girls=Chicas mágicas
38 | genre_medical=Medico
39 | genre_mystery=Misterio
40 | genre_philosophical=Filosófico
41 | genre_sci_fi=Ciencia ficción
42 | genre_slice_of_life=Recuentos de la vida
43 | genre_sports=Deportes
44 | genre_superhero=Superhéroes
45 | genre_tragedy=Tragedia
46 | has_available_chapters=Tiene capítulos disponibles
47 | included_tags_mode=Modo de etiquetas incluidas
48 | invalid_author_id=ID de autor inválida
49 | invalid_group_id=ID de grupo inválida
50 | migrate_warning=Migre la entrada MangaDex a MangaDex para actualizarla
51 | mode_and=Y
52 | mode_or=O
53 | no_group=Sin grupo
54 | no_series_in_list=No hay series en la lista
55 | original_language=Lenguaje original
56 | publication_demographic=Demografía
57 | publication_demographic_none=Ninguna
58 | sort=Ordenar
59 | sort_alphabetic=Alfabeticamente
60 | sort_chapter_uploaded_at=Capítulo subido en
61 | sort_content_created_at=Contenido creado en
62 | sort_content_info_updated_at=Información del contenido actualizada en
63 | sort_number_of_follows=Número de seguidores
64 | sort_rating=Calificación
65 | sort_relevance=Relevancia
66 | sort_year=Año
67 | standard_content_rating=Clasificación de contenido por defecto
68 | standard_content_rating_summary=Muestra el contenido con la clasificación de contenido seleccionada por defecto
69 | standard_https_port=Utilizar el puerto 443 de HTTPS
70 | standard_https_port_summary=Habilite esta opción solicitar las imágenes a los servidores que usan el puerto 443. Esto permite a los usuarios con restricciones estrictas de firewall acceder a las imagenes en MangaDex
71 | status=Estado
72 | status_cancelled=Cancelado
73 | status_completed=Completado
74 | status_hiatus=Pausado
75 | status_ongoing=Publicandose
76 | tags_mode=Modo de etiquetas
77 | theme=Tema
78 | theme_aliens=Alienígenas
79 | theme_animals=Animales
80 | theme_cooking=Cocina
81 | theme_crossdressing=Travestismo
82 | theme_delinquents=Delincuentes
83 | theme_demons=Demonios
84 | theme_gender_swap=Cambio de sexo
85 | theme_ghosts=Fantasmas
86 | theme_incest=Incesto
87 | theme_magic=Magia
88 | theme_martial_arts=Artes marciales
89 | theme_military=Militar
90 | theme_monster_girls=Chicas monstruo
91 | theme_monsters=Monstruos
92 | theme_music=Musica
93 | theme_office_workers=Oficinistas
94 | theme_police=Policial
95 | theme_post_apocalyptic=Post-apocalíptico
96 | theme_psychological=Psicológico
97 | theme_reincarnation=Reencarnación
98 | theme_reverse_harem=Harem inverso
99 | theme_school_life=Vida escolar
100 | theme_supernatural=Sobrenatural
101 | theme_survival=Supervivencia
102 | theme_time_travel=Viaje en el tiempo
103 | theme_traditional_games=Juegos tradicionales
104 | theme_vampires=Vampiros
105 | theme_villainess=Villana
106 | theme_virtual_reality=Realidad virtual
107 | unable_to_process_chapter_request=No se ha podido procesar la solicitud del capítulo. Código HTTP: %d
108 | uploaded_by=Subido por %s
--------------------------------------------------------------------------------
/src/all/mangadex/assets/i18n/messages_pt_br.properties:
--------------------------------------------------------------------------------
1 | alternative_titles=Títulos alternativos:
2 | alternative_titles_in_description=Títulos alternativos na descrição
3 | alternative_titles_in_description_summary=Inclui os títulos alternativos das séries no final de cada descrição
4 | block_group_by_uuid=Bloquear grupos por UUID
5 | block_group_by_uuid_summary=Capítulos de grupos bloqueados não irão aparecer no feed de Recentes ou Mangás. Digite uma lista de UUIDs dos grupos separados por vírgulas
6 | block_uploader_by_uuid=Bloquear uploaders por UUID
7 | block_uploader_by_uuid_summary=Capítulos de usuários bloqueados não irão aparecer no feed de Recentes ou Mangás. Digite uma lista de UUIDs dos usuários separados por vírgulas
8 | content=Conteúdo
9 | content_rating=Classificação de conteúdo
10 | content_rating_erotica=Erótico
11 | content_rating_genre=Classificação: %s
12 | content_rating_pornographic=Pornográfico
13 | content_rating_safe=Seguro
14 | content_rating_suggestive=Sugestivo
15 | content_sexual_violence=Violência sexual
16 | cover_quality=Qualidade da capa
17 | cover_quality_low=Baixa
18 | cover_quality_medium=Média
19 | data_saver=Economia de dados
20 | data_saver_summary=Utiliza imagens menores e mais compactadas
21 | excluded_tags_mode=Modo de exclusão de tags
22 | filter_original_languages=Filtrar os idiomas originais
23 | filter_original_languages_summary=Mostra somente conteúdos que foram publicados originalmente nos idiomas selecionados nas seções de recentes e navegar
24 | format=Formato
25 | format_adaptation=Adaptação
26 | format_anthology=Antologia
27 | format_award_winning=Premiado
28 | format_fan_colored=Colorizado por fãs
29 | format_full_color=Colorido
30 | format_long_strip=Vertical
31 | format_official_colored=Colorizado oficialmente
32 | format_user_created=Criado por usuários
33 | genre=Gênero
34 | genre_action=Ação
35 | genre_adventure=Aventura
36 | genre_comedy=Comédia
37 | genre_crime=Crime
38 | genre_fantasy=Fantasia
39 | genre_historical=Histórico
40 | genre_magical_girls=Garotas mágicas
41 | genre_medical=Médico
42 | genre_mystery=Mistério
43 | genre_philosophical=Filosófico
44 | genre_sci_fi=Ficção científica
45 | genre_slice_of_life=Cotidiano
46 | genre_sports=Esportes
47 | genre_superhero=Super-heroi
48 | genre_tragedy=Tragédia
49 | has_available_chapters=Há capítulos disponíveis
50 | included_tags_mode=Modo de inclusão de tags
51 | invalid_author_id=ID do autor inválido
52 | invalid_manga_id=ID do mangá inválido
53 | invalid_group_id=ID do grupo inválido
54 | invalid_uuids=O texto contém UUIDs inválidos
55 | migrate_warning=Migre esta entrada do MangaDex para o MangaDex para atualizar
56 | mode_and=E
57 | mode_or=Ou
58 | no_group=Sem grupo
59 | no_series_in_list=Sem séries na lista
60 | original_language=Idioma original
61 | original_language_filter_japanese=%s (Mangá)
62 | publication_demographic=Demografia da publicação
63 | publication_demographic_none=Nenhuma
64 | sort=Ordenar
65 | sort_alphabetic=Alfabeticamente
66 | sort_chapter_uploaded_at=Upload do capítulo
67 | sort_content_created_at=Criação do conteúdo
68 | sort_content_info_updated_at=Atualização das informações
69 | sort_number_of_follows=Número de seguidores
70 | sort_rating=Nota
71 | sort_relevance=Relevância
72 | sort_year=Ano de lançamento
73 | standard_content_rating=Classificação de conteúdo padrão
74 | standard_content_rating_summary=Mostra os conteúdos com as classificações selecionadas por padrão
75 | standard_https_port=Utilizar somente a porta 443 do HTTPS
76 | standard_https_port_summary=Ative para fazer requisições em somente servidores de imagem que usem a porta 443. Isso permite com que usuários com regras mais restritas de firewall possam acessar as imagens do MangaDex.
77 | status=Estado
78 | status_cancelled=Cancelado
79 | status_completed=Completo
80 | status_hiatus=Hiato
81 | status_ongoing=Em andamento
82 | tags_mode=Modo das tags
83 | theme=Tema
84 | theme_aliens=Alienígenas
85 | theme_animals=Animais
86 | theme_cooking=Culinária
87 | theme_delinquents=Delinquentes
88 | theme_demons=Demônios
89 | theme_gender_swap=Troca de gêneros
90 | theme_ghosts=Fantasmas
91 | theme_harem=Harém
92 | theme_incest=Incesto
93 | theme_mafia=Máfia
94 | theme_magic=Magia
95 | theme_martial_arts=Artes marciais
96 | theme_military=Militar
97 | theme_monster_girls=Garotas monstro
98 | theme_monsters=Monstros
99 | theme_music=Musical
100 | theme_office_workers=Funcionários de escritório
101 | theme_police=Policial
102 | theme_post_apocalyptic=Pós-apocalíptico
103 | theme_psychological=Psicológico
104 | theme_reincarnation=Reencarnação
105 | theme_reverse_harem=Harém reverso
106 | theme_school_life=Vida escolar
107 | theme_supernatural=Sobrenatural
108 | theme_survival=Sobrevivência
109 | theme_time_travel=Viagem no tempo
110 | theme_traditional_games=Jogos tradicionais
111 | theme_vampires=Vampiros
112 | theme_video_games=Videojuegos
113 | theme_villainess=Villainess
114 | theme_virtual_reality=Realidade virtual
115 | theme_zombies=Zumbis
116 | try_using_first_volume_cover=Tentar usar a capa do primeiro volume como capa
117 | try_using_first_volume_cover_summary=Pode ser necessário atualizar os itens já adicionados na biblioteca. Alternativamente, limpe o banco de dados para as novas capas aparecerem.
118 | unable_to_process_chapter_request=Não foi possível processar a requisição do capítulo. Código HTTP: %d
119 | uploaded_by=Enviado por %s
--------------------------------------------------------------------------------
/src/all/mangadex/assets/i18n/messages_ru.properties:
--------------------------------------------------------------------------------
1 | block_group_by_uuid=Заблокировать группы по UUID
2 | block_group_by_uuid_summary=Главы от заблокированных групп не будут отображаться в последних обновлениях и в списке глав тайтла. Введите через запятую список UUID групп.
3 | block_uploader_by_uuid=Заблокировать загрузчика по UUID
4 | block_uploader_by_uuid_summary=Главы от заблокированных загрузчиков не будут отображаться в последних обновлениях и в списке глав тайтла. Введите через запятую список UUID загрузчиков.
5 | content=Неприемлемый контент
6 | content_gore=Жестокость
7 | content_rating=Рейтинг контента
8 | content_rating_erotica=Эротический
9 | content_rating_genre=Рейтинг контента: %s
10 | content_rating_pornographic=Порнографический
11 | content_rating_safe=Безопасный
12 | content_rating_suggestive=Намекающий
13 | content_sexual_violence=Сексуальное насилие
14 | cover_quality=Качество обложки
15 | cover_quality_low=Низкое
16 | cover_quality_medium=Среднее
17 | cover_quality_original=Оригинальное
18 | data_saver=Экономия трафика
19 | data_saver_summary=Использует меньшие по размеру, сжатые изображения
20 | excluded_tags_mode=Исключая
21 | filter_original_languages=Фильтр по языку оригинала
22 | filter_original_languages_summary=Показывать тайтлы которые изначально были выпущены только в выбранных языках в последних обновлениях и при поиске
23 | format=Формат
24 | format_adaptation=Адаптация
25 | format_anthology=Антология
26 | format_award_winning=Отмеченный наградами
27 | format_doujinshi=Додзинси
28 | format_fan_colored=Раскрашенная фанатами
29 | format_full_color=В цвете
30 | format_long_strip=Веб
31 | format_official_colored=Официально раскрашенная
32 | format_oneshot=Сингл
33 | format_user_created=Созданная пользователями
34 | format_web_comic=Веб-комикс
35 | format_yonkoma=Ёнкома
36 | genre=Жанр
37 | genre_action=Боевик
38 | genre_adventure=Приключения
39 | genre_boys_love=BL
40 | genre_comedy=Комедия
41 | genre_crime=Криминал
42 | genre_drama=Драма
43 | genre_fantasy=Фэнтези
44 | genre_girls_love=GL
45 | genre_historical=История
46 | genre_horror=Ужасы
47 | genre_isekai=Исекай
48 | genre_magical_girls=Махо-сёдзё
49 | genre_mecha=Меха
50 | genre_medical=Медицина
51 | genre_mystery=Мистика
52 | genre_philosophical=Философия
53 | genre_romance=Романтика
54 | genre_sci_fi=Научная фантастика
55 | genre_slice_of_life=Повседневность
56 | genre_sports=Спорт
57 | genre_superhero=Супергерои
58 | genre_thriller=Триллер
59 | genre_tragedy=Трагедия
60 | genre_wuxia=Культивация
61 | has_available_chapters=Есть главы
62 | included_tags_mode=Включая
63 | invalid_author_id=Недействительный ID автора
64 | invalid_group_id=Недействительный ID группы
65 | mode_and=И
66 | mode_or=Или
67 | no_group=Нет группы
68 | no_series_in_list=Лист пуст
69 | original_language=Язык оригинала
70 | original_language_filter_chinese=%s (Манхуа)
71 | original_language_filter_japanese=%s (Манга)
72 | original_language_filter_korean=%s (Манхва)
73 | publication_demographic=Целевая аудитория
74 | publication_demographic_josei=Дзёсэй
75 | publication_demographic_none=Нет
76 | publication_demographic_seinen=Сэйнэн
77 | publication_demographic_shoujo=Сёдзё
78 | publication_demographic_shounen=Сёнэн
79 | sort=Сортировать по
80 | sort_alphabetic=Алфавиту
81 | sort_chapter_uploaded_at=Загруженной главе
82 | sort_content_created_at=По дате создания
83 | sort_content_info_updated_at=По дате обновления
84 | sort_number_of_follows=Количеству фолловеров
85 | sort_rating=Популярности
86 | sort_relevance=Лучшему соответствию
87 | sort_year=Год
88 | standard_content_rating=Рейтинг контента по умолчанию
89 | standard_content_rating_summary=Показывать контент с выбранным рейтингом по умолчанию
90 | standard_https_port=Использовать только HTTPS порт 443
91 | standard_https_port_summary=Запрашивает изображения только с серверов которые используют порт 443. Это позволяет пользователям со строгими правилами брандмауэра загружать изображения с MangaDex.
92 | status=Статус
93 | status_cancelled=Отменён
94 | status_completed=Завершён
95 | status_hiatus=Приостановлен
96 | status_ongoing=Онгоинг
97 | tags_mode=Режим поиска
98 | theme=Теги
99 | theme_aliens=Инопланетяне
100 | theme_animals=Животные
101 | theme_cooking=Животные
102 | theme_crossdressing=Кроссдрессинг
103 | theme_delinquents=Хулиганы
104 | theme_demons=Демоны
105 | theme_gender_swap=Смена гендера
106 | theme_ghosts=Призраки
107 | theme_gyaru=Гяру
108 | theme_harem=Гарем
109 | theme_incest=Инцест
110 | theme_loli=Лоли
111 | theme_mafia=Мафия
112 | theme_magic=Магия
113 | theme_martial_arts=Боевые исскуства
114 | theme_military=Военные
115 | theme_monster_girls=Монстродевушки
116 | theme_monsters=Монстры
117 | theme_music=Музыка
118 | theme_ninja=Ниндзя
119 | theme_office_workers=Офисные работники
120 | theme_police=Полиция
121 | theme_post_apocalyptic=Постапокалиптика
122 | theme_psychological=Психология
123 | theme_reincarnation=Реинкарнация
124 | theme_reverse_harem=Обратный гарем
125 | theme_samurai=Самураи
126 | theme_school_life=Школа
127 | theme_shota=Шота
128 | theme_supernatural=Сверхъестественное
129 | theme_survival=Выживание
130 | theme_time_travel=Путешествие во времени
131 | theme_traditional_games=Путешествие во времени
132 | theme_vampires=Вампиры
133 | theme_video_games=Видеоигры
134 | theme_villainess=Злодейка
135 | theme_virtual_reality=Виртуальная реальность
136 | theme_zombies=Зомби
137 | unable_to_process_chapter_request=Не удалось обработать ссылку на главу. Ошибка: %d
138 | uploaded_by=Загрузил %s
--------------------------------------------------------------------------------
/src/all/mangadex/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlinx-serialization'
4 |
5 | ext {
6 | extName = 'MangaDex'
7 | pkgNameSuffix = 'all.mangadex'
8 | extClass = '.MangaDexFactory'
9 | extVersionCode = 192
10 | isNsfw = true
11 | }
12 |
13 | dependencies {
14 | implementation(project(":lib-i18n"))
15 | }
16 |
17 | apply from: "$rootDir/common.gradle"
18 |
--------------------------------------------------------------------------------
/src/all/mangadex/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/mangadex/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/all/mangadex/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/mangadex/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/all/mangadex/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/mangadex/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/all/mangadex/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/mangadex/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/all/mangadex/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/mangadex/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/all/mangadex/res/web_hi_res_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/all/mangadex/res/web_hi_res_512.png
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex
2 |
3 | import eu.kanade.tachiyomi.lib.i18n.Intl
4 | import java.text.SimpleDateFormat
5 | import java.util.Locale
6 | import java.util.TimeZone
7 | import kotlin.time.Duration.Companion.minutes
8 |
9 | object MDConstants {
10 |
11 | val uuidRegex =
12 | Regex("[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")
13 |
14 | const val mangaLimit = 20
15 | const val latestChapterLimit = 100
16 |
17 | const val chapter = "chapter"
18 | const val manga = "manga"
19 | const val coverArt = "cover_art"
20 | const val scanlationGroup = "scanlation_group"
21 | const val user = "user"
22 | const val author = "author"
23 | const val artist = "artist"
24 | const val tag = "tag"
25 | const val list = "custom_list"
26 | const val legacyNoGroupId = "00e03853-1b96-4f41-9542-c71b8692033b"
27 |
28 | const val cdnUrl = "https://uploads.mangadex.org"
29 | const val apiUrl = "https://api.mangadex.org"
30 | const val apiMangaUrl = "$apiUrl/manga"
31 | const val apiChapterUrl = "$apiUrl/chapter"
32 | const val apiListUrl = "$apiUrl/list"
33 | const val atHomePostUrl = "https://api.mangadex.network/report"
34 | val whitespaceRegex = "\\s".toRegex()
35 |
36 | val mdAtHomeTokenLifespan = 5.minutes.inWholeMilliseconds
37 |
38 | val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
39 | .apply { timeZone = TimeZone.getTimeZone("UTC") }
40 |
41 | const val prefixIdSearch = "id:"
42 | const val prefixChSearch = "ch:"
43 | const val prefixGrpSearch = "grp:"
44 | const val prefixAuthSearch = "author:"
45 | const val prefixUsrSearch = "usr:"
46 | const val prefixListSearch = "list:"
47 |
48 | private const val coverQualityPref = "thumbnailQuality"
49 |
50 | fun getCoverQualityPreferenceKey(dexLang: String): String {
51 | return "${coverQualityPref}_$dexLang"
52 | }
53 |
54 | fun getCoverQualityPreferenceEntries(intl: Intl) =
55 | arrayOf(intl["cover_quality_original"], intl["cover_quality_medium"], intl["cover_quality_low"])
56 |
57 | fun getCoverQualityPreferenceEntryValues() = arrayOf("", ".512.jpg", ".256.jpg")
58 |
59 | fun getCoverQualityPreferenceDefaultValue() = getCoverQualityPreferenceEntryValues()[0]
60 |
61 | private const val dataSaverPref = "dataSaverV5"
62 |
63 | fun getDataSaverPreferenceKey(dexLang: String): String {
64 | return "${dataSaverPref}_$dexLang"
65 | }
66 |
67 | private const val standardHttpsPortPref = "usePort443"
68 |
69 | fun getStandardHttpsPreferenceKey(dexLang: String): String {
70 | return "${standardHttpsPortPref}_$dexLang"
71 | }
72 |
73 | private const val contentRatingPref = "contentRating"
74 | const val contentRatingPrefValSafe = "safe"
75 | const val contentRatingPrefValSuggestive = "suggestive"
76 | const val contentRatingPrefValErotica = "erotica"
77 | const val contentRatingPrefValPornographic = "pornographic"
78 | val contentRatingPrefDefaults = setOf(contentRatingPrefValSafe, contentRatingPrefValSuggestive)
79 | val allContentRatings = setOf(
80 | contentRatingPrefValSafe,
81 | contentRatingPrefValSuggestive,
82 | contentRatingPrefValErotica,
83 | contentRatingPrefValPornographic,
84 | )
85 |
86 | fun getContentRatingPrefKey(dexLang: String): String {
87 | return "${contentRatingPref}_$dexLang"
88 | }
89 |
90 | private const val originalLanguagePref = "originalLanguage"
91 | const val originalLanguagePrefValJapanese = MangaDexIntl.JAPANESE
92 | const val originalLanguagePrefValChinese = MangaDexIntl.CHINESE
93 | const val originalLanguagePrefValChineseHk = "zh-hk"
94 | const val originalLanguagePrefValKorean = MangaDexIntl.KOREAN
95 | val originalLanguagePrefDefaults = emptySet()
96 |
97 | fun getOriginalLanguagePrefKey(dexLang: String): String {
98 | return "${originalLanguagePref}_$dexLang"
99 | }
100 |
101 | private const val groupAzuki = "5fed0576-8b94-4f9a-b6a7-08eecd69800d"
102 | private const val groupBilibili = "06a9fecb-b608-4f19-b93c-7caab06b7f44"
103 | private const val groupComikey = "8d8ecf83-8d42-4f8c-add8-60963f9f28d9"
104 | private const val groupInkr = "caa63201-4a17-4b7f-95ff-ed884a2b7e60"
105 | private const val groupMangaHot = "319c1b10-cbd0-4f55-a46e-c4ee17e65139"
106 | private const val groupMangaPlus = "4f1de6a2-f0c5-4ac5-bce5-02c7dbb67deb"
107 | val defaultBlockedGroups = setOf(
108 | groupAzuki,
109 | groupBilibili,
110 | groupComikey,
111 | groupInkr,
112 | groupMangaHot,
113 | groupMangaPlus,
114 | )
115 | private const val blockedGroupsPref = "blockedGroups"
116 | fun getBlockedGroupsPrefKey(dexLang: String): String {
117 | return "${blockedGroupsPref}_$dexLang"
118 | }
119 |
120 | private const val blockedUploaderPref = "blockedUploader"
121 | fun getBlockedUploaderPrefKey(dexLang: String): String {
122 | return "${blockedUploaderPref}_$dexLang"
123 | }
124 |
125 | private const val hasSanitizedUuidsPref = "hasSanitizedUuids"
126 | fun getHasSanitizedUuidsPrefKey(dexLang: String): String {
127 | return "${hasSanitizedUuidsPref}_$dexLang"
128 | }
129 |
130 | private const val tryUsingFirstVolumeCoverPref = "tryUsingFirstVolumeCover"
131 | const val tryUsingFirstVolumeCoverDefault = false
132 | fun getTryUsingFirstVolumeCoverPrefKey(dexLang: String): String {
133 | return "${tryUsingFirstVolumeCoverPref}_$dexLang"
134 | }
135 |
136 | private const val altTitlesInDescPref = "altTitlesInDesc"
137 | fun getAltTitlesInDescPrefKey(dexLang: String): String {
138 | return "${altTitlesInDescPref}_$dexLang"
139 | }
140 |
141 | private const val customUserAgentPref = "customUserAgent"
142 | fun getCustomUserAgentPrefKey(dexLang: String): String {
143 | return "${customUserAgentPref}_$dexLang"
144 | }
145 |
146 | val defaultUserAgent = "Tachiyomi " + System.getProperty("http.agent")
147 |
148 | private const val tagGroupContent = "content"
149 | private const val tagGroupFormat = "format"
150 | private const val tagGroupGenre = "genre"
151 | private const val tagGroupTheme = "theme"
152 | val tagGroupsOrder = arrayOf(tagGroupContent, tagGroupFormat, tagGroupGenre, tagGroupTheme)
153 |
154 | const val tagAnthologyUuid = "51d83883-4103-437c-b4b1-731cb73d786c"
155 | const val tagOneShotUuid = "0234a31e-a729-4e28-9d6a-3f87c4966b9e"
156 |
157 | val romanizedLangCodes = mapOf(
158 | MangaDexIntl.JAPANESE to "ja-ro",
159 | MangaDexIntl.KOREAN to "ko-ro",
160 | MangaDexIntl.CHINESE to "zh-ro",
161 | "zh-hk" to "zh-ro",
162 | )
163 | }
164 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex
2 |
3 | import eu.kanade.tachiyomi.source.Source
4 | import eu.kanade.tachiyomi.source.SourceFactory
5 |
6 | class MangaDexFactory : SourceFactory {
7 | override fun createSources(): List = listOf(
8 | MangaDexEnglish(),
9 | MangaDexAlbanian(),
10 | MangaDexArabic(),
11 | MangaDexAzerbaijani(),
12 | MangaDexBengali(),
13 | MangaDexBulgarian(),
14 | MangaDexBurmese(),
15 | MangaDexCatalan(),
16 | MangaDexChineseSimplified(),
17 | MangaDexChineseTraditional(),
18 | MangaDexCroatian(),
19 | MangaDexCzech(),
20 | MangaDexDanish(),
21 | MangaDexDutch(),
22 | MangaDexEsperanto(),
23 | MangaDexEstonian(),
24 | MangaDexFilipino(),
25 | MangaDexFinnish(),
26 | MangaDexFrench(),
27 | MangaDexGeorgian(),
28 | MangaDexGerman(),
29 | MangaDexGreek(),
30 | MangaDexHebrew(),
31 | MangaDexHindi(),
32 | MangaDexHungarian(),
33 | MangaDexIndonesian(),
34 | MangaDexItalian(),
35 | MangaDexJapanese(),
36 | MangaDexKazakh(),
37 | MangaDexKorean(),
38 | MangaDexLatin(),
39 | MangaDexLithuanian(),
40 | MangaDexMalay(),
41 | MangaDexMongolian(),
42 | MangaDexNepali(),
43 | MangaDexNorwegian(),
44 | MangaDexPersian(),
45 | MangaDexPolish(),
46 | MangaDexPortugueseBrazil(),
47 | MangaDexPortuguesePortugal(),
48 | MangaDexRomanian(),
49 | MangaDexRussian(),
50 | MangaDexSerbian(),
51 | MangaDexSlovak(),
52 | MangaDexSpanishLatinAmerica(),
53 | MangaDexSpanishSpain(),
54 | MangaDexSwedish(),
55 | MangaDexTamil(),
56 | MangaDexTelugu(),
57 | MangaDexThai(),
58 | MangaDexTurkish(),
59 | MangaDexUkrainian(),
60 | MangaDexVietnamese(),
61 | )
62 | }
63 |
64 | class MangaDexAlbanian : MangaDex("sq")
65 | class MangaDexArabic : MangaDex("ar")
66 | class MangaDexAzerbaijani : MangaDex("az")
67 | class MangaDexBengali : MangaDex("bn")
68 | class MangaDexBulgarian : MangaDex("bg")
69 | class MangaDexBurmese : MangaDex("my")
70 | class MangaDexCatalan : MangaDex("ca")
71 | class MangaDexChineseSimplified : MangaDex("zh-Hans", "zh")
72 | class MangaDexChineseTraditional : MangaDex("zh-Hant", "zh-hk")
73 | class MangaDexCroatian : MangaDex("hr")
74 | class MangaDexCzech : MangaDex("cs")
75 | class MangaDexDanish : MangaDex("da")
76 | class MangaDexDutch : MangaDex("nl")
77 | class MangaDexEnglish : MangaDex("en")
78 | class MangaDexEsperanto : MangaDex("eo")
79 | class MangaDexEstonian : MangaDex("et")
80 | class MangaDexFilipino : MangaDex("fil", "tl")
81 | class MangaDexFinnish : MangaDex("fi")
82 | class MangaDexFrench : MangaDex("fr")
83 | class MangaDexGeorgian : MangaDex("ka")
84 | class MangaDexGerman : MangaDex("de")
85 | class MangaDexGreek : MangaDex("el")
86 | class MangaDexHebrew : MangaDex("he")
87 | class MangaDexHindi : MangaDex("hi")
88 | class MangaDexHungarian : MangaDex("hu")
89 | class MangaDexIndonesian : MangaDex("id")
90 | class MangaDexItalian : MangaDex("it")
91 | class MangaDexJapanese : MangaDex("ja")
92 | class MangaDexKazakh : MangaDex("kk")
93 | class MangaDexKorean : MangaDex("ko")
94 | class MangaDexLatin : MangaDex("la")
95 | class MangaDexLithuanian : MangaDex("lt")
96 | class MangaDexMalay : MangaDex("ms")
97 | class MangaDexMongolian : MangaDex("mn")
98 | class MangaDexNepali : MangaDex("ne")
99 | class MangaDexNorwegian : MangaDex("no")
100 | class MangaDexPersian : MangaDex("fa")
101 | class MangaDexPolish : MangaDex("pl")
102 | class MangaDexPortugueseBrazil : MangaDex("pt-BR", "pt-br")
103 | class MangaDexPortuguesePortugal : MangaDex("pt")
104 | class MangaDexRomanian : MangaDex("ro")
105 | class MangaDexRussian : MangaDex("ru")
106 | class MangaDexSerbian : MangaDex("sr")
107 | class MangaDexSlovak : MangaDex("sk")
108 | class MangaDexSpanishLatinAmerica : MangaDex("es-419", "es-la")
109 | class MangaDexSpanishSpain : MangaDex("es")
110 | class MangaDexSwedish : MangaDex("sv")
111 | class MangaDexTamil : MangaDex("ta")
112 | class MangaDexTelugu : MangaDex("te")
113 | class MangaDexThai : MangaDex("th")
114 | class MangaDexTurkish : MangaDex("tr")
115 | class MangaDexUkrainian : MangaDex("uk")
116 | class MangaDexVietnamese : MangaDex("vi")
117 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexIntl.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex
2 |
3 | object MangaDexIntl {
4 | const val BRAZILIAN_PORTUGUESE = "pt-BR"
5 | const val CHINESE = "zh"
6 | const val ENGLISH = "en"
7 | const val JAPANESE = "ja"
8 | const val KOREAN = "ko"
9 | const val PORTUGUESE = "pt"
10 | const val SPANISH_LATAM = "es-419"
11 | const val SPANISH = "es"
12 | const val RUSSIAN = "ru"
13 |
14 | val AVAILABLE_LANGS = setOf(
15 | ENGLISH,
16 | BRAZILIAN_PORTUGUESE,
17 | PORTUGUESE,
18 | SPANISH,
19 | SPANISH_LATAM,
20 | RUSSIAN,
21 | )
22 |
23 | const val MANGADEX_NAME = "MangaDex"
24 | }
25 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangadexUrlActivity.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex
2 |
3 | import android.app.Activity
4 | import android.content.ActivityNotFoundException
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import android.util.Log
8 | import kotlin.system.exitProcess
9 |
10 | /**
11 | * Springboard that accepts https://mangadex.com/title/xxx intents and redirects them to
12 | * the main tachiyomi process. The idea is to not install the intent filter unless
13 | * you have this extension installed, but still let the main tachiyomi app control
14 | * things.
15 | *
16 | * Main goal was to make it easier to open manga in Tachiyomi in spite of the DDoS blocking
17 | * the usual search screen from working.
18 | */
19 | class MangadexUrlActivity : Activity() {
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 | val pathSegments = intent?.data?.pathSegments
24 | if (pathSegments != null && pathSegments.size > 1) {
25 | val titleId = pathSegments[1]
26 | val mainIntent = Intent().apply {
27 | action = "eu.kanade.tachiyomi.SEARCH"
28 | with(pathSegments[0]) {
29 | when {
30 | equals("chapter") -> putExtra("query", MDConstants.prefixChSearch + titleId)
31 | equals("group") -> putExtra("query", MDConstants.prefixGrpSearch + titleId)
32 | equals("user") -> putExtra("query", MDConstants.prefixUsrSearch + titleId)
33 | equals("author") -> putExtra("query", MDConstants.prefixAuthSearch + titleId)
34 | equals("list") -> putExtra("query", MDConstants.prefixListSearch + titleId)
35 | else -> putExtra("query", MDConstants.prefixIdSearch + titleId)
36 | }
37 | }
38 | putExtra("filter", packageName)
39 | }
40 |
41 | try {
42 | startActivity(mainIntent)
43 | } catch (e: ActivityNotFoundException) {
44 | Log.e("MangadexUrlActivity", e.toString())
45 | }
46 | } else {
47 | Log.e("MangadexUrlActivity", "Could not parse URI from intent $intent")
48 | }
49 |
50 | finish()
51 | exitProcess(0)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MdAtHomeReportInterceptor.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex
2 |
3 | import android.util.Log
4 | import eu.kanade.tachiyomi.extension.all.mangadex.dto.ImageReportDto
5 | import eu.kanade.tachiyomi.network.POST
6 | import kotlinx.serialization.encodeToString
7 | import kotlinx.serialization.json.Json
8 | import okhttp3.Call
9 | import okhttp3.Callback
10 | import okhttp3.Headers
11 | import okhttp3.HttpUrl.Companion.toHttpUrl
12 | import okhttp3.Interceptor
13 | import okhttp3.MediaType.Companion.toMediaType
14 | import okhttp3.OkHttpClient
15 | import okhttp3.Request
16 | import okhttp3.RequestBody.Companion.toRequestBody
17 | import okhttp3.Response
18 | import uy.kohesive.injekt.injectLazy
19 |
20 | /**
21 | * Interceptor to post to MD@Home for MangaDex Stats
22 | */
23 | class MdAtHomeReportInterceptor(
24 | private val client: OkHttpClient,
25 | private val headers: Headers,
26 | ) : Interceptor {
27 |
28 | private val json: Json by injectLazy()
29 |
30 | override fun intercept(chain: Interceptor.Chain): Response {
31 | val originalRequest = chain.request()
32 | val response = chain.proceed(chain.request())
33 | val url = originalRequest.url.toString()
34 |
35 | if (!url.contains(MD_AT_HOME_URL_REGEX)) {
36 | return response
37 | }
38 |
39 | Log.e("MangaDex", "Connecting to MD@Home node at $url")
40 |
41 | val reportRequest = mdAtHomeReportRequest(response)
42 |
43 | // Execute the report endpoint network call asynchronously to avoid blocking
44 | // the reader from showing the image once it's fully loaded if the report call
45 | // gets stuck, as it tend to happens sometimes.
46 | client.newCall(reportRequest).enqueue(REPORT_CALLBACK)
47 |
48 | if (response.isSuccessful) {
49 | return response
50 | }
51 |
52 | response.close()
53 |
54 | Log.e("MangaDex", "Error connecting to MD@Home node, fallback to uploads server")
55 |
56 | val imagePath = originalRequest.url.pathSegments
57 | .dropWhile { it != "data" && it != "data-saver" }
58 | .joinToString("/")
59 |
60 | val fallbackUrl = MDConstants.cdnUrl.toHttpUrl().newBuilder()
61 | .addPathSegments(imagePath)
62 | .build()
63 |
64 | val fallbackRequest = originalRequest.newBuilder()
65 | .url(fallbackUrl)
66 | .headers(headers)
67 | .build()
68 |
69 | return chain.proceed(fallbackRequest)
70 | }
71 |
72 | private fun mdAtHomeReportRequest(response: Response): Request {
73 | val result = ImageReportDto(
74 | url = response.request.url.toString(),
75 | success = response.isSuccessful,
76 | bytes = response.peekBody(Long.MAX_VALUE).bytes().size,
77 | cached = response.headers["X-Cache"] == "HIT",
78 | duration = response.receivedResponseAtMillis - response.sentRequestAtMillis,
79 | )
80 |
81 | val payload = json.encodeToString(result)
82 |
83 | return POST(
84 | url = MDConstants.atHomePostUrl,
85 | headers = headers,
86 | body = payload.toRequestBody(JSON_MEDIA_TYPE),
87 | )
88 | }
89 |
90 | companion object {
91 | private val JSON_MEDIA_TYPE = "application/json".toMediaType()
92 | private val MD_AT_HOME_URL_REGEX =
93 | """^https://[\w\d]+\.[\w\d]+\.mangadex(\b-test\b)?\.network.*${'$'}""".toRegex()
94 |
95 | private val REPORT_CALLBACK = object : Callback {
96 | override fun onFailure(call: Call, e: okio.IOException) {
97 | Log.e("MangaDex", "Error trying to POST report to MD@Home: ${e.message}")
98 | }
99 |
100 | override fun onResponse(call: Call, response: Response) {
101 | if (!response.isSuccessful) {
102 | Log.e("MangaDex", "Error trying to POST report to MD@Home: HTTP error ${response.code}")
103 | }
104 |
105 | response.close()
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MdUserAgentInterceptor.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex
2 |
3 | import android.content.SharedPreferences
4 | import okhttp3.Interceptor
5 | import okhttp3.Response
6 | import java.io.IOException
7 |
8 | /**
9 | * Interceptor to set custom useragent for MangaDex
10 | */
11 | class MdUserAgentInterceptor(
12 | private val preferences: SharedPreferences,
13 | private val dexLang: String,
14 | ) : Interceptor {
15 |
16 | private val SharedPreferences.customUserAgent
17 | get() = getString(
18 | MDConstants.getCustomUserAgentPrefKey(dexLang),
19 | MDConstants.defaultUserAgent,
20 | )
21 |
22 | override fun intercept(chain: Interceptor.Chain): Response {
23 | val originalRequest = chain.request()
24 |
25 | val newUserAgent = preferences.customUserAgent
26 | ?: return chain.proceed(originalRequest)
27 |
28 | val originalHeaders = originalRequest.headers
29 |
30 | val modifiedHeaders = originalHeaders.newBuilder()
31 | .set("User-Agent", newUserAgent)
32 | .build()
33 |
34 | val modifiedRequest = originalRequest.newBuilder()
35 | .headers(modifiedHeaders)
36 | .build()
37 |
38 | return chain.proceed(modifiedRequest)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/AggregateDto.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex.dto
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class AggregateDto(
7 | val result: String,
8 | val volumes: Map?,
9 | )
10 |
11 | @Serializable
12 | data class AggregateVolume(
13 | val volume: String,
14 | val count: String,
15 | val chapters: Map,
16 | )
17 |
18 | @Serializable
19 | data class AggregateChapter(
20 | val chapter: String,
21 | val count: String,
22 | )
23 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/AtHomeDto.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex.dto
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class AtHomeDto(
7 | val baseUrl: String,
8 | val chapter: AtHomeChapterDto,
9 | )
10 |
11 | @Serializable
12 | data class AtHomeChapterDto(
13 | val hash: String,
14 | val data: List,
15 | val dataSaver: List,
16 | )
17 |
18 | @Serializable
19 | data class ImageReportDto(
20 | val url: String,
21 | val success: Boolean,
22 | val bytes: Int?,
23 | val cached: Boolean,
24 | val duration: Long,
25 | )
26 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/AuthorDto.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex.dto
2 |
3 | import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | @SerialName(MDConstants.author)
9 | data class AuthorDto(override val attributes: AuthorArtistAttributesDto? = null) : EntityDto()
10 |
11 | @Serializable
12 | @SerialName(MDConstants.artist)
13 | data class ArtistDto(override val attributes: AuthorArtistAttributesDto? = null) : EntityDto()
14 |
15 | @Serializable
16 | data class AuthorArtistAttributesDto(val name: String) : AttributesDto()
17 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ChapterDto.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex.dto
2 |
3 | import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | typealias ChapterListDto = PaginatedResponseDto
8 |
9 | typealias ChapterDto = ResponseDto
10 |
11 | @Serializable
12 | @SerialName(MDConstants.chapter)
13 | data class ChapterDataDto(override val attributes: ChapterAttributesDto? = null) : EntityDto()
14 |
15 | @Serializable
16 | data class ChapterAttributesDto(
17 | val title: String?,
18 | val volume: String?,
19 | val chapter: String?,
20 | val pages: Int,
21 | val publishAt: String,
22 | val externalUrl: String?,
23 | ) : AttributesDto() {
24 |
25 | /**
26 | * Returns true if the chapter is from an external website and have no pages.
27 | */
28 | val isInvalid: Boolean
29 | get() = externalUrl != null && pages == 0
30 | }
31 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/CoverArtDto.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex.dto
2 |
3 | import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | typealias CoverArtListDto = PaginatedResponseDto
8 |
9 | @Serializable
10 | @SerialName(MDConstants.coverArt)
11 | data class CoverArtDto(override val attributes: CoverArtAttributesDto? = null) : EntityDto()
12 |
13 | @Serializable
14 | data class CoverArtAttributesDto(
15 | val fileName: String? = null,
16 | val locale: String? = null,
17 | ) : AttributesDto()
18 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/EntityDto.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex.dto
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | abstract class EntityDto {
7 | val id: String = ""
8 | val relationships: List = emptyList()
9 | abstract val attributes: AttributesDto?
10 | }
11 |
12 | @Serializable
13 | abstract class AttributesDto
14 |
15 | @Serializable
16 | data class UnknownEntity(override val attributes: AttributesDto? = null) : EntityDto()
17 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ListDto.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex.dto
2 |
3 | import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | typealias ListDto = ResponseDto
8 |
9 | @Serializable
10 | @SerialName(MDConstants.list)
11 | data class ListDataDto(override val attributes: ListAttributesDto? = null) : EntityDto()
12 |
13 | @Serializable
14 | data class ListAttributesDto(
15 | val name: String,
16 | val visibility: String,
17 | val version: Int,
18 | ) : AttributesDto()
19 |
--------------------------------------------------------------------------------
/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/MangaDto.kt:
--------------------------------------------------------------------------------
1 | package eu.kanade.tachiyomi.extension.all.mangadex.dto
2 |
3 | import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
4 | import kotlinx.serialization.KSerializer
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 | import kotlinx.serialization.descriptors.buildClassSerialDescriptor
8 | import kotlinx.serialization.encoding.Decoder
9 | import kotlinx.serialization.encoding.Encoder
10 | import kotlinx.serialization.json.JsonDecoder
11 | import kotlinx.serialization.json.JsonObject
12 | import kotlinx.serialization.json.contentOrNull
13 | import kotlinx.serialization.json.jsonPrimitive
14 | import kotlinx.serialization.serializer
15 |
16 | typealias MangaListDto = PaginatedResponseDto
17 |
18 | typealias MangaDto = ResponseDto
19 |
20 | @Serializable
21 | @SerialName(MDConstants.manga)
22 | data class MangaDataDto(override val attributes: MangaAttributesDto? = null) : EntityDto()
23 |
24 | @Serializable
25 | data class MangaAttributesDto(
26 | val title: LocalizedString,
27 | val altTitles: List,
28 | val description: LocalizedString,
29 | val originalLanguage: String?,
30 | val lastVolume: String?,
31 | val lastChapter: String?,
32 | val contentRating: ContentRatingDto? = null,
33 | val publicationDemographic: PublicationDemographicDto? = null,
34 | val status: StatusDto? = null,
35 | val tags: List,
36 | ) : AttributesDto()
37 |
38 | @Serializable
39 | enum class ContentRatingDto(val value: String) {
40 | @SerialName("safe")
41 | SAFE("safe"),
42 |
43 | @SerialName("suggestive")
44 | SUGGESTIVE("suggestive"),
45 |
46 | @SerialName("erotica")
47 | EROTICA("erotica"),
48 |
49 | @SerialName("pornographic")
50 | PORNOGRAPHIC("pornographic"),
51 | }
52 |
53 | @Serializable
54 | enum class PublicationDemographicDto(val value: String) {
55 | @SerialName("none")
56 | NONE("none"),
57 |
58 | @SerialName("shounen")
59 | SHOUNEN("shounen"),
60 |
61 | @SerialName("shoujo")
62 | SHOUJO("shoujo"),
63 |
64 | @SerialName("josei")
65 | JOSEI("josei"),
66 |
67 | @SerialName("seinen")
68 | SEINEN("seinen"),
69 | }
70 |
71 | @Serializable
72 | enum class StatusDto(val value: String) {
73 | @SerialName("ongoing")
74 | ONGOING("ongoing"),
75 |
76 | @SerialName("completed")
77 | COMPLETED("completed"),
78 |
79 | @SerialName("hiatus")
80 | HIATUS("hiatus"),
81 |
82 | @SerialName("cancelled")
83 | CANCELLED("cancelled"),
84 | }
85 |
86 | @Serializable
87 | @SerialName(MDConstants.tag)
88 | data class TagDto(override val attributes: TagAttributesDto? = null) : EntityDto()
89 |
90 | @Serializable
91 | data class TagAttributesDto(val group: String) : AttributesDto()
92 |
93 | typealias LocalizedString = @Serializable(LocalizedStringSerializer::class)
94 | Map
95 |
96 | /**
97 | * Temporary workaround while Dex API still returns arrays instead of objects
98 | * in the places that uses [LocalizedString].
99 | */
100 | object LocalizedStringSerializer : KSerializer