mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-02 02:14:36 -05:00
Compare commits
5 Commits
v2.0.1727
...
socks-auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47d5a34012 | ||
|
|
d9ead789a2 | ||
|
|
0194a6d52e | ||
|
|
7b013bb391 | ||
|
|
532d5b9b9a |
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: "🐞 Bug report"
|
||||
title: "[Bug] <short description>"
|
||||
about: "Report a bug"
|
||||
labels: "bug"
|
||||
---
|
||||
|
||||
**PLEASE READ THIS**
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app.
|
||||
- I have tried the troubleshooting guide described in `README.md`
|
||||
- If this is a request for adding/changing an extension it should be brought up to your extension repo.
|
||||
- If this is an issue with some extension not working properly, It does work inside Tachiyomi as intended.
|
||||
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
---
|
||||
|
||||
## Device information
|
||||
- Suwayomi-Server version: (Example: v1.0.0-r1438-win32)
|
||||
- Server Operating System: (Example: Ubuntu 20.04)
|
||||
- Server Desktop Environment: N/A or (Example: Gnome 40)
|
||||
- Server JVM version: bundled with win32 or (Example: Java 8 Update 281 or OpenJDK 8u281)
|
||||
- Client Operating System: <usually the same as above Server Operating System>
|
||||
- Client Web Browser: (Example: Google Chrome 89.0.4389.82)
|
||||
|
||||
## Steps to reproduce
|
||||
1. First Step
|
||||
2. Second Step
|
||||
|
||||
### Expected behavior
|
||||
Describe what should have happened. Remove this line after you are done.
|
||||
|
||||
### Actual behavior
|
||||
Describe what happens instead. Remove this line after you are done.
|
||||
|
||||
## Other details
|
||||
Describe additional details If necessary. Remove this line after you are done.
|
||||
144
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
144
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,144 +0,0 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a bug in Suwayomi-Server
|
||||
labels: [bug]
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Provide an example of the issue.
|
||||
placeholder: |
|
||||
Example:
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Issue here
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Explain what you should expect to happen.
|
||||
placeholder: |
|
||||
Example: "This should happen..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: Explain what actually happens.
|
||||
placeholder: |
|
||||
Example: "This happened instead..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: suwayomi-server-version
|
||||
attributes:
|
||||
label: Suwayomi-Server version
|
||||
description: You can find your Suwayomi-Server version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "v2.0.1727"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: server-os
|
||||
attributes:
|
||||
label: Server operating system
|
||||
description: The operating system on which Suwayomi-Server is running on
|
||||
placeholder: |
|
||||
Example: "Windows 11 Pro 24H2 | Ubuntu 24.04.2 LTS"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: server-desktop-environment
|
||||
attributes:
|
||||
label: Server Desktop Environment
|
||||
description:
|
||||
placeholder: |
|
||||
Example: "Gnome 40"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: server-jvm-version
|
||||
attributes:
|
||||
label: Server JVM version
|
||||
description: The java version used to run Suwayomi-Server
|
||||
placeholder: |
|
||||
Example: "openjdk 21.0.5 2024-10-15 LTS"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: client-name
|
||||
attributes:
|
||||
label: Used client name
|
||||
description:
|
||||
placeholder: |
|
||||
Example: "Suwayomi-WebUI"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: client-version
|
||||
attributes:
|
||||
label: Client version
|
||||
description:
|
||||
placeholder: |
|
||||
Example: "v1.2.3"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: client-browser
|
||||
attributes:
|
||||
label: Used web browser
|
||||
description: The browser which is used to open Suwayomi-WebUI
|
||||
placeholder: |
|
||||
Example: "Chrome 134.0.6998.118 (64-Bit) | FireFox 136.0.2 (64-Bit) | Electron v35.0.2"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: client-os
|
||||
attributes:
|
||||
label: Client operating system
|
||||
description: The system on which the Suwayomi-WebUI is running on
|
||||
placeholder: |
|
||||
Example: "Windows 11 Pro 24H2 | Ubuntu 24.04.2 LTS"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-details
|
||||
attributes:
|
||||
label: Other details
|
||||
description: The more information that gets provided the better, especially via videos and images
|
||||
placeholder: |
|
||||
Additional details and attachments.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title (ideally less than ~100 characters).
|
||||
required: true
|
||||
- label: I have tried the troubleshooting guide described in [README.md](https://github.com/Suwayomi/Suwayomi-Server?tab=readme-ov-file#troubleshooting-and-support)
|
||||
required: true
|
||||
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
|
||||
required: true
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
- label: I understand that **Suwayomi does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions.
|
||||
required: true
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ☎️ Support
|
||||
url: https://discord.gg/DDZdqZWaHA
|
||||
about: Join our discord to get help for anything that is not a bug or a feature request
|
||||
|
||||
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: "🌟 Feature request"
|
||||
title: "[Feature Request] <short description>"
|
||||
about: "Suggest a feature to improve the project"
|
||||
labels: "enhancement"
|
||||
---
|
||||
|
||||
**PLEASE READ THIS**
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app.
|
||||
- I have tried the troubleshooting guide described in `README.md`
|
||||
- If this is a request for adding/changing an extension it should be brought up to your extension repo.
|
||||
- If this is an issue with some extension not working properly, It does work in Tachiyomi application as intended.
|
||||
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
---
|
||||
|
||||
## What feature should be added to Suwayomi?
|
||||
Explain What the feature is and how it should work in detail. Remove this line after you are done.
|
||||
|
||||
## Why/Project's Benefit/Existing Problem
|
||||
Explain why this should be added. Remove this line after you are done.
|
||||
37
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
37
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,37 +0,0 @@
|
||||
name: 🌟 Feature request
|
||||
description: Suggest a feature to improve Suwayomi-Server
|
||||
labels: [enhancement]
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Describe your suggested feature
|
||||
description: How can Suwayomi-Server be improved?
|
||||
placeholder: |
|
||||
Example:
|
||||
"It should work like this..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-details
|
||||
attributes:
|
||||
label: Other details
|
||||
placeholder: |
|
||||
Additional details and attachments.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title (ideally less than ~100 characters).
|
||||
required: true
|
||||
- label: I have updated to the **[latest version](https://github.com/suwayomi/suwayomi-server/releases/latest)**.
|
||||
required: true
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
15
.github/workflows/build_pull_request.yml
vendored
15
.github/workflows/build_pull_request.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v3
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build pull request
|
||||
@@ -32,15 +32,12 @@ jobs:
|
||||
path: master
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 21
|
||||
java-version: 8
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
cd master
|
||||
@@ -48,6 +45,8 @@ jobs:
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build Jar
|
||||
working-directory: master
|
||||
run: ./gradlew ktlintCheck :server:shadowJar --stacktrace
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
build-root-directory: master
|
||||
arguments: ktlintCheck :server:shadowJar --stacktrace
|
||||
|
||||
|
||||
94
.github/workflows/build_push.yml
vendored
94
.github/workflows/build_push.yml
vendored
@@ -15,10 +15,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v3
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build Jar
|
||||
@@ -32,15 +32,12 @@ jobs:
|
||||
path: master
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 21
|
||||
java-version: 8
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
cd master
|
||||
@@ -48,20 +45,22 @@ jobs:
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build Jar
|
||||
uses: gradle/gradle-build-action@v2
|
||||
env:
|
||||
ProductBuildType: "Preview"
|
||||
working-directory: master
|
||||
run: ./gradlew :server:shadowJar --stacktrace
|
||||
with:
|
||||
build-root-directory: master
|
||||
arguments: :server:shadowJar --stacktrace
|
||||
|
||||
- name: Upload Jar
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jar
|
||||
path: master/server/build/*.jar
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload icons
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: icon
|
||||
path: master/server/src/main/resources/icon
|
||||
@@ -71,43 +70,12 @@ jobs:
|
||||
run: tar -cvzf scripts.tar.gz -C master/ scripts/
|
||||
|
||||
- name: Upload scripts.tar.gz
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: scripts
|
||||
path: scripts.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
jlink:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
name: linux-x64
|
||||
- os: windows-latest
|
||||
name: windows-x64
|
||||
- os: macos-14
|
||||
name: macOS-arm64
|
||||
- os: macos-13
|
||||
name: macOS-x64
|
||||
os: [ubuntu-latest, windows-latest, macos-14, macos-13]
|
||||
|
||||
steps:
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 21
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Package JDK
|
||||
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.random,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
|
||||
|
||||
- name: Upload JRE package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.name }}-jre
|
||||
path: suwa
|
||||
|
||||
bundle:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -119,32 +87,26 @@ jobs:
|
||||
- macOS-x64
|
||||
- macOS-arm64
|
||||
- windows-x64
|
||||
- windows-x86
|
||||
|
||||
name: Make ${{ matrix.os }} release
|
||||
needs: [build,jlink]
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download Jar
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: jar
|
||||
path: server/build
|
||||
|
||||
- name: Download JRE
|
||||
uses: actions/download-artifact@v4
|
||||
if: matrix.os != 'linux-assets' && matrix.os != 'debian-all'
|
||||
with:
|
||||
name: ${{ matrix.os }}-jre
|
||||
path: jre
|
||||
|
||||
- name: Download icons
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: icon
|
||||
path: server/src/main/resources/icon
|
||||
|
||||
- name: Download scripts.tar.gz
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: scripts
|
||||
|
||||
@@ -155,7 +117,7 @@ jobs:
|
||||
scripts/bundler.sh -o upload/ ${{ matrix.os }}
|
||||
|
||||
- name: Upload ${{ matrix.os }} release
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: upload/*
|
||||
@@ -165,37 +127,41 @@ jobs:
|
||||
needs: bundle
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: jar
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: debian-all
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: linux-assets
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: linux-x64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: macOS-x64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: macOS-arm64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: windows-x64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: windows-x86
|
||||
path: release
|
||||
|
||||
- name: Checkout Preview branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: "Suwayomi/Suwayomi-Server-preview"
|
||||
ref: main
|
||||
@@ -227,7 +193,7 @@ jobs:
|
||||
git push origin $TAG
|
||||
|
||||
- name: Upload Preview Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||
repository: "Suwayomi/Suwayomi-Server-preview"
|
||||
|
||||
48
.github/workflows/issue_moderator.yml
vendored
Normal file
48
.github/workflows/issue_moderator.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Issue moderator
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Moderate issues
|
||||
uses: tachiyomiorg/issue-moderator-action@v1
|
||||
with:
|
||||
repo-token: ${{ github.token }}
|
||||
duplicate-check-enabled: true
|
||||
duplicate-check-label: Source request
|
||||
existing-check-enabled: true
|
||||
existing-check-label: Source request
|
||||
auto-close-rules: |
|
||||
[
|
||||
{
|
||||
"type": "title",
|
||||
"regex": ".*<short description>.*",
|
||||
"message": "You did not fill out the description in the title"
|
||||
},
|
||||
{
|
||||
"type": "title",
|
||||
"regex": ".*(<|>)+.*",
|
||||
"message": "You did not remove Angle brackets(< and >) from the title"
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed"
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*(Suwayomi-Server version|Server Operating System|Server Desktop Environment|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
|
||||
"message": "The requested information was not filled out"
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*Remove this line after you are done.*",
|
||||
"message": "The lines requesting to be removed were not removed."
|
||||
}
|
||||
]
|
||||
92
.github/workflows/publish.yml
vendored
92
.github/workflows/publish.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v3
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build Jar
|
||||
@@ -33,15 +33,12 @@ jobs:
|
||||
path: master
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 21
|
||||
java-version: 8
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
cd master
|
||||
@@ -50,20 +47,22 @@ jobs:
|
||||
~/.gradle/gradle.properties
|
||||
|
||||
- name: Build and copy webUI, Build Jar
|
||||
uses: gradle/gradle-build-action@v2
|
||||
env:
|
||||
ProductBuildType: "Stable"
|
||||
working-directory: master
|
||||
run: ./gradlew :server:downloadWebUI :server:shadowJar --stacktrace
|
||||
with:
|
||||
build-root-directory: master
|
||||
arguments: :server:downloadWebUI :server:shadowJar --stacktrace
|
||||
|
||||
- name: Upload Jar
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jar
|
||||
path: master/server/build/*.jar
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload icons
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: icon
|
||||
path: master/server/src/main/resources/icon
|
||||
@@ -73,43 +72,12 @@ jobs:
|
||||
run: tar -cvzf scripts.tar.gz -C master/ scripts/
|
||||
|
||||
- name: Upload scripts.tar.gz
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: scripts
|
||||
path: scripts.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
jlink:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
name: linux-x64
|
||||
- os: windows-latest
|
||||
name: windows-x64
|
||||
- os: macos-14
|
||||
name: macOS-arm64
|
||||
- os: macos-13
|
||||
name: macOS-x64
|
||||
os: [ubuntu-latest, windows-latest, macos-14, macos-13]
|
||||
|
||||
steps:
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 21
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Package JDK
|
||||
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.random,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
|
||||
|
||||
- name: Upload JDK package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.name }}-jre
|
||||
path: suwa
|
||||
|
||||
bundle:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -121,32 +89,26 @@ jobs:
|
||||
- macOS-x64
|
||||
- macOS-arm64
|
||||
- windows-x64
|
||||
- windows-x86
|
||||
|
||||
name: Make ${{ matrix.os }} release
|
||||
needs: [build, jlink]
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download Jar
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: jar
|
||||
path: server/build
|
||||
|
||||
- name: Download JRE
|
||||
uses: actions/download-artifact@v4
|
||||
if: matrix.os != 'linux-assets' && matrix.os != 'debian-all'
|
||||
with:
|
||||
name: ${{ matrix.os }}-jre
|
||||
path: jre
|
||||
|
||||
- name: Download icons
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: icon
|
||||
path: server/src/main/resources/icon
|
||||
|
||||
- name: Download scripts.tar.gz
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: scripts
|
||||
|
||||
@@ -157,7 +119,7 @@ jobs:
|
||||
scripts/bundler.sh -o upload/ ${{ matrix.os }}
|
||||
|
||||
- name: Upload ${{ matrix.os }} files
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: upload/*
|
||||
@@ -168,41 +130,45 @@ jobs:
|
||||
needs: bundle
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: jar
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: debian-all
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: linux-assets
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: linux-x64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: macOS-x64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: macOS-arm64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: windows-x64
|
||||
path: release
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: windows-x86
|
||||
path: release
|
||||
|
||||
- name: Generate checksums
|
||||
run: cd release && sha256sum * > Checksums.sha256
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.DEPLOY_RELEASE_TOKEN }}
|
||||
token: ${{ secrets.WINGET_PUBLISH_PAT }}
|
||||
draft: true
|
||||
files: release/*
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,7 +5,6 @@ gradle.properties
|
||||
.fleet
|
||||
# But we need these
|
||||
!.idea/runConfigurations
|
||||
.kotlin
|
||||
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
plugins {
|
||||
id(
|
||||
libs.plugins.kotlin.jvm
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
id(
|
||||
libs.plugins.kotlin.serialization
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
id(
|
||||
libs.plugins.ktlint
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
id(libs.plugins.kotlin.jvm.get().pluginId)
|
||||
id(libs.plugins.kotlin.serialization.get().pluginId)
|
||||
id(libs.plugins.ktlint.get().pluginId)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package xyz.nulldev.ts.config
|
||||
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.bind
|
||||
import org.kodein.di.singleton
|
||||
|
||||
class ConfigKodeinModule {
|
||||
fun create() =
|
||||
DI.Module("ConfigManager") {
|
||||
// Config module
|
||||
bind<ConfigManager>() with singleton { GlobalConfigManager }
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ import com.typesafe.config.ConfigValue
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import com.typesafe.config.parser.ConfigDocument
|
||||
import com.typesafe.config.parser.ConfigDocumentFactory
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import mu.KotlinLogging
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -47,10 +47,11 @@ open class ConfigManager {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
|
||||
|
||||
private fun getUserConfig(): Config =
|
||||
userConfigFile.let {
|
||||
private fun getUserConfig(): Config {
|
||||
return userConfigFile.let {
|
||||
ConfigFactory.parseFile(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configs
|
||||
@@ -71,8 +72,7 @@ open class ConfigManager {
|
||||
val userConfig = getUserConfig()
|
||||
|
||||
val config =
|
||||
ConfigFactory
|
||||
.empty()
|
||||
ConfigFactory.empty()
|
||||
.withFallback(baseConfig)
|
||||
.withFallback(userConfig)
|
||||
.withFallback(compatConfig)
|
||||
@@ -153,13 +153,11 @@ open class ConfigManager {
|
||||
}
|
||||
|
||||
var newUserConfigDoc: ConfigDocument = resetUserConfig(false)
|
||||
userConfig
|
||||
.entrySet()
|
||||
.filter {
|
||||
serverConfig.hasPath(
|
||||
it.key,
|
||||
)
|
||||
}.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) }
|
||||
userConfig.entrySet().filter {
|
||||
serverConfig.hasPath(
|
||||
it.key,
|
||||
)
|
||||
}.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) }
|
||||
|
||||
userConfigFile.writeText(newUserConfigDoc.render())
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package xyz.nulldev.ts.config
|
||||
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
|
||||
fun configManagerModule(): Module =
|
||||
module {
|
||||
single<ConfigManager> { GlobalConfigManager }
|
||||
}
|
||||
@@ -17,25 +17,17 @@ import kotlin.reflect.KProperty
|
||||
* Abstract config module.
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
abstract class ConfigModule(
|
||||
getConfig: () -> Config,
|
||||
)
|
||||
abstract class ConfigModule(getConfig: () -> Config)
|
||||
|
||||
/**
|
||||
* Abstract jvm-commandline-argument-overridable config module.
|
||||
*/
|
||||
abstract class SystemPropertyOverridableConfigModule(
|
||||
getConfig: () -> Config,
|
||||
moduleName: String,
|
||||
) : ConfigModule(getConfig) {
|
||||
abstract class SystemPropertyOverridableConfigModule(getConfig: () -> Config, moduleName: String) : ConfigModule(getConfig) {
|
||||
val overridableConfig = SystemPropertyOverrideDelegate(getConfig, moduleName)
|
||||
}
|
||||
|
||||
/** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */
|
||||
class SystemPropertyOverrideDelegate(
|
||||
val getConfig: () -> Config,
|
||||
val moduleName: String,
|
||||
) {
|
||||
class SystemPropertyOverrideDelegate(val getConfig: () -> Config, val moduleName: String) {
|
||||
inline operator fun <R, reified T> getValue(
|
||||
thisRef: R,
|
||||
property: KProperty<*>,
|
||||
|
||||
@@ -15,29 +15,13 @@ import ch.qos.logback.core.rolling.RollingFileAppender
|
||||
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy
|
||||
import ch.qos.logback.core.util.FileSize
|
||||
import com.typesafe.config.Config
|
||||
import io.github.oshai.kotlinlogging.DelegatingKLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import mu.KotlinLogging
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
private fun fileSizeValueOfOrDefault(
|
||||
fileSizeStr: String,
|
||||
default: String,
|
||||
): FileSize =
|
||||
try {
|
||||
FileSize.valueOf(fileSizeStr)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
FileSize.valueOf(default)
|
||||
}
|
||||
|
||||
private const val FILE_APPENDER_NAME = "SuwayomiDefaultAppender"
|
||||
|
||||
private fun createRollingFileAppender(
|
||||
logContext: LoggerContext,
|
||||
logDirPath: String,
|
||||
maxFiles: Int,
|
||||
maxFileSize: String,
|
||||
maxTotalSize: String,
|
||||
): RollingFileAppender<ILoggingEvent> {
|
||||
val logFilename = "application"
|
||||
|
||||
@@ -50,7 +34,7 @@ private fun createRollingFileAppender(
|
||||
|
||||
val appender =
|
||||
RollingFileAppender<ILoggingEvent>().apply {
|
||||
name = FILE_APPENDER_NAME
|
||||
name = "FILE"
|
||||
context = logContext
|
||||
encoder = logEncoder
|
||||
file = "$logDirPath/$logFilename.log"
|
||||
@@ -61,9 +45,9 @@ private fun createRollingFileAppender(
|
||||
context = logContext
|
||||
setParent(appender)
|
||||
fileNamePattern = "$logDirPath/${logFilename}_%d{yyyy-MM-dd}_%i.log.gz"
|
||||
maxHistory = maxFiles.coerceAtLeast(0)
|
||||
setMaxFileSize(fileSizeValueOfOrDefault(maxFileSize, "10mb"))
|
||||
setTotalSizeCap(fileSizeValueOfOrDefault(maxTotalSize, "100mb"))
|
||||
setMaxFileSize(FileSize.valueOf("10mb"))
|
||||
maxHistory = 14
|
||||
setTotalSizeCap(FileSize.valueOf("1gb"))
|
||||
start()
|
||||
}
|
||||
|
||||
@@ -73,52 +57,25 @@ private fun createRollingFileAppender(
|
||||
return appender
|
||||
}
|
||||
|
||||
private fun getBaseLogger(): ch.qos.logback.classic.Logger =
|
||||
((KotlinLogging.logger(Logger.ROOT_LOGGER_NAME) as DelegatingKLogger<*>).underlyingLogger as ch.qos.logback.classic.Logger)
|
||||
private fun getBaseLogger(): ch.qos.logback.classic.Logger {
|
||||
return (KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger)
|
||||
}
|
||||
|
||||
private fun getLogger(name: String): ch.qos.logback.classic.Logger {
|
||||
val context = LoggerFactory.getILoggerFactory() as LoggerContext
|
||||
return context.getLogger(name)
|
||||
}
|
||||
|
||||
fun initLoggerConfig(
|
||||
appRootPath: String,
|
||||
maxFiles: Int,
|
||||
maxFileSize: String,
|
||||
maxTotalSize: String,
|
||||
) {
|
||||
fun initLoggerConfig(appRootPath: String) {
|
||||
val context = LoggerFactory.getILoggerFactory() as LoggerContext
|
||||
val logger = getBaseLogger()
|
||||
|
||||
// logback logs to the console by default (at least when adding a console appender logs in the console are duplicated)
|
||||
logger.addAppender(createRollingFileAppender(context, "$appRootPath/logs", maxFiles, maxFileSize, maxTotalSize))
|
||||
logger.addAppender(createRollingFileAppender(context, "$appRootPath/logs"))
|
||||
|
||||
// set "kotlin exposed" log level
|
||||
setLogLevelFor("Exposed", Level.ERROR)
|
||||
}
|
||||
|
||||
fun updateFileAppender(
|
||||
maxFiles: Int,
|
||||
maxFileSize: String,
|
||||
maxTotalSize: String,
|
||||
) {
|
||||
val logger = getBaseLogger()
|
||||
|
||||
val appender = logger.getAppender(FILE_APPENDER_NAME) as RollingFileAppender<*>? ?: return
|
||||
val rollingPolicy = appender.rollingPolicy as SizeAndTimeBasedRollingPolicy<*>
|
||||
rollingPolicy.apply {
|
||||
maxHistory = maxFiles
|
||||
setMaxFileSize(FileSize.valueOf(maxFileSize))
|
||||
setTotalSizeCap(FileSize.valueOf(maxTotalSize))
|
||||
|
||||
rollingPolicy.stop()
|
||||
appender.stop()
|
||||
|
||||
rollingPolicy.start()
|
||||
appender.start()
|
||||
}
|
||||
}
|
||||
|
||||
const val BASE_LOGGER_NAME = "_BaseLogger"
|
||||
|
||||
fun setLogLevelFor(
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
plugins {
|
||||
id(
|
||||
libs.plugins.kotlin.jvm
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
id(
|
||||
libs.plugins.kotlin.serialization
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
id(
|
||||
libs.plugins.ktlint
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
id(libs.plugins.kotlin.jvm.get().pluginId)
|
||||
id(libs.plugins.kotlin.serialization.get().pluginId)
|
||||
id(libs.plugins.ktlint.get().pluginId)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -36,8 +24,8 @@ dependencies {
|
||||
// AndroidX annotations
|
||||
compileOnly(libs.android.annotations)
|
||||
|
||||
// substitute for duktape-android/quickjs
|
||||
implementation(libs.bundles.polyglot)
|
||||
// substitute for duktape-android
|
||||
implementation(libs.bundles.rhino)
|
||||
|
||||
// Kotlin wrapper around Java Preferences, makes certain things easier
|
||||
implementation(libs.bundles.settings)
|
||||
|
||||
@@ -25,7 +25,7 @@ import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
import kotlin.NotImplementedError;
|
||||
import xyz.nulldev.androidcompat.service.ServiceSupport;
|
||||
import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
|
||||
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.PrintWriter;
|
||||
@@ -299,7 +299,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
*/
|
||||
public abstract class Service extends ContextWrapper implements ComponentCallbacks2 {
|
||||
|
||||
private static final ServiceSupport serviceSupport = KoinGlobalHelper.instance(ServiceSupport.class);
|
||||
private static final ServiceSupport serviceSupport = KodeinGlobalHelper.instance(ServiceSupport.class);
|
||||
|
||||
private static final String TAG = "Service";
|
||||
/**
|
||||
@@ -328,7 +328,7 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac
|
||||
public Service() {
|
||||
//==================[THIS LINE MODIFIED FROM ANDROID SOURCE!]==================
|
||||
//Service must be initialized with a base context!
|
||||
super(KoinGlobalHelper.instance(Context.class));
|
||||
super(KodeinGlobalHelper.instance(Context.class));
|
||||
}
|
||||
/** Return the application that owns this service. */
|
||||
public final Application getApplication() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package android.os;
|
||||
|
||||
import xyz.nulldev.androidcompat.io.AndroidFiles;
|
||||
import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
|
||||
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.io.File;
|
||||
* Android compatibility layer for files
|
||||
*/
|
||||
public class Environment {
|
||||
private static AndroidFiles androidFiles = KoinGlobalHelper.instance(AndroidFiles.class);
|
||||
private static AndroidFiles androidFiles = KodeinGlobalHelper.instance(AndroidFiles.class);
|
||||
|
||||
public static String DIRECTORY_ALARMS = getHomeDirectory("Alarms").getAbsolutePath();
|
||||
public static String DIRECTORY_DCIM = getHomeDirectory("DCIM").getAbsolutePath();
|
||||
|
||||
@@ -1,99 +1,69 @@
|
||||
package app.cash.quickjs;
|
||||
|
||||
import org.graalvm.polyglot.*;
|
||||
import org.mozilla.javascript.ConsString;
|
||||
import org.mozilla.javascript.NativeArray;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineManager;
|
||||
import java.io.Closeable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class QuickJs implements Closeable {
|
||||
private Context context;
|
||||
private ScriptEngine engine;
|
||||
|
||||
public static QuickJs create() {
|
||||
return new QuickJs();
|
||||
return new QuickJs(new ScriptEngineManager());
|
||||
}
|
||||
|
||||
public QuickJs() {
|
||||
this.context = Context
|
||||
.newBuilder("js")
|
||||
.allowHostAccess(HostAccess.ALL)
|
||||
.allowPolyglotAccess(PolyglotAccess.NONE)
|
||||
.allowHostClassLoading(false)
|
||||
.build();
|
||||
context.enter();
|
||||
public QuickJs(ScriptEngineManager manager) {
|
||||
this.engine = manager.getEngineByName("rhino");
|
||||
}
|
||||
|
||||
public Object evaluate(String script, String ignoredFileName) {
|
||||
public Object evaluate(String script, String fileName) {
|
||||
return this.evaluate(script);
|
||||
}
|
||||
|
||||
public Object evaluate(String script) {
|
||||
try {
|
||||
Value value = context.eval("js", script);
|
||||
Object value = engine.eval(script);
|
||||
return translateType(value);
|
||||
} catch (Exception exception) {
|
||||
throw new QuickJsException(exception.getMessage(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Object translateType(Value obj) {
|
||||
if (obj.isBoolean()) {
|
||||
return obj.asBoolean();
|
||||
} else if (obj.hasArrayElements()) {
|
||||
if (obj.getArraySize() == 0) {
|
||||
return new int[0];
|
||||
} else {
|
||||
Value element = obj.getArrayElement(0);
|
||||
if (element.isBoolean()) {
|
||||
return obj.as(boolean[].class);
|
||||
} else if (element.isNumber()) {
|
||||
if (element.fitsInInt()) {
|
||||
return obj.as(int[].class);
|
||||
} else if (element.fitsInBigInteger()) {
|
||||
return Arrays.stream(obj.as(BigInteger[].class)).map(BigInteger::longValue).toArray();
|
||||
} else {
|
||||
return obj.as(double[].class);
|
||||
}
|
||||
} else if (element.isHostObject()) {
|
||||
return obj.as(Object[].class);
|
||||
} else if (element.isString()) {
|
||||
return obj.as(String[].class);
|
||||
}
|
||||
private Object translateType(Object obj) {
|
||||
if (obj instanceof NativeArray) {
|
||||
NativeArray array = (NativeArray) obj;
|
||||
long length = array.getLength();
|
||||
Object[] objects = new Object[(int) length];
|
||||
for (int i = 0; i < (int) length; i++) {
|
||||
objects[i] = translateType(array.get(i));
|
||||
}
|
||||
} else if (obj.isNumber()) {
|
||||
if (obj.fitsInInt()) {
|
||||
return obj.asInt();
|
||||
} else if (obj.fitsInBigInteger()) {
|
||||
return obj.asBigInteger().longValue();
|
||||
} else {
|
||||
return obj.asDouble();
|
||||
}
|
||||
} else if (obj.isHostObject()) {
|
||||
return obj.asHostObject();
|
||||
} else if (obj.isString()) {
|
||||
return obj.asString();
|
||||
return objects;
|
||||
}
|
||||
if (obj instanceof ConsString) {
|
||||
ConsString consString = (ConsString) obj;
|
||||
return consString.toString();
|
||||
}
|
||||
if (obj instanceof Long) {
|
||||
Long value = (Long) obj;
|
||||
return value.intValue();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
public byte[] compile(String sourceCode, String ignoredFileName) {
|
||||
public byte[] compile(String sourceCode, String fileName) {
|
||||
return sourceCode.getBytes();
|
||||
}
|
||||
|
||||
|
||||
public Object execute(byte[] bytecode) {
|
||||
return this.evaluate(new String(bytecode));
|
||||
}
|
||||
|
||||
public <T> void set(String name, Class<T> ignoredType, T object) {
|
||||
context.getBindings("js").putMember(name, object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (this.context != null) {
|
||||
this.context.leave();
|
||||
this.context.close();
|
||||
this.context = null;
|
||||
}
|
||||
this.engine = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package dalvik.system;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.nulldev.androidcompat.pm.PackageController;
|
||||
import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
|
||||
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -33,7 +33,7 @@ import java.util.Enumeration;
|
||||
* {@link ClassLoader} implementations.
|
||||
*/
|
||||
public class BaseDexClassLoader extends ClassLoader {
|
||||
private PackageController controller = KoinGlobalHelper.instance(PackageController.class);
|
||||
private PackageController controller = KodeinGlobalHelper.instance(PackageController.class);
|
||||
|
||||
private final URLClassLoader realClassloader;
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package xyz.nulldev.androidcompat
|
||||
|
||||
import android.app.Application
|
||||
import org.koin.mp.KoinPlatformTools
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import xyz.nulldev.androidcompat.androidimpl.CustomContext
|
||||
|
||||
class AndroidCompat {
|
||||
val context: CustomContext by KoinPlatformTools.defaultContext().get().inject()
|
||||
val context: CustomContext by DI.global.instance()
|
||||
|
||||
fun startApp(application: Application) {
|
||||
application.attach(context)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package xyz.nulldev.androidcompat
|
||||
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
|
||||
import xyz.nulldev.androidcompat.config.FilesConfigModule
|
||||
import xyz.nulldev.androidcompat.config.SystemConfigModule
|
||||
@@ -10,6 +12,8 @@ import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
*/
|
||||
class AndroidCompatInitializer {
|
||||
fun init() {
|
||||
DI.global.addImport(AndroidCompatModule().create())
|
||||
|
||||
// Register config modules
|
||||
GlobalConfigManager.registerModules(
|
||||
FilesConfigModule.register(GlobalConfigManager.config),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package xyz.nulldev.androidcompat
|
||||
|
||||
import android.content.Context
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.bind
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import org.kodein.di.singleton
|
||||
import xyz.nulldev.androidcompat.androidimpl.CustomContext
|
||||
import xyz.nulldev.androidcompat.androidimpl.FakePackageManager
|
||||
import xyz.nulldev.androidcompat.info.ApplicationInfoImpl
|
||||
@@ -14,19 +17,25 @@ import xyz.nulldev.androidcompat.service.ServiceSupport
|
||||
* AndroidCompatModule
|
||||
*/
|
||||
|
||||
fun androidCompatModule(): Module =
|
||||
module {
|
||||
single { AndroidFiles() }
|
||||
class AndroidCompatModule {
|
||||
fun create() =
|
||||
DI.Module("AndroidCompat") {
|
||||
bind<AndroidFiles>() with singleton { AndroidFiles() }
|
||||
|
||||
single { ApplicationInfoImpl(get()) }
|
||||
bind<ApplicationInfoImpl>() with singleton { ApplicationInfoImpl() }
|
||||
|
||||
single { ServiceSupport() }
|
||||
bind<ServiceSupport>() with singleton { ServiceSupport() }
|
||||
|
||||
single { FakePackageManager() }
|
||||
bind<FakePackageManager>() with singleton { FakePackageManager() }
|
||||
|
||||
single { PackageController() }
|
||||
bind<PackageController>() with singleton { PackageController() }
|
||||
|
||||
single { CustomContext() }
|
||||
|
||||
single<Context> { get<CustomContext>() }
|
||||
}
|
||||
// Context
|
||||
bind<CustomContext>() with singleton { CustomContext() }
|
||||
bind<Context>() with
|
||||
singleton {
|
||||
val context: Context by DI.global.instance<CustomContext>()
|
||||
context
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,14 +32,15 @@ import android.os.*;
|
||||
import android.view.Display;
|
||||
import android.view.DisplayAdjustments;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.koin.core.Koin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.kodein.di.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import xyz.nulldev.androidcompat.info.ApplicationInfoImpl;
|
||||
import xyz.nulldev.androidcompat.io.AndroidFiles;
|
||||
import xyz.nulldev.androidcompat.io.sharedprefs.JavaSharedPreferences;
|
||||
import xyz.nulldev.androidcompat.service.ServiceSupport;
|
||||
import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
|
||||
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.HashMap;
|
||||
@@ -50,25 +51,26 @@ import java.util.Map;
|
||||
* Custom context implementation.
|
||||
*
|
||||
*/
|
||||
public class CustomContext extends Context {
|
||||
private final Koin koin;
|
||||
public class CustomContext extends Context implements DIAware {
|
||||
private final DI kodein;
|
||||
public CustomContext() {
|
||||
this(KoinGlobalHelper.koin());
|
||||
this(KodeinGlobalHelper.kodein());
|
||||
}
|
||||
|
||||
public CustomContext(Koin koin) {
|
||||
this.koin = koin;
|
||||
public CustomContext(DI kodein) {
|
||||
this.kodein = kodein;
|
||||
|
||||
//Init configs
|
||||
androidFiles = KoinGlobalHelper.instance(AndroidFiles.class, getDi());
|
||||
applicationInfo = KoinGlobalHelper.instance(ApplicationInfoImpl.class, getDi());
|
||||
serviceSupport = KoinGlobalHelper.instance(ServiceSupport.class, getDi());
|
||||
fakePackageManager = KoinGlobalHelper.instance(FakePackageManager.class, getDi());
|
||||
androidFiles = KodeinGlobalHelper.instance(AndroidFiles.class, getDi());
|
||||
applicationInfo = KodeinGlobalHelper.instance(ApplicationInfoImpl.class, getDi());
|
||||
serviceSupport = KodeinGlobalHelper.instance(ServiceSupport.class, getDi());
|
||||
fakePackageManager = KodeinGlobalHelper.instance(FakePackageManager.class, getDi());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Koin getDi() {
|
||||
return koin;
|
||||
@Override
|
||||
public DI getDi() {
|
||||
return kodein;
|
||||
}
|
||||
|
||||
private AndroidFiles androidFiles;
|
||||
@@ -717,5 +719,17 @@ public class CustomContext extends Context {
|
||||
public boolean isCredentialProtectedStorage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DIContext<?> getDiContext() {
|
||||
return getDi().getDiContext();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DITrigger getDiTrigger() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ import android.os.UserHandle;
|
||||
import kotlin.NotImplementedError;
|
||||
import xyz.nulldev.androidcompat.pm.InstalledPackage;
|
||||
import xyz.nulldev.androidcompat.pm.PackageController;
|
||||
import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
|
||||
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class FakePackageManager extends PackageManager {
|
||||
private PackageController controller = KoinGlobalHelper.instance(PackageController.class);
|
||||
private PackageController controller = KodeinGlobalHelper.instance(PackageController.class);
|
||||
|
||||
@Override
|
||||
public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException {
|
||||
|
||||
@@ -8,9 +8,7 @@ import xyz.nulldev.ts.config.ConfigModule
|
||||
* Application info config.
|
||||
*/
|
||||
|
||||
class ApplicationInfoConfigModule(
|
||||
getConfig: () -> Config,
|
||||
) : ConfigModule(getConfig) {
|
||||
class ApplicationInfoConfigModule(getConfig: () -> Config) : ConfigModule(getConfig) {
|
||||
val packageName: String by getConfig()
|
||||
val debug: Boolean by getConfig()
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ import xyz.nulldev.ts.config.ConfigModule
|
||||
* Files configuration modules. Specifies where to store the Android files.
|
||||
*/
|
||||
|
||||
class FilesConfigModule(
|
||||
getConfig: () -> Config,
|
||||
) : ConfigModule(getConfig) {
|
||||
class FilesConfigModule(getConfig: () -> Config) : ConfigModule(getConfig) {
|
||||
val dataDir: String by getConfig()
|
||||
val filesDir: String by getConfig()
|
||||
val noBackupFilesDir: String by getConfig()
|
||||
|
||||
@@ -4,9 +4,7 @@ import com.typesafe.config.Config
|
||||
import io.github.config4k.getValue
|
||||
import xyz.nulldev.ts.config.ConfigModule
|
||||
|
||||
class SystemConfigModule(
|
||||
val getConfig: () -> Config,
|
||||
) : ConfigModule(getConfig) {
|
||||
class SystemConfigModule(val getConfig: () -> Config) : ConfigModule(getConfig) {
|
||||
val isDebuggable: Boolean by getConfig()
|
||||
|
||||
val propertyPrefix = "properties."
|
||||
|
||||
@@ -19,9 +19,7 @@ import java.sql.Timestamp
|
||||
import java.util.Calendar
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class ScrollableResultSet(
|
||||
val parent: ResultSet,
|
||||
) : ResultSet by parent {
|
||||
class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
||||
private val cachedContent = mutableListOf<ResultSetEntry>()
|
||||
private val columnCache = mutableMapOf<String, Int>()
|
||||
private var lastReturnWasNull = false
|
||||
@@ -31,10 +29,9 @@ class ScrollableResultSet(
|
||||
val parentMetadata = parent.metaData
|
||||
val columnCount = parentMetadata.columnCount
|
||||
val columnLabels =
|
||||
(1..columnCount)
|
||||
.map {
|
||||
parentMetadata.getColumnLabel(it)
|
||||
}.toTypedArray()
|
||||
(1..columnCount).map {
|
||||
parentMetadata.getColumnLabel(it)
|
||||
}.toTypedArray()
|
||||
|
||||
init {
|
||||
val columnCount = columnCount
|
||||
@@ -48,17 +45,20 @@ class ScrollableResultSet(
|
||||
while (parent.next()) {
|
||||
cachedContent +=
|
||||
ResultSetEntry().apply {
|
||||
for (i in 1..columnCount) {
|
||||
for (i in 1..columnCount)
|
||||
data += parent.getObject(i)
|
||||
}
|
||||
}
|
||||
resultSetLength++
|
||||
}
|
||||
}
|
||||
|
||||
private fun notImplemented(): Nothing = throw UnsupportedOperationException("This class currently does not support this operation!")
|
||||
private fun notImplemented(): Nothing {
|
||||
throw UnsupportedOperationException("This class currently does not support this operation!")
|
||||
}
|
||||
|
||||
private fun cursorValid(): Boolean = isAfterLast || isBeforeFirst
|
||||
private fun cursorValid(): Boolean {
|
||||
return isAfterLast || isBeforeFirst
|
||||
}
|
||||
|
||||
private fun internalMove(row: Int) {
|
||||
if (cursor < 0) {
|
||||
@@ -76,16 +76,22 @@ class ScrollableResultSet(
|
||||
return obj
|
||||
}
|
||||
|
||||
private fun obj(column: String?): Any? = obj(cachedFindColumn(column))
|
||||
private fun obj(column: String?): Any? {
|
||||
return obj(cachedFindColumn(column))
|
||||
}
|
||||
|
||||
private fun cachedFindColumn(column: String?) =
|
||||
columnCache.getOrPut(column!!, {
|
||||
findColumn(column)
|
||||
})
|
||||
|
||||
override fun getNClob(columnIndex: Int): NClob = obj(columnIndex) as NClob
|
||||
override fun getNClob(columnIndex: Int): NClob {
|
||||
return obj(columnIndex) as NClob
|
||||
}
|
||||
|
||||
override fun getNClob(columnLabel: String?): NClob = obj(columnLabel) as NClob
|
||||
override fun getNClob(columnLabel: String?): NClob {
|
||||
return obj(columnLabel) as NClob
|
||||
}
|
||||
|
||||
override fun updateNString(
|
||||
columnIndex: Int,
|
||||
@@ -254,11 +260,17 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun getBoolean(columnIndex: Int): Boolean = obj(columnIndex) as Boolean
|
||||
override fun getBoolean(columnIndex: Int): Boolean {
|
||||
return obj(columnIndex) as Boolean
|
||||
}
|
||||
|
||||
override fun getBoolean(columnLabel: String?): Boolean = obj(columnLabel) as Boolean
|
||||
override fun getBoolean(columnLabel: String?): Boolean {
|
||||
return obj(columnLabel) as Boolean
|
||||
}
|
||||
|
||||
override fun isFirst(): Boolean = cursor - 1 < resultSetLength
|
||||
override fun isFirst(): Boolean {
|
||||
return cursor - 1 < resultSetLength
|
||||
}
|
||||
|
||||
override fun getBigDecimal(
|
||||
columnIndex: Int,
|
||||
@@ -276,9 +288,13 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun getBigDecimal(columnIndex: Int): BigDecimal = obj(columnIndex) as BigDecimal
|
||||
override fun getBigDecimal(columnIndex: Int): BigDecimal {
|
||||
return obj(columnIndex) as BigDecimal
|
||||
}
|
||||
|
||||
override fun getBigDecimal(columnLabel: String?): BigDecimal = obj(columnLabel) as BigDecimal
|
||||
override fun getBigDecimal(columnLabel: String?): BigDecimal {
|
||||
return obj(columnLabel) as BigDecimal
|
||||
}
|
||||
|
||||
override fun updateBytes(
|
||||
columnIndex: Int,
|
||||
@@ -294,7 +310,9 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun isLast(): Boolean = cursor == resultSetLength
|
||||
override fun isLast(): Boolean {
|
||||
return cursor == resultSetLength
|
||||
}
|
||||
|
||||
override fun insertRow() {
|
||||
notImplemented()
|
||||
@@ -333,7 +351,9 @@ class ScrollableResultSet(
|
||||
return cursorValid()
|
||||
}
|
||||
|
||||
override fun isAfterLast(): Boolean = cursor > resultSetLength
|
||||
override fun isAfterLast(): Boolean {
|
||||
return cursor > resultSetLength
|
||||
}
|
||||
|
||||
override fun relative(rows: Int): Boolean {
|
||||
internalMove(cursor + rows)
|
||||
@@ -345,9 +365,8 @@ class ScrollableResultSet(
|
||||
internalMove(row)
|
||||
} else {
|
||||
last()
|
||||
for (i in 1..row) {
|
||||
for (i in 1..row)
|
||||
previous()
|
||||
}
|
||||
}
|
||||
return cursorValid()
|
||||
}
|
||||
@@ -375,13 +394,19 @@ class ScrollableResultSet(
|
||||
return cursorValid()
|
||||
}
|
||||
|
||||
override fun getFloat(columnIndex: Int): Float = obj(columnIndex) as Float
|
||||
override fun getFloat(columnIndex: Int): Float {
|
||||
return obj(columnIndex) as Float
|
||||
}
|
||||
|
||||
override fun getFloat(columnLabel: String?): Float = obj(columnLabel) as Float
|
||||
override fun getFloat(columnLabel: String?): Float {
|
||||
return obj(columnLabel) as Float
|
||||
}
|
||||
|
||||
override fun wasNull() = lastReturnWasNull
|
||||
|
||||
override fun getRow(): Int = cursor
|
||||
override fun getRow(): Int {
|
||||
return cursor
|
||||
}
|
||||
|
||||
override fun first(): Boolean {
|
||||
internalMove(1)
|
||||
@@ -434,9 +459,13 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun getURL(columnIndex: Int): URL = obj(columnIndex) as URL
|
||||
override fun getURL(columnIndex: Int): URL {
|
||||
return obj(columnIndex) as URL
|
||||
}
|
||||
|
||||
override fun getURL(columnLabel: String?): URL = obj(columnLabel) as URL
|
||||
override fun getURL(columnLabel: String?): URL {
|
||||
return obj(columnLabel) as URL
|
||||
}
|
||||
|
||||
override fun updateShort(
|
||||
columnIndex: Int,
|
||||
@@ -614,13 +643,21 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun getByte(columnIndex: Int): Byte = obj(columnIndex) as Byte
|
||||
override fun getByte(columnIndex: Int): Byte {
|
||||
return obj(columnIndex) as Byte
|
||||
}
|
||||
|
||||
override fun getByte(columnLabel: String?): Byte = obj(columnLabel) as Byte
|
||||
override fun getByte(columnLabel: String?): Byte {
|
||||
return obj(columnLabel) as Byte
|
||||
}
|
||||
|
||||
override fun getString(columnIndex: Int): String? = obj(columnIndex) as String?
|
||||
override fun getString(columnIndex: Int): String? {
|
||||
return obj(columnIndex) as String?
|
||||
}
|
||||
|
||||
override fun getString(columnLabel: String?): String? = obj(columnLabel) as String?
|
||||
override fun getString(columnLabel: String?): String? {
|
||||
return obj(columnLabel) as String?
|
||||
}
|
||||
|
||||
override fun updateSQLXML(
|
||||
columnIndex: Int,
|
||||
@@ -650,9 +687,13 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun getObject(columnIndex: Int): Any? = obj(columnIndex)
|
||||
override fun getObject(columnIndex: Int): Any? {
|
||||
return obj(columnIndex)
|
||||
}
|
||||
|
||||
override fun getObject(columnLabel: String?): Any? = obj(columnLabel)
|
||||
override fun getObject(columnLabel: String?): Any? {
|
||||
return obj(columnLabel)
|
||||
}
|
||||
|
||||
override fun getObject(
|
||||
columnIndex: Int,
|
||||
@@ -673,12 +714,16 @@ class ScrollableResultSet(
|
||||
override fun <T : Any?> getObject(
|
||||
columnIndex: Int,
|
||||
type: Class<T>?,
|
||||
): T = obj(columnIndex) as T
|
||||
): T {
|
||||
return obj(columnIndex) as T
|
||||
}
|
||||
|
||||
override fun <T : Any?> getObject(
|
||||
columnLabel: String?,
|
||||
type: Class<T>?,
|
||||
): T = obj(columnLabel) as T
|
||||
): T {
|
||||
return obj(columnLabel) as T
|
||||
}
|
||||
|
||||
override fun previous(): Boolean {
|
||||
internalMove(cursor - 1)
|
||||
@@ -711,9 +756,13 @@ class ScrollableResultSet(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLong(columnIndex: Int): Long = castToLong(obj(columnIndex))
|
||||
override fun getLong(columnIndex: Int): Long {
|
||||
return castToLong(obj(columnIndex))
|
||||
}
|
||||
|
||||
override fun getLong(columnLabel: String?): Long = castToLong(obj(columnLabel))
|
||||
override fun getLong(columnLabel: String?): Long {
|
||||
return castToLong(obj(columnLabel))
|
||||
}
|
||||
|
||||
override fun getClob(columnIndex: Int): Clob {
|
||||
// TODO Maybe?
|
||||
@@ -791,9 +840,13 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun getNString(columnIndex: Int): String = obj(columnIndex) as String
|
||||
override fun getNString(columnIndex: Int): String {
|
||||
return obj(columnIndex) as String
|
||||
}
|
||||
|
||||
override fun getNString(columnLabel: String?): String = obj(columnLabel) as String
|
||||
override fun getNString(columnLabel: String?): String {
|
||||
return obj(columnLabel) as String
|
||||
}
|
||||
|
||||
override fun getArray(columnIndex: Int): Array {
|
||||
// TODO Maybe?
|
||||
@@ -827,11 +880,17 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun getCharacterStream(columnIndex: Int): Reader = getNCharacterStream(columnIndex)
|
||||
override fun getCharacterStream(columnIndex: Int): Reader {
|
||||
return getNCharacterStream(columnIndex)
|
||||
}
|
||||
|
||||
override fun getCharacterStream(columnLabel: String?): Reader = getNCharacterStream(columnLabel)
|
||||
override fun getCharacterStream(columnLabel: String?): Reader {
|
||||
return getNCharacterStream(columnLabel)
|
||||
}
|
||||
|
||||
override fun isBeforeFirst(): Boolean = cursor - 1 < resultSetLength
|
||||
override fun isBeforeFirst(): Boolean {
|
||||
return cursor - 1 < resultSetLength
|
||||
}
|
||||
|
||||
override fun updateBoolean(
|
||||
columnIndex: Int,
|
||||
@@ -867,13 +926,21 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun getShort(columnIndex: Int): Short = obj(columnIndex) as Short
|
||||
override fun getShort(columnIndex: Int): Short {
|
||||
return obj(columnIndex) as Short
|
||||
}
|
||||
|
||||
override fun getShort(columnLabel: String?): Short = obj(columnLabel) as Short
|
||||
override fun getShort(columnLabel: String?): Short {
|
||||
return obj(columnLabel) as Short
|
||||
}
|
||||
|
||||
override fun getAsciiStream(columnIndex: Int): InputStream = getBinaryStream(columnIndex)
|
||||
override fun getAsciiStream(columnIndex: Int): InputStream {
|
||||
return getBinaryStream(columnIndex)
|
||||
}
|
||||
|
||||
override fun getAsciiStream(columnLabel: String?): InputStream = getBinaryStream(columnLabel)
|
||||
override fun getAsciiStream(columnLabel: String?): InputStream {
|
||||
return getBinaryStream(columnLabel)
|
||||
}
|
||||
|
||||
override fun updateTime(
|
||||
columnIndex: Int,
|
||||
@@ -941,9 +1008,13 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun getNCharacterStream(columnIndex: Int): Reader = getBinaryStream(columnIndex).reader()
|
||||
override fun getNCharacterStream(columnIndex: Int): Reader {
|
||||
return getBinaryStream(columnIndex).reader()
|
||||
}
|
||||
|
||||
override fun getNCharacterStream(columnLabel: String?): Reader = getBinaryStream(columnLabel).reader()
|
||||
override fun getNCharacterStream(columnLabel: String?): Reader {
|
||||
return getBinaryStream(columnLabel).reader()
|
||||
}
|
||||
|
||||
override fun updateArray(
|
||||
columnIndex: Int,
|
||||
@@ -959,27 +1030,45 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun getBytes(columnIndex: Int): ByteArray = obj(columnIndex) as ByteArray
|
||||
override fun getBytes(columnIndex: Int): ByteArray {
|
||||
return obj(columnIndex) as ByteArray
|
||||
}
|
||||
|
||||
override fun getBytes(columnLabel: String?): ByteArray = obj(columnLabel) as ByteArray
|
||||
override fun getBytes(columnLabel: String?): ByteArray {
|
||||
return obj(columnLabel) as ByteArray
|
||||
}
|
||||
|
||||
override fun getDouble(columnIndex: Int): Double = obj(columnIndex) as Double
|
||||
override fun getDouble(columnIndex: Int): Double {
|
||||
return obj(columnIndex) as Double
|
||||
}
|
||||
|
||||
override fun getDouble(columnLabel: String?): Double = obj(columnLabel) as Double
|
||||
override fun getDouble(columnLabel: String?): Double {
|
||||
return obj(columnLabel) as Double
|
||||
}
|
||||
|
||||
override fun getUnicodeStream(columnIndex: Int): InputStream = getBinaryStream(columnIndex)
|
||||
override fun getUnicodeStream(columnIndex: Int): InputStream {
|
||||
return getBinaryStream(columnIndex)
|
||||
}
|
||||
|
||||
override fun getUnicodeStream(columnLabel: String?): InputStream = getBinaryStream(columnLabel)
|
||||
override fun getUnicodeStream(columnLabel: String?): InputStream {
|
||||
return getBinaryStream(columnLabel)
|
||||
}
|
||||
|
||||
override fun rowInserted() = false
|
||||
|
||||
private fun thisIsWrapperFor(iface: Class<*>?) = this.javaClass.isInstance(iface)
|
||||
|
||||
override fun isWrapperFor(iface: Class<*>?): Boolean = thisIsWrapperFor(iface) || parent.isWrapperFor(iface)
|
||||
override fun isWrapperFor(iface: Class<*>?): Boolean {
|
||||
return thisIsWrapperFor(iface) || parent.isWrapperFor(iface)
|
||||
}
|
||||
|
||||
override fun getInt(columnIndex: Int): Int = obj(columnIndex) as Int
|
||||
override fun getInt(columnIndex: Int): Int {
|
||||
return obj(columnIndex) as Int
|
||||
}
|
||||
|
||||
override fun getInt(columnLabel: String?): Int = obj(columnLabel) as Int
|
||||
override fun getInt(columnLabel: String?): Int {
|
||||
return obj(columnLabel) as Int
|
||||
}
|
||||
|
||||
override fun updateNull(columnIndex: Int) {
|
||||
notImplemented()
|
||||
@@ -999,8 +1088,8 @@ class ScrollableResultSet(
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override fun getMetaData(): ResultSetMetaData =
|
||||
object : ResultSetMetaData by parentMetadata {
|
||||
override fun getMetaData(): ResultSetMetaData {
|
||||
return object : ResultSetMetaData by parentMetadata {
|
||||
override fun isReadOnly(column: Int) = true
|
||||
|
||||
override fun isWritable(column: Int) = false
|
||||
@@ -1009,12 +1098,19 @@ class ScrollableResultSet(
|
||||
|
||||
override fun getColumnCount() = this@ScrollableResultSet.columnCount
|
||||
|
||||
override fun getColumnLabel(column: Int): String = columnLabels[column - 1]
|
||||
override fun getColumnLabel(column: Int): String {
|
||||
return columnLabels[column - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBinaryStream(columnIndex: Int): InputStream = (obj(columnIndex) as ByteArray).inputStream()
|
||||
override fun getBinaryStream(columnIndex: Int): InputStream {
|
||||
return (obj(columnIndex) as ByteArray).inputStream()
|
||||
}
|
||||
|
||||
override fun getBinaryStream(columnLabel: String?): InputStream = (obj(columnLabel) as ByteArray).inputStream()
|
||||
override fun getBinaryStream(columnLabel: String?): InputStream {
|
||||
return (obj(columnLabel) as ByteArray).inputStream()
|
||||
}
|
||||
|
||||
override fun updateCharacterStream(
|
||||
columnIndex: Int,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package xyz.nulldev.androidcompat.info
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
|
||||
import xyz.nulldev.ts.config.ConfigManager
|
||||
|
||||
class ApplicationInfoImpl(
|
||||
private val configManager: ConfigManager,
|
||||
) : ApplicationInfo() {
|
||||
class ApplicationInfoImpl(override val di: DI = DI.global) : ApplicationInfo(), DIAware {
|
||||
val configManager: ConfigManager by di.instance()
|
||||
|
||||
val appInfoConfig: ApplicationInfoConfigModule
|
||||
get() = configManager.module()
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ import java.io.File
|
||||
/**
|
||||
* Android file constants.
|
||||
*/
|
||||
class AndroidFiles(
|
||||
val configManager: ConfigManager = GlobalConfigManager,
|
||||
) {
|
||||
class AndroidFiles(val configManager: ConfigManager = GlobalConfigManager) {
|
||||
val filesConfig: FilesConfigModule
|
||||
get() = configManager.module()
|
||||
|
||||
@@ -32,8 +30,9 @@ class AndroidFiles(
|
||||
|
||||
val packagesDir: File get() = registerFile(filesConfig.packageDir)
|
||||
|
||||
fun registerFile(file: String): File =
|
||||
File(file).apply {
|
||||
fun registerFile(file: String): File {
|
||||
return File(file).apply {
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.serialization.decodeValue
|
||||
import com.russhwolf.settings.serialization.decodeValueOrNull
|
||||
import com.russhwolf.settings.serialization.encodeValue
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.builtins.SetSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import mu.KotlinLogging
|
||||
import xyz.nulldev.androidcompat.util.SafePath
|
||||
import xyz.nulldev.ts.config.ApplicationRootDir
|
||||
import java.util.Properties
|
||||
@@ -30,9 +30,7 @@ import kotlin.io.path.inputStream
|
||||
import kotlin.io.path.outputStream
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
|
||||
class JavaSharedPreferences(
|
||||
key: String,
|
||||
) : SharedPreferences {
|
||||
class JavaSharedPreferences(key: String) : SharedPreferences {
|
||||
companion object {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
}
|
||||
@@ -74,17 +72,20 @@ class JavaSharedPreferences(
|
||||
private val listeners = mutableMapOf<SharedPreferences.OnSharedPreferenceChangeListener, (String) -> Unit>()
|
||||
|
||||
// TODO: 2021-05-29 Need to find a way to get this working with all pref types
|
||||
override fun getAll(): MutableMap<String, *> = preferences.keys.associateWith { preferences.getStringOrNull(it) }.toMutableMap()
|
||||
override fun getAll(): MutableMap<String, *> {
|
||||
return preferences.keys.associateWith { preferences.getStringOrNull(it) }.toMutableMap()
|
||||
}
|
||||
|
||||
override fun getString(
|
||||
key: String,
|
||||
defValue: String?,
|
||||
): String? =
|
||||
if (defValue != null) {
|
||||
): String? {
|
||||
return if (defValue != null) {
|
||||
preferences.getString(key, defValue)
|
||||
} else {
|
||||
preferences.getStringOrNull(key)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStringSet(
|
||||
key: String,
|
||||
@@ -104,48 +105,50 @@ class JavaSharedPreferences(
|
||||
override fun getInt(
|
||||
key: String,
|
||||
defValue: Int,
|
||||
): Int = preferences.getInt(key, defValue)
|
||||
): Int {
|
||||
return preferences.getInt(key, defValue)
|
||||
}
|
||||
|
||||
override fun getLong(
|
||||
key: String,
|
||||
defValue: Long,
|
||||
): Long = preferences.getLong(key, defValue)
|
||||
): Long {
|
||||
return preferences.getLong(key, defValue)
|
||||
}
|
||||
|
||||
override fun getFloat(
|
||||
key: String,
|
||||
defValue: Float,
|
||||
): Float = preferences.getFloat(key, defValue)
|
||||
): Float {
|
||||
return preferences.getFloat(key, defValue)
|
||||
}
|
||||
|
||||
override fun getBoolean(
|
||||
key: String,
|
||||
defValue: Boolean,
|
||||
): Boolean = preferences.getBoolean(key, defValue)
|
||||
): Boolean {
|
||||
return preferences.getBoolean(key, defValue)
|
||||
}
|
||||
|
||||
override fun contains(key: String): Boolean = key in preferences.keys
|
||||
override fun contains(key: String): Boolean {
|
||||
return key in preferences.keys
|
||||
}
|
||||
|
||||
override fun edit(): SharedPreferences.Editor =
|
||||
Editor(preferences) { key ->
|
||||
override fun edit(): SharedPreferences.Editor {
|
||||
return Editor(preferences) { key ->
|
||||
listeners.forEach { (_, listener) ->
|
||||
listener(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Editor(
|
||||
private val preferences: Settings,
|
||||
private val notify: (String) -> Unit,
|
||||
) : SharedPreferences.Editor {
|
||||
class Editor(private val preferences: Settings, private val notify: (String) -> Unit) : SharedPreferences.Editor {
|
||||
private val actions = mutableListOf<Action>()
|
||||
|
||||
private sealed class Action {
|
||||
data class Add(
|
||||
val key: String,
|
||||
val value: Any,
|
||||
) : Action()
|
||||
|
||||
data class Remove(
|
||||
val key: String,
|
||||
) : Action()
|
||||
data class Add(val key: String, val value: Any) : Action()
|
||||
|
||||
data class Remove(val key: String) : Action()
|
||||
data object Clear : Action()
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ import java.io.File
|
||||
import javax.imageio.ImageIO
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
data class InstalledPackage(
|
||||
val root: File,
|
||||
) {
|
||||
data class InstalledPackage(val root: File) {
|
||||
val apk = File(root, "package.apk")
|
||||
val jar = File(root, "translated.jar")
|
||||
val icon = File(root, "icon.png")
|
||||
@@ -36,21 +34,18 @@ data class InstalledPackage(
|
||||
Bundle().apply {
|
||||
val appTag = doc.getElementsByTagName("application").item(0)
|
||||
|
||||
appTag
|
||||
?.childNodes
|
||||
?.toList()
|
||||
?.filter {
|
||||
it.nodeType == Node.ELEMENT_NODE
|
||||
}?.map {
|
||||
it as Element
|
||||
}?.filter {
|
||||
it.tagName == "meta-data"
|
||||
}?.map {
|
||||
putString(
|
||||
it.attributes.getNamedItem("android:name").nodeValue,
|
||||
it.attributes.getNamedItem("android:value").nodeValue,
|
||||
)
|
||||
}
|
||||
appTag?.childNodes?.toList()?.filter {
|
||||
it.nodeType == Node.ELEMENT_NODE
|
||||
}?.map {
|
||||
it as Element
|
||||
}?.filter {
|
||||
it.tagName == "meta-data"
|
||||
}?.map {
|
||||
putString(
|
||||
it.attributes.getNamedItem("android:name").nodeValue,
|
||||
it.attributes.getNamedItem("android:value").nodeValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
it.signatures =
|
||||
@@ -58,14 +53,12 @@ data class InstalledPackage(
|
||||
parsed.apkSingers.flatMap { it.certificateMetas }
|
||||
// + parsed.apkV2Singers.flatMap { it.certificateMetas }
|
||||
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
|
||||
.map { Signature(it.data) }
|
||||
.toTypedArray()
|
||||
.map { Signature(it.data) }.toTypedArray()
|
||||
}
|
||||
|
||||
fun verify(): Boolean {
|
||||
val res =
|
||||
ApkVerifier
|
||||
.Builder(apk)
|
||||
ApkVerifier.Builder(apk)
|
||||
.build()
|
||||
.verify()
|
||||
|
||||
@@ -77,14 +70,11 @@ data class InstalledPackage(
|
||||
val icons = ApkFile(apk).allIcons
|
||||
|
||||
val read =
|
||||
icons
|
||||
.filter { it.isFile }
|
||||
.map {
|
||||
it.data.inputStream().use {
|
||||
ImageIO.read(it)
|
||||
}
|
||||
}.sortedByDescending { it.width * it.height }
|
||||
.firstOrNull() ?: return
|
||||
icons.filter { it.isFile }.map {
|
||||
it.data.inputStream().use {
|
||||
ImageIO.read(it)
|
||||
}
|
||||
}.sortedByDescending { it.width * it.height }.firstOrNull() ?: return
|
||||
|
||||
ImageIO.write(read, "png", icon)
|
||||
} catch (e: Exception) {
|
||||
@@ -104,9 +94,8 @@ data class InstalledPackage(
|
||||
fun NodeList.toList(): List<Node> {
|
||||
val out = mutableListOf<Node>()
|
||||
|
||||
for (i in 0 until length) {
|
||||
for (i in 0 until length)
|
||||
out += item(i)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package xyz.nulldev.androidcompat.pm
|
||||
|
||||
import net.dongliu.apk.parser.ApkParsers
|
||||
import org.koin.mp.KoinPlatformTools
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import xyz.nulldev.androidcompat.io.AndroidFiles
|
||||
import java.io.File
|
||||
|
||||
class PackageController {
|
||||
private val androidFiles: AndroidFiles by KoinPlatformTools.defaultContext().get().inject()
|
||||
private val androidFiles by DI.global.instance<AndroidFiles>()
|
||||
private val uninstallListeners = mutableListOf<(String) -> Unit>()
|
||||
|
||||
fun registerUninstallListener(listener: (String) -> Unit) {
|
||||
@@ -55,15 +57,13 @@ class PackageController {
|
||||
}
|
||||
}
|
||||
|
||||
fun listInstalled(): List<InstalledPackage> =
|
||||
androidFiles.packagesDir
|
||||
.listFiles()
|
||||
.orEmpty()
|
||||
.filter {
|
||||
it.isDirectory
|
||||
}.map {
|
||||
InstalledPackage(it)
|
||||
}
|
||||
fun listInstalled(): List<InstalledPackage> {
|
||||
return androidFiles.packagesDir.listFiles().orEmpty().filter {
|
||||
it.isDirectory
|
||||
}.map {
|
||||
InstalledPackage(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun deletePackage(pack: InstalledPackage) {
|
||||
if (!pack.root.exists()) error("Package was never installed!")
|
||||
|
||||
@@ -6,19 +6,18 @@ import android.content.pm.PackageInfo
|
||||
import net.dongliu.apk.parser.bean.ApkMeta
|
||||
import java.io.File
|
||||
|
||||
fun ApkMeta.toPackageInfo(apk: File): PackageInfo =
|
||||
PackageInfo().also {
|
||||
fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
|
||||
return PackageInfo().also {
|
||||
it.packageName = packageName
|
||||
it.versionCode = versionCode.toInt()
|
||||
it.versionName = versionName
|
||||
|
||||
it.reqFeatures =
|
||||
usesFeatures
|
||||
.map {
|
||||
FeatureInfo().apply {
|
||||
name = it.name
|
||||
}
|
||||
}.toTypedArray()
|
||||
usesFeatures.map {
|
||||
FeatureInfo().apply {
|
||||
name = it.name
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
it.applicationInfo =
|
||||
ApplicationInfo().apply {
|
||||
@@ -27,3 +26,4 @@ fun ApkMeta.toPackageInfo(apk: File): PackageInfo =
|
||||
sourceDir = apk.absolutePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package xyz.nulldev.androidcompat.res;
|
||||
|
||||
import xyz.nulldev.androidcompat.info.ApplicationInfoImpl;
|
||||
import xyz.nulldev.androidcompat.util.KoinGlobalHelper;
|
||||
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
@@ -10,7 +10,7 @@ import java.util.Calendar;
|
||||
* BuildConfig compat class.
|
||||
*/
|
||||
public class BuildConfigCompat {
|
||||
private static ApplicationInfoImpl applicationInfo = KoinGlobalHelper.instance(ApplicationInfoImpl.class);
|
||||
private static ApplicationInfoImpl applicationInfo = KodeinGlobalHelper.instance(ApplicationInfoImpl.class);
|
||||
|
||||
public static final boolean DEBUG = applicationInfo.getDebug();
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package xyz.nulldev.androidcompat.res
|
||||
|
||||
class DrawableResource(
|
||||
val location: String,
|
||||
) : Resource {
|
||||
class DrawableResource(val location: String) : Resource {
|
||||
override fun getType() = DrawableResource::class.java
|
||||
|
||||
override fun getValue() = javaClass.getResourceAsStream(location)
|
||||
|
||||
@@ -19,9 +19,7 @@ package xyz.nulldev.androidcompat.res
|
||||
/**
|
||||
* String resource.
|
||||
*/
|
||||
class StringResource(
|
||||
val string: String,
|
||||
) : Resource {
|
||||
class StringResource(val string: String) : Resource {
|
||||
override fun getValue() = string
|
||||
|
||||
override fun getType() = StringResource::class.java
|
||||
|
||||
@@ -3,7 +3,7 @@ package xyz.nulldev.androidcompat.service
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import mu.KotlinLogging
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package xyz.nulldev.androidcompat.util
|
||||
|
||||
import android.content.Context
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import xyz.nulldev.androidcompat.androidimpl.CustomContext
|
||||
import xyz.nulldev.androidcompat.androidimpl.FakePackageManager
|
||||
import xyz.nulldev.androidcompat.info.ApplicationInfoImpl
|
||||
import xyz.nulldev.androidcompat.io.AndroidFiles
|
||||
import xyz.nulldev.androidcompat.pm.PackageController
|
||||
import xyz.nulldev.androidcompat.service.ServiceSupport
|
||||
|
||||
/**
|
||||
* Helper class to allow access to Kodein from Java
|
||||
*/
|
||||
object KodeinGlobalHelper {
|
||||
/**
|
||||
* Get the Kodein object
|
||||
*/
|
||||
@JvmStatic
|
||||
fun kodein() = DI.global
|
||||
|
||||
/**
|
||||
* Get a dependency
|
||||
*/
|
||||
@JvmStatic
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> instance(
|
||||
type: Class<T>,
|
||||
kodein: DI? = null,
|
||||
): T {
|
||||
return when (type) {
|
||||
AndroidFiles::class.java -> {
|
||||
val instance: AndroidFiles by (kodein ?: kodein()).instance()
|
||||
instance as T
|
||||
}
|
||||
ApplicationInfoImpl::class.java -> {
|
||||
val instance: ApplicationInfoImpl by (kodein ?: kodein()).instance()
|
||||
instance as T
|
||||
}
|
||||
ServiceSupport::class.java -> {
|
||||
val instance: ServiceSupport by (kodein ?: kodein()).instance()
|
||||
instance as T
|
||||
}
|
||||
FakePackageManager::class.java -> {
|
||||
val instance: FakePackageManager by (kodein ?: kodein()).instance()
|
||||
instance as T
|
||||
}
|
||||
PackageController::class.java -> {
|
||||
val instance: PackageController by (kodein ?: kodein()).instance()
|
||||
instance as T
|
||||
}
|
||||
CustomContext::class.java -> {
|
||||
val instance: CustomContext by (kodein ?: kodein()).instance()
|
||||
instance as T
|
||||
}
|
||||
Context::class.java -> {
|
||||
val instance: Context by (kodein ?: kodein()).instance()
|
||||
instance as T
|
||||
}
|
||||
else -> throw IllegalArgumentException("Kodein instance not found")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun <T : Any> instance(type: Class<T>): T {
|
||||
return instance(type, null)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package xyz.nulldev.androidcompat.util
|
||||
|
||||
import org.koin.core.Koin
|
||||
import org.koin.mp.KoinPlatformTools
|
||||
|
||||
/**
|
||||
* Helper class to allow access to Kodein from Java
|
||||
*/
|
||||
object KoinGlobalHelper {
|
||||
/**
|
||||
* Get the Kodein object
|
||||
*/
|
||||
@JvmStatic
|
||||
fun koin() = KoinPlatformTools.defaultContext().get()
|
||||
|
||||
/**
|
||||
* Get a dependency
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> instance(
|
||||
type: Class<T>,
|
||||
koin: Koin? = null,
|
||||
): T = (koin ?: koin()).get(type.kotlin)
|
||||
|
||||
@JvmStatic
|
||||
fun <T : Any> instance(type: Class<T>): T = instance(type, null)
|
||||
}
|
||||
@@ -18,7 +18,9 @@ class CookieManagerImpl : CookieManager() {
|
||||
acceptCookie = accept
|
||||
}
|
||||
|
||||
override fun acceptCookie(): Boolean = acceptCookie
|
||||
override fun acceptCookie(): Boolean {
|
||||
return acceptCookie
|
||||
}
|
||||
|
||||
override fun setAcceptThirdPartyCookies(
|
||||
webview: WebView?,
|
||||
@@ -27,7 +29,9 @@ class CookieManagerImpl : CookieManager() {
|
||||
acceptThirdPartyCookies = accept
|
||||
}
|
||||
|
||||
override fun acceptThirdPartyCookies(webview: WebView?): Boolean = acceptThirdPartyCookies
|
||||
override fun acceptThirdPartyCookies(webview: WebView?): Boolean {
|
||||
return acceptThirdPartyCookies
|
||||
}
|
||||
|
||||
override fun setCookie(
|
||||
url: String,
|
||||
@@ -61,8 +65,7 @@ class CookieManagerImpl : CookieManager() {
|
||||
} else {
|
||||
URI("http://$url")
|
||||
}
|
||||
return cookieHandler.cookieStore
|
||||
.get(uri)
|
||||
return cookieHandler.cookieStore.get(uri)
|
||||
.joinToString("; ") { "${it.name}=${it.value}" }
|
||||
}
|
||||
|
||||
@@ -84,11 +87,15 @@ class CookieManagerImpl : CookieManager() {
|
||||
callback?.onReceiveValue(removedCookies)
|
||||
}
|
||||
|
||||
override fun hasCookies(): Boolean = cookieHandler.cookieStore.cookies.isNotEmpty()
|
||||
override fun hasCookies(): Boolean {
|
||||
return cookieHandler.cookieStore.cookies.isNotEmpty()
|
||||
}
|
||||
|
||||
override fun flush() {}
|
||||
|
||||
override fun allowFileSchemeCookiesImpl(): Boolean = allowFileSchemeCookies
|
||||
override fun allowFileSchemeCookiesImpl(): Boolean {
|
||||
return allowFileSchemeCookies
|
||||
}
|
||||
|
||||
override fun setAcceptFileSchemeCookiesImpl(accept: Boolean) {
|
||||
allowFileSchemeCookies = acceptCookie
|
||||
|
||||
1007
CHANGELOG.md
1007
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,9 @@ Checkout [This Kanban Board](https://github.com/Suwayomi/Suwayomi-Server/project
|
||||
- We hate big pull requests, make them as small as possible, change one meaningful thing. Spam pull requests, we don't mind.
|
||||
|
||||
### Project goals and vision
|
||||
- Porting Mihon (Tachiyomi) and covering its features
|
||||
- Syncing with Mihon (Tachiyomi), [main issue](https://github.com/Suwayomi/Suwayomi-Server/issues/159)
|
||||
- Generally rejecting features that Mihon (Tachiyomi) (main app) doesn't have,
|
||||
- Porting Tachiyomi and covering its features
|
||||
- Syncing with Tachiyomi, [main issue](https://github.com/Suwayomi/Suwayomi-Server/issues/159)
|
||||
- Generally rejecting features that Tachiyomi(main app) doesn't have,
|
||||
- Unless it's something that makes sense for desktop sizes or desktop form factor (keyboard + mouse)
|
||||
- Additional/crazy features can go in forks and alternative clients
|
||||
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI) should
|
||||
@@ -19,11 +19,13 @@ Checkout [This Kanban Board](https://github.com/Suwayomi/Suwayomi-Server/project
|
||||
|
||||
## How does Suwayomi-Server work?
|
||||
This project has two components:
|
||||
1. **Server:** contains the implementation of [Mihon (Tachiyomi)'s source library](https://github.com/mihonapp/mihon/tree/main/source-api) and uses an Android compatibility library to run jar libraries converted from apk extensions. All this concludes to serving a GraphQL API.
|
||||
1. **Server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run jar libraries converted from apk extensions. All this concludes to serving a GraphQL API.
|
||||
2. **WebUI:** A React SPA(`create-react-app`) project that works with the server to do the presentation located at https://github.com/Suwayomi/Suwayomi-WebUI
|
||||
|
||||
### API
|
||||
#### GraphQL
|
||||
*Only available in the preview at the moment*
|
||||
|
||||
The GraphQL API can be queried with a POST request to `/api/graphql`. There is also the GraphiQL IDE accessible by the browser at `/api/graphql` to perform ad-hoc queries and explore the API.
|
||||
|
||||
#### REST
|
||||
|
||||
93
README.md
93
README.md
@@ -5,8 +5,8 @@
|
||||
|
||||
## Table of Content
|
||||
- [What is Suwayomi?](#what-is-suwayomi)
|
||||
- [Features](#Features)
|
||||
- [Suwayomi client projects](#Suwayomi-client-projects)
|
||||
* [Is this application usable? Should I test it?](#is-this-application-usable-should-i-test-it)
|
||||
- [Downloading and Running the app](#downloading-and-running-the-app)
|
||||
* [Using Operating System Specific Bundles](#using-operating-system-specific-bundles)
|
||||
- [Launcher Scripts](#launcher-scripts)
|
||||
@@ -20,7 +20,7 @@
|
||||
* [Advanced Methods](#advanced-methods)
|
||||
+ [Running the jar release directly](#running-the-jar-release-directly)
|
||||
+ [Using Suwayomi Remotely](#using-suwayomi-remotely)
|
||||
- [Syncing With Mihon (Tachiyomi)](#syncing-with-mihon-tachiyomi)
|
||||
- [Syncing With Tachiyomi](#syncing-with-tachiyomi)
|
||||
- [Troubleshooting and Support](#troubleshooting-and-support)
|
||||
- [Contributing and Technical info](#contributing-and-technical-info)
|
||||
- [Credit](#credit)
|
||||
@@ -30,47 +30,40 @@
|
||||
# What is Suwayomi?
|
||||
<img src="https://github.com/Suwayomi/Suwayomi-Server/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
||||
|
||||
A free and open source manga reader server that runs extensions built for [Mihon (Tachiyomi)](https://mihon.app/).
|
||||
A free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||
|
||||
Suwayomi is an independent Mihon (Tachiyomi) compatible software and is **not a Fork of** Mihon (Tachiyomi).
|
||||
Suwayomi is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
|
||||
|
||||
Suwayomi-Server is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
|
||||
|
||||
You can use Mihon (Tachiyomi) to access your Suwayomi-Server. For more info look [here](#syncing-with-mihon-tachiyomi).
|
||||
|
||||
## Features
|
||||
> [!NOTE]
|
||||
>
|
||||
> These are capabilities of Suwayomi-Server, the actual working support is provided by each front-end app, checkout their respective readme for more info.
|
||||
|
||||
- Installing and executing Mihon (Tachiyomi)'s Extensions, So you'll get the same sources
|
||||
- Searching and browsing installed sources
|
||||
- A library to save your mangas and categories to put them into
|
||||
- Automated library updates to check for new chapters
|
||||
- Automated download of new chapters
|
||||
- Viewing latest updated chapters
|
||||
- Ability to download Manga for offline read
|
||||
- Backup and restore support powered by Mihon (Tachiyomi)-compatible Backups
|
||||
- Automated backup creations
|
||||
- Tracking via [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [MangaUpdates](https://www.mangaupdates.com/)
|
||||
- [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) support to bypass Cloudflare protection
|
||||
- Automated WebUI updates (supports the default WebUI and VUI)
|
||||
Ability to sync with Tachiyomi is a planned feature, for more info look [here](#syncing-with-tachiyomi).
|
||||
|
||||
# Suwayomi client projects
|
||||
**You need a client/user interface app as a front-end for Suwayomi-Server, if you [Directly Download Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/releases/latest) you'll get a bundled version of [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI) with it.**
|
||||
|
||||
Here's a list of known clients/user interfaces for Suwayomi-Server (checkout the respective GitHub repository for their features):
|
||||
Here's a list of known clients/user interfaces for Suwayomi-Server:
|
||||
##### Actively Developed Clients
|
||||
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): The web front-end that Suwayomi-Server ships with by default.
|
||||
- [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): A Suwayomi-Server preview focused web frontend built with svelte
|
||||
- [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): A Web front-end for Suwayomi-Server built with Vaadin.
|
||||
##### Inactive Clients (functional but outdated)
|
||||
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Suwayomi-Server.
|
||||
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android with a User Interface inspired by Mihon (Tachiyomi).
|
||||
##### Abandoned Clients (functionality unknown)
|
||||
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): The web/ElectronJS front-end that Suwayomi-Server ships with by default.
|
||||
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Suwayomi-Server. Currently, the most advanced.
|
||||
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), feature support is basic.
|
||||
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client.
|
||||
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android with a User Interface inspired by Tachiyomi.
|
||||
- [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): A Web front-end for Suwayomi-Server built with Vaadin.
|
||||
- [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): A preview focused web frontend built with svelte with some features the other UIs might not have (migration)
|
||||
##### Inctive/Abandoned Clients
|
||||
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js.
|
||||
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client.
|
||||
|
||||
## Is this application usable? Should I test it?
|
||||
Here is a list of current features:
|
||||
|
||||
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources
|
||||
- A library to save your mangas and categories to put them into
|
||||
- Searching and browsing installed sources
|
||||
- Ability to download Manga for offline read
|
||||
- Backup and restore support powered by Tachiyomi-compatible Backups
|
||||
- Viewing latest updated chapters.
|
||||
|
||||
**Note:** These are capabilities of Suwayomi-Server, the actual working support is provided by each front-end app, checkout their respective readme for more info.
|
||||
|
||||
# Downloading and Running the app
|
||||
## Using Operating System Specific Bundles
|
||||
@@ -79,7 +72,7 @@ To facilitate the use of Suwayomi we provide bundle releases that include The Ja
|
||||
If a bundle for your operating system or cpu architecture is not provided then refer to [Advanced Methods](#advanced-methods)
|
||||
|
||||
### Windows
|
||||
Download the latest `win64`(Windows 64-bit) release from [the releases section](https://github.com/Suwayomi/Suwayomi-Server/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Suwayomi-Server-preview/releases).
|
||||
Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) release from [the releases section](https://github.com/Suwayomi/Suwayomi-Server/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Suwayomi-Server-preview/releases).
|
||||
|
||||
Unzip the downloaded file and double-click on one of the launcher scripts.
|
||||
|
||||
@@ -94,9 +87,6 @@ Download the latest `linux-x64`(x86_64) release from [the releases section](http
|
||||
`tar xvf` the downloaded file and double-click on one of the launcher scripts or run them using the terminal.
|
||||
|
||||
## Other methods of getting Suwayomi
|
||||
### Docker
|
||||
Check our Official Docker release [Suwayomi Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Suwayomi Server in a docker container. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk), an example compose file can also be found there. By default, the server will be running on http://localhost:4567 open this url in your browser.
|
||||
|
||||
### Arch Linux
|
||||
You can install Suwayomi from the AUR:
|
||||
```
|
||||
@@ -118,26 +108,23 @@ sudo apt update
|
||||
sudo apt install suwayomi-server
|
||||
```
|
||||
|
||||
### NixOS
|
||||
You can deploy Suwayomi on NixOS using the module `services.suwayomi-server` in your configuration:
|
||||
### Docker
|
||||
Check our Official Docker release [Suwayomi Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Suwayomi Server in a docker container. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk). By default, the server will be running on http://localhost:4567 open this url in your browser.
|
||||
|
||||
Install from the command line:
|
||||
```
|
||||
{
|
||||
services.suwayomi-server = {
|
||||
enable = true;
|
||||
};
|
||||
}
|
||||
$ docker pull ghcr.io/suwayomi/tachidesk
|
||||
```
|
||||
Run Container from the command line:
|
||||
```
|
||||
$ docker run -p 4567:4567 ghcr.io/suwayomi/tachidesk
|
||||
```
|
||||
|
||||
For more information, see [the NixOS manual](https://nixos.org/manual/nixos/stable/#module-services-suwayomi-server).
|
||||
|
||||
You can also directly use the package from [nixpkgs](https://search.nixos.org/packages?channel=unstable&type=packages&query=suwayomi-server).
|
||||
|
||||
## Advanced Methods
|
||||
### Running the jar release directly
|
||||
In order to run the app you need the following:
|
||||
- The jar release of Suwayomi-Server
|
||||
- The Java Runtime Environment(JRE) 21 or newer
|
||||
- The Java Runtime Environment(JRE) 8 or newer
|
||||
- A Browser like Google Chrome, Firefox, Edge, etc.
|
||||
- ElectronJS (optional)
|
||||
|
||||
@@ -152,15 +139,13 @@ Check out [this wiki page](https://github.com/Suwayomi/Suwayomi-Server/wiki/Conf
|
||||
|
||||
If you face issues with your setup then we are happy to provide help, just join our discord server(a discord badge is on the top of the page, you are just a click-clack away!).
|
||||
|
||||
## Syncing With Mihon (Tachiyomi)
|
||||
## Syncing With Tachiyomi
|
||||
### The Suwayomi extension and tracker
|
||||
- You can install the `Suwayomi` extension inside Mihon (Tachiyomi).
|
||||
- You can install the `Suwayomi` extension inside tachiyomi.
|
||||
- The extension will load your Suwayomi library.
|
||||
- By manipulating extension search filters you can browse your categories.
|
||||
- You can enable the Suwayomi tracker to track reading progress with your Suwayomi server.
|
||||
- Note: to sync from
|
||||
- Mihon (Tachiyomi) to Suwayomi: Mihon (Tachiyomi) automatically updates the chapters read status when it's updating the tracker (e.g. while reading)
|
||||
- Suwayomi to Mihon (Tachiyomi): To sync Mihon (Tachiyomi) with Suwayomi, you have to open the manga's track information, then, Mihon (Tachiyomi) will automatically update its chapter list with the state from Suwayomi
|
||||
- Note: Tachiyomi [only allows tracking one way](https://github.com/tachiyomiorg/tachiyomi/issues/1626), meaning that by reading chapters on other Suwayomi clients the last read chapter number will update on the tracker but tachiyomi won't automatically mark them as read for you.
|
||||
|
||||
### Other methods
|
||||
Checkout [this issue](https://github.com/Suwayomi/Suwayomi-Server/issues/159) for tracking progress.
|
||||
@@ -176,7 +161,7 @@ This project is a spiritual successor of [TachiWeb-Server](https://github.com/Ta
|
||||
|
||||
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0` and `Copyright 2019 Andy Bao and contributors`.
|
||||
|
||||
Parts of [Mihon (Tachiyomi)](https://github.com/mihonapp/mihon) is adopted into this codebase, also licensed under `Apache License Version 2.0` and `Copyright 2015 Javier Tomás`.
|
||||
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0` and `Copyright 2015 Javier Tomás`.
|
||||
|
||||
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
||||
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
||||
import org.jlleitschuh.gradle.ktlint.KtlintPlugin
|
||||
@@ -27,8 +26,8 @@ allprojects {
|
||||
subprojects {
|
||||
plugins.withType<JavaPlugin> {
|
||||
extensions.configure<JavaPluginExtension> {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +43,12 @@ subprojects {
|
||||
tasks {
|
||||
withType<KotlinJvmCompile> {
|
||||
dependsOn("ktlintFormat")
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_21
|
||||
freeCompilerArgs.add("-Xcontext-receivers")
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
|
||||
freeCompilerArgs += listOf(
|
||||
"-Xcontext-receivers",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,14 @@ import java.io.BufferedReader
|
||||
const val MainClass = "suwayomi.tachidesk.MainKt"
|
||||
|
||||
// should be bumped with each stable release
|
||||
val getTachideskVersion = { "v2.0.${getCommitCount()}" }
|
||||
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.7.0"
|
||||
|
||||
val webUIRevisionTag = "r2467"
|
||||
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r1397"
|
||||
|
||||
private val getCommitCount = {
|
||||
// counts commits on the current checked out branch
|
||||
val getTachideskRevision = {
|
||||
runCatching {
|
||||
ProcessBuilder()
|
||||
System.getenv("ProductRevision") ?: ProcessBuilder()
|
||||
.command("git", "rev-list", "HEAD", "--count")
|
||||
.start()
|
||||
.let { process ->
|
||||
@@ -25,11 +26,8 @@ private val getCommitCount = {
|
||||
it.bufferedReader().use(BufferedReader::readText)
|
||||
}
|
||||
process.destroy()
|
||||
output.trim()
|
||||
"r" + output.trim()
|
||||
}
|
||||
}.getOrDefault("0")
|
||||
}.getOrDefault("r0")
|
||||
}
|
||||
|
||||
// counts commits on the current checked out branch
|
||||
val getTachideskRevision = { "r${getCommitCount()}" }
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
[versions]
|
||||
kotlin = "2.1.20"
|
||||
coroutines = "1.10.1"
|
||||
serialization = "1.8.0"
|
||||
okhttp = "5.0.0-alpha.14" # Major version is locked by Tachiyomi extensions
|
||||
javalin = "6.5.0"
|
||||
jackson = "2.18.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
exposed = "0.59.0"
|
||||
kotlin = "1.9.10"
|
||||
coroutines = "1.7.3"
|
||||
serialization = "1.6.0"
|
||||
okhttp = "5.0.0-alpha.11" # Major version is locked by Tachiyomi extensions
|
||||
javalin = "4.6.8" # Javalin 5.0.0+ requires Java 11
|
||||
jackson = "2.13.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
exposed = "0.40.1"
|
||||
dex2jar = "v64" # Stuck until https://github.com/ThexXTURBOXx/dex2jar/issues/27 is fixed
|
||||
polyglot = "24.2.0"
|
||||
settings = "1.3.0"
|
||||
twelvemonkeys = "3.12.0"
|
||||
graphqlkotlin = "8.4.0"
|
||||
xmlserialization = "0.90.3"
|
||||
ktlint = "1.5.0"
|
||||
koin = "4.0.2"
|
||||
rhino = "1.7.14"
|
||||
settings = "1.0.0-RC"
|
||||
twelvemonkeys = "3.9.4"
|
||||
graphqlkotlin = "6.5.6"
|
||||
xmlserialization = "0.86.2"
|
||||
ktlint = "1.0.0"
|
||||
|
||||
[libraries]
|
||||
# Kotlin
|
||||
@@ -30,20 +29,20 @@ coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", ve
|
||||
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
||||
serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization" }
|
||||
serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization" }
|
||||
serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core", version.ref = "xmlserialization" }
|
||||
serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core-jvm", version.ref = "xmlserialization" }
|
||||
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" }
|
||||
|
||||
# Logging
|
||||
slf4japi = "org.slf4j:slf4j-api:2.0.17"
|
||||
logback = "ch.qos.logback:logback-classic:1.5.18"
|
||||
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:7.0.5"
|
||||
slf4japi = "org.slf4j:slf4j-api:2.0.9"
|
||||
logback = "ch.qos.logback:logback-classic:1.3.11"
|
||||
kotlinlogging = "io.github.microutils:kotlin-logging:3.0.5"
|
||||
|
||||
# OkHttp
|
||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
|
||||
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
|
||||
okio = "com.squareup.okio:okio:3.10.2"
|
||||
okio = "com.squareup.okio:okio:3.3.0"
|
||||
|
||||
# Javalin api
|
||||
javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" }
|
||||
@@ -55,8 +54,7 @@ jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations
|
||||
# GraphQL
|
||||
graphql-kotlin-server = { module = "com.expediagroup:graphql-kotlin-server", version.ref = "graphqlkotlin" }
|
||||
graphql-kotlin-scheme = { module = "com.expediagroup:graphql-kotlin-schema-generator", version.ref = "graphqlkotlin" }
|
||||
graphql-java-core = "com.graphql-java:graphql-java:22.3" # Major version locked by graphql-kotlin
|
||||
graphql-java-scalars = "com.graphql-java:graphql-java-extended-scalars:22.0"
|
||||
graphql-scalars = "com.graphql-java:graphql-java-extended-scalars:20.2"
|
||||
|
||||
# Exposed ORM
|
||||
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
||||
@@ -66,24 +64,24 @@ exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version
|
||||
h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration
|
||||
|
||||
# Exposed Migrations
|
||||
exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.7.0"
|
||||
exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.2.0"
|
||||
|
||||
# Dependency Injection
|
||||
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||
kodein = "org.kodein.di:kodein-di-conf-jvm:7.20.2"
|
||||
|
||||
# tray icon
|
||||
systemtray-core = "com.dorkbox:SystemTray:4.4"
|
||||
systemtray-utils = "com.dorkbox:Utilities:1.46" # version locked by SystemTray
|
||||
systemtray-desktop = "com.dorkbox:Desktop:1.1" # version locked by SystemTray
|
||||
systemtray-core = "com.dorkbox:SystemTray:4.2.1"
|
||||
systemtray-utils = "com.dorkbox:Utilities:1.39" # version locked by SystemTray
|
||||
systemtray-desktop = "com.dorkbox:Desktop:1.0"
|
||||
|
||||
# dependencies of Tachiyomi extensions
|
||||
injekt = "com.github.null2264:injekt-koin:ee267b2e27"
|
||||
injekt = "com.github.inorichi.injekt:injekt-core:65b0440"
|
||||
rxjava = "io.reactivex:rxjava:1.3.8"
|
||||
jsoup = "org.jsoup:jsoup:1.19.1"
|
||||
jsoup = "org.jsoup:jsoup:1.16.1"
|
||||
|
||||
# Config
|
||||
config = "com.typesafe:config:1.4.3"
|
||||
config4k = "io.github.config4k:config4k:0.7.0"
|
||||
config = "com.typesafe:config:1.4.2"
|
||||
config4k = "io.github.config4k:config4k:0.5.0"
|
||||
|
||||
# Sort
|
||||
sort = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
|
||||
@@ -98,34 +96,33 @@ dex2jar-tools = { module = "com.github.ThexXTURBOXx.dex2jar:dex-tools", version.
|
||||
|
||||
# APK
|
||||
apk-parser = "net.dongliu:apk-parser:2.6.10"
|
||||
apksig = "com.android.tools.build:apksig:8.9.0"
|
||||
apksig = "com.android.tools.build:apksig:7.2.1"
|
||||
|
||||
# Xml
|
||||
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
||||
|
||||
# Disk & File
|
||||
appdirs = "net.harawata:appdirs:1.4.0"
|
||||
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
|
||||
appdirs = "net.harawata:appdirs:1.2.1"
|
||||
zip4j = "net.lingala.zip4j:zip4j:2.11.5"
|
||||
commonscompress = "org.apache.commons:commons-compress:1.27.1"
|
||||
commonscompress = "org.apache.commons:commons-compress:1.24.0"
|
||||
junrar = "com.github.junrar:junrar:7.5.5"
|
||||
|
||||
# AES/CBC/PKCS7Padding Cypher provider
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.80"
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.76"
|
||||
|
||||
# AndroidX annotations
|
||||
android-annotations = "androidx.annotation:annotation:1.9.1"
|
||||
android-annotations = "androidx.annotation:annotation:1.7.0"
|
||||
|
||||
# Substitute for duktape-android
|
||||
polyglot-core = { module = "org.graalvm.polyglot:polyglot", version.ref = "polyglot" }
|
||||
polyglot-graaljs = { module = "org.graalvm.polyglot:js-community", version.ref = "polyglot" }
|
||||
rhino-runtime = { module = "org.mozilla:rhino-runtime", version.ref = "rhino" } # slimmer version of 'org.mozilla:rhino'
|
||||
rhino-engine = { module = "org.mozilla:rhino-engine", version.ref = "rhino" } # provides the same interface as 'javax.script' a.k.a Nashorn
|
||||
|
||||
# Settings
|
||||
settings-core = { module = "com.russhwolf:multiplatform-settings-jvm", version.ref = "settings" }
|
||||
settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization-jvm", version.ref = "settings" }
|
||||
|
||||
# ICU4J
|
||||
icu4j = "com.ibm.icu:icu4j:77.1"
|
||||
icu4j = "com.ibm.icu:icu4j:73.2"
|
||||
|
||||
# Image Decoding implementation provider
|
||||
twelvemonkeys-common-lang = { module = "com.twelvemonkeys.common:common-lang", version.ref = "twelvemonkeys" }
|
||||
@@ -137,7 +134,7 @@ twelvemonkeys-imageio-jpeg = { module = "com.twelvemonkeys.imageio:imageio-jpeg"
|
||||
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
|
||||
|
||||
# Testing
|
||||
mockk = "io.mockk:mockk:1.13.17"
|
||||
mockk = "io.mockk:mockk:1.13.7"
|
||||
|
||||
# cron scheduler
|
||||
cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5"
|
||||
@@ -145,22 +142,19 @@ cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5"
|
||||
# cron-utils
|
||||
cronUtils = "com.cronutils:cron-utils:9.2.1"
|
||||
|
||||
# lint - used for renovate to update ktlint version
|
||||
ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
|
||||
|
||||
[plugins]
|
||||
# Kotlin
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"}
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
|
||||
|
||||
# Linter
|
||||
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "12.2.0"}
|
||||
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "11.6.0"}
|
||||
|
||||
# Build config
|
||||
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "5.5.4"}
|
||||
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "3.1.0"}
|
||||
|
||||
# Download
|
||||
download = { id = "de.undercouch.download", version = "5.6.0"}
|
||||
download = { id = "de.undercouch.download", version = "5.4.0"}
|
||||
|
||||
# ShadowJar
|
||||
shadowjar = { id = "com.github.johnrengelman.shadow", version = "8.1.1"}
|
||||
@@ -174,7 +168,7 @@ shared = [
|
||||
"serialization-json",
|
||||
"serialization-json-okio",
|
||||
"serialization-protobuf",
|
||||
"koin-core",
|
||||
"kodein",
|
||||
"slf4japi",
|
||||
"logback",
|
||||
"kotlinlogging",
|
||||
@@ -202,7 +196,7 @@ okhttp = [
|
||||
]
|
||||
javalin = [
|
||||
"javalin-core",
|
||||
#"javalin-openapi",
|
||||
"javalin-openapi",
|
||||
]
|
||||
jackson = [
|
||||
"jackson-databind",
|
||||
@@ -220,9 +214,9 @@ systemtray = [
|
||||
"systemtray-utils",
|
||||
"systemtray-desktop"
|
||||
]
|
||||
polyglot = [
|
||||
"polyglot-core",
|
||||
"polyglot-graaljs",
|
||||
rhino = [
|
||||
"rhino-runtime",
|
||||
"rhino-engine",
|
||||
]
|
||||
settings = [
|
||||
"settings-core",
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
37
gradlew
vendored
37
gradlew
vendored
@@ -15,8 +15,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -57,7 +55,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -82,11 +80,13 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -133,29 +133,22 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -200,15 +193,11 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
||||
23
gradlew.bat
vendored
23
gradlew.bat
vendored
@@ -13,8 +13,6 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -28,7 +26,6 @@ if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -45,11 +42,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -59,11 +56,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"semanticCommits": "disabled",
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"fileMatch": [
|
||||
"scripts/bundler.sh"
|
||||
],
|
||||
"matchStrings": [
|
||||
"JRE_RELEASE=[\"'](?<currentValue>.+?)[\"']\\s+"
|
||||
],
|
||||
"datasourceTemplate": "github-releases",
|
||||
"depNameTemplate": "adoptium/temurin21-binaries",
|
||||
"versioningTemplate": "regex:^jdk-?(?<major>\\d+).(?<minor>\\d+).+?(?<patch>[\\d+]+)$"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -28,7 +28,7 @@ main() {
|
||||
OS="$1"
|
||||
JAR="$(ls server/build/*.jar | tail -n1)"
|
||||
RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS"
|
||||
RELEASE_VERSION=$(echo "$JAR" | grep -oP "v\K[0-9]+\.[0-9]+\.[0-9]+")
|
||||
RELEASE_VERSION="$(tmp="${JAR%-*}"; echo "${tmp##*-}" | tr -d v)"
|
||||
#RELEASE_REVISION_NUMBER="$(tmp="${JAR%.*}" && echo "${tmp##*-}" | tr -d r)"
|
||||
local electron_version="v28.1.3"
|
||||
|
||||
@@ -51,64 +51,74 @@ main() {
|
||||
move_release_to_output_dir
|
||||
;;
|
||||
linux-x64)
|
||||
# https://github.com/adoptium/temurin21-binaries/releases/
|
||||
JRE_RELEASE="jdk-21.0.6+7"
|
||||
JRE="OpenJDK21U-jre_x64_linux_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').tar.gz"
|
||||
# https://github.com/adoptium/temurin8-binaries/releases/
|
||||
JRE_RELEASE="jdk8u392-b08"
|
||||
JRE="OpenJDK8U-jre_x64_linux_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g').tar.gz"
|
||||
JRE_DIR="$JRE_RELEASE-jre"
|
||||
JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
|
||||
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE"
|
||||
ELECTRON="electron-$electron_version-linux-x64.zip"
|
||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||
download_electron
|
||||
setup_jre
|
||||
tree "$RELEASE_NAME"
|
||||
download_jre_and_electron
|
||||
|
||||
RELEASE="$RELEASE_NAME.tar.gz"
|
||||
make_linux_bundle
|
||||
move_release_to_output_dir
|
||||
;;
|
||||
macOS-x64)
|
||||
# https://github.com/adoptium/temurin21-binaries/releases/
|
||||
JRE_RELEASE="jdk-21.0.6+7"
|
||||
JRE="OpenJDK21U-jre_x64_mac_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').tar.gz"
|
||||
# https://github.com/adoptium/temurin8-binaries/releases/
|
||||
JRE_RELEASE="jdk8u392-b08"
|
||||
JRE="OpenJDK8U-jre_x64_mac_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g').tar.gz"
|
||||
JRE_DIR="$JRE_RELEASE-jre"
|
||||
JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
|
||||
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE"
|
||||
ELECTRON="electron-$electron_version-darwin-x64.zip"
|
||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||
download_electron
|
||||
setup_jre
|
||||
tree "$RELEASE_NAME"
|
||||
download_jre_and_electron
|
||||
|
||||
RELEASE="$RELEASE_NAME.tar.gz"
|
||||
RELEASE="$RELEASE_NAME.zip"
|
||||
make_macos_bundle
|
||||
move_release_to_output_dir
|
||||
;;
|
||||
macOS-arm64)
|
||||
# https://github.com/adoptium/temurin21-binaries/releases/
|
||||
JRE_RELEASE="jdk-21.0.6+7"
|
||||
JRE="OpenJDK21U-jre_aarch64_mac_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').tar.gz"
|
||||
JRE_DIR="$JRE_RELEASE-jre"
|
||||
JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
|
||||
# https://cdn.azul.com/zulu/bin/
|
||||
JRE="zulu8.74.0.17-ca-jre8.0.392-macosx_aarch64.tar.gz"
|
||||
JRE_RELEASE="zulu8.74.0.17-ca-jre8.0.392-macosx_aarch64"
|
||||
JRE_DIR="$JRE_RELEASE/zulu-8.jre"
|
||||
JRE_URL="https://cdn.azul.com/zulu/bin/$JRE"
|
||||
ELECTRON="electron-$electron_version-darwin-arm64.zip"
|
||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||
download_electron
|
||||
setup_jre
|
||||
tree "$RELEASE_NAME"
|
||||
download_jre_and_electron
|
||||
|
||||
RELEASE="$RELEASE_NAME.tar.gz"
|
||||
RELEASE="$RELEASE_NAME.zip"
|
||||
make_macos_bundle
|
||||
move_release_to_output_dir
|
||||
;;
|
||||
windows-x64)
|
||||
# https://github.com/adoptium/temurin21-binaries/releases/
|
||||
JRE_RELEASE="jdk-21.0.6+7"
|
||||
JRE="OpenJDK21U-jre_x64_windows_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').zip"
|
||||
windows-x86)
|
||||
# https://github.com/adoptium/temurin8-binaries/releases/
|
||||
JRE_RELEASE="jdk8u392-b08"
|
||||
JRE="OpenJDK8U-jre_x86-32_windows_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g').zip"
|
||||
JRE_DIR="$JRE_RELEASE-jre"
|
||||
JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
|
||||
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE"
|
||||
ELECTRON="electron-$electron_version-win32-ia32.zip"
|
||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||
download_jre_and_electron
|
||||
|
||||
RELEASE="$RELEASE_NAME.zip"
|
||||
make_windows_bundle
|
||||
move_release_to_output_dir
|
||||
|
||||
RELEASE="$RELEASE_NAME.msi"
|
||||
make_windows_package
|
||||
move_release_to_output_dir
|
||||
;;
|
||||
windows-x64)
|
||||
# https://github.com/adoptium/temurin8-binaries/releases/
|
||||
JRE_RELEASE="jdk8u392-b08"
|
||||
JRE="OpenJDK8U-jre_x64_windows_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g').zip"
|
||||
JRE_DIR="$JRE_RELEASE-jre"
|
||||
JRE_URL="https://github.com/adoptium/temurin8-binaries/releases/download/$JRE_RELEASE/$JRE"
|
||||
ELECTRON="electron-$electron_version-win32-x64.zip"
|
||||
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
|
||||
download_electron
|
||||
setup_jre
|
||||
tree "$RELEASE_NAME"
|
||||
download_jre_and_electron
|
||||
|
||||
RELEASE="$RELEASE_NAME.zip"
|
||||
make_windows_bundle
|
||||
@@ -138,32 +148,26 @@ download_launcher() {
|
||||
mv "Suwayomi-Launcher.jar" "$RELEASE_NAME/Suwayomi-Launcher.jar"
|
||||
}
|
||||
|
||||
download_electron() {
|
||||
download_jre_and_electron() {
|
||||
if [ ! -f "$JRE" ]; then
|
||||
curl -L "$JRE_URL" -o "$JRE"
|
||||
fi
|
||||
if [ ! -f "$ELECTRON" ]; then
|
||||
curl -L "$ELECTRON_URL" -o "$ELECTRON"
|
||||
fi
|
||||
|
||||
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/"
|
||||
}
|
||||
|
||||
setup_jre() {
|
||||
if [ -d "jre" ]; then
|
||||
chmod +x ./jre/bin/java
|
||||
chmod +x ./jre/lib/jspawnhelper
|
||||
mv "jre" "$RELEASE_NAME/jre"
|
||||
local ext="${JRE##*.}"
|
||||
if [ "$ext" = "zip" ]; then
|
||||
unzip "$JRE"
|
||||
else
|
||||
if [ ! -f "$JRE" ]; then
|
||||
curl -L "$JRE_URL" -o "$JRE"
|
||||
fi
|
||||
|
||||
local ext="${JRE##*.}"
|
||||
if [ "$ext" = "zip" ]; then
|
||||
unzip "$JRE"
|
||||
else
|
||||
tar xvf "$JRE"
|
||||
fi
|
||||
mv "$JRE_DIR" "$RELEASE_NAME/jre"
|
||||
tar xvf "$JRE"
|
||||
fi
|
||||
mv "$JRE_DIR" "$RELEASE_NAME/jre"
|
||||
unzip "$ELECTRON" -d "$RELEASE_NAME/electron/"
|
||||
|
||||
mkdir "$RELEASE_NAME/bin"
|
||||
|
||||
tree
|
||||
}
|
||||
|
||||
copy_linux_package_assets_to() {
|
||||
@@ -180,7 +184,6 @@ copy_linux_package_assets_to() {
|
||||
}
|
||||
|
||||
make_linux_bundle() {
|
||||
mkdir "$RELEASE_NAME/bin"
|
||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
||||
cp "scripts/resources/suwayomi-launcher.sh" "$RELEASE_NAME/"
|
||||
cp "scripts/resources/suwayomi-server.sh" "$RELEASE_NAME/"
|
||||
@@ -189,11 +192,10 @@ make_linux_bundle() {
|
||||
}
|
||||
|
||||
make_macos_bundle() {
|
||||
mkdir "$RELEASE_NAME/bin"
|
||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
||||
cp "scripts/resources/Suwayomi Launcher.command" "$RELEASE_NAME/"
|
||||
|
||||
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
|
||||
zip -9 -r "$RELEASE" "$RELEASE_NAME/"
|
||||
}
|
||||
|
||||
# https://wiki.debian.org/SimplePackagingTutorial
|
||||
@@ -253,7 +255,6 @@ make_windows_bundle() {
|
||||
#WINEARCH=win32 wine "$rcedit" "$RELEASE_NAME/electron/electron.exe" \
|
||||
# --set-icon "$icon"
|
||||
|
||||
mkdir "$RELEASE_NAME/bin"
|
||||
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
|
||||
cp "scripts/resources/Suwayomi Launcher.bat" "$RELEASE_NAME"
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
cd "`dirname "$0"`"
|
||||
|
||||
./jre/bin/java -jar Suwayomi-Launcher.jar
|
||||
./jre/Contents/Home/bin/java -jar Suwayomi-Launcher.jar
|
||||
@@ -8,7 +8,7 @@ Homepage: https://github.com/Suwayomi/Suwayomi-Server
|
||||
|
||||
Package: suwayomi-server
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, openjdk-21-jre, libc++-dev
|
||||
Depends: ${misc:Depends}, java8-runtime, libc++-dev
|
||||
Description: Manga Reader
|
||||
A free and open source manga reader server that runs extensions built for Tachiyomi.
|
||||
Suwayomi is an independent Tachiyomi compatible software and is not a Fork of Tachiyomi.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product Id="*" UpgradeCode="174c8f36-0bec-4585-9ddd-469c3d889dc1"
|
||||
<Product Id="*" UpgradeCode="*"
|
||||
Version="$(var.ProductVersion)" Language="1033" Name="Suwayomi Server" Manufacturer="Suwayomi">
|
||||
<Package InstallerVersion="300" Compressed="yes" />
|
||||
<Media Id="1" Cabinet="Suwayomi_Server.cab" EmbedCab="yes" />
|
||||
@@ -9,8 +9,6 @@
|
||||
VersionNT64
|
||||
</Condition>
|
||||
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
|
||||
|
||||
<!-- Directory -->
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ProgramFiles64Folder">
|
||||
@@ -50,10 +48,6 @@
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<RemoveExistingProducts After="InstallValidate" />
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<!-- Feature -->
|
||||
<Feature Id="Suwayomi_Server" Title="Suwayomi-Server" Level="1">
|
||||
<ComponentGroupRef Id="jre" />
|
||||
|
||||
60
scripts/resources/msi/suwayomi-server-x86.wxs
Normal file
60
scripts/resources/msi/suwayomi-server-x86.wxs
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product Id="*" UpgradeCode="*"
|
||||
Version="$(var.ProductVersion)" Language="1033" Name="Suwayomi Server" Manufacturer="Suwayomi">
|
||||
<Package InstallerVersion="300" Compressed="yes" />
|
||||
<Media Id="1" Cabinet="Suwayomi_Server.cab" EmbedCab="yes" />
|
||||
|
||||
<!-- Directory -->
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ProgramFilesFolder">
|
||||
<Directory Id="INSTALLDIR" Name="Suwayomi-Server" >
|
||||
<Directory Id="jre"/>
|
||||
<Directory Id="electron"/>
|
||||
<Directory Id="bin"/>
|
||||
</Directory>
|
||||
</Directory>
|
||||
<Directory Id="ProgramMenuFolder">
|
||||
<Directory Id="ProgramMenuDir" Name="Suwayomi-Server">
|
||||
<Component Id="ProgramMenuDir" Guid="*">
|
||||
<RemoveFolder Id="ProgramMenuDir" On="uninstall"/>
|
||||
<RegistryValue Root="HKCU" Key="Software\[Manufacturer]\[ProductName]" Type="string" Value="" KeyPath="yes"/>
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
<Directory Id="DesktopFolder" />
|
||||
</Directory>
|
||||
|
||||
<!-- Component -->
|
||||
<DirectoryRef Id="INSTALLDIR">
|
||||
<Component Id="SuwayomiJAR" Guid="*">
|
||||
<File Id="Suwayomi-Launcher.jar" Source="$(var.SourceDir)/Suwayomi-Launcher.jar" KeyPath="yes" />
|
||||
</Component>
|
||||
|
||||
<Component Id="SuwayomiLauncherBAT" Guid="*" Win64="yes">
|
||||
<File Id="SuwayomiLauncher.bat" Source="$(var.SourceDir)/Suwayomi Launcher.bat" KeyPath="yes" >
|
||||
<Shortcut Id="SuwayomiLauncher.lnk" Name="Suwayomi Launcher" Directory="INSTALLDIR"
|
||||
WorkingDirectory="INSTALLDIR" Icon="Suwayomi.ico" IconIndex="0" Advertise="yes" />
|
||||
<Shortcut Id="DesktopSuwayomiLauncher.lnk" Name="Suwayomi Launcher" Directory="DesktopFolder"
|
||||
WorkingDirectory="INSTALLDIR" Icon="Suwayomi.ico" IconIndex="0" Advertise="yes" />
|
||||
<Shortcut Id="ProgramMenuSuwayomiLauncher.lnk" Name="Suwayomi Launcher" Directory="ProgramMenuDir"
|
||||
WorkingDirectory="INSTALLDIR" Icon="Suwayomi.ico" IconIndex="0" Advertise="yes"
|
||||
Description="A free and open source manga reader that runs extensions built for Tachiyomi." />
|
||||
</File>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<!-- Feature -->
|
||||
<Feature Id="Suwayomi_Server" Title="Suwayomi-Server" Level="1">
|
||||
<ComponentGroupRef Id="jre" />
|
||||
<ComponentGroupRef Id="bin" />
|
||||
<ComponentRef Id="SuwayomiJAR" />
|
||||
<ComponentRef Id="SuwayomiLauncherBAT" />
|
||||
<ComponentRef Id="ProgramMenuDir" />
|
||||
<ComponentGroupRef Id="electron" />
|
||||
</Feature>
|
||||
|
||||
<Icon Id="Suwayomi.ico" SourceFile="$(var.Icon)" />
|
||||
<Property Id="ARPPRODUCTICON" Value="Suwayomi.ico" /> <!-- Icon in Add/Remove Programs -->
|
||||
</Product>
|
||||
</Wix>
|
||||
@@ -3,28 +3,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
||||
import java.time.Instant
|
||||
|
||||
plugins {
|
||||
id(
|
||||
libs.plugins.kotlin.jvm
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
id(
|
||||
libs.plugins.kotlin.serialization
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
id(
|
||||
libs.plugins.ktlint
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
id(libs.plugins.kotlin.jvm.get().pluginId)
|
||||
id(libs.plugins.kotlin.serialization.get().pluginId)
|
||||
id(libs.plugins.ktlint.get().pluginId)
|
||||
application
|
||||
alias(libs.plugins.shadowjar)
|
||||
id(
|
||||
libs.plugins.buildconfig
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
id(libs.plugins.buildconfig.get().pluginId)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -43,8 +27,7 @@ dependencies {
|
||||
// GraphQL
|
||||
implementation(libs.graphql.kotlin.server)
|
||||
implementation(libs.graphql.kotlin.scheme)
|
||||
implementation(libs.graphql.java.core)
|
||||
implementation(libs.graphql.java.scalars)
|
||||
implementation(libs.graphql.scalars)
|
||||
|
||||
// Exposed ORM
|
||||
implementation(libs.bundles.exposed)
|
||||
@@ -56,7 +39,7 @@ dependencies {
|
||||
// tray icon
|
||||
implementation(libs.bundles.systemtray)
|
||||
|
||||
// dependencies of Mihon (Tachiyomi) extensions, some are duplicate, keeping it here for reference
|
||||
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
|
||||
implementation(libs.injekt)
|
||||
implementation(libs.okhttp.core)
|
||||
implementation(libs.rxjava)
|
||||
@@ -73,7 +56,6 @@ dependencies {
|
||||
implementation(libs.asm)
|
||||
|
||||
// Disk & File
|
||||
implementation(libs.cache4k)
|
||||
implementation(libs.zip4j)
|
||||
implementation(libs.commonscompress)
|
||||
implementation(libs.junrar)
|
||||
@@ -121,7 +103,7 @@ buildConfig {
|
||||
fun quoteWrap(obj: Any): String = """"$obj""""
|
||||
|
||||
buildConfigField("String", "NAME", quoteWrap(rootProject.name))
|
||||
buildConfigField("String", "VERSION", quoteWrap(getTachideskVersion()))
|
||||
buildConfigField("String", "VERSION", quoteWrap(tachideskVersion))
|
||||
buildConfigField("String", "REVISION", quoteWrap(getTachideskRevision()))
|
||||
buildConfigField("String", "BUILD_TYPE", quoteWrap(if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview"))
|
||||
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
|
||||
@@ -140,15 +122,14 @@ tasks {
|
||||
"Main-Class" to MainClass,
|
||||
"Implementation-Title" to rootProject.name,
|
||||
"Implementation-Vendor" to "The Suwayomi Project",
|
||||
"Specification-Version" to getTachideskVersion(),
|
||||
"Specification-Version" to tachideskVersion,
|
||||
"Implementation-Version" to getTachideskRevision(),
|
||||
)
|
||||
}
|
||||
archiveBaseName.set(rootProject.name)
|
||||
archiveVersion.set(getTachideskVersion())
|
||||
archiveClassifier.set("")
|
||||
archiveVersion.set(tachideskVersion)
|
||||
archiveClassifier.set(getTachideskRevision())
|
||||
destinationDirectory.set(File("$rootDir/server/build"))
|
||||
mergeServiceFiles()
|
||||
}
|
||||
|
||||
test {
|
||||
@@ -160,10 +141,11 @@ tasks {
|
||||
}
|
||||
|
||||
withType<KotlinJvmCompile> {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add(
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
kotlinOptions {
|
||||
freeCompilerArgs +=
|
||||
listOf(
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,16 @@ package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.InjektScope
|
||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||
|
||||
open class App : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Injekt = InjektScope(DefaultRegistrar())
|
||||
Injekt.importModule(AppModule(this))
|
||||
|
||||
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,7 @@ object AppInfo {
|
||||
*
|
||||
* @since extension-lib 1.3
|
||||
*/
|
||||
fun getVersionCode() =
|
||||
BuildConfig.VERSION
|
||||
.replace("v", "")
|
||||
.split('.')
|
||||
.joinToString("")
|
||||
.toInt()
|
||||
fun getVersionCode() = BuildConfig.REVISION.substring(1).toInt()
|
||||
|
||||
/**
|
||||
* should be something like "0.13.1"
|
||||
|
||||
@@ -19,16 +19,17 @@ import android.app.Application
|
||||
import eu.kanade.tachiyomi.network.JavaScriptEngine
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import nl.adaptivity.xmlutil.XmlDeclMode
|
||||
import nl.adaptivity.xmlutil.core.XmlVersion
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addSingleton
|
||||
import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
fun createAppModule(app: Application): Module {
|
||||
return module {
|
||||
single { app }
|
||||
class AppModule(val app: Application) : InjektModule {
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
// addSingletonFactory { PreferencesHelper(app) }
|
||||
//
|
||||
@@ -38,9 +39,9 @@ fun createAppModule(app: Application): Module {
|
||||
//
|
||||
// addSingletonFactory { CoverCache(app) }
|
||||
|
||||
single { NetworkHelper(app) }
|
||||
addSingletonFactory { NetworkHelper(app) }
|
||||
|
||||
single { JavaScriptEngine(app) }
|
||||
addSingletonFactory { JavaScriptEngine(app) }
|
||||
|
||||
// addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } }
|
||||
//
|
||||
@@ -52,38 +53,23 @@ fun createAppModule(app: Application): Module {
|
||||
//
|
||||
// addSingletonFactory { LibrarySyncManager(app) }
|
||||
|
||||
single {
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
}
|
||||
}
|
||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
single {
|
||||
XML {
|
||||
defaultPolicy {
|
||||
ignoreUnknownChildren()
|
||||
}
|
||||
autoPolymorphic = true
|
||||
xmlDeclMode = XmlDeclMode.Charset
|
||||
indent = 2
|
||||
xmlVersion = XmlVersion.XML10
|
||||
}
|
||||
}
|
||||
|
||||
single {
|
||||
ProtoBuf
|
||||
}
|
||||
}
|
||||
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
|
||||
// rxAsync { get<PreferencesHelper>() }
|
||||
|
||||
// rxAsync {
|
||||
rxAsync { get<NetworkHelper>() }
|
||||
|
||||
rxAsync {
|
||||
// get<SourceManager>()
|
||||
// get<DownloadManager>()
|
||||
// }
|
||||
}
|
||||
|
||||
// rxAsync { get<DatabaseHelper>() }
|
||||
}
|
||||
|
||||
private fun rxAsync(block: () -> Unit) {
|
||||
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +49,7 @@ class MemoryCookieJar : CookieJar {
|
||||
}
|
||||
}
|
||||
|
||||
class WrappedCookie private constructor(
|
||||
val cookie: Cookie,
|
||||
) {
|
||||
class WrappedCookie private constructor(val cookie: Cookie) {
|
||||
fun unwrap() = cookie
|
||||
|
||||
fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
|
||||
|
||||
@@ -12,13 +12,13 @@ import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import mu.KotlinLogging
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.brotli.BrotliInterceptor
|
||||
@@ -30,9 +30,7 @@ import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NetworkHelper(
|
||||
context: Context,
|
||||
) {
|
||||
class NetworkHelper(context: Context) {
|
||||
// private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
// private val cacheDir = File(context.cacheDir, "network_cache")
|
||||
@@ -55,7 +53,9 @@ class NetworkHelper(
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
)
|
||||
|
||||
fun defaultUserAgentProvider(): String = userAgent.value
|
||||
fun defaultUserAgentProvider(): String {
|
||||
return userAgent.value
|
||||
}
|
||||
|
||||
init {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@@ -63,14 +63,14 @@ class NetworkHelper(
|
||||
.drop(1)
|
||||
.onEach {
|
||||
GetCatalogueSource.unregisterAllCatalogueSources() // need to reset the headers
|
||||
}.launchIn(GlobalScope)
|
||||
}
|
||||
.launchIn(GlobalScope)
|
||||
}
|
||||
|
||||
private val baseClientBuilder: OkHttpClient.Builder
|
||||
get() {
|
||||
val builder =
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
OkHttpClient.Builder()
|
||||
.cookieJar(PersistentCookieJar(cookieStore))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
@@ -80,7 +80,8 @@ class NetworkHelper(
|
||||
directory = File.createTempFile("tachidesk_network_cache", null),
|
||||
maxSize = 5L * 1024 * 1024, // 5 MiB
|
||||
),
|
||||
).addInterceptor(UncaughtExceptionInterceptor())
|
||||
)
|
||||
.addInterceptor(UncaughtExceptionInterceptor())
|
||||
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
|
||||
.addNetworkInterceptor(IgnoreGzipInterceptor())
|
||||
.addNetworkInterceptor(BrotliInterceptor)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
@@ -49,7 +50,9 @@ fun Call.asObservable(): Observable<Response> {
|
||||
// call.cancel()
|
||||
}
|
||||
|
||||
override fun isUnsubscribed(): Boolean = call.isCanceled()
|
||||
override fun isUnsubscribed(): Boolean {
|
||||
return call.isCanceled()
|
||||
}
|
||||
}
|
||||
|
||||
subscriber.add(requestArbiter)
|
||||
@@ -57,16 +60,18 @@ fun Call.asObservable(): Observable<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> =
|
||||
asObservable()
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable()
|
||||
.doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw HttpException(response.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val callback =
|
||||
@@ -75,9 +80,8 @@ private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
||||
call: Call,
|
||||
response: Response,
|
||||
) {
|
||||
continuation.resume(response) { _, resourceToClose, _ ->
|
||||
continuation.resume(response) {
|
||||
response.body.close()
|
||||
resourceToClose.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,28 +135,29 @@ fun OkHttpClient.newCachelessCallWithProgress(
|
||||
.cache(null)
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse
|
||||
.newBuilder()
|
||||
originalResponse.newBuilder()
|
||||
.body(ProgressResponseBody(originalResponse.body, listener))
|
||||
.build()
|
||||
}.build()
|
||||
}
|
||||
.build()
|
||||
|
||||
return progressClient.newCall(request)
|
||||
}
|
||||
|
||||
context(Json)
|
||||
inline fun <reified T> Response.parseAs(): T = decodeFromJsonResponse(serializer(), this)
|
||||
inline fun <reified T> Response.parseAs(): T {
|
||||
return decodeFromJsonResponse(serializer(), this)
|
||||
}
|
||||
|
||||
context(Json)
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun <T> decodeFromJsonResponse(
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
response: Response,
|
||||
): T =
|
||||
response.body.source().use {
|
||||
): T {
|
||||
return response.body.source().use {
|
||||
decodeFromBufferedSource(deserializer, it)
|
||||
}
|
||||
}
|
||||
|
||||
class HttpException(
|
||||
val code: Int,
|
||||
) : IllegalStateException("HTTP error $code")
|
||||
class HttpException(val code: Int) : IllegalStateException("HTTP error $code")
|
||||
|
||||
@@ -5,9 +5,7 @@ import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
// from TachiWeb-Server
|
||||
class PersistentCookieJar(
|
||||
private val store: PersistentCookieStore,
|
||||
) : CookieJar {
|
||||
class PersistentCookieJar(private val store: PersistentCookieStore) : CookieJar {
|
||||
override fun saveFromResponse(
|
||||
url: HttpUrl,
|
||||
cookies: List<Cookie>,
|
||||
@@ -15,5 +13,7 @@ class PersistentCookieJar(
|
||||
store.addAll(url, cookies)
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> = store.get(url)
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return store.get(url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,13 @@ import okio.withLock
|
||||
import java.net.CookieStore
|
||||
import java.net.HttpCookie
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
// from TachiWeb-Server
|
||||
class PersistentCookieStore(
|
||||
context: Context,
|
||||
) : CookieStore {
|
||||
class PersistentCookieStore(context: Context) : CookieStore {
|
||||
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
||||
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
||||
|
||||
@@ -25,8 +22,7 @@ class PersistentCookieStore(
|
||||
|
||||
init {
|
||||
val domains =
|
||||
prefs.all.keys
|
||||
.map { it.substringBeforeLast(".") }
|
||||
prefs.all.keys.map { it.substringBeforeLast(".") }
|
||||
.toSet()
|
||||
domains.forEach { domain ->
|
||||
val cookies = prefs.getStringSet(domain, emptySet())
|
||||
@@ -34,8 +30,7 @@ class PersistentCookieStore(
|
||||
try {
|
||||
val url = "http://$domain".toHttpUrlOrNull() ?: return@forEach
|
||||
val nonExpiredCookies =
|
||||
cookies
|
||||
.mapNotNull { Cookie.parse(url, it) }
|
||||
cookies.mapNotNull { Cookie.parse(url, it) }
|
||||
.filter { !it.hasExpired() }
|
||||
cookieMap[domain] = nonExpiredCookies
|
||||
} catch (e: Exception) {
|
||||
@@ -50,8 +45,10 @@ class PersistentCookieStore(
|
||||
cookies: List<Cookie>,
|
||||
) {
|
||||
lock.withLock {
|
||||
val uri = url.toUri()
|
||||
|
||||
// Append or replace the cookies for this domain.
|
||||
val cookiesForDomain = cookieMap[url.host].orEmpty().toMutableList()
|
||||
val cookiesForDomain = cookieMap[uri.host].orEmpty().toMutableList()
|
||||
for (cookie in cookies) {
|
||||
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
||||
val pos = cookiesForDomain.indexOfFirst { it.name == cookie.name }
|
||||
@@ -61,36 +58,36 @@ class PersistentCookieStore(
|
||||
cookiesForDomain[pos] = cookie
|
||||
}
|
||||
}
|
||||
cookieMap[url.host] = cookiesForDomain
|
||||
cookieMap[uri.host] = cookiesForDomain
|
||||
|
||||
saveToDisk(url.toUrl())
|
||||
saveToDisk(uri)
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeAll(): Boolean =
|
||||
lock.withLock {
|
||||
override fun removeAll(): Boolean {
|
||||
return lock.withLock {
|
||||
val wasNotEmpty = cookieMap.isEmpty()
|
||||
prefs.edit().clear().apply()
|
||||
cookieMap.clear()
|
||||
wasNotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(uri: URI) {
|
||||
val url = uri.toURL()
|
||||
lock.withLock {
|
||||
prefs.edit().remove(url.host).apply()
|
||||
cookieMap.remove(url.host)
|
||||
prefs.edit().remove(uri.host).apply()
|
||||
cookieMap.remove(uri.host)
|
||||
}
|
||||
}
|
||||
|
||||
override fun get(uri: URI): List<HttpCookie> {
|
||||
val url = uri.toURL()
|
||||
return get(url.host).map {
|
||||
override fun get(uri: URI): List<HttpCookie> =
|
||||
get(uri.host).map {
|
||||
it.toHttpCookie()
|
||||
}
|
||||
}
|
||||
|
||||
fun get(url: HttpUrl): List<Cookie> = get(url.host)
|
||||
fun get(url: HttpUrl): List<Cookie> {
|
||||
return get(url.toUri().host ?: return emptyList())
|
||||
}
|
||||
|
||||
override fun add(
|
||||
uri: URI?,
|
||||
@@ -98,25 +95,26 @@ class PersistentCookieStore(
|
||||
) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val uri = uri ?: URI("http://" + cookie.domain.removePrefix("."))
|
||||
val url = uri.toURL()
|
||||
lock.withLock {
|
||||
val cookies = cookieMap[url.host]
|
||||
cookieMap[url.host] = cookies.orEmpty() + cookie.toCookie(uri)
|
||||
saveToDisk(url)
|
||||
val cookies = cookieMap[uri.host]
|
||||
cookieMap[uri.host] = cookies.orEmpty() + cookie.toCookie(uri)
|
||||
saveToDisk(uri)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCookies(): List<HttpCookie> =
|
||||
cookieMap.values.flatMap {
|
||||
override fun getCookies(): List<HttpCookie> {
|
||||
return cookieMap.values.flatMap {
|
||||
it.map {
|
||||
it.toHttpCookie()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getURIs(): List<URI> =
|
||||
cookieMap.keys().toList().map {
|
||||
override fun getURIs(): List<URI> {
|
||||
return cookieMap.keys().toList().map {
|
||||
URI("http://$it")
|
||||
}
|
||||
}
|
||||
|
||||
override fun remove(
|
||||
uri: URI?,
|
||||
@@ -124,9 +122,8 @@ class PersistentCookieStore(
|
||||
): Boolean {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val uri = uri ?: URI("http://" + cookie.domain.removePrefix("."))
|
||||
val url = uri.toURL()
|
||||
return lock.withLock {
|
||||
val cookies = cookieMap[url.host].orEmpty()
|
||||
val cookies = cookieMap[uri.host].orEmpty()
|
||||
val index =
|
||||
cookies.indexOfFirst {
|
||||
it.name == cookie.name &&
|
||||
@@ -135,8 +132,8 @@ class PersistentCookieStore(
|
||||
if (index >= 0) {
|
||||
val newList = cookies.toMutableList()
|
||||
newList.removeAt(index)
|
||||
cookieMap[url.host] = newList.toList()
|
||||
saveToDisk(url)
|
||||
cookieMap[uri.host] = newList.toList()
|
||||
saveToDisk(uri)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -144,29 +141,30 @@ class PersistentCookieStore(
|
||||
}
|
||||
}
|
||||
|
||||
private fun get(url: String): List<Cookie> = cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
||||
private fun get(url: String): List<Cookie> {
|
||||
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
||||
}
|
||||
|
||||
private fun saveToDisk(url: URL) {
|
||||
private fun saveToDisk(uri: URI) {
|
||||
// Get cookies to be stored in disk
|
||||
val newValues =
|
||||
cookieMap[url.host]
|
||||
cookieMap[uri.host]
|
||||
.orEmpty()
|
||||
.asSequence()
|
||||
.filter { it.persistent && !it.hasExpired() }
|
||||
.map(Cookie::toString)
|
||||
.toSet()
|
||||
|
||||
prefs.edit().putStringSet(url.host, newValues).apply()
|
||||
prefs.edit().putStringSet(uri.host, newValues).apply()
|
||||
}
|
||||
|
||||
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt
|
||||
|
||||
private fun HttpCookie.toCookie(uri: URI) =
|
||||
Cookie
|
||||
.Builder()
|
||||
Cookie.Builder()
|
||||
.name(name)
|
||||
.value(value)
|
||||
.domain(uri.toURL().host)
|
||||
.domain(uri.host)
|
||||
.path(path ?: "/")
|
||||
.let {
|
||||
if (maxAge != -1L) {
|
||||
@@ -174,19 +172,22 @@ class PersistentCookieStore(
|
||||
} else {
|
||||
it.expiresAt(Long.MAX_VALUE)
|
||||
}
|
||||
}.let {
|
||||
}
|
||||
.let {
|
||||
if (secure) {
|
||||
it.secure()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.let {
|
||||
}
|
||||
.let {
|
||||
if (isHttpOnly) {
|
||||
it.httpOnly()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
.build()
|
||||
|
||||
private fun Cookie.toHttpCookie(): HttpCookie {
|
||||
val it = this
|
||||
|
||||
@@ -9,19 +9,22 @@ import okio.Source
|
||||
import okio.buffer
|
||||
import java.io.IOException
|
||||
|
||||
class ProgressResponseBody(
|
||||
private val responseBody: ResponseBody,
|
||||
private val progressListener: ProgressListener,
|
||||
) : ResponseBody() {
|
||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
||||
private val bufferedSource: BufferedSource by lazy {
|
||||
source(responseBody.source()).buffer()
|
||||
}
|
||||
|
||||
override fun contentType(): MediaType? = responseBody.contentType()
|
||||
override fun contentType(): MediaType? {
|
||||
return responseBody.contentType()
|
||||
}
|
||||
|
||||
override fun contentLength(): Long = responseBody.contentLength()
|
||||
override fun contentLength(): Long {
|
||||
return responseBody.contentLength()
|
||||
}
|
||||
|
||||
override fun source(): BufferedSource = bufferedSource
|
||||
override fun source(): BufferedSource {
|
||||
return bufferedSource
|
||||
}
|
||||
|
||||
private fun source(source: Source): Source {
|
||||
return object : ForwardingSource(source) {
|
||||
|
||||
@@ -18,13 +18,13 @@ fun GET(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request =
|
||||
Request
|
||||
.Builder()
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* @since extensions-lib 1.4
|
||||
@@ -33,52 +33,52 @@ fun GET(
|
||||
url: HttpUrl,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request =
|
||||
Request
|
||||
.Builder()
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun POST(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
body: RequestBody = DEFAULT_BODY,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request =
|
||||
Request
|
||||
.Builder()
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.post(body)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun PUT(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
body: RequestBody = DEFAULT_BODY,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request =
|
||||
Request
|
||||
.Builder()
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.put(body)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun DELETE(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
body: RequestBody = DEFAULT_BODY,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request =
|
||||
Request
|
||||
.Builder()
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.delete(body)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -16,6 +15,7 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import mu.KotlinLogging
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
@@ -23,7 +23,6 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
@@ -57,38 +56,11 @@ class CloudflareInterceptor(
|
||||
originalResponse.close()
|
||||
// network.cookieStore.remove(originalRequest.url.toUri())
|
||||
|
||||
val flareResponseFallback = serverConfig.flareSolverrAsResponseFallback.value
|
||||
val flareResponse =
|
||||
val request =
|
||||
runBlocking {
|
||||
CFClearance.resolveWithFlareSolver(originalRequest, !flareResponseFallback)
|
||||
CFClearance.resolveWithFlareSolverr(setUserAgent, originalRequest)
|
||||
}
|
||||
|
||||
if (flareResponse.message.contains("not detected", ignoreCase = true)) {
|
||||
logger.debug { "FlareSolverr failed to detect Cloudflare challenge" }
|
||||
|
||||
if (flareResponseFallback &&
|
||||
flareResponse.solution.status in 200..299 &&
|
||||
flareResponse.solution.response != null
|
||||
) {
|
||||
val isImage = flareResponse.solution.response.contains(CHROME_IMAGE_TEMPLATE_REGEX)
|
||||
if (!isImage) {
|
||||
logger.debug { "Falling back to FlareSolverr response" }
|
||||
|
||||
setUserAgent(flareResponse.solution.userAgent)
|
||||
|
||||
return originalResponse
|
||||
.newBuilder()
|
||||
.code(flareResponse.solution.status)
|
||||
.body(flareResponse.solution.response.toResponseBody())
|
||||
.build()
|
||||
} else {
|
||||
logger.debug { "FlareSolverr response is an image html template, not falling back" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val request = CFClearance.requestWithFlareSolverr(flareResponse, setUserAgent, originalRequest)
|
||||
|
||||
chain.proceed(request)
|
||||
} catch (e: Exception) {
|
||||
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||
@@ -101,7 +73,6 @@ class CloudflareInterceptor(
|
||||
private val ERROR_CODES = listOf(403, 503)
|
||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||
private val CHROME_IMAGE_TEMPLATE_REGEX = Regex("""<title>(.*?) \(\d+×\d+\)</title>""")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,12 +88,12 @@ object CFClearance {
|
||||
serverConfig.flareSolverrTimeout
|
||||
.map { timeoutInt ->
|
||||
val timeout = timeoutInt.seconds
|
||||
network.client
|
||||
.newBuilder()
|
||||
network.client.newBuilder()
|
||||
.callTimeout(timeout.plus(10.seconds).toJavaDuration())
|
||||
.readTimeout(timeout.plus(5.seconds).toJavaDuration())
|
||||
.build()
|
||||
}.stateIn(GlobalScope, SharingStarted.Eagerly, network.client)
|
||||
}
|
||||
.stateIn(GlobalScope, SharingStarted.Eagerly, network.client)
|
||||
}
|
||||
private val json: Json by injectLazy()
|
||||
private val jsonMediaType = "application/json".toMediaType()
|
||||
@@ -153,13 +124,13 @@ object CFClearance {
|
||||
val name: String,
|
||||
val value: String,
|
||||
val domain: String,
|
||||
val path: String? = null,
|
||||
val path: String,
|
||||
val expires: Double? = null,
|
||||
val size: Int? = null,
|
||||
val httpOnly: Boolean? = null,
|
||||
val secure: Boolean? = null,
|
||||
val httpOnly: Boolean,
|
||||
val secure: Boolean,
|
||||
val session: Boolean? = null,
|
||||
val sameSite: String? = null,
|
||||
val sameSite: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -182,68 +153,58 @@ object CFClearance {
|
||||
val version: String,
|
||||
)
|
||||
|
||||
suspend fun resolveWithFlareSolver(
|
||||
originalRequest: Request,
|
||||
onlyCookies: Boolean,
|
||||
): FlareSolverResponse {
|
||||
val timeout = serverConfig.flareSolverrTimeout.value.seconds
|
||||
|
||||
return with(json) {
|
||||
mutex.withLock {
|
||||
client.value
|
||||
.newCall(
|
||||
POST(
|
||||
url = serverConfig.flareSolverrUrl.value.removeSuffix("/") + "/v1",
|
||||
body =
|
||||
Json
|
||||
.encodeToString(
|
||||
FlareSolverRequest(
|
||||
"request.get",
|
||||
originalRequest.url.toString(),
|
||||
session = serverConfig.flareSolverrSessionName.value,
|
||||
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
|
||||
cookies =
|
||||
network.cookieStore.get(originalRequest.url).map {
|
||||
FlareSolverCookie(it.name, it.value)
|
||||
},
|
||||
returnOnlyCookies = onlyCookies,
|
||||
maxTimeout = timeout.inWholeMilliseconds.toInt(),
|
||||
),
|
||||
).toRequestBody(jsonMediaType),
|
||||
),
|
||||
).awaitSuccess()
|
||||
.parseAs<FlareSolverResponse>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestWithFlareSolverr(
|
||||
flareSolverResponse: FlareSolverResponse,
|
||||
suspend fun resolveWithFlareSolverr(
|
||||
setUserAgent: (String) -> Unit,
|
||||
originalRequest: Request,
|
||||
): Request {
|
||||
val timeout = serverConfig.flareSolverrTimeout.value.seconds
|
||||
val flareSolverResponse =
|
||||
with(json) {
|
||||
mutex.withLock {
|
||||
client.value.newCall(
|
||||
POST(
|
||||
url = serverConfig.flareSolverrUrl.value.removeSuffix("/") + "/v1",
|
||||
body =
|
||||
Json.encodeToString(
|
||||
FlareSolverRequest(
|
||||
"request.get",
|
||||
originalRequest.url.toString(),
|
||||
session = serverConfig.flareSolverrSessionName.value,
|
||||
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
|
||||
cookies =
|
||||
network.cookieStore.get(originalRequest.url).map {
|
||||
FlareSolverCookie(it.name, it.value)
|
||||
},
|
||||
returnOnlyCookies = true,
|
||||
maxTimeout = timeout.inWholeMilliseconds.toInt(),
|
||||
),
|
||||
).toRequestBody(jsonMediaType),
|
||||
),
|
||||
).awaitSuccess().parseAs<FlareSolverResponse>()
|
||||
}
|
||||
}
|
||||
|
||||
if (flareSolverResponse.solution.status in 200..299) {
|
||||
setUserAgent(flareSolverResponse.solution.userAgent)
|
||||
val cookies =
|
||||
flareSolverResponse.solution.cookies
|
||||
.map { cookie ->
|
||||
Cookie
|
||||
.Builder()
|
||||
Cookie.Builder()
|
||||
.name(cookie.name)
|
||||
.value(cookie.value)
|
||||
.domain(cookie.domain.removePrefix("."))
|
||||
.path(cookie.path)
|
||||
.expiresAt(cookie.expires?.takeUnless { it < 0.0 }?.toLong() ?: Long.MAX_VALUE)
|
||||
.also {
|
||||
if (cookie.httpOnly != null && cookie.httpOnly) it.httpOnly()
|
||||
if (cookie.secure != null && cookie.secure) it.secure()
|
||||
if (!cookie.path.isNullOrEmpty()) it.path(cookie.path)
|
||||
// We need to convert the expires time to milliseconds for the persistent cookie store
|
||||
if (cookie.expires != null && cookie.expires > 0) it.expiresAt((cookie.expires * 1000).toLong())
|
||||
}.build()
|
||||
}.groupBy { it.domain }
|
||||
if (cookie.httpOnly) it.httpOnly()
|
||||
if (cookie.secure) it.secure()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
.groupBy { it.domain }
|
||||
.flatMap { (domain, cookies) ->
|
||||
network.cookieStore.addAll(
|
||||
HttpUrl
|
||||
.Builder()
|
||||
HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.host(domain.removePrefix("."))
|
||||
.build(),
|
||||
@@ -258,8 +219,7 @@ object CFClearance {
|
||||
"${it.name}=${it.value}"
|
||||
}
|
||||
logger.trace { "Final cookies\n$finalCookies" }
|
||||
return originalRequest
|
||||
.newBuilder()
|
||||
return originalRequest.newBuilder()
|
||||
.header("Cookie", finalCookies)
|
||||
.header("User-Agent", flareSolverResponse.solution.userAgent)
|
||||
.build()
|
||||
|
||||
@@ -13,8 +13,8 @@ import java.io.IOException
|
||||
* See https://square.github.io/okhttp/4.x/okhttp/okhttp3/-interceptor/
|
||||
*/
|
||||
class UncaughtExceptionInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response =
|
||||
try {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
return try {
|
||||
chain.proceed(chain.request())
|
||||
} catch (e: Exception) {
|
||||
if (e is IOException) {
|
||||
@@ -23,4 +23,5 @@ class UncaughtExceptionInterceptor : Interceptor {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.network.interceptor
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class UserAgentInterceptor(
|
||||
private val userAgentProvider: () -> String,
|
||||
) : Interceptor {
|
||||
class UserAgentInterceptor(private val userAgentProvider: () -> String) : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ interface CatalogueSource : Source {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle()
|
||||
suspend fun getPopularManga(page: Int): MangasPage {
|
||||
return fetchPopularManga(page).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page with a list of manga.
|
||||
@@ -38,7 +40,9 @@ interface CatalogueSource : Source {
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): MangasPage = fetchSearchManga(page, query, filters).awaitSingle()
|
||||
): MangasPage {
|
||||
return fetchSearchManga(page, query, filters).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page with a list of latest manga updates.
|
||||
@@ -47,7 +51,9 @@ interface CatalogueSource : Source {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getLatestUpdates(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle()
|
||||
suspend fun getLatestUpdates(page: Int): MangasPage {
|
||||
return fetchLatestUpdates(page).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
|
||||
@@ -31,7 +31,9 @@ interface Source {
|
||||
* @return the updated manga.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
|
||||
suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
return fetchMangaDetails(manga).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the available chapters for a manga.
|
||||
@@ -41,7 +43,9 @@ interface Source {
|
||||
* @return the chapters for the manga.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getChapterList(manga: SManga): List<SChapter> = fetchChapterList(manga).awaitSingle()
|
||||
suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
return fetchChapterList(manga).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of pages a chapter has. Pages should be returned
|
||||
@@ -52,7 +56,9 @@ interface Source {
|
||||
* @return the pages for the chapter.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
||||
suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||
return fetchPageList(chapter).awaitSingle()
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
|
||||
@@ -26,20 +26,23 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import mu.KotlinLogging
|
||||
import nl.adaptivity.xmlutil.ExperimentalXmlUtilApi
|
||||
import nl.adaptivity.xmlutil.core.KtXmlReader
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.registerCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
@@ -56,8 +59,7 @@ import com.github.junrar.Archive as JunrarArchive
|
||||
class LocalSource(
|
||||
private val fileSystem: LocalSourceFileSystem,
|
||||
private val coverManager: LocalCoverManager,
|
||||
) : CatalogueSource,
|
||||
UnmeteredSource {
|
||||
) : CatalogueSource, UnmeteredSource {
|
||||
private val json: Json by injectLazy()
|
||||
private val xml: XML by injectLazy()
|
||||
|
||||
@@ -91,8 +93,7 @@ class LocalSource(
|
||||
// Filter out files that are hidden and is not a folder
|
||||
.filter { it.isDirectory && !it.name.startsWith('.') }
|
||||
.distinctBy { it.name }
|
||||
.filter {
|
||||
// Filter by query or last modified
|
||||
.filter { // Filter by query or last modified
|
||||
if (lastModifiedLimit == 0L) {
|
||||
it.name.contains(query, ignoreCase = true)
|
||||
} else {
|
||||
@@ -133,8 +134,7 @@ class LocalSource(
|
||||
url = mangaDir.name
|
||||
|
||||
// Try to find the cover
|
||||
coverManager
|
||||
.find(mangaDir.name)
|
||||
coverManager.find(mangaDir.name)
|
||||
?.takeIf(File::exists)
|
||||
?.let { thumbnail_url = it.absolutePath }
|
||||
}
|
||||
@@ -238,7 +238,7 @@ class LocalSource(
|
||||
for (chapter in chapterArchives) {
|
||||
when (Format.valueOf(chapter)) {
|
||||
is Format.Zip -> {
|
||||
ZipFile.builder().setFile(chapter).get().use { zip: ZipFile ->
|
||||
ZipFile(chapter).use { zip: ZipFile ->
|
||||
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
|
||||
zip.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||
return copyComicInfoFile(stream, folderPath)
|
||||
@@ -264,12 +264,13 @@ class LocalSource(
|
||||
private fun copyComicInfoFile(
|
||||
comicInfoFileStream: InputStream,
|
||||
folderPath: String?,
|
||||
): File =
|
||||
File("$folderPath/$COMIC_INFO_FILE").apply {
|
||||
): File {
|
||||
return File("$folderPath/$COMIC_INFO_FILE").apply {
|
||||
outputStream().use { outputStream ->
|
||||
comicInfoFileStream.use { it.copyTo(outputStream) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalXmlUtilApi::class)
|
||||
private fun setMangaDetailsFromComicInfoFile(
|
||||
@@ -285,9 +286,8 @@ class LocalSource(
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> =
|
||||
fileSystem
|
||||
.getFilesInMangaDirectory(manga.url)
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
return fileSystem.getFilesInMangaDirectory(manga.url)
|
||||
// Only keep supported formats
|
||||
.filter { it.isDirectory || Archive.isSupported(it) }
|
||||
.map { chapterFile ->
|
||||
@@ -312,21 +312,22 @@ class LocalSource(
|
||||
}
|
||||
}
|
||||
}
|
||||
}.sortedWith { c1, c2 ->
|
||||
}
|
||||
.sortedWith { c1, c2 ->
|
||||
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||
}.toList()
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(OrderBy.Popular())
|
||||
|
||||
// TODO Fix Memory Leak
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> =
|
||||
when (val format = getFormat(chapter)) {
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||
return when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
format.file
|
||||
.listFiles()
|
||||
.orEmpty()
|
||||
format.file.listFiles().orEmpty()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name, it::inputStream) }
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.mapIndexed { index, page ->
|
||||
@@ -358,11 +359,11 @@ class LocalSource(
|
||||
pages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getFormat(chapter: SChapter): Format {
|
||||
try {
|
||||
return fileSystem
|
||||
.getBaseDirectories()
|
||||
return fileSystem.getBaseDirectories()
|
||||
.map { dir -> File(dir, chapter.url) }
|
||||
.find { it.exists() }
|
||||
?.let(Format.Companion::valueOf)
|
||||
@@ -377,23 +378,21 @@ class LocalSource(
|
||||
private fun updateCover(
|
||||
chapter: SChapter,
|
||||
manga: SManga,
|
||||
): File? =
|
||||
try {
|
||||
): File? {
|
||||
return try {
|
||||
when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
val entry =
|
||||
format.file
|
||||
.listFiles()
|
||||
format.file.listFiles()
|
||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
|
||||
entry?.let { coverManager.update(manga, it.inputStream()) }
|
||||
}
|
||||
is Format.Zip -> {
|
||||
ZipFile.builder().setFile(format.file).get().use { zip ->
|
||||
ZipFile(format.file).use { zip ->
|
||||
val entry =
|
||||
zip.entries
|
||||
.toList()
|
||||
zip.entries.toList()
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
|
||||
@@ -413,8 +412,7 @@ class LocalSource(
|
||||
is Format.Epub -> {
|
||||
EpubFile(format.file).use { epub ->
|
||||
val entry =
|
||||
epub
|
||||
.getImagesFromPages()
|
||||
epub.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
|
||||
@@ -426,6 +424,7 @@ class LocalSource(
|
||||
logger.error(e) { "Error updating cover for ${manga.title}" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
@@ -438,13 +437,13 @@ class LocalSource(
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private val applicationDirs: ApplicationDirs by injectLazy()
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
|
||||
val pageCache: MutableMap<String, List<() -> InputStream>> = mutableMapOf()
|
||||
|
||||
fun register() {
|
||||
transaction {
|
||||
val sourceRecord = SourceTable.selectAll().where { SourceTable.id eq ID }.firstOrNull()
|
||||
val sourceRecord = SourceTable.select { SourceTable.id eq ID }.firstOrNull()
|
||||
|
||||
if (sourceRecord == null) {
|
||||
// must do this to avoid database integrity errors
|
||||
|
||||
@@ -2,14 +2,12 @@ package eu.kanade.tachiyomi.source.local.filter
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
sealed class OrderBy(
|
||||
selection: Selection,
|
||||
) : Filter.Sort(
|
||||
"Order by",
|
||||
arrayOf("Title", "Date"),
|
||||
selection,
|
||||
) {
|
||||
class Popular : OrderBy(Selection(0, true))
|
||||
sealed class OrderBy(selection: Selection) : Filter.Sort(
|
||||
"Order by",
|
||||
arrayOf("Title", "Date"),
|
||||
selection,
|
||||
) {
|
||||
class Popular() : OrderBy(Selection(0, true))
|
||||
|
||||
class Latest : OrderBy(Selection(1, false))
|
||||
class Latest() : OrderBy(Selection(1, false))
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ private const val DEFAULT_COVER_NAME = "cover.jpg"
|
||||
class LocalCoverManager(
|
||||
private val fileSystem: LocalSourceFileSystem,
|
||||
) {
|
||||
fun find(mangaUrl: String): File? =
|
||||
fileSystem
|
||||
.getFilesInMangaDirectory(mangaUrl)
|
||||
fun find(mangaUrl: String): File? {
|
||||
return fileSystem.getFilesInMangaDirectory(mangaUrl)
|
||||
// Get all file whose names start with 'cover'
|
||||
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
|
||||
// Get the first actual image
|
||||
.firstOrNull {
|
||||
ImageUtil.isImage(it.name) { it.inputStream() }
|
||||
}
|
||||
}
|
||||
|
||||
fun update(
|
||||
manga: SManga,
|
||||
|
||||
@@ -3,21 +3,13 @@ package eu.kanade.tachiyomi.source.local.io
|
||||
import java.io.File
|
||||
|
||||
sealed interface Format {
|
||||
data class Directory(
|
||||
val file: File,
|
||||
) : Format
|
||||
data class Directory(val file: File) : Format
|
||||
|
||||
data class Zip(
|
||||
val file: File,
|
||||
) : Format
|
||||
data class Zip(val file: File) : Format
|
||||
|
||||
data class Rar(
|
||||
val file: File,
|
||||
) : Format
|
||||
data class Rar(val file: File) : Format
|
||||
|
||||
data class Epub(
|
||||
val file: File,
|
||||
) : Format
|
||||
data class Epub(val file: File) : Format
|
||||
|
||||
class UnknownFormatException : Exception()
|
||||
|
||||
|
||||
@@ -6,22 +6,27 @@ import java.io.File
|
||||
class LocalSourceFileSystem(
|
||||
private val applicationDirs: ApplicationDirs,
|
||||
) {
|
||||
fun getBaseDirectories(): Sequence<File> = sequenceOf(File(applicationDirs.localMangaRoot))
|
||||
fun getBaseDirectories(): Sequence<File> {
|
||||
return sequenceOf(File(applicationDirs.localMangaRoot))
|
||||
}
|
||||
|
||||
fun getFilesInBaseDirectories(): Sequence<File> =
|
||||
getBaseDirectories()
|
||||
fun getFilesInBaseDirectories(): Sequence<File> {
|
||||
return getBaseDirectories()
|
||||
// Get all the files inside all baseDir
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
|
||||
fun getMangaDirectory(name: String): File? =
|
||||
getFilesInBaseDirectories()
|
||||
fun getMangaDirectory(name: String): File? {
|
||||
return getFilesInBaseDirectories()
|
||||
// Get the first mangaDir or null
|
||||
.firstOrNull { it.isDirectory && it.name == name }
|
||||
}
|
||||
|
||||
fun getFilesInMangaDirectory(name: String): Sequence<File> =
|
||||
getFilesInBaseDirectories()
|
||||
fun getFilesInMangaDirectory(name: String): Sequence<File> {
|
||||
return getFilesInBaseDirectories()
|
||||
// Filter out ones that are not related to the manga and is not a directory
|
||||
.filter { it.isDirectory && it.name == name }
|
||||
// Get all the files inside the filtered folders
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,18 @@ import java.io.File
|
||||
/**
|
||||
* Loader used to load a chapter from a .epub file.
|
||||
*/
|
||||
class EpubPageLoader(
|
||||
file: File,
|
||||
) : PageLoader {
|
||||
class EpubPageLoader(file: File) : PageLoader {
|
||||
private val epub = EpubFile(file)
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> =
|
||||
epub
|
||||
.getImagesFromPages()
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return epub.getImagesFromPages()
|
||||
.mapIndexed { i, path ->
|
||||
val streamFn = { epub.getInputStream(epub.getEntry(path)!!) }
|
||||
ReaderPage(i).apply {
|
||||
stream = streamFn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
epub.close()
|
||||
|
||||
@@ -12,21 +12,20 @@ import java.io.PipedOutputStream
|
||||
/**
|
||||
* Loader used to load a chapter from a .rar or .cbr file.
|
||||
*/
|
||||
class RarPageLoader(
|
||||
file: File,
|
||||
) : PageLoader {
|
||||
class RarPageLoader(file: File) : PageLoader {
|
||||
private val rar = Archive(file)
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> =
|
||||
rar.fileHeaders
|
||||
.asSequence()
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return rar.fileHeaders.asSequence()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.mapIndexed { i, header ->
|
||||
ReaderPage(i).apply {
|
||||
stream = { getStream(rar, header) }
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
rar.close()
|
||||
|
||||
@@ -8,21 +8,20 @@ import java.io.File
|
||||
/**
|
||||
* Loader used to load a chapter from a .zip or .cbz file.
|
||||
*/
|
||||
class ZipPageLoader(
|
||||
file: File,
|
||||
) : PageLoader {
|
||||
private val zip = ZipFile.builder().setFile(file).get()
|
||||
class ZipPageLoader(file: File) : PageLoader {
|
||||
private val zip = ZipFile(file)
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> =
|
||||
zip.entries
|
||||
.asSequence()
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return zip.entries.asSequence()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.mapIndexed { i, entry ->
|
||||
ReaderPage(i).apply {
|
||||
stream = { zip.getInputStream(entry) }
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
zip.close()
|
||||
|
||||
@@ -17,7 +17,8 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||
comicInfo.genre?.value,
|
||||
comicInfo.tags?.value,
|
||||
comicInfo.categories?.value,
|
||||
).distinct()
|
||||
)
|
||||
.distinct()
|
||||
.joinToString(", ") { it.trim() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { genre = it }
|
||||
@@ -28,7 +29,8 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||
comicInfo.colorist?.value,
|
||||
comicInfo.letterer?.value,
|
||||
comicInfo.coverArtist?.value,
|
||||
).flatMap { it.split(", ") }
|
||||
)
|
||||
.flatMap { it.split(", ") }
|
||||
.distinct()
|
||||
.joinToString(", ") { it.trim() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
@@ -56,9 +58,6 @@ data class ComicInfo(
|
||||
val web: Web?,
|
||||
val publishingStatus: PublishingStatusTachiyomi?,
|
||||
val categories: CategoriesTachiyomi?,
|
||||
val day: Day?,
|
||||
val month: Month?,
|
||||
val year: Year?,
|
||||
) {
|
||||
@Suppress("UNUSED")
|
||||
@XmlElement(false)
|
||||
@@ -88,24 +87,6 @@ data class ComicInfo(
|
||||
@XmlValue(true) val value: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Day", "", "")
|
||||
data class Day(
|
||||
@XmlValue(true) val value: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Month", "", "")
|
||||
data class Month(
|
||||
@XmlValue(true) val value: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Year", "", "")
|
||||
data class Year(
|
||||
@XmlValue(true) val value: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Summary", "", "")
|
||||
data class Summary(
|
||||
@@ -200,12 +181,14 @@ enum class ComicInfoPublishingStatus(
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun toComicInfoValue(value: Long): String =
|
||||
entries.firstOrNull { it.sMangaModelValue == value.toInt() }?.comicInfoValue
|
||||
fun toComicInfoValue(value: Long): String {
|
||||
return entries.firstOrNull { it.sMangaModelValue == value.toInt() }?.comicInfoValue
|
||||
?: UNKNOWN.comicInfoValue
|
||||
}
|
||||
|
||||
fun toSMangaValue(value: String?): Int =
|
||||
entries.firstOrNull { it.comicInfoValue == value }?.sMangaModelValue
|
||||
fun toSMangaValue(value: String?): Int {
|
||||
return entries.firstOrNull { it.comicInfoValue == value }?.sMangaModelValue
|
||||
?: UNKNOWN.sMangaModelValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,40 +2,20 @@ package eu.kanade.tachiyomi.source.model
|
||||
|
||||
// The class is originally sealed, Tachidesk adds new subclasses for serialization
|
||||
// sealed class Filter<T>(val name: String, var state: T) {
|
||||
open class Filter<T>(
|
||||
val name: String,
|
||||
var state: T,
|
||||
) {
|
||||
open class Header(
|
||||
name: String,
|
||||
) : Filter<Any>(name, 0)
|
||||
open class Filter<T>(val name: String, var state: T) {
|
||||
open class Header(name: String) : Filter<Any>(name, 0)
|
||||
|
||||
open class Separator(
|
||||
name: String = "",
|
||||
) : Filter<Any>(name, 0)
|
||||
open class Separator(name: String = "") : Filter<Any>(name, 0)
|
||||
|
||||
abstract class Select<V>(
|
||||
name: String,
|
||||
val values: Array<V>,
|
||||
state: Int = 0,
|
||||
) : Filter<Int>(name, state) {
|
||||
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) {
|
||||
val displayValues get() = values.map { it.toString() }
|
||||
}
|
||||
|
||||
abstract class Text(
|
||||
name: String,
|
||||
state: String = "",
|
||||
) : Filter<String>(name, state)
|
||||
abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
|
||||
|
||||
abstract class CheckBox(
|
||||
name: String,
|
||||
state: Boolean = false,
|
||||
) : Filter<Boolean>(name, state)
|
||||
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
|
||||
|
||||
abstract class TriState(
|
||||
name: String,
|
||||
state: Int = STATE_IGNORE,
|
||||
) : Filter<Int>(name, state) {
|
||||
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
|
||||
fun isIgnored() = state == STATE_IGNORE
|
||||
|
||||
fun isIncluded() = state == STATE_INCLUDE
|
||||
@@ -49,20 +29,11 @@ open class Filter<T>(
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Group<V>(
|
||||
name: String,
|
||||
state: List<V>,
|
||||
) : Filter<List<V>>(name, state)
|
||||
abstract class Group<V>(name: String, state: List<V>) : Filter<List<V>>(name, state)
|
||||
|
||||
abstract class Sort(
|
||||
name: String,
|
||||
val values: Array<String>,
|
||||
state: Selection? = null,
|
||||
) : Filter<Sort.Selection?>(name, state) {
|
||||
data class Selection(
|
||||
val index: Int,
|
||||
val ascending: Boolean,
|
||||
)
|
||||
abstract class Sort(name: String, val values: Array<String>, state: Selection? = null) :
|
||||
Filter<Sort.Selection?>(name, state) {
|
||||
data class Selection(val index: Int, val ascending: Boolean)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class FilterList(
|
||||
val list: List<Filter<*>>,
|
||||
) : List<Filter<*>> by list {
|
||||
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
|
||||
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class MangasPage(
|
||||
val mangas: List<SManga>,
|
||||
val hasNextPage: Boolean,
|
||||
)
|
||||
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
|
||||
|
||||
@@ -24,6 +24,8 @@ interface SChapter : Serializable {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(): SChapter = SChapterImpl()
|
||||
fun create(): SChapter {
|
||||
return SChapterImpl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,34 @@ interface SManga : Serializable {
|
||||
|
||||
var initialized: Boolean
|
||||
|
||||
fun copyFrom(other: SManga) {
|
||||
if (other.author != null) {
|
||||
author = other.author
|
||||
}
|
||||
|
||||
if (other.artist != null) {
|
||||
artist = other.artist
|
||||
}
|
||||
|
||||
if (other.description != null) {
|
||||
description = other.description
|
||||
}
|
||||
|
||||
if (other.genre != null) {
|
||||
genre = other.genre
|
||||
}
|
||||
|
||||
if (other.thumbnail_url != null) {
|
||||
thumbnail_url = other.thumbnail_url
|
||||
}
|
||||
|
||||
status = other.status
|
||||
|
||||
if (!initialized) {
|
||||
initialized = other.initialized
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val UNKNOWN = 0
|
||||
const val ONGOING = 1
|
||||
@@ -34,7 +62,9 @@ interface SManga : Serializable {
|
||||
const val CANCELLED = 5
|
||||
const val ON_HIATUS = 6
|
||||
|
||||
fun create(): SManga = SMangaImpl()
|
||||
fun create(): SManga {
|
||||
return SMangaImpl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,9 @@ abstract class HttpSource : CatalogueSource {
|
||||
open val client: OkHttpClient
|
||||
get() = network.client
|
||||
|
||||
private fun generateId(): Long = generateId("${name.lowercase()}/$lang/$versionId")
|
||||
private fun generateId(): Long {
|
||||
return generateId("${name.lowercase()}/$lang/$versionId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique ID for the source based on the provided [name], [lang] and
|
||||
@@ -119,13 +121,13 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> =
|
||||
client
|
||||
.newCall(popularMangaRequest(page))
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
popularMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
@@ -154,19 +156,20 @@ abstract class HttpSource : CatalogueSource {
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> =
|
||||
Observable
|
||||
.defer {
|
||||
try {
|
||||
client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
// RxJava doesn't handle Errors, which tends to happen during global searches
|
||||
// if an old extension using non-existent classes is still around
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}.map { response ->
|
||||
): Observable<MangasPage> {
|
||||
return Observable.defer {
|
||||
try {
|
||||
client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
// RxJava doesn't handle Errors, which tends to happen during global searches
|
||||
// if an old extension using non-existent classes is still around
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
@@ -194,13 +197,13 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
|
||||
client
|
||||
.newCall(latestUpdatesRequest(page))
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
latestUpdatesParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
@@ -224,16 +227,18 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @return the updated manga.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
return fetchMangaDetails(manga).awaitSingle()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||
client
|
||||
.newCall(mangaDetailsRequest(manga))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the details of a manga. Override only if it's needed to change the
|
||||
@@ -241,7 +246,9 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
open fun mangaDetailsRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
||||
open fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
@@ -268,10 +275,9 @@ abstract class HttpSource : CatalogueSource {
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
||||
if (manga.status != SManga.LICENSED) {
|
||||
client
|
||||
.newCall(chapterListRequest(manga))
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return if (manga.status != SManga.LICENSED) {
|
||||
client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response)
|
||||
@@ -279,6 +285,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
} else {
|
||||
Observable.error(LicensedMangaChaptersException())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for updating the chapter list. Override only if it's needed to override
|
||||
@@ -286,7 +293,9 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
protected open fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
||||
protected open fun chapterListRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
@@ -303,16 +312,18 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @return the pages for the chapter.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||
return fetchPageList(chapter).awaitSingle()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
||||
client
|
||||
.newCall(pageListRequest(chapter))
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
pageListParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the page list. Override only if it's needed to override the
|
||||
@@ -320,7 +331,9 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
protected open fun pageListRequest(chapter: SChapter): Request = GET(baseUrl + chapter.url, headers)
|
||||
protected open fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(baseUrl + chapter.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
@@ -337,14 +350,16 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle()
|
||||
open suspend fun getImageUrl(page: Page): String {
|
||||
return fetchImageUrl(page).awaitSingle()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
|
||||
open fun fetchImageUrl(page: Page): Observable<String> =
|
||||
client
|
||||
.newCall(imageUrlRequest(page))
|
||||
open fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return client.newCall(imageUrlRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { imageUrlParse(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the url to the source image. Override only if it's needed to
|
||||
@@ -352,7 +367,9 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
protected open fun imageUrlRequest(page: Page): Request = GET(page.url, headers)
|
||||
protected open fun imageUrlRequest(page: Page): Request {
|
||||
return GET(page.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
@@ -368,10 +385,10 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
open suspend fun getImage(page: Page): Response =
|
||||
client
|
||||
.newCachelessCallWithProgress(imageRequest(page), page)
|
||||
open suspend fun getImage(page: Page): Response {
|
||||
return client.newCachelessCallWithProgress(imageRequest(page), page)
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the source image. Override only if it's needed to override
|
||||
@@ -379,7 +396,9 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
protected open fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headers)
|
||||
protected open fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
|
||||
@@ -406,8 +425,8 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param orig the full url.
|
||||
*/
|
||||
private fun getUrlWithoutDomain(orig: String): String =
|
||||
try {
|
||||
private fun getUrlWithoutDomain(orig: String): String {
|
||||
return try {
|
||||
val uri = URI(orig.replace(" ", "%20"))
|
||||
var out = uri.path
|
||||
if (uri.query != null) {
|
||||
@@ -420,6 +439,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
} catch (e: URISyntaxException) {
|
||||
orig
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url of the provided manga
|
||||
@@ -428,7 +448,9 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param manga the manga
|
||||
* @return url of the manga
|
||||
*/
|
||||
open fun getMangaUrl(manga: SManga): String = mangaDetailsRequest(manga).url.toString()
|
||||
open fun getMangaUrl(manga: SManga): String {
|
||||
return mangaDetailsRequest(manga).url.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url of the provided chapter
|
||||
@@ -437,7 +459,9 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param chapter the chapter
|
||||
* @return url of the chapter
|
||||
*/
|
||||
open fun getChapterUrl(chapter: SChapter): String = pageListRequest(chapter).url.toString()
|
||||
open fun getChapterUrl(chapter: SChapter): String {
|
||||
return pageListRequest(chapter).url.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before inserting a new chapter into database. Use it if you need to override chapter
|
||||
|
||||
@@ -138,7 +138,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response): SManga = mangaDetailsParse(response.asJsoup())
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return mangaDetailsParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the details of the manga from the given [document].
|
||||
@@ -174,7 +176,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response): List<Page> = pageListParse(response.asJsoup())
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return pageListParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a page list from the given document.
|
||||
@@ -188,7 +192,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response): String = imageUrlParse(response.asJsoup())
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
return imageUrlParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute url to the source image from the document.
|
||||
|
||||
@@ -8,17 +8,25 @@ import org.jsoup.nodes.Element
|
||||
fun Element.selectText(
|
||||
css: String,
|
||||
defaultValue: String? = null,
|
||||
): String? = select(css).first()?.text() ?: defaultValue
|
||||
): String? {
|
||||
return select(css).first()?.text() ?: defaultValue
|
||||
}
|
||||
|
||||
fun Element.selectInt(
|
||||
css: String,
|
||||
defaultValue: Int = 0,
|
||||
): Int = select(css).first()?.text()?.toInt() ?: defaultValue
|
||||
): Int {
|
||||
return select(css).first()?.text()?.toInt() ?: defaultValue
|
||||
}
|
||||
|
||||
fun Element.attrOrText(css: String): String = if (css != "text") attr(css) else text()
|
||||
fun Element.attrOrText(css: String): String {
|
||||
return if (css != "text") attr(css) else text()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Jsoup document for this response.
|
||||
* @param html the body of the response. Use only if the body was read before calling this method.
|
||||
*/
|
||||
fun Response.asJsoup(html: String? = null): Document = Jsoup.parse(html ?: body.string(), request.url.toString())
|
||||
fun Response.asJsoup(html: String? = null): Document {
|
||||
return Jsoup.parse(html ?: body.string(), request.url.toString())
|
||||
}
|
||||
|
||||
@@ -68,14 +68,15 @@ object ChapterRecognition {
|
||||
* @param match result of regex
|
||||
* @return chapter number if found else null
|
||||
*/
|
||||
private fun getChapterNumberFromMatch(match: MatchResult): Double =
|
||||
match.let {
|
||||
private fun getChapterNumberFromMatch(match: MatchResult): Double {
|
||||
return match.let {
|
||||
val initial = it.groups[1]?.value?.toDouble()!!
|
||||
val subChapterDecimal = it.groups[2]?.value
|
||||
val subChapterAlpha = it.groups[3]?.value
|
||||
val addition = checkForDecimal(subChapterDecimal, subChapterAlpha)
|
||||
initial.plus(addition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for decimal in received strings
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package eu.kanade.tachiyomi.util.chapter
|
||||
|
||||
object ChapterSanitizer {
|
||||
fun String.sanitize(title: String): String =
|
||||
trim()
|
||||
fun String.sanitize(title: String): String {
|
||||
return trim()
|
||||
.removePrefix(title)
|
||||
.trim(*CHAPTER_TRIM_CHARS)
|
||||
}
|
||||
|
||||
private val CHAPTER_TRIM_CHARS =
|
||||
arrayOf(
|
||||
|
||||
@@ -5,35 +5,29 @@ import java.security.MessageDigest
|
||||
object Hash {
|
||||
private val chars =
|
||||
charArrayOf(
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
)
|
||||
|
||||
private val MD5 get() = MessageDigest.getInstance("MD5")
|
||||
|
||||
private val SHA256 get() = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
fun sha256(bytes: ByteArray): String = encodeHex(SHA256.digest(bytes))
|
||||
fun sha256(bytes: ByteArray): String {
|
||||
return encodeHex(SHA256.digest(bytes))
|
||||
}
|
||||
|
||||
fun sha256(string: String): String = sha256(string.toByteArray())
|
||||
fun sha256(string: String): String {
|
||||
return sha256(string.toByteArray())
|
||||
}
|
||||
|
||||
fun md5(bytes: ByteArray): String = encodeHex(MD5.digest(bytes))
|
||||
fun md5(bytes: ByteArray): String {
|
||||
return encodeHex(MD5.digest(bytes))
|
||||
}
|
||||
|
||||
fun md5(string: String): String = md5(string.toByteArray())
|
||||
fun md5(string: String): String {
|
||||
return md5(string.toByteArray())
|
||||
}
|
||||
|
||||
private fun encodeHex(data: ByteArray): String {
|
||||
val l = data.size
|
||||
|
||||
@@ -10,12 +10,13 @@ import kotlin.math.floor
|
||||
fun String.chop(
|
||||
count: Int,
|
||||
replacement: String = "…",
|
||||
): String =
|
||||
if (length > count) {
|
||||
): String {
|
||||
return if (length > count) {
|
||||
take(count - replacement.length) + replacement
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the given string to have at most [count] characters using [replacement] near the center.
|
||||
@@ -45,7 +46,9 @@ fun String.compareToCaseInsensitiveNaturalOrder(other: String): Int {
|
||||
/**
|
||||
* Returns the size of the string as the number of bytes.
|
||||
*/
|
||||
fun String.byteSize(): Int = toByteArray(Charsets.UTF_8).size
|
||||
fun String.byteSize(): Int {
|
||||
return toByteArray(Charsets.UTF_8).size
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the first [n] bytes from this string, or the entire string if this
|
||||
|
||||
@@ -11,13 +11,11 @@ import java.io.InputStream
|
||||
/**
|
||||
* Wrapper over ZipFile to load files in epub format.
|
||||
*/
|
||||
class EpubFile(
|
||||
file: File,
|
||||
) : Closeable {
|
||||
class EpubFile(file: File) : Closeable {
|
||||
/**
|
||||
* Zip file of this epub.
|
||||
*/
|
||||
private val zip = ZipFile.builder().setFile(file).get()
|
||||
private val zip = ZipFile(file)
|
||||
|
||||
/**
|
||||
* Path separator used by this epub.
|
||||
@@ -34,12 +32,16 @@ class EpubFile(
|
||||
/**
|
||||
* Returns an input stream for reading the contents of the specified zip file entry.
|
||||
*/
|
||||
fun getInputStream(entry: ZipArchiveEntry): InputStream = zip.getInputStream(entry)
|
||||
fun getInputStream(entry: ZipArchiveEntry): InputStream {
|
||||
return zip.getInputStream(entry)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the zip file entry for the specified name, or null if not found.
|
||||
*/
|
||||
fun getEntry(name: String): ZipArchiveEntry? = zip.getEntry(name)
|
||||
fun getEntry(name: String): ZipArchiveEntry? {
|
||||
return zip.getEntry(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path of all the images found in the epub file.
|
||||
@@ -79,8 +81,7 @@ class EpubFile(
|
||||
*/
|
||||
fun getPagesFromDocument(document: Document): List<String> {
|
||||
val pages =
|
||||
document
|
||||
.select("manifest > item")
|
||||
document.select("manifest > item")
|
||||
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
|
||||
.associateBy { it.attr("id") }
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ package suwayomi.tachidesk.global.controller
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import io.javalin.http.HttpStatus
|
||||
import io.javalin.http.HttpCode
|
||||
import suwayomi.tachidesk.global.impl.GlobalMeta
|
||||
import suwayomi.tachidesk.server.util.formParam
|
||||
import suwayomi.tachidesk.server.util.handler
|
||||
@@ -28,7 +28,7 @@ object GlobalMetaController {
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpStatus.OK)
|
||||
httpCode(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -48,8 +48,8 @@ object GlobalMetaController {
|
||||
ctx.status(200)
|
||||
},
|
||||
withResults = {
|
||||
httpCode(HttpStatus.OK)
|
||||
httpCode(HttpStatus.NOT_FOUND)
|
||||
httpCode(HttpCode.OK)
|
||||
httpCode(HttpCode.NOT_FOUND)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ package suwayomi.tachidesk.global.controller
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import io.javalin.http.HttpStatus
|
||||
import io.javalin.http.HttpCode
|
||||
import suwayomi.tachidesk.global.impl.About
|
||||
import suwayomi.tachidesk.global.impl.AboutDataClass
|
||||
import suwayomi.tachidesk.global.impl.AppUpdate
|
||||
@@ -31,7 +31,7 @@ object SettingsController {
|
||||
ctx.json(About.getAbout())
|
||||
},
|
||||
withResults = {
|
||||
json<AboutDataClass>(HttpStatus.OK)
|
||||
json<AboutDataClass>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -45,13 +45,12 @@ object SettingsController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.future {
|
||||
future { AppUpdate.checkUpdate() }
|
||||
.thenApply { ctx.json(it) }
|
||||
}
|
||||
ctx.future(
|
||||
future { AppUpdate.checkUpdate() },
|
||||
)
|
||||
},
|
||||
withResults = {
|
||||
json<Array<UpdateDataClass>>(HttpStatus.OK)
|
||||
json<Array<UpdateDataClass>>(HttpCode.OK)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import suwayomi.tachidesk.server.generated.BuildConfig
|
||||
data class AboutDataClass(
|
||||
val name: String,
|
||||
val version: String,
|
||||
@Deprecated("The version includes the revision as the patch number")
|
||||
val revision: String,
|
||||
val buildType: String,
|
||||
val buildTime: Long,
|
||||
@@ -21,8 +20,8 @@ data class AboutDataClass(
|
||||
)
|
||||
|
||||
object About {
|
||||
fun getAbout(): AboutDataClass =
|
||||
AboutDataClass(
|
||||
fun getAbout(): AboutDataClass {
|
||||
return AboutDataClass(
|
||||
BuildConfig.NAME,
|
||||
BuildConfig.VERSION,
|
||||
BuildConfig.REVISION,
|
||||
@@ -31,4 +30,5 @@ object About {
|
||||
BuildConfig.GITHUB,
|
||||
BuildConfig.DISCORD,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user