├── .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 | 12 | 13 | 14 | 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 | 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 | 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> { 101 | override val descriptor = buildClassSerialDescriptor("LocalizedString") 102 | 103 | override fun deserialize(decoder: Decoder): Map { 104 | require(decoder is JsonDecoder) 105 | 106 | return (decoder.decodeJsonElement() as? JsonObject) 107 | ?.mapValues { it.value.jsonPrimitive.contentOrNull ?: "" } 108 | .orEmpty() 109 | } 110 | 111 | override fun serialize(encoder: Encoder, value: Map) { 112 | encoder.encodeSerializableValue(serializer(), value) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ResponseDto.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.extension.all.mangadex.dto 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class PaginatedResponseDto( 7 | val result: String, 8 | val response: String = "", 9 | val data: List = emptyList(), 10 | val limit: Int = 0, 11 | val offset: Int = 0, 12 | val total: Int = 0, 13 | ) { 14 | 15 | val hasNextPage: Boolean 16 | get() = limit + offset < total 17 | } 18 | 19 | @Serializable 20 | data class ResponseDto( 21 | val result: String, 22 | val response: String = "", 23 | val data: T? = null, 24 | ) 25 | -------------------------------------------------------------------------------- /src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ScanlationGroupDto.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.scanlationGroup) 9 | data class ScanlationGroupDto(override val attributes: ScanlationGroupAttributes? = null) : EntityDto() 10 | 11 | @Serializable 12 | data class ScanlationGroupAttributes(val name: String) : AttributesDto() 13 | -------------------------------------------------------------------------------- /src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/UserDto.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.user) 9 | data class UserDto(override val attributes: UserAttributes? = null) : EntityDto() 10 | 11 | @Serializable 12 | data class UserAttributes(val username: String) : AttributesDto() 13 | -------------------------------------------------------------------------------- /src/en/reaperscans/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/en/reaperscans/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlinx-serialization' 4 | 5 | ext { 6 | extName = 'Reaper Scans' 7 | pkgNameSuffix = 'en.reaperscans' 8 | extClass = '.ReaperScans' 9 | extVersionCode = 47 10 | } 11 | 12 | apply from: "$rootDir/common.gradle" 13 | -------------------------------------------------------------------------------- /src/en/reaperscans/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/en/reaperscans/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/en/reaperscans/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/en/reaperscans/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/en/reaperscans/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/en/reaperscans/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/en/reaperscans/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/en/reaperscans/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/en/reaperscans/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/en/reaperscans/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/en/reaperscans/res/web_hi_res_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/en/reaperscans/res/web_hi_res_512.png -------------------------------------------------------------------------------- /src/en/reaperscans/src/eu/kanade/tachiyomi/extension/en/reaperscans/ReaperScansDto.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.extension.en.reaperscans 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonObject 5 | 6 | @Serializable 7 | data class LiveWireResponseDto( 8 | val effects: LiveWireEffectsDto, 9 | val serverMemo: JsonObject, 10 | ) 11 | 12 | @Serializable 13 | data class LiveWireEffectsDto( 14 | val html: String, 15 | ) 16 | 17 | @Serializable 18 | data class LiveWireDataDto( 19 | val fingerprint: JsonObject, 20 | val serverMemo: JsonObject, 21 | ) 22 | -------------------------------------------------------------------------------- /src/en/reaperscans/src/eu/kanade/tachiyomi/extension/en/reaperscans/ReaperScansUrlActivity.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.extension.en.reaperscans 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 ReaperScansUrlActivity : Activity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | val pathSegments = intent?.data?.pathSegments 14 | if (pathSegments != null && pathSegments.size >= 2) { 15 | val id = pathSegments[1] 16 | val mainIntent = Intent().apply { 17 | action = "eu.kanade.tachiyomi.SEARCH" 18 | putExtra("query", ReaperScans.PREFIX_ID_SEARCH + id) 19 | putExtra("filter", packageName) 20 | } 21 | 22 | try { 23 | startActivity(mainIntent) 24 | } catch (e: ActivityNotFoundException) { 25 | Log.e("ReaperScansUrlActivity", e.toString()) 26 | } 27 | } else { 28 | Log.e("ReaperScansUrlActivity", "could not parse uri from intent $intent") 29 | } 30 | 31 | finish() 32 | exitProcess(0) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ko/newtoki/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/ko/newtoki/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | ext { 5 | extName = 'NewToki / ManaToki' 6 | pkgNameSuffix = 'ko.newtoki' 7 | extClass = '.TokiFactory' 8 | extVersionCode = 29 9 | isNsfw = true 10 | } 11 | 12 | apply from: "$rootDir/common.gradle" 13 | 14 | def domainNumberFileName = "src/ko/newtoki/src/eu/kanade/tachiyomi/extension/ko/newtoki/FallbackDomainNumber.kt" 15 | def domainNumberFile = new File(domainNumberFileName) 16 | def backupFile = new File(domainNumberFileName + "_bak") 17 | 18 | task updateDomainNumber { 19 | doLast { 20 | def domainNumberUrl = "https://stevenyomi.github.io/source-domains/newtoki.txt" 21 | def number = new URL(domainNumberUrl).withInputStream { it.readLines()[0] } 22 | println("[NewToki] Updating domain number to $number") 23 | domainNumberFile.renameTo(backupFile) 24 | domainNumberFile.withPrintWriter { 25 | it.println("// THIS FILE IS AUTO-GENERATED, DO NOT COMMIT") 26 | it.println("package eu.kanade.tachiyomi.extension.ko.newtoki") 27 | it.println("const val fallbackDomainNumber = \"$number\"") 28 | } 29 | } 30 | } 31 | 32 | preBuild.dependsOn updateDomainNumber 33 | 34 | task restoreBackup { 35 | doLast { 36 | if (backupFile.exists()) { 37 | println("[NewToki] Restoring placeholder file") 38 | domainNumberFile.delete() 39 | backupFile.renameTo(domainNumberFile) 40 | } 41 | } 42 | } 43 | 44 | tasks.whenTaskAdded { task -> 45 | if (task.name == "assembleDebug" || task.name == "assembleRelease") { 46 | task.finalizedBy(restoreBackup) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ko/newtoki/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/ko/newtoki/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/ko/newtoki/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/ko/newtoki/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/ko/newtoki/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/ko/newtoki/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/ko/newtoki/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/ko/newtoki/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/ko/newtoki/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/ko/newtoki/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/ko/newtoki/res/web_hi_res_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaciin/tachiyomi-extensions-source/818553d9598a055ccb6755ccea768de0ca5dfcb5/src/ko/newtoki/res/web_hi_res_512.png -------------------------------------------------------------------------------- /src/ko/newtoki/src/eu/kanade/tachiyomi/extension/ko/newtoki/DomainNumber.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.extension.ko.newtoki 2 | 3 | import android.util.Log 4 | import eu.kanade.tachiyomi.network.GET 5 | import okhttp3.Interceptor 6 | import okhttp3.Response 7 | import java.io.IOException 8 | 9 | var domainNumber = "" 10 | get() { 11 | val currentValue = field 12 | if (currentValue.isNotEmpty()) return currentValue 13 | 14 | val prefValue = newTokiPreferences.domainNumber 15 | if (prefValue.isNotEmpty()) { 16 | field = prefValue 17 | return prefValue 18 | } 19 | 20 | val fallback = fallbackDomainNumber 21 | domainNumber = fallback 22 | return fallback 23 | } 24 | set(value) { 25 | for (preference in arrayOf(manaTokiPreferences, newTokiPreferences)) { 26 | preference.domainNumber = value 27 | } 28 | field = value 29 | } 30 | 31 | object DomainInterceptor : Interceptor { 32 | override fun intercept(chain: Interceptor.Chain): Response { 33 | val request = chain.request() 34 | 35 | val response = try { 36 | chain.proceed(request) 37 | } catch (e: IOException) { 38 | if (chain.call().isCanceled()) throw e 39 | Log.e("NewToki", "failed to fetch ${request.url}", e) 40 | 41 | val newDomainNumber = try { 42 | val domainNumberUrl = "https://stevenyomi.github.io/source-domains/newtoki.txt" 43 | chain.proceed(GET(domainNumberUrl)).body.string().also { it.toInt() } 44 | } catch (_: Throwable) { 45 | throw IOException(editDomainNumber(), e) 46 | } 47 | domainNumber = newDomainNumber 48 | 49 | val url = request.url 50 | val newHost = numberRegex.replaceFirst(url.host, newDomainNumber) 51 | val newUrl = url.newBuilder().host(newHost).build() 52 | try { 53 | chain.proceed(request.newBuilder().url(newUrl).build()) 54 | } catch (e: IOException) { 55 | Log.e("NewToki", "failed to fetch $newUrl", e) 56 | throw IOException(editDomainNumber(), e) 57 | } 58 | } 59 | 60 | if (response.priorResponse == null) return response 61 | 62 | val newUrl = response.request.url 63 | if ("captcha" in newUrl.toString()) throw IOException(solveCaptcha()) 64 | 65 | val newHost = newUrl.host 66 | if (newHost.startsWith(MANATOKI_PREFIX) || newHost.startsWith(NEWTOKI_PREFIX)) { 67 | numberRegex.find(newHost)?.run { domainNumber = value } 68 | } 69 | return response 70 | } 71 | 72 | private val numberRegex by lazy { Regex("""\d+|$fallbackDomainNumber""") } 73 | } 74 | -------------------------------------------------------------------------------- /src/ko/newtoki/src/eu/kanade/tachiyomi/extension/ko/newtoki/FallbackDomainNumber.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.extension.ko.newtoki 2 | 3 | /** 4 | * This value will be automatically overwritten when building the extension. 5 | * After building, this file will be restored. 6 | * 7 | * Even if this value is built into the extension, the network call will fail 8 | * because of underscore character and the extension will update it on its own. 9 | */ 10 | const val fallbackDomainNumber = "_failed_to_fetch_domain_number" 11 | -------------------------------------------------------------------------------- /src/ko/newtoki/src/eu/kanade/tachiyomi/extension/ko/newtoki/ManaToki.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.extension.ko.newtoki 2 | 3 | import eu.kanade.tachiyomi.network.GET 4 | import eu.kanade.tachiyomi.source.model.Filter 5 | import eu.kanade.tachiyomi.source.model.FilterList 6 | import eu.kanade.tachiyomi.source.model.SManga 7 | import okhttp3.HttpUrl.Companion.toHttpUrl 8 | import okhttp3.Request 9 | import org.jsoup.nodes.Element 10 | 11 | /* 12 | * ManaToki is too big to support in a Factory File., So split into separate file. 13 | */ 14 | 15 | object ManaToki : NewToki("ManaToki", "comic", manaTokiPreferences) { 16 | // / ! DO NOT CHANGE THIS ! Only the site name changed from newtoki. 17 | override val id = MANATOKI_ID 18 | 19 | override val baseUrl get() = "https://$MANATOKI_PREFIX$domainNumber.net" 20 | 21 | private val chapterRegex by lazy { Regex(""" [ \d,~.-]+화$""") } 22 | 23 | fun latestUpdatesElementParse(element: Element): SManga { 24 | val linkElement = element.select("a.btn-primary") 25 | val rawTitle = element.select(".post-subject > a").first()!!.ownText().trim() 26 | 27 | val title = rawTitle.trim().replace(chapterRegex, "") 28 | 29 | val manga = SManga.create() 30 | manga.url = getUrlPath(linkElement.attr("href")) 31 | manga.title = title 32 | manga.thumbnail_url = element.select(".img-item > img").attr("src") 33 | manga.initialized = false 34 | return manga 35 | } 36 | 37 | override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { 38 | val url = ("$baseUrl/comic" + (if (page > 1) "/p$page" else "")).toHttpUrl().newBuilder() 39 | 40 | filters.forEach { filter -> 41 | when (filter) { 42 | is SearchPublishTypeList -> { 43 | if (filter.state > 0) { 44 | url.addQueryParameter("publish", filter.values[filter.state]) 45 | } 46 | } 47 | 48 | is SearchJaumTypeList -> { 49 | if (filter.state > 0) { 50 | url.addQueryParameter("jaum", filter.values[filter.state]) 51 | } 52 | } 53 | 54 | is SearchGenreTypeList -> { 55 | val genres = filter.state.filter { it.state }.joinToString(",") { it.id } 56 | url.addQueryParameter("tag", genres) 57 | } 58 | 59 | is SearchSortTypeList -> { 60 | val state = filter.state ?: return@forEach 61 | url.addQueryParameter("sst", arrayOf("wr_datetime", "wr_hit", "wr_good", "as_update")[state.index]) 62 | url.addQueryParameter("sod", if (state.ascending) "asc" else "desc") 63 | } 64 | 65 | else -> {} 66 | } 67 | } 68 | 69 | if (query.isNotBlank()) { 70 | url.addQueryParameter("stx", query) 71 | 72 | // Remove some filter QueryParams that not working with query 73 | url.removeAllQueryParameters("publish") 74 | url.removeAllQueryParameters("jaum") 75 | url.removeAllQueryParameters("tag") 76 | } 77 | 78 | return GET(url.toString(), headers) 79 | } 80 | 81 | private class SearchCheckBox(name: String, val id: String = name) : Filter.CheckBox(name) 82 | 83 | // [...document.querySelectorAll("form.form td")[3].querySelectorAll("span.btn")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') 84 | private class SearchPublishTypeList : Filter.Select( 85 | "Publish", 86 | arrayOf( 87 | "전체", 88 | "주간", 89 | "격주", 90 | "월간", 91 | "단편", 92 | "단행본", 93 | "완결", 94 | ), 95 | ) 96 | 97 | // [...document.querySelectorAll("form.form td")[4].querySelectorAll("span.btn")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') 98 | private class SearchJaumTypeList : Filter.Select( 99 | "Jaum", 100 | arrayOf( 101 | "전체", 102 | "ㄱ", 103 | "ㄴ", 104 | "ㄷ", 105 | "ㄹ", 106 | "ㅁ", 107 | "ㅂ", 108 | "ㅅ", 109 | "ㅇ", 110 | "ㅈ", 111 | "ㅊ", 112 | "ㅋ", 113 | "ㅌ", 114 | "ㅍ", 115 | "ㅎ", 116 | "0-9", 117 | "a-z", 118 | ), 119 | ) 120 | 121 | // [...document.querySelectorAll("form.form td")[6].querySelectorAll("span.btn")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') 122 | private class SearchGenreTypeList : Filter.Group( 123 | "Genres", 124 | arrayOf( 125 | "전체", 126 | "17", 127 | "BL", 128 | "SF", 129 | "TS", 130 | "개그", 131 | "게임", 132 | "도박", 133 | "드라마", 134 | "라노벨", 135 | "러브코미디", 136 | "먹방", 137 | "백합", 138 | "붕탁", 139 | "순정", 140 | "스릴러", 141 | "스포츠", 142 | "시대", 143 | "애니화", 144 | "액션", 145 | "음악", 146 | "이세계", 147 | "일상", 148 | "전생", 149 | "추리", 150 | "판타지", 151 | "학원", 152 | "호러", 153 | ).map { SearchCheckBox(it) }, 154 | ) 155 | 156 | private class SearchSortTypeList : Filter.Sort( 157 | "Sort", 158 | arrayOf( 159 | "기본(날짜순)", 160 | "인기순", 161 | "추천순", 162 | "업데이트순", 163 | ), 164 | ) 165 | 166 | override fun getFilterList() = FilterList( 167 | SearchSortTypeList(), 168 | Filter.Separator(), 169 | Filter.Header(ignoredForTextSearch()), 170 | SearchPublishTypeList(), 171 | SearchJaumTypeList(), 172 | SearchGenreTypeList(), 173 | ) 174 | } 175 | -------------------------------------------------------------------------------- /src/ko/newtoki/src/eu/kanade/tachiyomi/extension/ko/newtoki/NewTokiWebtoon.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.extension.ko.newtoki 2 | 3 | import eu.kanade.tachiyomi.network.GET 4 | import eu.kanade.tachiyomi.source.model.Filter 5 | import eu.kanade.tachiyomi.source.model.FilterList 6 | import okhttp3.HttpUrl.Companion.toHttpUrl 7 | import okhttp3.Request 8 | 9 | object NewTokiWebtoon : NewToki("NewToki", "webtoon", newTokiPreferences) { 10 | // / ! DO NOT CHANGE THIS ! Prevent to treating as a new site 11 | override val id = NEWTOKI_ID 12 | 13 | override val baseUrl get() = "https://$NEWTOKI_PREFIX$domainNumber.com" 14 | 15 | override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { 16 | val url = ("$baseUrl/webtoon" + (if (page > 1) "/p$page" else "")).toHttpUrl().newBuilder() 17 | filters.forEach { filter -> 18 | when (filter) { 19 | is SearchTargetTypeList -> { 20 | if (filter.state > 0) { 21 | url.addQueryParameter("toon", filter.values[filter.state]) 22 | } 23 | } 24 | 25 | is SearchSortTypeList -> { 26 | val state = filter.state ?: return@forEach 27 | url.addQueryParameter("sst", arrayOf("as_update", "wr_hit", "wr_good")[state.index]) 28 | url.addQueryParameter("sod", if (state.ascending) "asc" else "desc") 29 | } 30 | 31 | else -> {} 32 | } 33 | } 34 | 35 | // Incompatible with Other Search Parameter 36 | if (!query.isBlank()) { 37 | url.addQueryParameter("stx", query) 38 | } else { 39 | filters.forEach { filter -> 40 | when (filter) { 41 | is SearchYoilTypeList -> { 42 | if (filter.state > 0) { 43 | url.addQueryParameter("yoil", filter.values[filter.state]) 44 | } 45 | } 46 | 47 | is SearchJaumTypeList -> { 48 | if (filter.state > 0) { 49 | url.addQueryParameter("jaum", filter.values[filter.state]) 50 | } 51 | } 52 | 53 | is SearchGenreTypeList -> { 54 | if (filter.state > 0) { 55 | url.addQueryParameter("tag", filter.values[filter.state]) 56 | } 57 | } 58 | 59 | else -> {} 60 | } 61 | } 62 | } 63 | 64 | return GET(url.toString(), headers) 65 | } 66 | 67 | private class SearchTargetTypeList : Filter.Select("Type", arrayOf("전체", "일반웹툰", "성인웹툰", "BL/GL", "완결웹툰")) 68 | 69 | // [...document.querySelectorAll("form.form td")[1].querySelectorAll("a")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') 70 | private class SearchYoilTypeList : Filter.Select( 71 | "Day of the Week", 72 | arrayOf( 73 | "전체", 74 | "월", 75 | "화", 76 | "수", 77 | "목", 78 | "금", 79 | "토", 80 | "일", 81 | "열흘", 82 | ), 83 | ) 84 | 85 | // [...document.querySelectorAll("form.form td")[2].querySelectorAll("a")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') 86 | private class SearchJaumTypeList : Filter.Select( 87 | "Jaum", 88 | arrayOf( 89 | "전체", 90 | "ㄱ", 91 | "ㄴ", 92 | "ㄷ", 93 | "ㄹ", 94 | "ㅁ", 95 | "ㅂ", 96 | "ㅅ", 97 | "ㅇ", 98 | "ㅈ", 99 | "ㅊ", 100 | "ㅋ", 101 | "ㅌ", 102 | "ㅍ", 103 | "ㅎ", 104 | "a-z", 105 | "0-9", 106 | ), 107 | ) 108 | 109 | // [...document.querySelectorAll("form.form td")[3].querySelectorAll("a")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') 110 | private class SearchGenreTypeList : Filter.Select( 111 | "Genre", 112 | arrayOf( 113 | "전체", 114 | "판타지", 115 | "액션", 116 | "개그", 117 | "미스터리", 118 | "로맨스", 119 | "드라마", 120 | "무협", 121 | "스포츠", 122 | "일상", 123 | "학원", 124 | "성인", 125 | ), 126 | ) 127 | 128 | private class SearchSortTypeList : Filter.Sort( 129 | "Sort", 130 | arrayOf( 131 | "기본(업데이트순)", 132 | "인기순", 133 | "추천순", 134 | ), 135 | ) 136 | 137 | override fun getFilterList() = FilterList( 138 | SearchTargetTypeList(), 139 | SearchSortTypeList(), 140 | Filter.Separator(), 141 | Filter.Header(ignoredForTextSearch()), 142 | SearchYoilTypeList(), 143 | SearchJaumTypeList(), 144 | SearchGenreTypeList(), 145 | ) 146 | } 147 | -------------------------------------------------------------------------------- /src/ko/newtoki/src/eu/kanade/tachiyomi/extension/ko/newtoki/Preferences.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.extension.ko.newtoki 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import androidx.preference.EditTextPreference 7 | import androidx.preference.ListPreference 8 | import uy.kohesive.injekt.Injekt 9 | import uy.kohesive.injekt.api.get 10 | 11 | const val MANATOKI_ID = 2526381983439079467L // "NewToki/ko/1" 12 | const val NEWTOKI_ID = 1977818283770282459L // "NewToki (Webtoon)/ko/1" 13 | 14 | const val MANATOKI_PREFIX = "manatoki" 15 | const val NEWTOKI_PREFIX = "newtoki" 16 | 17 | val manaTokiPreferences = getSharedPreferences(MANATOKI_ID).migrate() 18 | val newTokiPreferences = getSharedPreferences(NEWTOKI_ID).migrate() 19 | 20 | fun getPreferencesInternal(context: Context) = arrayOf( 21 | 22 | EditTextPreference(context).apply { 23 | key = DOMAIN_NUMBER_PREF 24 | title = domainNumberTitle() 25 | summary = domainNumberSummary() 26 | setOnPreferenceChangeListener { _, newValue -> 27 | val value = newValue as String 28 | if (value.isEmpty() || value != value.trim()) { 29 | false 30 | } else { 31 | domainNumber = value 32 | true 33 | } 34 | } 35 | }, 36 | 37 | ListPreference(context).apply { 38 | key = RATE_LIMIT_PERIOD_PREF 39 | title = rateLimitTitle() 40 | summary = "%s\n" + requiresAppRestart() 41 | 42 | val values = Array(RATE_LIMIT_PERIOD_MAX) { (it + 1).toString() } 43 | entries = Array(RATE_LIMIT_PERIOD_MAX) { rateLimitEntry(values[it]) } 44 | entryValues = values 45 | 46 | setDefaultValue(RATE_LIMIT_PERIOD_DEFAULT) 47 | }, 48 | ) 49 | 50 | var SharedPreferences.domainNumber: String 51 | get() = getString(DOMAIN_NUMBER_PREF, "")!! 52 | set(value) = edit().putString(DOMAIN_NUMBER_PREF, value).apply() 53 | 54 | val SharedPreferences.rateLimitPeriod: Int 55 | get() = getString(RATE_LIMIT_PERIOD_PREF, RATE_LIMIT_PERIOD_DEFAULT)!!.toInt().coerceIn(1, RATE_LIMIT_PERIOD_MAX) 56 | 57 | private fun SharedPreferences.migrate(): SharedPreferences { 58 | if ("Override BaseUrl" !in this) return this // already migrated 59 | val editor = edit().clear() // clear all legacy preferences listed below 60 | val oldValue = try { // this was a long 61 | getLong(RATE_LIMIT_PERIOD_PREF, -1).toInt() 62 | } catch (_: ClassCastException) { 63 | -1 64 | } 65 | if (oldValue != -1) { // convert to string 66 | val newValue = oldValue.coerceIn(1, RATE_LIMIT_PERIOD_MAX) 67 | editor.putString(RATE_LIMIT_PERIOD_PREF, newValue.toString()) 68 | } 69 | editor.apply() 70 | return this 71 | } 72 | 73 | /** 74 | * Don't use the following legacy keys: 75 | * - "Override BaseUrl" 76 | * - "overrideBaseUrl_v${AppInfo.getVersionName()}" 77 | * - "Enable Latest (Experimental)" 78 | * - "fetchLatestExperiment" 79 | * - "Fetch Latest with detail (Optional)" 80 | * - "fetchLatestWithDetail" 81 | * - "Rate Limit Request Period Seconds" 82 | */ 83 | 84 | private const val DOMAIN_NUMBER_PREF = "domainNumber" 85 | private const val RATE_LIMIT_PERIOD_PREF = "rateLimitPeriod" 86 | private const val RATE_LIMIT_PERIOD_DEFAULT = 2.toString() 87 | private const val RATE_LIMIT_PERIOD_MAX = 9 88 | 89 | private fun getSharedPreferences(id: Long): SharedPreferences = 90 | Injekt.get().getSharedPreferences("source_$id", 0x0000) 91 | -------------------------------------------------------------------------------- /src/ko/newtoki/src/eu/kanade/tachiyomi/extension/ko/newtoki/Strings.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.extension.ko.newtoki 2 | 3 | import android.os.Build 4 | import android.os.LocaleList 5 | import java.util.Locale 6 | 7 | private val useKorean by lazy { 8 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 9 | LocaleList.getDefault().getFirstMatch(arrayOf("ko", "en"))?.language == "ko" 10 | } else { 11 | Locale.getDefault().language == "ko" 12 | } 13 | } 14 | 15 | // region Prompts 16 | 17 | fun solveCaptcha() = when { 18 | useKorean -> "WebView에서 캡챠 풀기" 19 | else -> "Solve Captcha with WebView" 20 | } 21 | 22 | fun titleNotMatch(realTitle: String) = when { 23 | useKorean -> "이 만화를 찾으시려면 '$realTitle'으로 검색하세요" 24 | else -> "Find this manga by searching '$realTitle'" 25 | } 26 | 27 | fun needMigration() = when { 28 | useKorean -> "이 항목은 URL 포맷이 틀립니다. 중복된 항목을 피하려면 동일한 소스로 이전하세요.\n\n" 29 | else -> "This entry has wrong URL format. Please migrate to the same source to avoid duplicates.\n\n" 30 | } 31 | 32 | // endregion 33 | 34 | // region Filters 35 | 36 | fun ignoredForTextSearch() = when { 37 | useKorean -> "검색에서 다음 필터 항목은 무시됩니다" 38 | else -> "The following filters are ignored for text search" 39 | } 40 | 41 | // endregion 42 | 43 | // region Preferences 44 | 45 | fun domainNumberTitle() = when { 46 | useKorean -> "도메인 번호" 47 | else -> "Domain number" 48 | } 49 | 50 | fun domainNumberSummary() = when { 51 | useKorean -> "도메인 번호는 자동으로 갱신됩니다" 52 | else -> "This number is updated automatically" 53 | } 54 | 55 | fun editDomainNumber() = when { 56 | useKorean -> "확장기능 설정에서 도메인 번호를 수정해 주세요" 57 | else -> "Please edit domain number in extension settings" 58 | } 59 | 60 | fun rateLimitTitle() = when { 61 | useKorean -> "요청 제한" 62 | else -> "Rate limit" 63 | } 64 | 65 | fun rateLimitEntry(period: String) = when { 66 | useKorean -> "${period}초마다 요청" 67 | else -> "1 request every $period seconds" 68 | } 69 | 70 | // taken from app strings 71 | fun requiresAppRestart() = when { 72 | useKorean -> "설정을 적용하려면 앱을 재시작하세요" 73 | else -> "Requires app restart to take effect" 74 | } 75 | 76 | // endregion 77 | -------------------------------------------------------------------------------- /src/ko/newtoki/src/eu/kanade/tachiyomi/extension/ko/newtoki/TokiFactory.kt: -------------------------------------------------------------------------------- 1 | package eu.kanade.tachiyomi.extension.ko.newtoki 2 | 3 | import eu.kanade.tachiyomi.source.SourceFactory 4 | 5 | class TokiFactory : SourceFactory { 6 | override fun createSources() = listOf(ManaToki, NewTokiWebtoon) 7 | } 8 | --------------------------------------------------------------------------------