Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70402a6d3a | |||
| 5c4143224a | |||
| f943e924f7 | |||
| 7d2f542f8a | |||
| fb1f88e971 | |||
| f566f13423 | |||
| 0f88baf1c1 | |||
| a04cbcd814 | |||
| a17d6a2ea4 | |||
| 08f49e0ac4 | |||
| 7787dd1ecc | |||
| 3d0765d4ab | |||
| b51651ace4 | |||
| 568fa56d59 | |||
| 50dee9251c | |||
| e575aaf4fb | |||
| bdd5caae1a | |||
| 3af7de3460 | |||
| caa219f8d6 | |||
| afabaccf1d | |||
| 53cc73701c | |||
| c69b954ffd | |||
| d05c447fe4 | |||
| 5810a24cb0 | |||
| e04c6a9f4d | |||
| e3a9f7af42 | |||
| 5eb58a73ee | |||
| 68ad1f72ce | |||
| 704a52d943 | |||
| 67ec9ccc4e | |||
| c7112ec67f | |||
| 1e1e2034eb | |||
| 5f316c6f44 | |||
| 0c39718f82 | |||
| 8a5ac4a0af | |||
| 492d5f5e84 | |||
| 5a3621fe39 | |||
| fb862e23e5 | |||
| 3d69348301 | |||
| 30787846a2 | |||
| 345ca27f85 | |||
| b7a6d6cae8 | |||
| dadb686514 | |||
| 75f635a28b | |||
| f2bd5b8149 | |||
| 6ed4c79ca4 | |||
| 333f954919 | |||
| 2494d0821d | |||
| e349d0cef3 | |||
| eddad2ba89 | |||
| bfaf88afd6 | |||
| cd59aed8c7 | |||
| f18ca5811f | |||
| 1ed9bcf7c8 | |||
| 29a79ab079 | |||
| 7c03c73419 | |||
| 74f3b9b609 | |||
| a3953d530e | |||
| 2280e8c725 | |||
| b327df732c | |||
| 21d7cf5d6a | |||
| 5b64bdc5b7 | |||
| a3a25b6263 | |||
| c7611c8024 | |||
| f08170504c | |||
| 863dccb5ea | |||
| fc8bb10ca3 | |||
| d06c3586fd | |||
| 395989b528 |
@@ -50,19 +50,12 @@ jobs:
|
||||
cd master
|
||||
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/webUI/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
|
||||
|
||||
- name: Build and copy webUI, Build Jar
|
||||
- name: Build Jar
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
build-root-directory: master
|
||||
wrapper-directory: master
|
||||
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||
arguments: :server:shadowJar --stacktrace
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
@@ -52,21 +52,14 @@ jobs:
|
||||
cd master
|
||||
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/webUI/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
|
||||
|
||||
- name: Build and copy webUI, Build Jar
|
||||
- name: Build Jar
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
env:
|
||||
ProductBuildType: "Preview"
|
||||
with:
|
||||
build-root-directory: master
|
||||
wrapper-directory: master
|
||||
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||
arguments: :server:shadowJar --stacktrace
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
@@ -99,7 +92,7 @@ jobs:
|
||||
- name: Checkout preview branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: 'Suwayomi/Tachidesk-preview'
|
||||
repository: 'Suwayomi/Tachidesk-Server-preview'
|
||||
ref: main
|
||||
path: preview
|
||||
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||
@@ -125,7 +118,7 @@ jobs:
|
||||
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||
artifacts: "master/server/build/*.jar,master/server/build/*.zip"
|
||||
owner: "Suwayomi"
|
||||
repo: "Tachidesk-preview"
|
||||
repo: "Tachidesk-Server-preview"
|
||||
tag: ${{ steps.GenTagName.outputs.value }}
|
||||
|
||||
- name: Run Docker build workflow
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
with:
|
||||
build-root-directory: master
|
||||
wrapper-directory: master
|
||||
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||
arguments: :server:downloadWebUI :server:shadowJar --stacktrace
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ gradle.properties
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
|
||||
server/src/main/resources/webUI
|
||||
server/src/main/resources/WebUI.zip
|
||||
server/tmp/
|
||||
server/tachiserver-data/
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@ dependencies {
|
||||
// compileOnly( fileTree(dir: new File(rootProject.rootDir, "libs/other"), include: "*.jar")
|
||||
|
||||
// JSON
|
||||
compileOnly( "com.google.code.gson:gson:2.8.6")
|
||||
compileOnly("com.google.code.gson:gson:2.8.6")
|
||||
|
||||
// Javassist
|
||||
compileOnly( "org.javassist:javassist:3.27.0-GA")
|
||||
compileOnly("org.javassist:javassist:3.27.0-GA")
|
||||
|
||||
// XML
|
||||
compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
|
||||
compileOnly(group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
|
||||
|
||||
// Config API
|
||||
implementation(project(":AndroidCompat:Config"))
|
||||
@@ -40,7 +40,7 @@ dependencies {
|
||||
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13")
|
||||
|
||||
// AndroidX annotations
|
||||
compileOnly( "androidx.annotation:annotation:1.2.0-alpha01")
|
||||
compileOnly("androidx.annotation:annotation:1.2.0-alpha01")
|
||||
|
||||
// substitute for duktape-android
|
||||
// 'org.mozilla:rhino' includes some code that we don't need so use 'org.mozilla:rhino-runtime' instead
|
||||
@@ -52,6 +52,9 @@ dependencies {
|
||||
val multiplatformSettingsVersion = "0.7.7"
|
||||
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
|
||||
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
|
||||
|
||||
// Android version of SimpleDateFormat
|
||||
implementation("com.ibm.icu:icu4j:69.1")
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
@@ -15,7 +15,7 @@ Write-Output "Getting required Android.jar..."
|
||||
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
|
||||
|
||||
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" -UseBasicParsing).content
|
||||
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/6cd31be5e4e25901aadf838120d71a79b46d9add/30/public/android.jar?format=TEXT" -UseBasicParsing).content
|
||||
|
||||
$android_jar = (Get-Location).Path + "\tmp\android.jar"
|
||||
|
||||
@@ -24,7 +24,7 @@ $android_jar = (Get-Location).Path + "\tmp\android.jar"
|
||||
# We need to remove any stub classes that we have implementations for
|
||||
Write-Output "Patching JAR..."
|
||||
|
||||
function Remove-Files-Zip($zipfile, $path)
|
||||
function Remove-Files-Zip($zipfile, $paths)
|
||||
{
|
||||
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') | Out-Null
|
||||
|
||||
@@ -32,7 +32,18 @@ function Remove-Files-Zip($zipfile, $path)
|
||||
$mode = [IO.Compression.ZipArchiveMode]::Update
|
||||
$zip = New-Object IO.Compression.ZipArchive($stream, $mode)
|
||||
|
||||
($zip.Entries | Where-Object { $_.FullName -like $path }) | ForEach-Object { Write-Output "Deleting: $($_.FullName)"; $_.Delete() }
|
||||
if ($paths.getType().Name -eq "Object[]")
|
||||
{
|
||||
$paths | ForEach-Object {
|
||||
$path = $_
|
||||
($zip.Entries | Where-Object { $_.FullName -like $path }) | ForEach-Object { Write-Output "Deleting: $($_.FullName)"; $_.Delete() }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
($zip.Entries | Where-Object { $_.FullName -like $paths }) | ForEach-Object { Write-Output "Deleting: $($_.FullName)"; $_.Delete() }
|
||||
}
|
||||
|
||||
|
||||
$zip.Dispose()
|
||||
$stream.Close()
|
||||
@@ -78,10 +89,7 @@ function Dedupe($path)
|
||||
$classes = Get-ChildItem . *.* -Recurse | Where-Object { !$_.PSIsContainer }
|
||||
$classes | ForEach-Object {
|
||||
"Processing class: $($_.FullName)"
|
||||
Remove-Files-Zip $android_jar "$($_.Name).class" | Out-Null
|
||||
Remove-Files-Zip $android_jar "$($_.Name)$*.class" | Out-Null
|
||||
Remove-Files-Zip $android_jar "$($_.Name)Kt.class" | Out-Null
|
||||
Remove-Files-Zip $android_jar "$($_.Name)Kt$*.class" | Out-Null
|
||||
Remove-Files-Zip $android_jar ("$($_.Name).class","$($_.Name)$*.class","$($_.Name)Kt.class","$($_.Name)Kt$*.class") | Out-Null
|
||||
}
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ do
|
||||
which $dep >/dev/null 2>&1 || { echo >&2 "Error: This script needs $dep installed."; abort=yes; }
|
||||
done
|
||||
|
||||
if [ $abort = yes ]; then
|
||||
if [ "$abort" = yes ]; then
|
||||
echo "Some of the dependencies didn't exist. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
@@ -30,7 +30,7 @@ rm -rf "tmp"
|
||||
mkdir -p "tmp"
|
||||
pushd "tmp"
|
||||
|
||||
curl "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" | base64 --decode > android.jar
|
||||
curl "https://android.googlesource.com/platform/prebuilts/sdk/+/6cd31be5e4e25901aadf838120d71a79b46d9add/30/public/android.jar?format=TEXT" | base64 --decode > android.jar
|
||||
|
||||
# We need to remove any stub classes that we have implementations for
|
||||
echo "Patching JAR..."
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v4.content;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.StatFs;
|
||||
import android.support.v4.os.EnvironmentCompat;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Helper for accessing features in {@link android.content.Context}
|
||||
* introduced after API level 4 in a backwards compatible fashion.
|
||||
*/
|
||||
public class ContextCompat {
|
||||
/**
|
||||
* Start a set of activities as a synthesized task stack, if able.
|
||||
*
|
||||
* <p>In API level 11 (Android 3.0/Honeycomb) the recommended conventions for
|
||||
* app navigation using the back key changed. The back key's behavior is local
|
||||
* to the current task and does not capture navigation across different tasks.
|
||||
* Navigating across tasks and easily reaching the previous task is accomplished
|
||||
* through the "recents" UI, accessible through the software-provided Recents key
|
||||
* on the navigation or system bar. On devices with the older hardware button configuration
|
||||
* the recents UI can be accessed with a long press on the Home key.</p>
|
||||
*
|
||||
* <p>When crossing from one task stack to another post-Android 3.0,
|
||||
* the application should synthesize a back stack/history for the new task so that
|
||||
* the user may navigate out of the new task and back to the Launcher by repeated
|
||||
* presses of the back key. Back key presses should not navigate across task stacks.</p>
|
||||
*
|
||||
* <p>startActivities provides a mechanism for constructing a synthetic task stack of
|
||||
* multiple activities. If the underlying API is not available on the system this method
|
||||
* will return false.</p>
|
||||
*
|
||||
* @param context Start activities using this activity as the starting context
|
||||
* @param intents Array of intents defining the activities that will be started. The element
|
||||
* length-1 will correspond to the top activity on the resulting task stack.
|
||||
* @return true if the underlying API was available and the call was successful, false otherwise
|
||||
*/
|
||||
public static boolean startActivities(Context context, Intent[] intents) {
|
||||
return startActivities(context, intents, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a set of activities as a synthesized task stack, if able.
|
||||
*
|
||||
* <p>In API level 11 (Android 3.0/Honeycomb) the recommended conventions for
|
||||
* app navigation using the back key changed. The back key's behavior is local
|
||||
* to the current task and does not capture navigation across different tasks.
|
||||
* Navigating across tasks and easily reaching the previous task is accomplished
|
||||
* through the "recents" UI, accessible through the software-provided Recents key
|
||||
* on the navigation or system bar. On devices with the older hardware button configuration
|
||||
* the recents UI can be accessed with a long press on the Home key.</p>
|
||||
*
|
||||
* <p>When crossing from one task stack to another post-Android 3.0,
|
||||
* the application should synthesize a back stack/history for the new task so that
|
||||
* the user may navigate out of the new task and back to the Launcher by repeated
|
||||
* presses of the back key. Back key presses should not navigate across task stacks.</p>
|
||||
*
|
||||
* <p>startActivities provides a mechanism for constructing a synthetic task stack of
|
||||
* multiple activities. If the underlying API is not available on the system this method
|
||||
* will return false.</p>
|
||||
*
|
||||
* @param context Start activities using this activity as the starting context
|
||||
* @param intents Array of intents defining the activities that will be started. The element
|
||||
* length-1 will correspond to the top activity on the resulting task stack.
|
||||
* @param options Additional options for how the Activity should be started.
|
||||
* See {@link android.content.Context#startActivity(Intent, Bundle)
|
||||
* @return true if the underlying API was available and the call was successful, false otherwise
|
||||
*/
|
||||
public static boolean startActivities(Context context, Intent[] intents,
|
||||
Bundle options) {
|
||||
context.startActivities(intents, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns absolute paths to application-specific directories on all
|
||||
* external storage devices where the application's OBB files (if there are
|
||||
* any) can be found. Note if the application does not have any OBB files,
|
||||
* these directories may not exist.
|
||||
* <p>
|
||||
* This is like {@link Context#getFilesDir()} in that these files will be
|
||||
* deleted when the application is uninstalled, however there are some
|
||||
* important differences:
|
||||
* <ul>
|
||||
* <li>External files are not always available: they will disappear if the
|
||||
* user mounts the external storage on a computer or removes it.
|
||||
* <li>There is no security enforced with these files.
|
||||
* </ul>
|
||||
* <p>
|
||||
* External storage devices returned here are considered a permanent part of
|
||||
* the device, including both emulated external storage and physical media
|
||||
* slots, such as SD cards in a battery compartment. The returned paths do
|
||||
* not include transient devices, such as USB flash drives.
|
||||
* <p>
|
||||
* An application may store data on any or all of the returned devices. For
|
||||
* example, an app may choose to store large files on the device with the
|
||||
* most available space, as measured by {@link StatFs}.
|
||||
* <p>
|
||||
* Starting in {@link android.os.Build.VERSION_CODES#KITKAT}, no permissions
|
||||
* are required to write to the returned paths; they're always accessible to
|
||||
* the calling app. Before then,
|
||||
* {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} is required to
|
||||
* write. Write access outside of these paths on secondary external storage
|
||||
* devices is not available. To request external storage access in a
|
||||
* backwards compatible way, consider using {@code android:maxSdkVersion}
|
||||
* like this:
|
||||
*
|
||||
* <pre class="prettyprint"><uses-permission
|
||||
* android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
* android:maxSdkVersion="18" /></pre>
|
||||
* <p>
|
||||
* The first path returned is the same as {@link Context#getObbDir()}.
|
||||
* Returned paths may be {@code null} if a storage device is unavailable.
|
||||
*
|
||||
* @see Context#getObbDir()
|
||||
* @see EnvironmentCompat#getStorageState(File)
|
||||
*/
|
||||
public static File[] getObbDirs(Context context) {
|
||||
return context.getObbDirs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns absolute paths to application-specific directories on all
|
||||
* external storage devices where the application can place persistent files
|
||||
* it owns. These files are internal to the application, and not typically
|
||||
* visible to the user as media.
|
||||
* <p>
|
||||
* This is like {@link Context#getFilesDir()} in that these files will be
|
||||
* deleted when the application is uninstalled, however there are some
|
||||
* important differences:
|
||||
* <ul>
|
||||
* <li>External files are not always available: they will disappear if the
|
||||
* user mounts the external storage on a computer or removes it.
|
||||
* <li>There is no security enforced with these files.
|
||||
* </ul>
|
||||
* <p>
|
||||
* External storage devices returned here are considered a permanent part of
|
||||
* the device, including both emulated external storage and physical media
|
||||
* slots, such as SD cards in a battery compartment. The returned paths do
|
||||
* not include transient devices, such as USB flash drives.
|
||||
* <p>
|
||||
* An application may store data on any or all of the returned devices. For
|
||||
* example, an app may choose to store large files on the device with the
|
||||
* most available space, as measured by {@link StatFs}.
|
||||
* <p>
|
||||
* Starting in {@link android.os.Build.VERSION_CODES#KITKAT}, no permissions
|
||||
* are required to write to the returned paths; they're always accessible to
|
||||
* the calling app. Before then,
|
||||
* {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} is required to
|
||||
* write. Write access outside of these paths on secondary external storage
|
||||
* devices is not available. To request external storage access in a
|
||||
* backwards compatible way, consider using {@code android:maxSdkVersion}
|
||||
* like this:
|
||||
*
|
||||
* <pre class="prettyprint"><uses-permission
|
||||
* android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
* android:maxSdkVersion="18" /></pre>
|
||||
* <p>
|
||||
* The first path returned is the same as
|
||||
* {@link Context#getExternalFilesDir(String)}. Returned paths may be
|
||||
* {@code null} if a storage device is unavailable.
|
||||
*
|
||||
* @see Context#getExternalFilesDir(String)
|
||||
* @see EnvironmentCompat#getStorageState(File)
|
||||
*/
|
||||
public static File[] getExternalFilesDirs(Context context, String type) {
|
||||
return context.getExternalFilesDirs(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns absolute paths to application-specific directories on all
|
||||
* external storage devices where the application can place cache files it
|
||||
* owns. These files are internal to the application, and not typically
|
||||
* visible to the user as media.
|
||||
* <p>
|
||||
* This is like {@link Context#getCacheDir()} in that these files will be
|
||||
* deleted when the application is uninstalled, however there are some
|
||||
* important differences:
|
||||
* <ul>
|
||||
* <li>External files are not always available: they will disappear if the
|
||||
* user mounts the external storage on a computer or removes it.
|
||||
* <li>There is no security enforced with these files.
|
||||
* </ul>
|
||||
* <p>
|
||||
* External storage devices returned here are considered a permanent part of
|
||||
* the device, including both emulated external storage and physical media
|
||||
* slots, such as SD cards in a battery compartment. The returned paths do
|
||||
* not include transient devices, such as USB flash drives.
|
||||
* <p>
|
||||
* An application may store data on any or all of the returned devices. For
|
||||
* example, an app may choose to store large files on the device with the
|
||||
* most available space, as measured by {@link StatFs}.
|
||||
* <p>
|
||||
* Starting in {@link android.os.Build.VERSION_CODES#KITKAT}, no permissions
|
||||
* are required to write to the returned paths; they're always accessible to
|
||||
* the calling app. Before then,
|
||||
* {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} is required to
|
||||
* write. Write access outside of these paths on secondary external storage
|
||||
* devices is not available. To request external storage access in a
|
||||
* backwards compatible way, consider using {@code android:maxSdkVersion}
|
||||
* like this:
|
||||
*
|
||||
* <pre class="prettyprint"><uses-permission
|
||||
* android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
* android:maxSdkVersion="18" /></pre>
|
||||
* <p>
|
||||
* The first path returned is the same as
|
||||
* {@link Context#getExternalCacheDir()}. Returned paths may be {@code null}
|
||||
* if a storage device is unavailable.
|
||||
*
|
||||
* @see Context#getExternalCacheDir()
|
||||
* @see EnvironmentCompat#getStorageState(File)
|
||||
*/
|
||||
public static File[] getExternalCacheDirs(Context context) {
|
||||
return context.getExternalCacheDirs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a drawable object associated with a particular resource ID.
|
||||
* <p>
|
||||
* Starting in {@link android.os.Build.VERSION_CODES#LOLLIPOP}, the returned
|
||||
* drawable will be styled for the specified Context's theme.
|
||||
*
|
||||
* @param id The desired resource identifier, as generated by the aapt tool.
|
||||
* This integer encodes the package, type, and resource entry.
|
||||
* The value 0 is an invalid identifier.
|
||||
* @return Drawable An object that can be used to draw this resource.
|
||||
*/
|
||||
public static final Drawable getDrawable(Context context, int id) {
|
||||
return context.getDrawable(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the directory on the filesystem similar to
|
||||
* {@link Context#getFilesDir()}. The difference is that files placed under this
|
||||
* directory will be excluded from automatic backup to remote storage on
|
||||
* devices running {@link android.os.Build.VERSION_CODES#LOLLIPOP} or later. See
|
||||
* {@link android.app.backup.BackupAgent BackupAgent} for a full discussion
|
||||
* of the automatic backup mechanism in Android.
|
||||
*
|
||||
* <p>No permissions are required to read or write to the returned path, since this
|
||||
* path is internal storage.
|
||||
*
|
||||
* @return The path of the directory holding application files that will not be
|
||||
* automatically backed up to remote storage.
|
||||
*
|
||||
* @see android.content.Context.getFilesDir
|
||||
*/
|
||||
public final File getNoBackupFilesDir(Context context) {
|
||||
return context.getNoBackupFilesDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the application specific cache directory on
|
||||
* the filesystem designed for storing cached code. On devices running
|
||||
* {@link android.os.Build.VERSION_CODES#LOLLIPOP} or later, the system will delete
|
||||
* any files stored in this location both when your specific application is
|
||||
* upgraded, and when the entire platform is upgraded.
|
||||
* <p>
|
||||
* This location is optimal for storing compiled or optimized code generated
|
||||
* by your application at runtime.
|
||||
* <p>
|
||||
* Apps require no extra permissions to read or write to the returned path,
|
||||
* since this path lives in their private storage.
|
||||
*
|
||||
* @return The path of the directory holding application code cache files.
|
||||
*/
|
||||
public final File getCodeCacheDir(Context context) {
|
||||
return context.getCodeCacheDir();
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v4.os;
|
||||
|
||||
import android.os.Environment;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Helper for accessing features in {@link Environment} introduced after API
|
||||
* level 4 in a backwards compatible fashion.
|
||||
*/
|
||||
public class EnvironmentCompat {
|
||||
/**
|
||||
* Unknown storage state, such as when a path isn't backed by known storage
|
||||
* media.
|
||||
*
|
||||
* @see #getStorageState(File)
|
||||
*/
|
||||
public static final String MEDIA_UNKNOWN = "unknown";
|
||||
|
||||
/**
|
||||
* Returns the current state of the storage device that provides the given
|
||||
* path.
|
||||
*
|
||||
* @return one of {@link #MEDIA_UNKNOWN}, {@link Environment#MEDIA_REMOVED},
|
||||
* {@link Environment#MEDIA_UNMOUNTED},
|
||||
* {@link Environment#MEDIA_CHECKING},
|
||||
* {@link Environment#MEDIA_NOFS},
|
||||
* {@link Environment#MEDIA_MOUNTED},
|
||||
* {@link Environment#MEDIA_MOUNTED_READ_ONLY},
|
||||
* {@link Environment#MEDIA_SHARED},
|
||||
* {@link Environment#MEDIA_BAD_REMOVAL}, or
|
||||
* {@link Environment#MEDIA_UNMOUNTABLE}.
|
||||
*/
|
||||
public static String getStorageState(File path) {
|
||||
return Environment.getStorageState(path);
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.preference;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A data store interface to be implemented and provided to the Preferences framework. This can be
|
||||
* used to replace the default {@link android.content.SharedPreferences}, if needed.
|
||||
*
|
||||
* <p>In most cases you want to use {@link android.content.SharedPreferences} as it is automatically
|
||||
* backed up and migrated to new devices. However, providing custom data store to preferences can be
|
||||
* useful if your app stores its preferences in a local db, cloud or they are device specific like
|
||||
* "Developer settings". It might be also useful when you want to use the preferences UI but
|
||||
* the data are not supposed to be stored at all because they are valid per session only.
|
||||
*
|
||||
* <p>Once a put method is called it is full responsibility of the data store implementation to
|
||||
* safely store the given values. Time expensive operations need to be done in the background to
|
||||
* prevent from blocking the UI. You also need to have a plan on how to serialize the data in case
|
||||
* the activity holding this object gets destroyed.
|
||||
*
|
||||
* <p>By default, all "put" methods throw {@link UnsupportedOperationException}.
|
||||
*/
|
||||
public abstract class PreferenceDataStore {
|
||||
|
||||
/**
|
||||
* Sets a {@link String} value to the data store.
|
||||
*
|
||||
* <p>Once the value is set the data store is responsible for holding it.
|
||||
*
|
||||
* @param key the name of the preference to modify
|
||||
* @param value the new value for the preference
|
||||
* @see #getString(String, String)
|
||||
*/
|
||||
public void putString(String key, @Nullable String value) {
|
||||
throw new UnsupportedOperationException("Not implemented on this data store");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a set of Strings to the data store.
|
||||
*
|
||||
* <p>Once the value is set the data store is responsible for holding it.
|
||||
*
|
||||
* @param key the name of the preference to modify
|
||||
* @param values the set of new values for the preference
|
||||
* @see #getStringSet(String, Set<String>)
|
||||
*/
|
||||
public void putStringSet(String key, @Nullable Set<String> values) {
|
||||
throw new UnsupportedOperationException("Not implemented on this data store");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an {@link Integer} value to the data store.
|
||||
*
|
||||
* <p>Once the value is set the data store is responsible for holding it.
|
||||
*
|
||||
* @param key the name of the preference to modify
|
||||
* @param value the new value for the preference
|
||||
* @see #getInt(String, int)
|
||||
*/
|
||||
public void putInt(String key, int value) {
|
||||
throw new UnsupportedOperationException("Not implemented on this data store");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a {@link Long} value to the data store.
|
||||
*
|
||||
* <p>Once the value is set the data store is responsible for holding it.
|
||||
*
|
||||
* @param key the name of the preference to modify
|
||||
* @param value the new value for the preference
|
||||
* @see #getLong(String, long)
|
||||
*/
|
||||
public void putLong(String key, long value) {
|
||||
throw new UnsupportedOperationException("Not implemented on this data store");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a {@link Float} value to the data store.
|
||||
*
|
||||
* <p>Once the value is set the data store is responsible for holding it.
|
||||
*
|
||||
* @param key the name of the preference to modify
|
||||
* @param value the new value for the preference
|
||||
* @see #getFloat(String, float)
|
||||
*/
|
||||
public void putFloat(String key, float value) {
|
||||
throw new UnsupportedOperationException("Not implemented on this data store");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a {@link Boolean} value to the data store.
|
||||
*
|
||||
* <p>Once the value is set the data store is responsible for holding it.
|
||||
*
|
||||
* @param key the name of the preference to modify
|
||||
* @param value the new value for the preference
|
||||
* @see #getBoolean(String, boolean)
|
||||
*/
|
||||
public void putBoolean(String key, boolean value) {
|
||||
throw new UnsupportedOperationException("Not implemented on this data store");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a {@link String} value from the data store.
|
||||
*
|
||||
* @param key the name of the preference to retrieve
|
||||
* @param defValue value to return if this preference does not exist in the storage
|
||||
* @return the value from the data store or the default return value
|
||||
* @see #putString(String, String)
|
||||
*/
|
||||
@Nullable
|
||||
public String getString(String key, @Nullable String defValue) {
|
||||
return defValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a set of Strings from the data store.
|
||||
*
|
||||
* @param key the name of the preference to retrieve
|
||||
* @param defValues values to return if this preference does not exist in the storage
|
||||
* @return the values from the data store or the default return values
|
||||
* @see #putStringSet(String, Set<String>)
|
||||
*/
|
||||
@Nullable
|
||||
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
|
||||
return defValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an {@link Integer} value from the data store.
|
||||
*
|
||||
* @param key the name of the preference to retrieve
|
||||
* @param defValue value to return if this preference does not exist in the storage
|
||||
* @return the value from the data store or the default return value
|
||||
* @see #putInt(String, int)
|
||||
*/
|
||||
public int getInt(String key, int defValue) {
|
||||
return defValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a {@link Long} value from the data store.
|
||||
*
|
||||
* @param key the name of the preference to retrieve
|
||||
* @param defValue value to return if this preference does not exist in the storage
|
||||
* @return the value from the data store or the default return value
|
||||
* @see #putLong(String, long)
|
||||
*/
|
||||
public long getLong(String key, long defValue) {
|
||||
return defValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a {@link Float} value from the data store.
|
||||
*
|
||||
* @param key the name of the preference to retrieve
|
||||
* @param defValue value to return if this preference does not exist in the storage
|
||||
* @return the value from the data store or the default return value
|
||||
* @see #putFloat(String, float)
|
||||
*/
|
||||
public float getFloat(String key, float defValue) {
|
||||
return defValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a {@link Boolean} value from the data store.
|
||||
*
|
||||
* @param key the name of the preference to retrieve
|
||||
* @param defValue value to return if this preference does not exist in the storage
|
||||
* @return the value from the data store or the default return value
|
||||
* @see #getBoolean(String, boolean)
|
||||
*/
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
return defValue;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package android.support.v7.preference;
|
||||
|
||||
public class PreferenceScreen {
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package android.widget;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
public class EditText {
|
||||
public EditText(android.content.Context context) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public EditText(android.content.Context context, android.util.AttributeSet attrs) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public boolean getFreezesText() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
protected boolean getDefaultEditable() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
protected android.text.method.MovementMethod getDefaultMovementMethod() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public android.text.Editable getText() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public void setText(java.lang.CharSequence text, android.widget.TextView.BufferType type) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public void setSelection(int start, int stop) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public void setSelection(int index) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public void selectAll() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public void extendSelection(int index) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public void setEllipsize(android.text.TextUtils.TruncateAt ellipsis) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public java.lang.CharSequence getAccessibilityClassName() { throw new RuntimeException("Stub!"); }
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package android.widget;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
public class Toast {
|
||||
public static final int LENGTH_LONG = 1;
|
||||
public static final int LENGTH_SHORT = 0;
|
||||
|
||||
private CharSequence text;
|
||||
|
||||
private Toast(CharSequence text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public Toast(android.content.Context context) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public void show() {
|
||||
System.out.printf("made a Toast: \"%s\"\n", text.toString());
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public void setView(android.view.View view) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public android.view.View getView() {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public void setDuration(int duration) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public int getDuration() {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public void setMargin(float horizontalMargin, float verticalMargin) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public float getHorizontalMargin() {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public float getVerticalMargin() {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public void setGravity(int gravity, int xOffset, int yOffset) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public int getGravity() {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public int getXOffset() {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public int getYOffset() {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public static Toast makeText(android.content.Context context, java.lang.CharSequence text, int duration) {
|
||||
return new Toast(text);
|
||||
}
|
||||
|
||||
public static android.widget.Toast makeText(android.content.Context context, int resId, int duration) throws android.content.res.Resources.NotFoundException {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public void setText(int resId) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public void setText(java.lang.CharSequence s) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package androidx.preference;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 android.content.Context;
|
||||
|
||||
public class CheckBoxPreference extends TwoStatePreference {
|
||||
// reference: https://android.googlesource.com/platform/frameworks/support/+/996971f962fcd554339a7cb2859cef9ca89dbcb7/preference/preference/src/main/java/androidx/preference/CheckBoxPreference.java
|
||||
|
||||
public CheckBoxPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package androidx.preference;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 android.content.Context;
|
||||
|
||||
public abstract class DialogPreference extends Preference {
|
||||
private CharSequence dialogTitle;
|
||||
private CharSequence dialogMessage;
|
||||
|
||||
public DialogPreference(Context context) { super(context); }
|
||||
|
||||
public CharSequence getDialogTitle() {
|
||||
return dialogTitle;
|
||||
}
|
||||
|
||||
public void setDialogTitle(CharSequence dialogTitle) {
|
||||
this.dialogTitle = dialogTitle;
|
||||
}
|
||||
|
||||
public CharSequence getDialogMessage() {
|
||||
return dialogMessage;
|
||||
}
|
||||
|
||||
public void setDialogMessage(CharSequence dialogMessage) {
|
||||
this.dialogMessage = dialogMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package androidx.preference;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.widget.EditText;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
public class EditTextPreference extends DialogPreference {
|
||||
// reference: https://android.googlesource.com/platform/frameworks/support/+/996971f962fcd554339a7cb2859cef9ca89dbcb7/preference/preference/src/main/java/androidx/preference/EditTextPreference.java
|
||||
|
||||
private String text;
|
||||
|
||||
@JsonIgnore
|
||||
private OnBindEditTextListener onBindEditTextListener;
|
||||
|
||||
public EditTextPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public OnBindEditTextListener getOnBindEditTextListener() {
|
||||
return onBindEditTextListener;
|
||||
}
|
||||
|
||||
public void setOnBindEditTextListener(@Nullable OnBindEditTextListener onBindEditTextListener) {
|
||||
this.onBindEditTextListener = onBindEditTextListener;
|
||||
}
|
||||
|
||||
public interface OnBindEditTextListener {
|
||||
void onBindEditText(@NonNull EditText editText);
|
||||
}
|
||||
|
||||
/** Tachidesk specific API */
|
||||
@Override
|
||||
public String getDefaultValueType() {
|
||||
return "String";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package androidx.preference;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
public class ListPreference extends Preference {
|
||||
// reference: https://android.googlesource.com/platform/frameworks/support/+/996971f962fcd554339a7cb2859cef9ca89dbcb7/preference/preference/src/main/java/androidx/preference/ListPreference.java
|
||||
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
|
||||
|
||||
private CharSequence[] entries;
|
||||
private CharSequence[] entryValues;
|
||||
|
||||
public ListPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public CharSequence[] getEntries() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
public void setEntries(CharSequence[] entries) {
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
public int findIndexOfValue(String value) {
|
||||
if (value != null && entryValues != null) {
|
||||
for (int i = entryValues.length - 1; i >= 0; i--) {
|
||||
if (TextUtils.equals(entryValues[i].toString(), value)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public CharSequence[] getEntryValues() {
|
||||
return entryValues;
|
||||
}
|
||||
|
||||
public void setEntryValues(CharSequence[] entryValues) {
|
||||
this.entryValues = entryValues;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public void setValueIndex(int index) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public String getValue() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public void setValue(String value) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
/** Tachidesk specific API */
|
||||
@Override
|
||||
public String getDefaultValueType() {
|
||||
return "String";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package androidx.preference;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 android.content.Context;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class MultiSelectListPreference extends DialogPreference {
|
||||
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
|
||||
|
||||
public MultiSelectListPreference(Context context) { super(context); }
|
||||
|
||||
@JsonIgnore
|
||||
public void setEntries(CharSequence[] entries) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public CharSequence[] getEntries() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public void setEntryValues(CharSequence[] entryValues) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public CharSequence[] getEntryValues() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public void setValues(Set<String> values) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public Set<String> getValues() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
public int findIndexOfValue(String value) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
/** Tachidesk specific API */
|
||||
@Override
|
||||
public String getDefaultValueType() {
|
||||
return "Set";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package androidx.preference;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
/**
|
||||
* A minimal implementation of androidx.preference.Preference
|
||||
*/
|
||||
public class Preference {
|
||||
// reference: https://android.googlesource.com/platform/frameworks/support/+/996971f962fcd554339a7cb2859cef9ca89dbcb7/preference/preference/src/main/java/androidx/preference/Preference.java
|
||||
// Note: `Preference` doesn't actually hold or persist the value, `OnPreferenceChangeListener` is called and it's up to the extension to persist it.
|
||||
|
||||
@JsonIgnore
|
||||
protected Context context;
|
||||
|
||||
private String key;
|
||||
private CharSequence title;
|
||||
private CharSequence summary;
|
||||
private Object defaultValue;
|
||||
|
||||
/** Tachidesk specific API */
|
||||
@JsonIgnore
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
@JsonIgnore
|
||||
public OnPreferenceChangeListener onChangeListener;
|
||||
|
||||
public Preference(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public void setOnPreferenceChangeListener(OnPreferenceChangeListener onPreferenceChangeListener) {
|
||||
this.onChangeListener = onPreferenceChangeListener;
|
||||
}
|
||||
|
||||
public void setOnPreferenceClickListener(OnPreferenceClickListener onPreferenceClickListener) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public CharSequence getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(CharSequence title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public CharSequence getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(CharSequence summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public void setDefaultValue(Object defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public boolean callChangeListener(Object newValue) {
|
||||
return onChangeListener == null || onChangeListener.onPreferenceChange(this, newValue);
|
||||
}
|
||||
|
||||
public Object getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/** Tachidesk specific API */
|
||||
public String getDefaultValueType() {
|
||||
return defaultValue.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
/** Tachidesk specific API */
|
||||
public SharedPreferences getSharedPreferences() {
|
||||
return sharedPreferences;
|
||||
}
|
||||
|
||||
/** Tachidesk specific API */
|
||||
public void setSharedPreferences(SharedPreferences sharedPreferences) {
|
||||
this.sharedPreferences = sharedPreferences;
|
||||
}
|
||||
|
||||
public interface OnPreferenceChangeListener {
|
||||
boolean onPreferenceChange(Preference preference, Object newValue);
|
||||
}
|
||||
|
||||
public interface OnPreferenceClickListener {
|
||||
boolean onPreferenceClick(Preference preference);
|
||||
}
|
||||
|
||||
/** Tachidesk specific API */
|
||||
public Object getCurrentValue() {
|
||||
switch (getDefaultValueType()) {
|
||||
case "String":
|
||||
return sharedPreferences.getString(key, (String)defaultValue);
|
||||
case "Boolean":
|
||||
return sharedPreferences.getBoolean(key, (Boolean)defaultValue);
|
||||
default:
|
||||
throw new RuntimeException("Unsupported type");
|
||||
}
|
||||
}
|
||||
|
||||
/** Tachidesk specific API */
|
||||
public void saveNewValue(Object value) {
|
||||
switch (getDefaultValueType()) {
|
||||
case "String":
|
||||
sharedPreferences.edit().putString(key, (String)value).apply();
|
||||
break;
|
||||
case "Boolean":
|
||||
sharedPreferences.edit().putBoolean(key, (Boolean)value).apply();
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unsupported type");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package androidx.preference;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 android.content.Context;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class PreferenceScreen extends Preference {
|
||||
/** Tachidesk specific API */
|
||||
private List<Preference> preferences = new LinkedList<>();
|
||||
|
||||
public PreferenceScreen(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public boolean addPreference(Preference preference) {
|
||||
// propagate own shared preferences
|
||||
preference.setSharedPreferences(getSharedPreferences());
|
||||
|
||||
preferences.add(preference);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Tachidesk specific API */
|
||||
public List<Preference> getPreferences(){
|
||||
return preferences;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package androidx.preference;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 android.content.Context;
|
||||
|
||||
public class SwitchPreferenceCompat extends TwoStatePreference {
|
||||
// reference: https://android.googlesource.com/platform/frameworks/support/+/996971f962fcd554339a7cb2859cef9ca89dbcb7/preference/preference/src/main/java/androidx/preference/CheckBoxPreference.java
|
||||
|
||||
public SwitchPreferenceCompat(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package androidx.preference;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 android.content.Context;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
public class TwoStatePreference extends Preference {
|
||||
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
|
||||
|
||||
public TwoStatePreference(Context context) { super(context); }
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isChecked() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public void setChecked(boolean checked) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public CharSequence getSummaryOn() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public void setSummaryOn(CharSequence summary) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public CharSequence getSummaryOff() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public void setSummaryOff(CharSequence summary) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public boolean getDisableDependentsState() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@JsonIgnore
|
||||
public void setDisableDependentsState(boolean disableDependentsState) { throw new RuntimeException("Stub!"); }
|
||||
|
||||
/** Tachidesk specific API */
|
||||
@Override
|
||||
public String getDefaultValueType() {
|
||||
return "Boolean";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package rx.android.schedulers
|
||||
|
||||
import rx.Scheduler
|
||||
import rx.internal.schedulers.ImmediateScheduler
|
||||
|
||||
class AndroidSchedulers {
|
||||
@@ -11,6 +12,7 @@ class AndroidSchedulers {
|
||||
/**
|
||||
* Simulated main thread scheduler
|
||||
*/
|
||||
fun mainThread() = mainThreadScheduler
|
||||
@JvmStatic
|
||||
fun mainThread(): Scheduler = mainThreadScheduler
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,5 +26,8 @@ class AndroidCompatInitializer {
|
||||
ApplicationInfoConfigModule.register(GlobalConfigManager.config),
|
||||
SystemConfigModule.register(GlobalConfigManager.config)
|
||||
)
|
||||
|
||||
// Set some properties extensions use
|
||||
System.setProperty("http.agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -50,10 +50,9 @@ import java.util.Map;
|
||||
/**
|
||||
* Custom context implementation.
|
||||
*
|
||||
* TODO Deal with packagemanager for extension sources
|
||||
*/
|
||||
public class CustomContext extends Context implements DIAware {
|
||||
private DI kodein;
|
||||
private final DI kodein;
|
||||
public CustomContext() {
|
||||
this(KodeinGlobalHelper.kodein());
|
||||
}
|
||||
@@ -734,4 +733,3 @@ public class CustomContext extends Context implements DIAware {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ class AndroidFiles(val configManager: ConfigManager = GlobalConfigManager) {
|
||||
val downloadCacheDir: File get() = registerFile(filesConfig.downloadCacheDir)
|
||||
val databasesDir: File get() = registerFile(filesConfig.databasesDir)
|
||||
|
||||
val prefsDir: File get() = registerFile(filesConfig.prefsDir)
|
||||
|
||||
val packagesDir: File get() = registerFile(filesConfig.packageDir)
|
||||
|
||||
fun registerFile(file: String): File {
|
||||
|
||||
+13
-4
@@ -1,12 +1,18 @@
|
||||
package xyz.nulldev.androidcompat.io.sharedprefs
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 android.content.SharedPreferences
|
||||
import com.russhwolf.settings.ExperimentalSettingsApi
|
||||
import com.russhwolf.settings.ExperimentalSettingsImplementation
|
||||
import com.russhwolf.settings.JvmPreferencesSettings
|
||||
import com.russhwolf.settings.serialization.decodeValue
|
||||
import com.russhwolf.settings.serialization.encodeValue
|
||||
import com.russhwolf.settings.set
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.builtins.SetSerializer
|
||||
@@ -138,9 +144,12 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (value) {
|
||||
is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), key, value as Set<String>)
|
||||
else -> {
|
||||
preferences[key] = value
|
||||
}
|
||||
is String -> preferences.putString(key, value)
|
||||
is Int -> preferences.putInt(key, value)
|
||||
is Long -> preferences.putLong(key, value)
|
||||
is Float -> preferences.putFloat(key, value)
|
||||
is Double -> preferences.putDouble(key, value)
|
||||
is Boolean -> preferences.putBoolean(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -233,7 +233,7 @@ public class JsonSharedPreferences implements SharedPreferences {
|
||||
private JsonSharedPreferencesEditor() {
|
||||
}
|
||||
|
||||
private void recordChange(String key) {
|
||||
private void recordChange(String key) {
|
||||
if (!affectedKeys.contains(key)) {
|
||||
affectedKeys.add(key);
|
||||
}
|
||||
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
package xyz.nulldev.androidcompat.replace.java.text;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 com.ibm.icu.text.DisplayContext;
|
||||
import com.ibm.icu.util.Currency;
|
||||
import com.ibm.icu.util.CurrencyAmount;
|
||||
import com.ibm.icu.util.ULocale;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.text.AttributedCharacterIterator;
|
||||
import java.text.FieldPosition;
|
||||
import java.text.ParseException;
|
||||
import java.text.ParsePosition;
|
||||
import java.util.Locale;
|
||||
|
||||
public class NumberFormat extends java.text.NumberFormat {
|
||||
private com.ibm.icu.text.NumberFormat delegate;
|
||||
|
||||
public NumberFormat(com.ibm.icu.text.NumberFormat delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public StringBuffer format(Object number, StringBuffer toAppendTo, FieldPosition pos) {
|
||||
return delegate.format(number, toAppendTo, pos);
|
||||
}
|
||||
|
||||
public String format(BigInteger number) {
|
||||
return delegate.format(number);
|
||||
}
|
||||
|
||||
public String format(BigDecimal number) {
|
||||
return delegate.format(number);
|
||||
}
|
||||
|
||||
public String format(com.ibm.icu.math.BigDecimal number) {
|
||||
return delegate.format(number);
|
||||
}
|
||||
|
||||
public String format(CurrencyAmount currAmt) {
|
||||
return delegate.format(currAmt);
|
||||
}
|
||||
|
||||
public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
|
||||
return delegate.format(number, toAppendTo, pos);
|
||||
}
|
||||
|
||||
public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
|
||||
return delegate.format(number, toAppendTo, pos);
|
||||
}
|
||||
|
||||
public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
|
||||
return delegate.format(number, toAppendTo, pos);
|
||||
}
|
||||
|
||||
public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
|
||||
return delegate.format(number, toAppendTo, pos);
|
||||
}
|
||||
|
||||
public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
|
||||
return delegate.format(number, toAppendTo, pos);
|
||||
}
|
||||
|
||||
public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) {
|
||||
return delegate.format(currAmt, toAppendTo, pos);
|
||||
}
|
||||
|
||||
public Number parse(String text, ParsePosition parsePosition) {
|
||||
return delegate.parse(text, parsePosition);
|
||||
}
|
||||
|
||||
public Number parse(String text) throws ParseException {
|
||||
return delegate.parse(text);
|
||||
}
|
||||
|
||||
public CurrencyAmount parseCurrency(CharSequence text, ParsePosition pos) {
|
||||
return delegate.parseCurrency(text, pos);
|
||||
}
|
||||
|
||||
public boolean isParseIntegerOnly() {
|
||||
return delegate.isParseIntegerOnly();
|
||||
}
|
||||
|
||||
public void setParseIntegerOnly(boolean value) {
|
||||
delegate.setParseIntegerOnly(value);
|
||||
}
|
||||
|
||||
public void setParseStrict(boolean value) {
|
||||
delegate.setParseStrict(value);
|
||||
}
|
||||
|
||||
public boolean isParseStrict() {
|
||||
return delegate.isParseStrict();
|
||||
}
|
||||
|
||||
public void setContext(DisplayContext context) {
|
||||
delegate.setContext(context);
|
||||
}
|
||||
|
||||
public DisplayContext getContext(DisplayContext.Type type) {
|
||||
return delegate.getContext(type);
|
||||
}
|
||||
|
||||
public static java.text.NumberFormat getInstance(Locale inLocale) {
|
||||
return new NumberFormat(com.ibm.icu.text.NumberFormat.getInstance(inLocale));
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.NumberFormat getInstance(ULocale inLocale) {
|
||||
return com.ibm.icu.text.NumberFormat.getInstance(inLocale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.NumberFormat getInstance(int style) {
|
||||
return com.ibm.icu.text.NumberFormat.getInstance(style);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.NumberFormat getInstance(Locale inLocale, int style) {
|
||||
return com.ibm.icu.text.NumberFormat.getInstance(inLocale, style);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.NumberFormat getNumberInstance(ULocale inLocale) {
|
||||
return com.ibm.icu.text.NumberFormat.getNumberInstance(inLocale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.NumberFormat getIntegerInstance(ULocale inLocale) {
|
||||
return com.ibm.icu.text.NumberFormat.getIntegerInstance(inLocale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.NumberFormat getCurrencyInstance(ULocale inLocale) {
|
||||
return com.ibm.icu.text.NumberFormat.getCurrencyInstance(inLocale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.NumberFormat getPercentInstance(ULocale inLocale) {
|
||||
return com.ibm.icu.text.NumberFormat.getPercentInstance(inLocale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.NumberFormat getScientificInstance(ULocale inLocale) {
|
||||
return com.ibm.icu.text.NumberFormat.getScientificInstance(inLocale);
|
||||
}
|
||||
|
||||
public static Locale[] getAvailableLocales() {
|
||||
return com.ibm.icu.text.NumberFormat.getAvailableLocales();
|
||||
}
|
||||
|
||||
public static ULocale[] getAvailableULocales() {
|
||||
return com.ibm.icu.text.NumberFormat.getAvailableULocales();
|
||||
}
|
||||
|
||||
public static Object registerFactory(com.ibm.icu.text.NumberFormat.NumberFormatFactory factory) {
|
||||
return com.ibm.icu.text.NumberFormat.registerFactory(factory);
|
||||
}
|
||||
|
||||
public static boolean unregister(Object registryKey) {
|
||||
return com.ibm.icu.text.NumberFormat.unregister(registryKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return delegate.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return delegate.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone() {
|
||||
return delegate.clone();
|
||||
}
|
||||
|
||||
public boolean isGroupingUsed() {
|
||||
return delegate.isGroupingUsed();
|
||||
}
|
||||
|
||||
public void setGroupingUsed(boolean newValue) {
|
||||
delegate.setGroupingUsed(newValue);
|
||||
}
|
||||
|
||||
public int getMaximumIntegerDigits() {
|
||||
return delegate.getMaximumIntegerDigits();
|
||||
}
|
||||
|
||||
public void setMaximumIntegerDigits(int newValue) {
|
||||
delegate.setMaximumIntegerDigits(newValue);
|
||||
}
|
||||
|
||||
public int getMinimumIntegerDigits() {
|
||||
return delegate.getMinimumIntegerDigits();
|
||||
}
|
||||
|
||||
public void setMinimumIntegerDigits(int newValue) {
|
||||
delegate.setMinimumIntegerDigits(newValue);
|
||||
}
|
||||
|
||||
public int getMaximumFractionDigits() {
|
||||
return delegate.getMaximumFractionDigits();
|
||||
}
|
||||
|
||||
public void setMaximumFractionDigits(int newValue) {
|
||||
delegate.setMaximumFractionDigits(newValue);
|
||||
}
|
||||
|
||||
public int getMinimumFractionDigits() {
|
||||
return delegate.getMinimumFractionDigits();
|
||||
}
|
||||
|
||||
public void setMinimumFractionDigits(int newValue) {
|
||||
delegate.setMinimumFractionDigits(newValue);
|
||||
}
|
||||
|
||||
public void setCurrency(Currency theCurrency) {
|
||||
delegate.setCurrency(theCurrency);
|
||||
}
|
||||
|
||||
public java.util.Currency getCurrency() {
|
||||
return java.util.Currency.getInstance(delegate.getCurrency().getCurrencyCode());
|
||||
}
|
||||
|
||||
public void setRoundingMode(int roundingMode) {
|
||||
delegate.setRoundingMode(roundingMode);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.NumberFormat getInstance(ULocale desiredLocale, int choice) {
|
||||
return com.ibm.icu.text.NumberFormat.getInstance(desiredLocale, choice);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static String getPatternForStyle(ULocale forLocale, int choice) {
|
||||
return com.ibm.icu.text.NumberFormat.getPatternForStyle(forLocale, choice);
|
||||
}
|
||||
|
||||
public ULocale getLocale(ULocale.Type type) {
|
||||
return delegate.getLocale(type);
|
||||
}
|
||||
|
||||
public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
|
||||
return delegate.formatToCharacterIterator(obj);
|
||||
}
|
||||
|
||||
public Object parseObject(String source) throws ParseException {
|
||||
return delegate.parseObject(source);
|
||||
}
|
||||
}
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
package xyz.nulldev.androidcompat.replace.java.text;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 com.ibm.icu.text.DateFormatSymbols;
|
||||
import com.ibm.icu.text.DisplayContext;
|
||||
import com.ibm.icu.text.TimeZoneFormat;
|
||||
import com.ibm.icu.util.ULocale;
|
||||
import xyz.nulldev.androidcompat.replace.java.util.Calendar;
|
||||
import xyz.nulldev.androidcompat.replace.java.util.TimeZone;
|
||||
|
||||
import java.text.*;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Overridden to switch to Android implementation
|
||||
*/
|
||||
public class SimpleDateFormat extends java.text.DateFormat {
|
||||
private com.ibm.icu.text.SimpleDateFormat delegate;
|
||||
|
||||
public SimpleDateFormat() {
|
||||
delegate = new com.ibm.icu.text.SimpleDateFormat();
|
||||
}
|
||||
|
||||
private SimpleDateFormat(com.ibm.icu.text.SimpleDateFormat delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public SimpleDateFormat(String pattern) {
|
||||
delegate = new com.ibm.icu.text.SimpleDateFormat(pattern);
|
||||
}
|
||||
|
||||
public SimpleDateFormat(String pattern, Locale loc) {
|
||||
delegate = new com.ibm.icu.text.SimpleDateFormat(pattern, loc);
|
||||
}
|
||||
|
||||
public SimpleDateFormat(String pattern, ULocale loc) {
|
||||
delegate = new com.ibm.icu.text.SimpleDateFormat(pattern, loc);
|
||||
}
|
||||
|
||||
public SimpleDateFormat(String pattern, String override, ULocale loc) {
|
||||
delegate = new com.ibm.icu.text.SimpleDateFormat(pattern, override, loc);
|
||||
}
|
||||
|
||||
public SimpleDateFormat(String pattern, DateFormatSymbols formatData) {
|
||||
delegate = new com.ibm.icu.text.SimpleDateFormat(pattern, formatData);
|
||||
}
|
||||
|
||||
public SimpleDateFormat(String pattern, DateFormatSymbols formatData, ULocale loc) {
|
||||
delegate = new com.ibm.icu.text.SimpleDateFormat(pattern, formatData, loc);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static SimpleDateFormat getInstance(com.ibm.icu.util.Calendar.FormatConfiguration formatConfig) {
|
||||
return new SimpleDateFormat(com.ibm.icu.text.SimpleDateFormat.getInstance(formatConfig));
|
||||
}
|
||||
|
||||
public void set2DigitYearStart(Date startDate) {
|
||||
delegate.set2DigitYearStart(startDate);
|
||||
}
|
||||
|
||||
public Date get2DigitYearStart() {
|
||||
return delegate.get2DigitYearStart();
|
||||
}
|
||||
|
||||
public void setContext(DisplayContext context) {
|
||||
delegate.setContext(context);
|
||||
}
|
||||
|
||||
public StringBuffer format(com.ibm.icu.util.Calendar cal, StringBuffer toAppendTo, FieldPosition pos) {
|
||||
return delegate.format(cal, toAppendTo, pos);
|
||||
}
|
||||
|
||||
public void setNumberFormat(com.ibm.icu.text.NumberFormat newNumberFormat) {
|
||||
delegate.setNumberFormat(newNumberFormat);
|
||||
}
|
||||
|
||||
public void parse(String text, com.ibm.icu.util.Calendar cal, ParsePosition parsePos) {
|
||||
delegate.parse(text, cal, parsePos);
|
||||
}
|
||||
|
||||
public String toPattern() {
|
||||
return delegate.toPattern();
|
||||
}
|
||||
|
||||
public String toLocalizedPattern() {
|
||||
return delegate.toLocalizedPattern();
|
||||
}
|
||||
|
||||
public void applyPattern(String pat) {
|
||||
delegate.applyPattern(pat);
|
||||
}
|
||||
|
||||
public void applyLocalizedPattern(String pat) {
|
||||
delegate.applyLocalizedPattern(pat);
|
||||
}
|
||||
|
||||
public DateFormatSymbols getDateFormatSymbols() {
|
||||
return delegate.getDateFormatSymbols();
|
||||
}
|
||||
|
||||
public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) {
|
||||
delegate.setDateFormatSymbols(newFormatSymbols);
|
||||
}
|
||||
|
||||
public TimeZoneFormat getTimeZoneFormat() {
|
||||
return delegate.getTimeZoneFormat();
|
||||
}
|
||||
|
||||
public void setTimeZoneFormat(TimeZoneFormat tzfmt) {
|
||||
delegate.setTimeZoneFormat(tzfmt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone() {
|
||||
return delegate.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return delegate.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return delegate.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
|
||||
return delegate.formatToCharacterIterator(obj);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public StringBuffer intervalFormatByAlgorithm(com.ibm.icu.util.Calendar fromCalendar, com.ibm.icu.util.Calendar toCalendar, StringBuffer appendTo, FieldPosition pos) throws IllegalArgumentException {
|
||||
return delegate.intervalFormatByAlgorithm(fromCalendar, toCalendar, appendTo, pos);
|
||||
}
|
||||
|
||||
public void setNumberFormat(String fields, com.ibm.icu.text.NumberFormat overrideNF) {
|
||||
delegate.setNumberFormat(fields, overrideNF);
|
||||
}
|
||||
|
||||
public com.ibm.icu.text.NumberFormat getNumberFormat(char field) {
|
||||
return delegate.getNumberFormat(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
|
||||
return delegate.format(date, toAppendTo, fieldPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date parse(String text) throws ParseException {
|
||||
return delegate.parse(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date parse(String text, ParsePosition pos) {
|
||||
return delegate.parse(text, pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseObject(String source, ParsePosition pos) {
|
||||
return delegate.parseObject(source, pos);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getTimeInstance(int style, ULocale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getTimeInstance(style, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getDateInstance(int style, ULocale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getDateInstance(style, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getDateTimeInstance(int dateStyle, int timeStyle, ULocale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale);
|
||||
}
|
||||
|
||||
public static Locale[] getAvailableLocales() {
|
||||
return com.ibm.icu.text.DateFormat.getAvailableLocales();
|
||||
}
|
||||
|
||||
public static ULocale[] getAvailableULocales() {
|
||||
return com.ibm.icu.text.DateFormat.getAvailableULocales();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCalendar(java.util.Calendar newCalendar) {
|
||||
com.ibm.icu.util.Calendar cal = com.ibm.icu.util.Calendar.getInstance(com.ibm.icu.util.TimeZone.getTimeZone(newCalendar.getTimeZone().getID()));
|
||||
cal.setTimeInMillis(newCalendar.getTimeInMillis());
|
||||
delegate.setCalendar(cal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Calendar getCalendar() {
|
||||
return new Calendar(delegate.getCalendar());
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.text.NumberFormat getNumberFormat() {
|
||||
return new NumberFormat(delegate.getNumberFormat());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTimeZone(java.util.TimeZone zone) {
|
||||
delegate.setTimeZone(com.ibm.icu.util.TimeZone.getTimeZone(zone.getID()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.TimeZone getTimeZone() {
|
||||
return new TimeZone(delegate.getTimeZone());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLenient(boolean lenient) {
|
||||
delegate.setLenient(lenient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLenient() {
|
||||
return delegate.isLenient();
|
||||
}
|
||||
|
||||
public void setCalendarLenient(boolean lenient) {
|
||||
delegate.setCalendarLenient(lenient);
|
||||
}
|
||||
|
||||
public boolean isCalendarLenient() {
|
||||
return delegate.isCalendarLenient();
|
||||
}
|
||||
|
||||
public com.ibm.icu.text.DateFormat setBooleanAttribute(com.ibm.icu.text.DateFormat.BooleanAttribute key, boolean value) {
|
||||
return delegate.setBooleanAttribute(key, value);
|
||||
}
|
||||
|
||||
public boolean getBooleanAttribute(com.ibm.icu.text.DateFormat.BooleanAttribute key) {
|
||||
return delegate.getBooleanAttribute(key);
|
||||
}
|
||||
|
||||
public DisplayContext getContext(DisplayContext.Type type) {
|
||||
return delegate.getContext(type);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getDateInstance(com.ibm.icu.util.Calendar cal, int dateStyle, Locale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getDateInstance(cal, dateStyle, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getDateInstance(com.ibm.icu.util.Calendar cal, int dateStyle, ULocale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getDateInstance(cal, dateStyle, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getTimeInstance(com.ibm.icu.util.Calendar cal, int timeStyle, Locale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getTimeInstance(cal, timeStyle, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getTimeInstance(com.ibm.icu.util.Calendar cal, int timeStyle, ULocale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getTimeInstance(cal, timeStyle, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getDateTimeInstance(com.ibm.icu.util.Calendar cal, int dateStyle, int timeStyle, Locale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getDateTimeInstance(cal, dateStyle, timeStyle, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getDateTimeInstance(com.ibm.icu.util.Calendar cal, int dateStyle, int timeStyle, ULocale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getDateTimeInstance(cal, dateStyle, timeStyle, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getInstance(com.ibm.icu.util.Calendar cal, Locale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getInstance(cal, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getInstance(com.ibm.icu.util.Calendar cal, ULocale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getInstance(cal, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getInstance(com.ibm.icu.util.Calendar cal) {
|
||||
return com.ibm.icu.text.DateFormat.getInstance(cal);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getDateInstance(com.ibm.icu.util.Calendar cal, int dateStyle) {
|
||||
return com.ibm.icu.text.DateFormat.getDateInstance(cal, dateStyle);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getTimeInstance(com.ibm.icu.util.Calendar cal, int timeStyle) {
|
||||
return com.ibm.icu.text.DateFormat.getTimeInstance(cal, timeStyle);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getDateTimeInstance(com.ibm.icu.util.Calendar cal, int dateStyle, int timeStyle) {
|
||||
return com.ibm.icu.text.DateFormat.getDateTimeInstance(cal, dateStyle, timeStyle);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getInstanceForSkeleton(String skeleton) {
|
||||
return com.ibm.icu.text.DateFormat.getInstanceForSkeleton(skeleton);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getInstanceForSkeleton(String skeleton, Locale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getInstanceForSkeleton(skeleton, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getInstanceForSkeleton(String skeleton, ULocale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getInstanceForSkeleton(skeleton, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getInstanceForSkeleton(com.ibm.icu.util.Calendar cal, String skeleton, Locale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getInstanceForSkeleton(cal, skeleton, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getInstanceForSkeleton(com.ibm.icu.util.Calendar cal, String skeleton, ULocale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getInstanceForSkeleton(cal, skeleton, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getPatternInstance(String skeleton) {
|
||||
return com.ibm.icu.text.DateFormat.getPatternInstance(skeleton);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getPatternInstance(String skeleton, Locale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getPatternInstance(skeleton, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getPatternInstance(String skeleton, ULocale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getPatternInstance(skeleton, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getPatternInstance(com.ibm.icu.util.Calendar cal, String skeleton, Locale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getPatternInstance(cal, skeleton, locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.text.DateFormat getPatternInstance(com.ibm.icu.util.Calendar cal, String skeleton, ULocale locale) {
|
||||
return com.ibm.icu.text.DateFormat.getPatternInstance(cal, skeleton, locale);
|
||||
}
|
||||
|
||||
public ULocale getLocale(ULocale.Type type) {
|
||||
return delegate.getLocale(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseObject(String source) throws ParseException {
|
||||
return delegate.parseObject(source);
|
||||
}
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
package xyz.nulldev.androidcompat.replace.java.util;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 com.ibm.icu.text.DateFormat;
|
||||
import com.ibm.icu.util.ULocale;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class Calendar extends java.util.Calendar {
|
||||
private com.ibm.icu.util.Calendar delegate;
|
||||
|
||||
public Calendar(com.ibm.icu.util.Calendar delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public static java.util.Calendar getInstance() {
|
||||
return new Calendar(com.ibm.icu.util.Calendar.getInstance());
|
||||
}
|
||||
|
||||
public static com.ibm.icu.util.Calendar getInstance(com.ibm.icu.util.TimeZone zone) {
|
||||
return com.ibm.icu.util.Calendar.getInstance(zone);
|
||||
}
|
||||
|
||||
public static java.util.Calendar getInstance(Locale aLocale) {
|
||||
return new Calendar(com.ibm.icu.util.Calendar.getInstance(aLocale));
|
||||
}
|
||||
|
||||
public static com.ibm.icu.util.Calendar getInstance(ULocale locale) {
|
||||
return com.ibm.icu.util.Calendar.getInstance(locale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.util.Calendar getInstance(com.ibm.icu.util.TimeZone zone, Locale aLocale) {
|
||||
return com.ibm.icu.util.Calendar.getInstance(zone, aLocale);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.util.Calendar getInstance(com.ibm.icu.util.TimeZone zone, ULocale locale) {
|
||||
return com.ibm.icu.util.Calendar.getInstance(zone, locale);
|
||||
}
|
||||
|
||||
public static Locale[] getAvailableLocales() {
|
||||
return com.ibm.icu.util.Calendar.getAvailableLocales();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void computeTime() {}
|
||||
|
||||
@Override
|
||||
protected void computeFields() {}
|
||||
|
||||
public static ULocale[] getAvailableULocales() {
|
||||
return com.ibm.icu.util.Calendar.getAvailableULocales();
|
||||
}
|
||||
|
||||
public static String[] getKeywordValuesForLocale(String key, ULocale locale, boolean commonlyUsed) {
|
||||
return com.ibm.icu.util.Calendar.getKeywordValuesForLocale(key, locale, commonlyUsed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeInMillis() {
|
||||
return delegate.getTimeInMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTimeInMillis(long millis) {
|
||||
delegate.setTimeInMillis(millis);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public int getRelatedYear() {
|
||||
return delegate.getRelatedYear();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setRelatedYear(int year) {
|
||||
delegate.setRelatedYear(year);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return delegate.equals(obj);
|
||||
}
|
||||
|
||||
public boolean isEquivalentTo(com.ibm.icu.util.Calendar other) {
|
||||
return delegate.isEquivalentTo(other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return delegate.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean before(Object when) {
|
||||
return delegate.before(when);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean after(Object when) {
|
||||
return delegate.after(when);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getActualMaximum(int field) {
|
||||
return delegate.getActualMaximum(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getActualMinimum(int field) {
|
||||
return delegate.getActualMinimum(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roll(int field, int amount) {
|
||||
delegate.roll(field, amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int field, int amount) {
|
||||
delegate.add(field, amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roll(int field, boolean up) {
|
||||
roll(field, up ? 1 : -1);
|
||||
}
|
||||
|
||||
public String getDisplayName(Locale loc) {
|
||||
return delegate.getDisplayName(loc);
|
||||
}
|
||||
|
||||
public String getDisplayName(ULocale loc) {
|
||||
return delegate.getDisplayName(loc);
|
||||
}
|
||||
|
||||
public int compareTo(com.ibm.icu.util.Calendar that) {
|
||||
return delegate.compareTo(that);
|
||||
}
|
||||
|
||||
public DateFormat getDateTimeFormat(int dateStyle, int timeStyle, Locale loc) {
|
||||
return delegate.getDateTimeFormat(dateStyle, timeStyle, loc);
|
||||
}
|
||||
|
||||
public DateFormat getDateTimeFormat(int dateStyle, int timeStyle, ULocale loc) {
|
||||
return delegate.getDateTimeFormat(dateStyle, timeStyle, loc);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static String getDateTimePattern(com.ibm.icu.util.Calendar cal, ULocale uLocale, int dateStyle) {
|
||||
return com.ibm.icu.util.Calendar.getDateTimePattern(cal, uLocale, dateStyle);
|
||||
}
|
||||
|
||||
public int fieldDifference(Date when, int field) {
|
||||
return delegate.fieldDifference(when, field);
|
||||
}
|
||||
|
||||
public void setTimeZone(com.ibm.icu.util.TimeZone value) {
|
||||
delegate.setTimeZone(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.TimeZone getTimeZone() {
|
||||
return new TimeZone(delegate.getTimeZone());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLenient(boolean lenient) {
|
||||
delegate.setLenient(lenient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLenient() {
|
||||
return delegate.isLenient();
|
||||
}
|
||||
|
||||
public void setRepeatedWallTimeOption(int option) {
|
||||
delegate.setRepeatedWallTimeOption(option);
|
||||
}
|
||||
|
||||
public int getRepeatedWallTimeOption() {
|
||||
return delegate.getRepeatedWallTimeOption();
|
||||
}
|
||||
|
||||
public void setSkippedWallTimeOption(int option) {
|
||||
delegate.setSkippedWallTimeOption(option);
|
||||
}
|
||||
|
||||
public int getSkippedWallTimeOption() {
|
||||
return delegate.getSkippedWallTimeOption();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFirstDayOfWeek(int value) {
|
||||
delegate.setFirstDayOfWeek(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirstDayOfWeek() {
|
||||
return delegate.getFirstDayOfWeek();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMinimalDaysInFirstWeek(int value) {
|
||||
delegate.setMinimalDaysInFirstWeek(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinimalDaysInFirstWeek() {
|
||||
return delegate.getMinimalDaysInFirstWeek();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinimum(int field) {
|
||||
return delegate.getMinimum(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaximum(int field) {
|
||||
return delegate.getMaximum(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGreatestMinimum(int field) {
|
||||
return delegate.getGreatestMinimum(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeastMaximum(int field) {
|
||||
return delegate.getLeastMaximum(field);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public int getDayOfWeekType(int dayOfWeek) {
|
||||
return delegate.getDayOfWeekType(dayOfWeek);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public int getWeekendTransition(int dayOfWeek) {
|
||||
return delegate.getWeekendTransition(dayOfWeek);
|
||||
}
|
||||
|
||||
public boolean isWeekend(Date date) {
|
||||
return delegate.isWeekend(date);
|
||||
}
|
||||
|
||||
public boolean isWeekend() {
|
||||
return delegate.isWeekend();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone() {
|
||||
return delegate.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return delegate.toString();
|
||||
}
|
||||
|
||||
public static com.ibm.icu.util.Calendar.WeekData getWeekDataForRegion(String region) {
|
||||
return com.ibm.icu.util.Calendar.getWeekDataForRegion(region);
|
||||
}
|
||||
|
||||
public com.ibm.icu.util.Calendar.WeekData getWeekData() {
|
||||
return delegate.getWeekData();
|
||||
}
|
||||
|
||||
public com.ibm.icu.util.Calendar setWeekData(com.ibm.icu.util.Calendar.WeekData wdata) {
|
||||
return delegate.setWeekData(wdata);
|
||||
}
|
||||
|
||||
public int getFieldCount() {
|
||||
return delegate.getFieldCount();
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return delegate.getType();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public boolean haveDefaultCentury() {
|
||||
return delegate.haveDefaultCentury();
|
||||
}
|
||||
|
||||
public ULocale getLocale(ULocale.Type type) {
|
||||
return delegate.getLocale(type);
|
||||
}
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
package xyz.nulldev.androidcompat.replace.java.util;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 com.ibm.icu.util.ULocale;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class TimeZone extends java.util.TimeZone {
|
||||
private com.ibm.icu.util.TimeZone delegate;
|
||||
|
||||
public TimeZone(com.ibm.icu.util.TimeZone delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOffset(int era, int year, int month, int day, int dayOfWeek, int milliseconds) {
|
||||
return delegate.getOffset(era, year, month, day, dayOfWeek, milliseconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOffset(long date) {
|
||||
return delegate.getOffset(date);
|
||||
}
|
||||
|
||||
public void getOffset(long date, boolean local, int[] offsets) {
|
||||
delegate.getOffset(date, local, offsets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRawOffset(int offsetMillis) {
|
||||
delegate.setRawOffset(offsetMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRawOffset() {
|
||||
return delegate.getRawOffset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getID() {
|
||||
return delegate.getID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setID(String ID) {
|
||||
delegate.setID(ID);
|
||||
}
|
||||
|
||||
public String getDisplayName(ULocale locale) {
|
||||
return delegate.getDisplayName(locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName(boolean daylight, int style, Locale locale) {
|
||||
return delegate.getDisplayName(daylight, style, locale);
|
||||
}
|
||||
|
||||
public String getDisplayName(boolean daylight, int style, ULocale locale) {
|
||||
return delegate.getDisplayName(daylight, style, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDSTSavings() {
|
||||
return delegate.getDSTSavings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useDaylightTime() {
|
||||
return delegate.useDaylightTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean observesDaylightTime() {
|
||||
return delegate.observesDaylightTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean inDaylightTime(Date date) {
|
||||
return delegate.inDaylightTime(date);
|
||||
}
|
||||
|
||||
public static java.util.TimeZone getTimeZone(String ID) {
|
||||
return new TimeZone(com.ibm.icu.util.TimeZone.getTimeZone(ID));
|
||||
}
|
||||
|
||||
public static com.ibm.icu.util.TimeZone getFrozenTimeZone(String ID) {
|
||||
return com.ibm.icu.util.TimeZone.getFrozenTimeZone(ID);
|
||||
}
|
||||
|
||||
public static com.ibm.icu.util.TimeZone getTimeZone(String ID, int type) {
|
||||
return com.ibm.icu.util.TimeZone.getTimeZone(ID, type);
|
||||
}
|
||||
|
||||
public static void setDefaultTimeZoneType(int type) {
|
||||
com.ibm.icu.util.TimeZone.setDefaultTimeZoneType(type);
|
||||
}
|
||||
|
||||
public static int getDefaultTimeZoneType() {
|
||||
return com.ibm.icu.util.TimeZone.getDefaultTimeZoneType();
|
||||
}
|
||||
|
||||
public static Set<String> getAvailableIDs(com.ibm.icu.util.TimeZone.SystemTimeZoneType zoneType, String region, Integer rawOffset) {
|
||||
return com.ibm.icu.util.TimeZone.getAvailableIDs(zoneType, region, rawOffset);
|
||||
}
|
||||
|
||||
public static String[] getAvailableIDs(int rawOffset) {
|
||||
return com.ibm.icu.util.TimeZone.getAvailableIDs(rawOffset);
|
||||
}
|
||||
|
||||
public static String[] getAvailableIDs(String country) {
|
||||
return com.ibm.icu.util.TimeZone.getAvailableIDs(country);
|
||||
}
|
||||
|
||||
public static String[] getAvailableIDs() {
|
||||
return com.ibm.icu.util.TimeZone.getAvailableIDs();
|
||||
}
|
||||
|
||||
public static int countEquivalentIDs(String id) {
|
||||
return com.ibm.icu.util.TimeZone.countEquivalentIDs(id);
|
||||
}
|
||||
|
||||
public static String getEquivalentID(String id, int index) {
|
||||
return com.ibm.icu.util.TimeZone.getEquivalentID(id, index);
|
||||
}
|
||||
|
||||
public static java.util.TimeZone getDefault() {
|
||||
return new TimeZone(com.ibm.icu.util.TimeZone.getDefault());
|
||||
}
|
||||
|
||||
public static void setDefault(com.ibm.icu.util.TimeZone tz) {
|
||||
com.ibm.icu.util.TimeZone.setDefault(tz);
|
||||
}
|
||||
|
||||
public boolean hasSameRules(com.ibm.icu.util.TimeZone other) {
|
||||
return delegate.hasSameRules(other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone() {
|
||||
return delegate.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return delegate.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return delegate.hashCode();
|
||||
}
|
||||
|
||||
public static String getTZDataVersion() {
|
||||
return com.ibm.icu.util.TimeZone.getTZDataVersion();
|
||||
}
|
||||
|
||||
public static String getCanonicalID(String id) {
|
||||
return com.ibm.icu.util.TimeZone.getCanonicalID(id);
|
||||
}
|
||||
|
||||
public static String getCanonicalID(String id, boolean[] isSystemID) {
|
||||
return com.ibm.icu.util.TimeZone.getCanonicalID(id, isSystemID);
|
||||
}
|
||||
|
||||
public static String getRegion(String id) {
|
||||
return com.ibm.icu.util.TimeZone.getRegion(id);
|
||||
}
|
||||
|
||||
public static String getWindowsID(String id) {
|
||||
return com.ibm.icu.util.TimeZone.getWindowsID(id);
|
||||
}
|
||||
|
||||
public static String getIDForWindowsID(String winid, String region) {
|
||||
return com.ibm.icu.util.TimeZone.getIDForWindowsID(winid, region);
|
||||
}
|
||||
|
||||
public boolean isFrozen() {
|
||||
return delegate.isFrozen();
|
||||
}
|
||||
|
||||
public com.ibm.icu.util.TimeZone freeze() {
|
||||
return delegate.freeze();
|
||||
}
|
||||
|
||||
public com.ibm.icu.util.TimeZone cloneAsThawed() {
|
||||
return delegate.cloneAsThawed();
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ Here is a list of current features:
|
||||
- A library to save your mangas and categories to put them into.
|
||||
- Searching and browsing installed sources.
|
||||
- A decent chapter reader.
|
||||
- Ability to download Mangas for offline read
|
||||
- Ability to download Manga for offline read
|
||||
- Backup and restore support powered by Tachiyomi Legacy Backups
|
||||
|
||||
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting) if it happens.
|
||||
|
||||
+4
-1
@@ -43,7 +43,7 @@ configure(projects) {
|
||||
// Kotlin
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
implementation(kotlin("reflect"))
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation(kotlin("test-junit5"))
|
||||
|
||||
// coroutines
|
||||
val coroutinesVersion = "1.4.3"
|
||||
@@ -84,5 +84,8 @@ configure(projects) {
|
||||
|
||||
// APK parser
|
||||
implementation("net.dongliu:apk-parser:2.6.10")
|
||||
|
||||
// Jackson
|
||||
implementation("com.fasterxml.jackson.core:jackson-annotations:2.10.3")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
int main() {
|
||||
system("start jre\\bin\\javaw -jar Tachidesk.jar");
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# Building `Tachidesk Launcher.exe`
|
||||
1. compile `Tachidesk Launcher.c` statically with MSVC compiler.
|
||||
2. Add `server/src/main/resources/icon/faviconlogo.ico` into the exe with `rcedit` from the electron project: `rcedit "Tachidesk Launcher.exe" --set-icon faviconlogo.ico`
|
||||
@@ -65,7 +65,6 @@ WINEARCH=win32 wine $rcedit $release_name/electron/electron.exe --set-icon ../se
|
||||
|
||||
# copy artifacts
|
||||
cp $jar $release_name/Tachidesk.jar
|
||||
#cp "resources/Tachidesk Launcher-$arch.exe" "$release_name/Tachidesk Launcher.exe"
|
||||
cp "resources/Tachidesk Browser Launcher.bat" $release_name
|
||||
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||
cp "resources/Tachidesk Electron Launcher.bat" $release_name
|
||||
|
||||
+25
-6
@@ -44,11 +44,16 @@ dependencies {
|
||||
// current database driver
|
||||
implementation("com.h2database:h2:1.4.200")
|
||||
|
||||
// Exposed Migrations
|
||||
val exposedMigrationsVersion = "3.1.0"
|
||||
implementation("com.github.Suwayomi:exposed-migrations:$exposedMigrationsVersion")
|
||||
|
||||
// tray icon
|
||||
implementation("com.dorkbox:SystemTray:4.1")
|
||||
implementation("com.dorkbox:Utilities:1.9")
|
||||
|
||||
|
||||
|
||||
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.9.1")
|
||||
@@ -58,6 +63,12 @@ dependencies {
|
||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||
|
||||
|
||||
// asm for fixing SimpleDateFormat (must match Dex2Jar version)
|
||||
implementation("org.ow2.asm:asm-debug-all:5.0.3")
|
||||
|
||||
// extracting zip files
|
||||
implementation("net.lingala.zip4j:zip4j:2.9.0")
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
||||
// implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||
@@ -68,9 +79,6 @@ dependencies {
|
||||
|
||||
// uncomment to test extensions directly
|
||||
// implementation(fileTree("lib/"))
|
||||
|
||||
// Testing
|
||||
testImplementation(kotlin("test-junit5"))
|
||||
}
|
||||
|
||||
val MainClass = "suwayomi.tachidesk.MainKt"
|
||||
@@ -93,7 +101,8 @@ sourceSets {
|
||||
}
|
||||
|
||||
// should be bumped with each stable release
|
||||
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.3"
|
||||
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.4"
|
||||
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r20"
|
||||
|
||||
// counts commit count on master
|
||||
val tachideskRevision = runCatching {
|
||||
@@ -112,7 +121,7 @@ val tachideskRevision = runCatching {
|
||||
|
||||
buildConfig {
|
||||
clsName = "BuildConfig"
|
||||
packageName = "suwayomi.server"
|
||||
packageName = "suwayomi.tachidesk.server"
|
||||
|
||||
|
||||
buildConfigField("String", "NAME", rootProject.name)
|
||||
@@ -121,6 +130,11 @@ buildConfig {
|
||||
buildConfigField("String", "BUILD_TYPE", if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview")
|
||||
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
|
||||
|
||||
|
||||
buildConfigField("String", "WEBUI_REPO", "https://github.com/Suwayomi/Tachidesk-WebUI-preview")
|
||||
buildConfigField("String", "WEBUI_TAG", webUIRevisionTag)
|
||||
|
||||
|
||||
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk")
|
||||
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA")
|
||||
}
|
||||
@@ -167,7 +181,12 @@ tasks {
|
||||
|
||||
named<Copy>("processResources") {
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
mustRunAfter(":webUI:copyBuild")
|
||||
mustRunAfter("downloadWebUI")
|
||||
}
|
||||
|
||||
register<de.undercouch.gradle.tasks.download.Download>("downloadWebUI") {
|
||||
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
|
||||
dest("src/main/resources/WebUI.zip")
|
||||
}
|
||||
|
||||
withType<LintTask> {
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.google.gson.Gson
|
||||
// import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
// import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.serialization.json.Json
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
@@ -54,6 +55,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
addSingletonFactory { Gson() }
|
||||
|
||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
|
||||
// rxAsync { get<PreferencesHelper>() }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animesource
|
||||
|
||||
import android.support.v7.preference.PreferenceScreen
|
||||
import androidx.preference.PreferenceScreen
|
||||
|
||||
interface ConfigurableAnimeSource : AnimeSource {
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
// import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.PreferenceScreen
|
||||
|
||||
interface ConfigurableSource : Source {
|
||||
|
||||
// fun setupPreferenceScreen(screen: PreferenceScreen)
|
||||
fun setupPreferenceScreen(screen: PreferenceScreen)
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@ interface Source {
|
||||
|
||||
// fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||
|
||||
// fun Source.getPreferenceKey(): String = "source_$id"
|
||||
fun Source.getPreferenceKey(): String = "source_$id"
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.Node
|
||||
import suwayomi.tachidesk.manga.impl.util.BytecodeEditor
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList
|
||||
import xyz.nulldev.androidcompat.pm.toPackageInfo
|
||||
@@ -80,6 +81,8 @@ object PackageTools {
|
||||
""".trimIndent()
|
||||
)
|
||||
handler.dump(errorFile, emptyArray<String>())
|
||||
} else {
|
||||
BytecodeEditor.fixAndroidClasses(jarFilePath.toFile())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package suwayomi.tachidesk.global
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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.Javalin
|
||||
import io.javalin.apibuilder.ApiBuilder.get
|
||||
import io.javalin.apibuilder.ApiBuilder.path
|
||||
import suwayomi.tachidesk.global.controller.SettingsController
|
||||
|
||||
object GlobalAPI {
|
||||
fun defineEndpoints(app: Javalin) {
|
||||
app.routes {
|
||||
path("api/v1/settings") {
|
||||
get("about", SettingsController::about)
|
||||
get("check-update", SettingsController::checkUpdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package suwayomi.tachidesk.global.controller
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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.Context
|
||||
import suwayomi.tachidesk.global.impl.About
|
||||
import suwayomi.tachidesk.global.impl.AppUpdate
|
||||
import suwayomi.tachidesk.server.JavalinSetup
|
||||
|
||||
/** Settings Page/Screen */
|
||||
object SettingsController {
|
||||
/** returns some static info about the current app build */
|
||||
fun about(ctx: Context): Context {
|
||||
return ctx.json(About.getAbout())
|
||||
}
|
||||
|
||||
/** check for app updates */
|
||||
fun checkUpdate(ctx: Context): Context {
|
||||
return ctx.json(
|
||||
JavalinSetup.future { AppUpdate.checkUpdate() }
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package suwayomi.tachidesk.server.impl
|
||||
package suwayomi.tachidesk.global.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
@@ -7,7 +7,7 @@ package suwayomi.tachidesk.server.impl
|
||||
* 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 suwayomi.server.BuildConfig
|
||||
import suwayomi.tachidesk.server.BuildConfig
|
||||
|
||||
data class AboutDataClass(
|
||||
val name: String,
|
||||
@@ -0,0 +1,58 @@
|
||||
package suwayomi.tachidesk.global.impl
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
data class UpdateDataClass(
|
||||
/** [channel] mirrors [suwayomi.tachidesk.server.BuildConfig.BUILD_TYPE] */
|
||||
val channel: String,
|
||||
val tag: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
object AppUpdate {
|
||||
private const val LATEST_STABLE_CHANNEL_URL = "https://api.github.com/repos/Suwayomi/Tachidesk/releases/latest"
|
||||
private const val LATEST_PREVIEW_CHANNEL_URL = "https://api.github.com/repos/Suwayomi/Tachidesk-preview/releases/latest"
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
private val network: NetworkHelper by injectLazy()
|
||||
|
||||
suspend fun checkUpdate(): List<UpdateDataClass> {
|
||||
val stableJson = json.parseToJsonElement(
|
||||
network.client.newCall(
|
||||
GET(LATEST_STABLE_CHANNEL_URL)
|
||||
).await().body!!.string()
|
||||
).jsonObject
|
||||
|
||||
val previewJson = json.parseToJsonElement(
|
||||
network.client.newCall(
|
||||
GET(LATEST_PREVIEW_CHANNEL_URL)
|
||||
).await().body!!.string()
|
||||
).jsonObject
|
||||
|
||||
return listOf(
|
||||
UpdateDataClass(
|
||||
"Stable",
|
||||
stableJson["tag_name"]!!.jsonPrimitive.content,
|
||||
stableJson["html_url"]!!.jsonPrimitive.content,
|
||||
),
|
||||
UpdateDataClass(
|
||||
"Preview",
|
||||
previewJson["tag_name"]!!.jsonPrimitive.content,
|
||||
previewJson["html_url"]!!.jsonPrimitive.content,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+20
-10
@@ -28,8 +28,11 @@ import suwayomi.tachidesk.manga.impl.Page.getPageImage
|
||||
import suwayomi.tachidesk.manga.impl.Search.sourceFilters
|
||||
import suwayomi.tachidesk.manga.impl.Search.sourceGlobalSearch
|
||||
import suwayomi.tachidesk.manga.impl.Search.sourceSearch
|
||||
import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSource
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSourceList
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSourcePreferences
|
||||
import suwayomi.tachidesk.manga.impl.Source.setSourcePreference
|
||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
|
||||
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
|
||||
@@ -40,11 +43,10 @@ import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
import suwayomi.tachidesk.server.impl.About
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
object TachideskAPI {
|
||||
object MangaAPI {
|
||||
fun defineEndpoints(app: Javalin) {
|
||||
// list all extensions
|
||||
app.get("/api/v1/extension/list") { ctx ->
|
||||
@@ -86,7 +88,7 @@ object TachideskAPI {
|
||||
}
|
||||
|
||||
// icon for extension named `apkName`
|
||||
app.get("/api/v1/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
|
||||
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
|
||||
ctx.result(
|
||||
@@ -109,6 +111,19 @@ object TachideskAPI {
|
||||
ctx.json(getSource(sourceId))
|
||||
}
|
||||
|
||||
// fetch preferences of source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId/preferences") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(getSourcePreferences(sourceId))
|
||||
}
|
||||
|
||||
// fetch preferences of source with id `sourceId`
|
||||
app.post("/api/v1/source/:sourceId/preferences") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
|
||||
ctx.json(setSourcePreference(sourceId, preferenceChange))
|
||||
}
|
||||
|
||||
// popular mangas from source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
@@ -187,7 +202,7 @@ object TachideskAPI {
|
||||
ctx.json(future { getChapterList(mangaId, onlineFetch) })
|
||||
}
|
||||
|
||||
// used to modify a manga's meta paramaters
|
||||
// used to modify a manga's meta parameters
|
||||
app.patch("/api/v1/manga/:mangaId/meta") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
@@ -221,7 +236,7 @@ object TachideskAPI {
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// used to modify a chapter's meta paramaters
|
||||
// used to modify a chapter's meta parameters
|
||||
app.patch("/api/v1/manga/:mangaId/chapter/:chapterIndex/meta") { ctx ->
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
@@ -314,11 +329,6 @@ object TachideskAPI {
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// returns some static info of the current app build
|
||||
app.get("/api/v1/about/") { ctx ->
|
||||
ctx.json(About.getAbout())
|
||||
}
|
||||
|
||||
// category modification
|
||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
@@ -7,15 +7,27 @@ package suwayomi.tachidesk.manga.impl
|
||||
* 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 android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.getPreferenceKey
|
||||
import mu.KotlinLogging
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
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.extension.Extension.getExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
|
||||
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.invalidateSourceCache
|
||||
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import xyz.nulldev.androidcompat.androidimpl.CustomContext
|
||||
|
||||
object Source {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
@@ -28,7 +40,8 @@ object Source {
|
||||
it[SourceTable.name],
|
||||
it[SourceTable.lang],
|
||||
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
|
||||
getHttpSource(it[SourceTable.id].value).supportsLatest
|
||||
getHttpSource(it[SourceTable.id].value).supportsLatest,
|
||||
getHttpSource(it[SourceTable.id].value) is ConfigurableSource
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -42,9 +55,71 @@ object Source {
|
||||
sourceId.toString(),
|
||||
source?.get(SourceTable.name),
|
||||
source?.get(SourceTable.lang),
|
||||
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
|
||||
source?.let { getHttpSource(sourceId).supportsLatest }
|
||||
source?.let { getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.apkName]) },
|
||||
source?.let { getHttpSource(sourceId).supportsLatest },
|
||||
source?.let { getHttpSource(sourceId) is ConfigurableSource },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val context by DI.global.instance<CustomContext>()
|
||||
|
||||
/**
|
||||
* Clients should support these types for extensions to work properly (in order of importance)
|
||||
* - EditTextPreference
|
||||
* - SwitchPreferenceCompat
|
||||
* - ListPreference
|
||||
* - CheckBoxPreference
|
||||
*/
|
||||
data class PreferenceObject(
|
||||
val type: String,
|
||||
val props: Any
|
||||
)
|
||||
|
||||
var preferenceScreenMap: MutableMap<Long, PreferenceScreen> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* Gets a source's PreferenceScreen, puts the result into [preferenceScreenMap]
|
||||
*/
|
||||
fun getSourcePreferences(sourceId: Long): List<PreferenceObject> {
|
||||
val source = getHttpSource(sourceId)
|
||||
|
||||
if (source is ConfigurableSource) {
|
||||
val sourceShardPreferences = Injekt.get<Application>().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||
|
||||
val screen = PreferenceScreen(context)
|
||||
screen.sharedPreferences = sourceShardPreferences
|
||||
|
||||
source.setupPreferenceScreen(screen)
|
||||
|
||||
preferenceScreenMap[sourceId] = screen
|
||||
|
||||
return screen.preferences.map {
|
||||
PreferenceObject(it::class.java.simpleName, it)
|
||||
}
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
data class SourcePreferenceChange(
|
||||
val position: Int,
|
||||
val value: String
|
||||
)
|
||||
|
||||
fun setSourcePreference(sourceId: Long, change: SourcePreferenceChange) {
|
||||
val screen = preferenceScreenMap[sourceId]!!
|
||||
val pref = screen.preferences[change.position]
|
||||
|
||||
val newValue = when (pref.defaultValueType) {
|
||||
"String" -> change.value
|
||||
"Boolean" -> change.value.toBoolean()
|
||||
else -> throw RuntimeException("Unsupported type conversion")
|
||||
}
|
||||
|
||||
pref.saveNewValue(newValue)
|
||||
pref.callChangeListener(newValue)
|
||||
|
||||
// must reload the source cache because a preference was changed
|
||||
invalidateSourceCache(sourceId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
package suwayomi.tachidesk.manga.impl.util
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 mu.KotlinLogging
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.FieldVisitor
|
||||
import org.objectweb.asm.Handle
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.use
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
object BytecodeEditor {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* Replace some java class references inside a jar with new ones that behave like Androids
|
||||
*
|
||||
* @param jarFile The JarFile to replace class references in
|
||||
*/
|
||||
fun fixAndroidClasses(jarFile: File) {
|
||||
val nodes = loadClasses(jarFile)
|
||||
.mapValues { (className, classFileBuffer) ->
|
||||
logger.trace { "Processing class $className" }
|
||||
transform(classFileBuffer)
|
||||
} + loadNonClasses(jarFile)
|
||||
|
||||
saveAsJar(nodes, jarFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all classes inside the [jar] [File]
|
||||
*
|
||||
* @param jar The JarFile to load classes from
|
||||
*
|
||||
* @return [Map] with class names and [ByteArray]s of bytecode
|
||||
*/
|
||||
private fun loadClasses(jar: File): Map<String, ByteArray> {
|
||||
return JarFile(jar).use { jarFile ->
|
||||
jarFile.entries()
|
||||
.asSequence()
|
||||
.mapNotNull {
|
||||
readJar(jarFile, it)
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get class file in [jar] for [entry]
|
||||
*
|
||||
* @param jar The jar to get the class from
|
||||
* @param entry The entry in the jar
|
||||
*
|
||||
* @return [Pair] of the class name plus the class [ByteArray], or null if it's not a valid class
|
||||
*/
|
||||
private fun readJar(jar: JarFile, entry: JarEntry): Pair<String, ByteArray>? {
|
||||
return try {
|
||||
jar.getInputStream(entry).use { stream ->
|
||||
if (entry.name.endsWith(".class")) {
|
||||
val bytes = stream.readBytes()
|
||||
if (bytes.size < 4) {
|
||||
// Invalid class size
|
||||
return@use null
|
||||
}
|
||||
val cafebabe = String.format(
|
||||
"%02X%02X%02X%02X",
|
||||
bytes[0],
|
||||
bytes[1],
|
||||
bytes[2],
|
||||
bytes[3]
|
||||
)
|
||||
if (cafebabe.toLowerCase() != "cafebabe") {
|
||||
// Corrupted class
|
||||
return@use null
|
||||
}
|
||||
|
||||
getNode(bytes).name to bytes
|
||||
} else null
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
logger.error(e) { "Error loading jar file" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNode(bytes: ByteArray): ClassNode {
|
||||
val cr = ClassReader(bytes)
|
||||
return ClassNode().also { cr.accept(it, ClassReader.EXPAND_FRAMES) }
|
||||
}
|
||||
|
||||
/**
|
||||
* The path where replacement classes will reside
|
||||
*/
|
||||
private const val replacementPath = "xyz/nulldev/androidcompat/replace"
|
||||
|
||||
/**
|
||||
* List of classes that will be replaced
|
||||
*/
|
||||
private val classesToReplace = listOf(
|
||||
"java/text/SimpleDateFormat"
|
||||
)
|
||||
|
||||
/**
|
||||
* Replace direct references to the class, used on places
|
||||
* that don't have any other text then the class
|
||||
*
|
||||
* @return [String] of class or null if [String] was null
|
||||
*/
|
||||
private fun String?.replaceDirectly() = when (this) {
|
||||
null -> this
|
||||
in classesToReplace -> "$replacementPath/$this"
|
||||
else -> this
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace references to the class, used in places that have
|
||||
* other text around the class references
|
||||
*
|
||||
* @return [String] with class references replaced,
|
||||
* or null if [String] was null
|
||||
*/
|
||||
private fun String?.replaceIndirectly(): String? {
|
||||
var classReference = this
|
||||
if (classReference != null) {
|
||||
classesToReplace.forEach {
|
||||
classReference = classReference?.replace(it, "$replacementPath/$it")
|
||||
}
|
||||
}
|
||||
return classReference
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all references to certain classes inside the class file
|
||||
* with ones that behave more like Androids
|
||||
*
|
||||
* @param classfileBuffer Class bytecode to load into ASM for ease of modification
|
||||
*
|
||||
* @return [ByteArray] with modified bytecode
|
||||
*/
|
||||
private fun transform(classfileBuffer: ByteArray): ByteArray {
|
||||
// Read the class and prepare to modify it
|
||||
val cr = ClassReader(classfileBuffer)
|
||||
val cw = ClassWriter(cr, 0)
|
||||
// Modify the class
|
||||
cr.accept(
|
||||
object : ClassVisitor(Opcodes.ASM5, cw) {
|
||||
// Modify field descriptor, for example
|
||||
// class MangaYes {
|
||||
// val format = SimpleDateFormat("YYYY-MM-dd")
|
||||
// }
|
||||
override fun visitField(
|
||||
access: Int,
|
||||
name: String?,
|
||||
desc: String?,
|
||||
signature: String?,
|
||||
cst: Any?
|
||||
): FieldVisitor? {
|
||||
logger.trace { "CLass Field" to "${desc.replaceIndirectly()}: ${cst?.let { it::class.java.simpleName }}: $cst" }
|
||||
return super.visitField(access, name, desc.replaceIndirectly(), signature, cst)
|
||||
}
|
||||
|
||||
override fun visit(
|
||||
version: Int,
|
||||
access: Int,
|
||||
name: String?,
|
||||
signature: String?,
|
||||
superName: String?,
|
||||
interfaces: Array<out String>?
|
||||
) {
|
||||
logger.trace { "Visiting $name: $signature: $superName" }
|
||||
super.visit(version, access, name, signature, superName, interfaces)
|
||||
}
|
||||
|
||||
// Modify method bytecode, for example
|
||||
// class MangaYes {
|
||||
// fun fetchChapterList() {
|
||||
// SimpleDateFormat("YYYY-MM-dd")
|
||||
// }
|
||||
// }
|
||||
override fun visitMethod(
|
||||
access: Int,
|
||||
name: String,
|
||||
desc: String,
|
||||
signature: String?,
|
||||
exceptions: Array<String?>?
|
||||
): MethodVisitor {
|
||||
logger.trace { "Processing method $name: ${desc.replaceIndirectly()}: $signature" }
|
||||
val mv: MethodVisitor? = super.visitMethod(
|
||||
access, name, desc.replaceIndirectly(), signature, exceptions
|
||||
)
|
||||
return object : MethodVisitor(Opcodes.ASM5, mv) {
|
||||
override fun visitLdcInsn(cst: Any?) {
|
||||
logger.trace { "Ldc" to "${cst?.let { "${it::class.java.simpleName}: $it" }}" }
|
||||
super.visitLdcInsn(cst)
|
||||
}
|
||||
|
||||
// Replace method type, for example
|
||||
// val format = DateFormat()
|
||||
// fun fetchChapterList() {
|
||||
// if (format is SimpleDateFormat)
|
||||
// }
|
||||
override fun visitTypeInsn(opcode: Int, type: String?) {
|
||||
logger.trace {
|
||||
"Type" to "$opcode: ${type.replaceDirectly()}"
|
||||
}
|
||||
super.visitTypeInsn(
|
||||
opcode,
|
||||
type.replaceDirectly()
|
||||
)
|
||||
}
|
||||
|
||||
// Replace method field, for example
|
||||
// fun fetchChapterList() {
|
||||
// val format = SimpleDateFormat("YYYY-MM-dd")
|
||||
// }
|
||||
override fun visitMethodInsn(
|
||||
opcode: Int,
|
||||
owner: String?,
|
||||
name: String?,
|
||||
desc: String?,
|
||||
itf: Boolean
|
||||
) {
|
||||
logger.trace {
|
||||
"Method" to "$opcode: ${owner.replaceDirectly()}: $name: ${desc.replaceIndirectly()}"
|
||||
}
|
||||
super.visitMethodInsn(
|
||||
opcode,
|
||||
owner.replaceDirectly(),
|
||||
name,
|
||||
desc.replaceIndirectly(),
|
||||
itf
|
||||
)
|
||||
}
|
||||
|
||||
// Replace class field call from method, for example
|
||||
// val format = SimpleDateFormat("YYYY-MM-dd")
|
||||
// fun fetchChapterList() {
|
||||
// format.format(Date())
|
||||
// }
|
||||
override fun visitFieldInsn(
|
||||
opcode: Int,
|
||||
owner: String?,
|
||||
name: String?,
|
||||
desc: String?
|
||||
) {
|
||||
logger.trace { "Field" to "$opcode: $owner: $name: ${desc.replaceIndirectly()}" }
|
||||
super.visitFieldInsn(opcode, owner, name, desc.replaceIndirectly())
|
||||
}
|
||||
|
||||
override fun visitInvokeDynamicInsn(
|
||||
name: String?,
|
||||
desc: String?,
|
||||
bsm: Handle?,
|
||||
vararg bsmArgs: Any?
|
||||
) {
|
||||
logger.trace { "InvokeDynamic" to "$name: $desc" }
|
||||
super.visitInvokeDynamicInsn(name, desc, bsm, *bsmArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
0
|
||||
)
|
||||
return cw.toByteArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load non-class files from the jar, such as icons and the manifest
|
||||
*
|
||||
* @param [jarFile] The file to load resources from
|
||||
*
|
||||
* @return [Map] of resources
|
||||
*/
|
||||
private fun loadNonClasses(jarFile: File): Map<String, ByteArray> {
|
||||
val entries = mutableMapOf<String, ByteArray>()
|
||||
ZipInputStream(jarFile.inputStream()).use { stream ->
|
||||
var nextEntry: ZipEntry?
|
||||
while (stream.nextEntry.also { nextEntry = it } != null) {
|
||||
nextEntry?.use(stream) { entry ->
|
||||
// If it ends with class or is a directory ignore it
|
||||
if (!entry.name.endsWith(".class") && !entry.isDirectory) {
|
||||
val bytes = stream.readBytes()
|
||||
entries[entry.name] = bytes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
/**
|
||||
* Save jar with modified content
|
||||
*
|
||||
* @param outBytes [Map] of names and [ByteArray]s of content to save inside the jar
|
||||
* @param file JarFile to save to
|
||||
*/
|
||||
private fun saveAsJar(outBytes: Map<String, ByteArray>, file: File) {
|
||||
JarOutputStream(file.outputStream()).use { out ->
|
||||
outBytes.forEach { (entry, value) ->
|
||||
// Append extension to class entries
|
||||
out.putNextEntry(
|
||||
ZipEntry(
|
||||
entry + if (entry.contains(".")) "" else ".class"
|
||||
)
|
||||
)
|
||||
out.write(value)
|
||||
out.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,4 +54,8 @@ object GetHttpSource {
|
||||
}
|
||||
return sourceCache[sourceId]!!
|
||||
}
|
||||
|
||||
fun invalidateSourceCache(sourceId: Long) {
|
||||
sourceCache.remove(sourceId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ object PackageTools {
|
||||
""".trimIndent()
|
||||
)
|
||||
handler.dump(errorFile, emptyArray<String>())
|
||||
} else {
|
||||
BytecodeEditor.fixAndroidClasses(jarFilePath.toFile())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +97,7 @@ object PackageTools {
|
||||
dBuilder.parse(it)
|
||||
}
|
||||
|
||||
logger.debug(parsed.manifestXml)
|
||||
logger.trace(parsed.manifestXml)
|
||||
|
||||
applicationInfo.metaData = Bundle().apply {
|
||||
val appTag = doc.getElementsByTagName("application").item(0)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package suwayomi.tachidesk.manga.impl.util.storage
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
fun ZipEntry.use(stream: ZipInputStream, block: (ZipEntry) -> Unit) {
|
||||
var exception: Throwable? = null
|
||||
try {
|
||||
return block(this)
|
||||
} catch (e: Throwable) {
|
||||
exception = e
|
||||
throw e
|
||||
} finally {
|
||||
if (exception == null) {
|
||||
stream.closeEntry()
|
||||
} else {
|
||||
try {
|
||||
stream.closeEntry()
|
||||
} catch (closeException: Throwable) {
|
||||
exception.addSuppressed(closeException)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,5 +12,6 @@ data class SourceDataClass(
|
||||
val name: String?,
|
||||
val lang: String?,
|
||||
val iconUrl: String?,
|
||||
val supportsLatest: Boolean?
|
||||
val supportsLatest: Boolean?,
|
||||
val isConfigurable: Boolean?
|
||||
)
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
package suwayomi.tachidesk.manga.model.table
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.ReferenceOption
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable.ref
|
||||
|
||||
/**
|
||||
* Meta data storage for clients, about Chapter with id == [ref].
|
||||
*
|
||||
* For example, if you added reader mode(with the key juiReaderMode) such as webtoon to a manga object,
|
||||
* this is what will show up when you request that manga from the api again
|
||||
*
|
||||
* {
|
||||
* "id": 10,
|
||||
* "title": "Isekai manga",
|
||||
* "meta": {
|
||||
* "juiReaderMode": "webtoon"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
object ChapterMetaTable : IntIdTable() {
|
||||
val key = varchar("key", 256)
|
||||
val value = varchar("value", 4096)
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
package suwayomi.tachidesk.manga.model.table
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.ReferenceOption
|
||||
import suwayomi.tachidesk.manga.model.table.MangaMetaTable.ref
|
||||
|
||||
/**
|
||||
* Meta data storage for clients, about Manga with id == [ref].
|
||||
*
|
||||
* For example, if you added reader mode(with the key juiReaderMode) such as webtoon to a manga object,
|
||||
* this is what will show up when you request that manga from the api again
|
||||
*
|
||||
* {
|
||||
* "id": 10,
|
||||
* "title": "Isekai manga",
|
||||
* "meta": {
|
||||
* "juiReaderMode": "webtoon"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
object MangaMetaTable : IntIdTable() {
|
||||
val key = varchar("key", 256)
|
||||
val value = varchar("value", 4096)
|
||||
|
||||
@@ -55,13 +55,13 @@ fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||
meta = getMangaMetaMap(mangaEntry[id])
|
||||
)
|
||||
|
||||
enum class MangaStatus(val status: Int) {
|
||||
enum class MangaStatus(val value: Int) {
|
||||
UNKNOWN(0),
|
||||
ONGOING(1),
|
||||
COMPLETED(2),
|
||||
LICENSED(3);
|
||||
|
||||
companion object {
|
||||
fun valueOf(value: Int): MangaStatus = values().find { it.status == value } ?: UNKNOWN
|
||||
fun valueOf(value: Int): MangaStatus = values().find { it.value == value } ?: UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
package suwayomi.tachidesk.server
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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.Javalin
|
||||
import io.javalin.http.staticfiles.Location
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.future.future
|
||||
import mu.KotlinLogging
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import suwayomi.tachidesk.anime.AnimeAPI
|
||||
import suwayomi.tachidesk.manga.TachideskAPI
|
||||
import suwayomi.tachidesk.global.GlobalAPI
|
||||
import suwayomi.tachidesk.manga.MangaAPI
|
||||
import suwayomi.tachidesk.server.util.Browser
|
||||
import suwayomi.tachidesk.server.util.setupWebUI
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
object JavalinSetup {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> {
|
||||
@@ -30,25 +38,19 @@ object JavalinSetup {
|
||||
}
|
||||
|
||||
fun javalinSetup() {
|
||||
var hasWebUiBundled = false
|
||||
|
||||
val app = Javalin.create { config ->
|
||||
try {
|
||||
// if the bellow line throws an exception then webUI is not bundled
|
||||
this::class.java.getResource("/webUI/index.html")
|
||||
if (serverConfig.webUIEnabled) {
|
||||
setupWebUI()
|
||||
|
||||
// no exception so we can tell javalin to serve webUI
|
||||
hasWebUiBundled = true
|
||||
config.addStaticFiles("/webUI")
|
||||
config.addSinglePageRoot("/", "/webUI/index.html")
|
||||
} catch (e: RuntimeException) {
|
||||
logger.warn("react build files are missing.")
|
||||
hasWebUiBundled = false
|
||||
logger.info { "Serving webUI static files" }
|
||||
config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL)
|
||||
config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL)
|
||||
}
|
||||
|
||||
config.enableCorsForAllOrigins()
|
||||
}.events { event ->
|
||||
event.serverStarted {
|
||||
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
||||
if (serverConfig.webUIEnabled && serverConfig.initialOpenInBrowserEnabled) {
|
||||
Browser.openInBrowser()
|
||||
}
|
||||
}
|
||||
@@ -75,7 +77,8 @@ object JavalinSetup {
|
||||
ctx.result(e.message ?: "Internal Server Error")
|
||||
}
|
||||
|
||||
TachideskAPI.defineEndpoints(app)
|
||||
GlobalAPI.defineEndpoints(app)
|
||||
MangaAPI.defineEndpoints(app)
|
||||
AnimeAPI.defineEndpoints(app)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ class ServerConfig(config: Config) : ConfigModule(config) {
|
||||
val ip: String by config
|
||||
val port: Int by config
|
||||
|
||||
val webUIEnabled: Boolean = System.getProperty(
|
||||
"suwayomi.tachidesk.server.webUIEnabled", config.getString("webUIEnabled")
|
||||
).toBoolean()
|
||||
|
||||
// proxy
|
||||
val socksProxyEnabled: Boolean by config
|
||||
val socksProxyHost: String by config
|
||||
|
||||
@@ -13,7 +13,6 @@ import org.kodein.di.DI
|
||||
import org.kodein.di.bind
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.singleton
|
||||
import suwayomi.server.BuildConfig
|
||||
import suwayomi.tachidesk.server.database.databaseUp
|
||||
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
||||
import suwayomi.tachidesk.server.util.SystemTray.systemTray
|
||||
@@ -23,6 +22,7 @@ import xyz.nulldev.ts.config.ApplicationRootDir
|
||||
import xyz.nulldev.ts.config.ConfigKodeinModule
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@@ -33,6 +33,7 @@ class ApplicationDirs(
|
||||
val mangaThumbnailsRoot = "$dataRoot/manga-thumbnails"
|
||||
val animeThumbnailsRoot = "$dataRoot/anime-thumbnails"
|
||||
val mangaRoot = "$dataRoot/manga"
|
||||
val webUIRoot = "$dataRoot/webUI"
|
||||
}
|
||||
|
||||
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
||||
@@ -46,12 +47,15 @@ fun applicationSetup() {
|
||||
|
||||
// Application dirs
|
||||
val applicationDirs = ApplicationDirs()
|
||||
|
||||
DI.global.addImport(
|
||||
DI.Module("Server") {
|
||||
bind<ApplicationDirs>() with singleton { applicationDirs }
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug("Data Root directory is set to: ${applicationDirs.dataRoot}")
|
||||
|
||||
// make dirs we need
|
||||
listOf(
|
||||
applicationDirs.dataRoot,
|
||||
@@ -92,6 +96,9 @@ fun applicationSetup() {
|
||||
logger.error("Exception while creating initial server.conf:\n", e)
|
||||
}
|
||||
|
||||
// fixes #119 , ref: https://github.com/Suwayomi/Tachidesk-Server/issues/119#issuecomment-894681292
|
||||
Locale.setDefault(Locale.ENGLISH)
|
||||
|
||||
databaseUp()
|
||||
|
||||
// create system tray
|
||||
|
||||
@@ -7,13 +7,14 @@ package suwayomi.tachidesk.server.database
|
||||
* 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 de.neonew.exposed.migrations.loadMigrationsFrom
|
||||
import de.neonew.exposed.migrations.runMigrations
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import suwayomi.tachidesk.server.database.migration.lib.loadMigrationsFrom
|
||||
import suwayomi.tachidesk.server.database.migration.lib.runMigrations
|
||||
import suwayomi.tachidesk.server.ServerConfig
|
||||
|
||||
object DBManager {
|
||||
val db by lazy {
|
||||
@@ -27,6 +28,6 @@ fun databaseUp() {
|
||||
val db = DBManager.db
|
||||
db.useNestedTransactions = true
|
||||
|
||||
val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration")
|
||||
val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration", ServerConfig::class.java)
|
||||
runMigrations(migrations)
|
||||
}
|
||||
|
||||
+8
-9
@@ -7,15 +7,15 @@ package suwayomi.tachidesk.server.database.migration
|
||||
* 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 de.neonew.exposed.migrations.helpers.AddTableMigration
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import org.jetbrains.exposed.dao.id.IdTable
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.server.database.migration.lib.Migration
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0001_Initial : Migration() {
|
||||
/** initial migration, create all tables */
|
||||
class M0001_Initial : AddTableMigration() {
|
||||
private class ExtensionTable : IntIdTable() {
|
||||
init {
|
||||
varchar("apk_name", 1024)
|
||||
@@ -111,9 +111,8 @@ class M0001_Initial : Migration() {
|
||||
}
|
||||
}
|
||||
|
||||
/** initial migration, create all tables */
|
||||
override fun run() {
|
||||
transaction {
|
||||
override val tables: Array<Table>
|
||||
get() {
|
||||
val extensionTable = ExtensionTable()
|
||||
val sourceTable = SourceTable(extensionTable)
|
||||
val mangaTable = MangaTable()
|
||||
@@ -121,7 +120,8 @@ class M0001_Initial : Migration() {
|
||||
val pageTable = PageTable(chapterTable)
|
||||
val categoryTable = CategoryTable()
|
||||
val categoryMangaTable = CategoryMangaTable(categoryTable, mangaTable)
|
||||
SchemaUtils.create(
|
||||
|
||||
return arrayOf(
|
||||
extensionTable,
|
||||
sourceTable,
|
||||
mangaTable,
|
||||
@@ -131,5 +131,4 @@ class M0001_Initial : Migration() {
|
||||
categoryMangaTable,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-13
@@ -7,18 +7,11 @@ package suwayomi.tachidesk.server.database.migration
|
||||
* 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 org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||
import suwayomi.tachidesk.server.database.migration.lib.Migration
|
||||
import de.neonew.exposed.migrations.helpers.RenameFieldMigration
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0002_ChapterTableIndexRename : Migration() {
|
||||
/** this migration renamed ChapterTable.NUMBER_IN_LIST to ChapterTable.INDEX */
|
||||
override fun run() {
|
||||
with(TransactionManager.current()) {
|
||||
exec("ALTER TABLE CHAPTER ALTER COLUMN NUMBER_IN_LIST RENAME TO INDEX")
|
||||
commit()
|
||||
currentDialect.resetCaches()
|
||||
}
|
||||
}
|
||||
}
|
||||
class M0002_ChapterTableIndexRename : RenameFieldMigration(
|
||||
"Chapter",
|
||||
"number_in_list",
|
||||
"index"
|
||||
)
|
||||
|
||||
+6
-13
@@ -7,18 +7,11 @@ package suwayomi.tachidesk.server.database.migration
|
||||
* 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 org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||
import suwayomi.tachidesk.server.database.migration.lib.Migration
|
||||
import de.neonew.exposed.migrations.helpers.RenameFieldMigration
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0003_DefaultCategory : Migration() {
|
||||
/** this migration renamed CategoryTable.IS_LANDING to ChapterTable.IS_DEFAULT */
|
||||
override fun run() {
|
||||
with(TransactionManager.current()) {
|
||||
exec("ALTER TABLE CATEGORY ALTER COLUMN IS_LANDING RENAME TO IS_DEFAULT")
|
||||
commit()
|
||||
currentDialect.resetCaches()
|
||||
}
|
||||
}
|
||||
}
|
||||
class M0003_DefaultCategory : RenameFieldMigration(
|
||||
"Category",
|
||||
"is_landing",
|
||||
"is_default"
|
||||
)
|
||||
|
||||
+13
-16
@@ -1,19 +1,19 @@
|
||||
package suwayomi.tachidesk.server.database.migration
|
||||
|
||||
import org.jetbrains.exposed.dao.id.IdTable
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.server.database.migration.lib.Migration
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
class M0004_AnimeTablesBatch1 : Migration() {
|
||||
import de.neonew.exposed.migrations.helpers.AddTableMigration
|
||||
import org.jetbrains.exposed.dao.id.IdTable
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0004_AnimeTablesBatch1 : AddTableMigration() {
|
||||
private class AnimeExtensionTable : IntIdTable() {
|
||||
val apkName = varchar("apk_name", 1024)
|
||||
|
||||
@@ -43,12 +43,9 @@ class M0004_AnimeTablesBatch1 : Migration() {
|
||||
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
transaction {
|
||||
SchemaUtils.create(
|
||||
AnimeExtensionTable(),
|
||||
AnimeSourceTable()
|
||||
)
|
||||
}
|
||||
}
|
||||
override val tables: Array<Table>
|
||||
get() = arrayOf(
|
||||
AnimeExtensionTable(),
|
||||
AnimeSourceTable()
|
||||
)
|
||||
}
|
||||
|
||||
+12
-15
@@ -1,19 +1,19 @@
|
||||
package suwayomi.tachidesk.server.database.migration
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.server.database.migration.lib.Migration
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
class M0005_AnimeTablesBatch2 : Migration() {
|
||||
import de.neonew.exposed.migrations.helpers.AddTableMigration
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0005_AnimeTablesBatch2 : AddTableMigration() {
|
||||
private class AnimeTable : IntIdTable() {
|
||||
val url = varchar("url", 2048)
|
||||
val title = varchar("title", 512)
|
||||
@@ -35,11 +35,8 @@ class M0005_AnimeTablesBatch2 : Migration() {
|
||||
val sourceReference = long("source")
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
transaction {
|
||||
SchemaUtils.create(
|
||||
AnimeTable()
|
||||
)
|
||||
}
|
||||
}
|
||||
override val tables: Array<Table>
|
||||
get() = arrayOf(
|
||||
AnimeTable()
|
||||
)
|
||||
}
|
||||
|
||||
+12
-15
@@ -1,19 +1,19 @@
|
||||
package suwayomi.tachidesk.server.database.migration
|
||||
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeTable
|
||||
import suwayomi.tachidesk.server.database.migration.lib.Migration
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
class M0006_AnimeTablesBatch3 : Migration() {
|
||||
import de.neonew.exposed.migrations.helpers.AddTableMigration
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeTable
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0006_AnimeTablesBatch3 : AddTableMigration() {
|
||||
private class EpisodeTable : IntIdTable() {
|
||||
val url = varchar("url", 2048)
|
||||
val name = varchar("name", 512)
|
||||
@@ -31,11 +31,8 @@ class M0006_AnimeTablesBatch3 : Migration() {
|
||||
val anime = reference("anime", AnimeTable)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
transaction {
|
||||
SchemaUtils.create(
|
||||
EpisodeTable()
|
||||
)
|
||||
}
|
||||
}
|
||||
override val tables: Array<Table>
|
||||
get() = arrayOf(
|
||||
EpisodeTable()
|
||||
)
|
||||
}
|
||||
|
||||
+7
-13
@@ -7,18 +7,12 @@ package suwayomi.tachidesk.server.database.migration
|
||||
* 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 org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||
import suwayomi.tachidesk.server.database.migration.lib.Migration
|
||||
import de.neonew.exposed.migrations.helpers.AddColumnMigration
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0007_ChapterIsDownloaded : Migration() {
|
||||
/** this migration added IS_DOWNLOADED to CHAPTER */
|
||||
override fun run() {
|
||||
with(TransactionManager.current()) {
|
||||
exec("ALTER TABLE CHAPTER ADD COLUMN IS_DOWNLOADED BOOLEAN DEFAULT FALSE")
|
||||
commit()
|
||||
currentDialect.resetCaches()
|
||||
}
|
||||
}
|
||||
}
|
||||
class M0007_ChapterIsDownloaded : AddColumnMigration(
|
||||
"Chapter",
|
||||
"is_downloaded",
|
||||
"BOOLEAN",
|
||||
"FALSE"
|
||||
)
|
||||
|
||||
+7
-13
@@ -7,18 +7,12 @@ package suwayomi.tachidesk.server.database.migration
|
||||
* 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 org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||
import suwayomi.tachidesk.server.database.migration.lib.Migration
|
||||
import de.neonew.exposed.migrations.helpers.AddColumnMigration
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0008_ChapterPageCount : Migration() {
|
||||
/** this migration added PAGE_COUNT to CHAPTER */
|
||||
override fun run() {
|
||||
with(TransactionManager.current()) {
|
||||
exec("ALTER TABLE CHAPTER ADD COLUMN PAGE_COUNT INT DEFAULT -1")
|
||||
commit()
|
||||
currentDialect.resetCaches()
|
||||
}
|
||||
}
|
||||
}
|
||||
class M0008_ChapterPageCount : AddColumnMigration(
|
||||
"Chapter",
|
||||
"page_count",
|
||||
"INT",
|
||||
"-1"
|
||||
)
|
||||
|
||||
+7
-14
@@ -7,19 +7,12 @@ package suwayomi.tachidesk.server.database.migration
|
||||
* 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 org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||
import suwayomi.tachidesk.server.database.migration.lib.Migration
|
||||
import de.neonew.exposed.migrations.helpers.AddColumnMigration
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0009_ChapterLastReadAt : Migration() {
|
||||
/** this migration added PAGE_COUNT to CHAPTER */
|
||||
override fun run() {
|
||||
with(TransactionManager.current()) {
|
||||
// BIGINT == Long
|
||||
exec("ALTER TABLE CHAPTER ADD COLUMN LAST_READ_AT BIGINT DEFAULT 0")
|
||||
commit()
|
||||
currentDialect.resetCaches()
|
||||
}
|
||||
}
|
||||
}
|
||||
class M0009_ChapterLastReadAt : AddColumnMigration(
|
||||
"Chapter",
|
||||
"last_read_at",
|
||||
"BIGINT", // BIGINT == Long
|
||||
"0"
|
||||
)
|
||||
|
||||
+16
-18
@@ -1,38 +1,36 @@
|
||||
package suwayomi.tachidesk.server.database.migration
|
||||
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.ReferenceOption
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.server.database.migration.lib.Migration
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
class M0010_MangaAndChapterMeta : Migration() {
|
||||
import de.neonew.exposed.migrations.helpers.AddTableMigration
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.ReferenceOption
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0010_MangaAndChapterMeta : AddTableMigration() {
|
||||
private class ChapterMetaTable : IntIdTable() {
|
||||
val key = varchar("key", 256)
|
||||
val value = varchar("value", 4096)
|
||||
val ref = reference("chapter_ref", ChapterTable, ReferenceOption.CASCADE)
|
||||
}
|
||||
|
||||
private class MangaMetaTable : IntIdTable() {
|
||||
val key = varchar("key", 256)
|
||||
val value = varchar("value", 4096)
|
||||
val ref = reference("manga_ref", MangaTable, ReferenceOption.CASCADE)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
transaction {
|
||||
SchemaUtils.create(
|
||||
ChapterMetaTable(),
|
||||
MangaMetaTable()
|
||||
)
|
||||
}
|
||||
}
|
||||
override val tables: Array<Table>
|
||||
get() = arrayOf(
|
||||
ChapterMetaTable(),
|
||||
MangaMetaTable()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Andreas Mausch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,25 +0,0 @@
|
||||
package suwayomi.tachidesk.server.database.migration.lib
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
|
||||
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
|
||||
|
||||
abstract class Migration {
|
||||
val name: String
|
||||
val version: Int
|
||||
|
||||
init {
|
||||
val groups = Regex("^M(\\d+)_(.*)$").matchEntire(this::class.simpleName!!)?.groupValues
|
||||
?: throw IllegalArgumentException("Migration class name doesn't match convention")
|
||||
version = groups[1].toInt()
|
||||
name = groups[2]
|
||||
}
|
||||
|
||||
abstract fun run()
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
package suwayomi.tachidesk.server.database.migration.lib
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
|
||||
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
|
||||
|
||||
import org.jetbrains.exposed.dao.IntEntity
|
||||
import org.jetbrains.exposed.dao.IntEntityClass
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.dao.id.IdTable
|
||||
import org.jetbrains.exposed.sql.`java-time`.timestamp
|
||||
|
||||
object MigrationsTable : IdTable<Int>() {
|
||||
override val id = integer("version").entityId()
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
val name = varchar("name", length = 400)
|
||||
val executedAt = timestamp("executed_at")
|
||||
|
||||
init {
|
||||
index(true, name)
|
||||
}
|
||||
}
|
||||
|
||||
class MigrationEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<MigrationEntity>(MigrationsTable)
|
||||
|
||||
var version by MigrationsTable.id
|
||||
var name by MigrationsTable.name
|
||||
var executedAt by MigrationsTable.executedAt
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
package suwayomi.tachidesk.server.database.migration.lib
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
|
||||
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.SchemaUtils.create
|
||||
import org.jetbrains.exposed.sql.exists
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.server.ServerConfig
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.time.Clock
|
||||
import java.time.Instant.now
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.name
|
||||
import kotlin.streams.toList
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun runMigrations(migrations: List<Migration>, database: Database = TransactionManager.defaultDatabase!!, clock: Clock = Clock.systemUTC()) {
|
||||
checkVersions(migrations)
|
||||
|
||||
logger.info { "Running migrations on database ${database.url}" }
|
||||
|
||||
val latestVersion = transaction(database) {
|
||||
createTableIfNotExists(database)
|
||||
MigrationEntity.all().maxByOrNull { it.version }?.version?.value
|
||||
}
|
||||
|
||||
logger.info { "Database version before migrations: $latestVersion" }
|
||||
|
||||
migrations
|
||||
.sortedBy { it.version }
|
||||
.filter { shouldRun(latestVersion, it) }
|
||||
.forEach {
|
||||
logger.info { "Running migration version ${it.version}: ${it.name}" }
|
||||
transaction(database) {
|
||||
it.run()
|
||||
|
||||
MigrationEntity.new {
|
||||
version = EntityID(it.version, MigrationsTable)
|
||||
name = it.name
|
||||
executedAt = now(clock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info { "Migrations finished successfully" }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
private fun getTopLevelClasses(packageName: String): List<Class<*>> {
|
||||
ServerConfig::class.java.getResource("/" + packageName.replace('.', '/'))
|
||||
val path = "/" + packageName.replace('.', '/')
|
||||
val uri = ServerConfig::class.java.getResource(path).toURI()
|
||||
|
||||
return when (uri.scheme) {
|
||||
"jar" -> {
|
||||
val fileSystem = FileSystems.newFileSystem(uri, emptyMap<String, Any>())
|
||||
fileSystem.getPath(path)
|
||||
}
|
||||
else -> Paths.get(uri)
|
||||
}.let { Files.walk(it, 1) }
|
||||
.toList()
|
||||
.filterNot { it.isDirectory() || it.name.contains('$') } // '$' means it's not a top level class
|
||||
.filter { it.name.endsWith(".class") }
|
||||
.map { Class.forName("$packageName.${it.name.substringBefore(".class")}") }
|
||||
}
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
fun loadMigrationsFrom(packageName: String): List<Migration> {
|
||||
return getTopLevelClasses(packageName)
|
||||
.map {
|
||||
logger.debug("found Migration class ${it.name}")
|
||||
val clazz = it.getDeclaredConstructor().newInstance()
|
||||
if (clazz is Migration)
|
||||
clazz
|
||||
else
|
||||
throw RuntimeException("found a class that's not a Migration")
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkVersions(migrations: List<Migration>) {
|
||||
val sorted = migrations.map { it.version }.sorted()
|
||||
if ((1..migrations.size).toList() != sorted) {
|
||||
throw IllegalStateException("List of migrations version is not consecutive: $sorted")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTableIfNotExists(database: Database) {
|
||||
if (MigrationsTable.exists()) {
|
||||
return
|
||||
}
|
||||
val tableNames = database.dialect.allTablesNames()
|
||||
when (tableNames.isEmpty()) {
|
||||
true -> {
|
||||
logger.info { "Empty database found, creating table for migrations" }
|
||||
create(MigrationsTable)
|
||||
}
|
||||
false -> throw IllegalStateException("Tried to run migrations against a non-empty database without a Migrations table. This is not supported.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldRun(latestVersion: Int?, migration: Migration): Boolean {
|
||||
val run = latestVersion?.let { migration.version > it } ?: true
|
||||
if (!run) {
|
||||
logger.debug { "Skipping migration version ${migration.version}: ${migration.name}" }
|
||||
}
|
||||
return run
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import io.javalin.plugin.json.JavalinJackson
|
||||
import mu.KotlinLogging
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request.Builder
|
||||
import suwayomi.tachidesk.server.impl.AboutDataClass
|
||||
import suwayomi.tachidesk.global.impl.AboutDataClass
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.server.util.Browser.openInBrowser
|
||||
import suwayomi.tachidesk.server.util.ExitCode.MutexCheckFailedAnotherAppRunning
|
||||
@@ -36,7 +36,7 @@ object AppMutex {
|
||||
.build()
|
||||
|
||||
val request = Builder()
|
||||
.url("http://$appIP:${serverConfig.port}/api/v1/about/")
|
||||
.url("http://$appIP:${serverConfig.port}/api/v1/settings/about/")
|
||||
.build()
|
||||
|
||||
val response = try {
|
||||
|
||||
@@ -10,7 +10,7 @@ package suwayomi.tachidesk.server.util
|
||||
import dorkbox.systemTray.MenuItem
|
||||
import dorkbox.systemTray.SystemTray
|
||||
import dorkbox.util.CacheUtil
|
||||
import suwayomi.server.BuildConfig
|
||||
import suwayomi.tachidesk.server.BuildConfig
|
||||
import suwayomi.tachidesk.server.ServerConfig
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.server.util.Browser.openInBrowser
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package suwayomi.tachidesk.server.util
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 mu.KotlinLogging
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import suwayomi.tachidesk.server.BuildConfig
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
private val tmpDir = System.getProperty("java.io.tmpdir")
|
||||
|
||||
private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
|
||||
|
||||
private fun directoryMD5(fileDir: String): String {
|
||||
var sum = ""
|
||||
File(fileDir).walk().toList().sortedBy { it.path }.forEach { file ->
|
||||
if (file.isFile) {
|
||||
val md5 = MessageDigest.getInstance("MD5")
|
||||
md5.update(file.readBytes())
|
||||
val digest = md5.digest()
|
||||
sum += digest.toHex()
|
||||
}
|
||||
}
|
||||
|
||||
val md5 = MessageDigest.getInstance("MD5")
|
||||
md5.update(sum.toByteArray(StandardCharsets.UTF_8))
|
||||
val digest = md5.digest()
|
||||
return digest.toHex()
|
||||
}
|
||||
|
||||
fun setupWebUI() {
|
||||
// check if we have webUI installed and is correct version
|
||||
val webUIRevisionFile = File(applicationDirs.webUIRoot + "/revision")
|
||||
if (webUIRevisionFile.exists() && webUIRevisionFile.readText().trim() == BuildConfig.WEBUI_TAG) {
|
||||
logger.info { "WebUI Static files exists and is the correct revision" }
|
||||
logger.info { "Verifying WebUI Static files..." }
|
||||
logger.info { "md5: " + directoryMD5(applicationDirs.webUIRoot) }
|
||||
} else {
|
||||
File(applicationDirs.webUIRoot).deleteRecursively()
|
||||
|
||||
val webUIZip = "Tachidesk-WebUI-${BuildConfig.WEBUI_TAG}.zip"
|
||||
val webUIZipPath = "$tmpDir/$webUIZip"
|
||||
val webUIZipFile = File(webUIZipPath)
|
||||
|
||||
// try with resources first
|
||||
val resourceWebUI = try {
|
||||
BuildConfig::class.java.getResourceAsStream("/WebUI.zip")
|
||||
} catch (e: NullPointerException) { null }
|
||||
|
||||
if (resourceWebUI == null) { // is not bundled
|
||||
// download webUI zip
|
||||
val webUIZipURL = "${BuildConfig.WEBUI_REPO}/releases/download/${BuildConfig.WEBUI_TAG}/$webUIZip"
|
||||
webUIZipFile.delete()
|
||||
|
||||
logger.info { "Downloading WebUI zip from the Internet..." }
|
||||
val data = ByteArray(1024)
|
||||
|
||||
webUIZipFile.outputStream().use { webUIZipFileOut ->
|
||||
BufferedInputStream(URL(webUIZipURL).openStream()).use { inp ->
|
||||
var totalCount = 0
|
||||
var tresh = 0
|
||||
while (true) {
|
||||
val count = inp.read(data, 0, 1024)
|
||||
totalCount += count
|
||||
if (totalCount > tresh + 10 * 1024) {
|
||||
tresh = totalCount
|
||||
print(" *")
|
||||
}
|
||||
if (count == -1)
|
||||
break
|
||||
webUIZipFileOut.write(data, 0, count)
|
||||
}
|
||||
println()
|
||||
logger.info { "Downloading WebUI Done." }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info { "Using the bundled WebUI zip..." }
|
||||
|
||||
resourceWebUI.use { input ->
|
||||
webUIZipFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extract webUI zip
|
||||
logger.info { "Extracting WebUI zip..." }
|
||||
File(applicationDirs.webUIRoot).mkdirs()
|
||||
ZipFile(webUIZipPath).extractAll(applicationDirs.webUIRoot)
|
||||
logger.info { "Extracting WebUI zip Done." }
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,7 @@ server.socksProxyPort = ""
|
||||
# misc
|
||||
server.debugLogsEnabled = false
|
||||
server.systemTrayEnabled = true
|
||||
|
||||
# webUI
|
||||
server.webUIEnabled = true
|
||||
server.initialOpenInBrowserEnabled = true
|
||||
|
||||
@@ -31,6 +31,7 @@ import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||
import suwayomi.tachidesk.server.applicationSetup
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class TestExtensions {
|
||||
@@ -48,7 +49,7 @@ class TestExtensions {
|
||||
@BeforeAll
|
||||
fun setup() {
|
||||
val dataRoot = File("tmp/TestDesk").absolutePath
|
||||
System.setProperty("suwayomi.tachidesk.rootDir", dataRoot)
|
||||
System.setProperty("suwayomi.tachidesk.server.rootDir", dataRoot)
|
||||
applicationSetup()
|
||||
setLoggingEnabled(false)
|
||||
|
||||
@@ -63,6 +64,7 @@ class TestExtensions {
|
||||
updateExtension(it.pkgName)
|
||||
}
|
||||
else -> {
|
||||
uninstallExtension(it.pkgName)
|
||||
installExtension(it.pkgName)
|
||||
}
|
||||
}
|
||||
@@ -77,10 +79,11 @@ class TestExtensions {
|
||||
fun runTest() {
|
||||
runBlocking(Dispatchers.Default) {
|
||||
val semaphore = Semaphore(10)
|
||||
sources.mapIndexed { index, source ->
|
||||
val popularCount = AtomicInteger(1)
|
||||
sources.map { source ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
logger.info { "$index - Now fetching popular manga from $source" }
|
||||
logger.info { "${popularCount.getAndIncrement()} - Now fetching popular manga from $source" }
|
||||
try {
|
||||
mangaToFetch += source to (
|
||||
source.fetchPopularManga(1)
|
||||
@@ -102,10 +105,11 @@ class TestExtensions {
|
||||
)
|
||||
logger.info { "Now fetching manga info from ${mangaToFetch.size} sources" }
|
||||
|
||||
mangaToFetch.mapIndexed { index, (source, manga) ->
|
||||
val mangaCount = AtomicInteger(1)
|
||||
mangaToFetch.map { (source, manga) ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
logger.info { "$index - Now fetching manga from $source" }
|
||||
logger.info { "${mangaCount.getAndIncrement()} - Now fetching manga from $source" }
|
||||
try {
|
||||
manga.copyFrom(source.fetchMangaDetails(manga).awaitSingleRepeat())
|
||||
manga.initialized = true
|
||||
@@ -127,10 +131,11 @@ class TestExtensions {
|
||||
)
|
||||
logger.info { "Now fetching manga chapters from ${mangaToFetch.size} sources" }
|
||||
|
||||
mangaToFetch.filter { it.second.initialized }.mapIndexed { index, (source, manga) ->
|
||||
val chapterCount = AtomicInteger(1)
|
||||
mangaToFetch.filter { it.second.initialized }.map { (source, manga) ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
logger.info { "$index - Now fetching manga chapters from $source" }
|
||||
logger.info { "${chapterCount.getAndIncrement()} - Now fetching manga chapters from $source" }
|
||||
try {
|
||||
chaptersToFetch += Triple(
|
||||
source,
|
||||
@@ -160,10 +165,11 @@ class TestExtensions {
|
||||
}
|
||||
)
|
||||
|
||||
chaptersToFetch.mapIndexed { index, (source, manga, chapter) ->
|
||||
val pageListCount = AtomicInteger(1)
|
||||
chaptersToFetch.map { (source, manga, chapter) ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
logger.info { "$index - Now fetching page list from $source" }
|
||||
logger.info { "${pageListCount.getAndIncrement()} - Now fetching page list from $source" }
|
||||
try {
|
||||
source.fetchPageList(chapter).awaitSingleRepeat()
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -2,7 +2,5 @@ rootProject.name = System.getenv("ProductName") ?: "Tachidesk"
|
||||
|
||||
include("server")
|
||||
|
||||
include("webUI")
|
||||
|
||||
include("AndroidCompat")
|
||||
include("AndroidCompat:Config")
|
||||
@@ -1,21 +0,0 @@
|
||||
plugins {
|
||||
id("com.github.node-gradle.node") version "3.0.1"
|
||||
}
|
||||
|
||||
val nodeRoot = "${project.projectDir}/src"
|
||||
node {
|
||||
nodeProjectDir.set(file(nodeRoot))
|
||||
}
|
||||
|
||||
tasks {
|
||||
register<Copy>("copyBuild") {
|
||||
from(file("$nodeRoot/build"))
|
||||
into(file("$rootDir/server/src/main/resources/webUI"))
|
||||
|
||||
dependsOn("yarn_build")
|
||||
}
|
||||
|
||||
named("yarn_build") {
|
||||
dependsOn("yarn") // install node_modules
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
.eslintrc.js
|
||||
@@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['airbnb-typescript'],
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
rules: {
|
||||
// Indent with 4 spaces
|
||||
'@typescript-eslint/indent': ['error', 4],
|
||||
|
||||
// Indent JSX with 4 spaces
|
||||
'react/jsx-indent': ['error', 4],
|
||||
|
||||
// Indent props with 4 spaces
|
||||
'react/jsx-indent-props': ['error', 4],
|
||||
|
||||
'no-plusplus': ['error', { 'allowForLoopAfterthoughts': true }]
|
||||
},
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules/
|
||||
.eslintcache
|
||||
.vscode
|
||||
.env
|
||||
@@ -1,70 +0,0 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `yarn start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `yarn build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"name": "project",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^4.3.0",
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.58",
|
||||
"axios": "^0.21.1",
|
||||
"file-selector": "^0.2.4",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-lazyload": "^3.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-virtuoso": "^1.8.6",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react-lazyload": "^3.1.0",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@typescript-eslint/eslint-plugin": "4.23.0",
|
||||
"@typescript-eslint/parser": "4.23.0",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint-config-airbnb-typescript": "^12.3.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"typescript": "^4.2.4"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 579 KiB |
@@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
|
||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta
|
||||
name="description"
|
||||
content="A manga reader that runs tachiyomi's extensions"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png"/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Tachidesk</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"short_name": "Tachidesk",
|
||||
"name": "Tachidesk",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#ff2323",
|
||||
"background_color": "#ff2323"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 React, { useState } from 'react';
|
||||
import {
|
||||
BrowserRouter as Router, Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
} from 'react-router-dom';
|
||||
import { Container } from '@material-ui/core';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
|
||||
import NavBar from 'components/navbar/NavBar';
|
||||
import NavbarContext from 'context/NavbarContext';
|
||||
import DarkTheme from 'context/DarkTheme';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
import MangaSources from 'screens/manga/MangaSources';
|
||||
import AnimeSources from 'screens/anime/AnimeSources';
|
||||
import Settings from 'screens/Settings';
|
||||
import About from 'screens/settings/About';
|
||||
import Categories from 'screens/settings/Categories';
|
||||
import Backup from 'screens/settings/Backup';
|
||||
import Library from 'screens/manga/Library';
|
||||
import SearchSingle from 'screens/manga/SearchSingle';
|
||||
import Manga from 'screens/manga/Manga';
|
||||
import Anime from 'screens/anime/Anime';
|
||||
import MangaExtensions from 'screens/manga/MangaExtensions';
|
||||
import SourceMangas from 'screens/manga/SourceMangas';
|
||||
import SourceAnimes from 'screens/anime/SourceAnimes';
|
||||
import Reader from 'screens/manga/Reader';
|
||||
import Player from 'screens/anime/Player';
|
||||
import AnimeExtensions from 'screens/anime/AnimeExtensions';
|
||||
import DownloadQueue from 'screens/manga/DownloadQueue';
|
||||
|
||||
export default function App() {
|
||||
const [title, setTitle] = useState<string>('Tachidesk');
|
||||
const [action, setAction] = useState<any>(<div />);
|
||||
const [override, setOverride] = useState<INavbarOverride>({ status: false, value: <div /> });
|
||||
|
||||
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
|
||||
|
||||
const navBarContext = {
|
||||
title, setTitle, action, setAction, override, setOverride,
|
||||
};
|
||||
const darkThemeContext = { darkTheme, setDarkTheme };
|
||||
|
||||
const theme = React.useMemo(
|
||||
() => createMuiTheme({
|
||||
palette: {
|
||||
type: darkTheme ? 'dark' : 'light',
|
||||
},
|
||||
overrides: {
|
||||
MuiCssBaseline: {
|
||||
'@global': {
|
||||
'*::-webkit-scrollbar': {
|
||||
width: '10px',
|
||||
background: darkTheme ? '#222' : '#e1e1e1',
|
||||
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb': {
|
||||
background: darkTheme ? '#111' : '#aaa',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[darkTheme],
|
||||
);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<ThemeProvider theme={theme}>
|
||||
<NavbarContext.Provider value={navBarContext}>
|
||||
<CssBaseline />
|
||||
<NavBar />
|
||||
<Container
|
||||
id="appMainContainer"
|
||||
maxWidth={false}
|
||||
disableGutters
|
||||
style={{ paddingTop: '64px' }}
|
||||
>
|
||||
<Switch>
|
||||
{/* general routes */}
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => (
|
||||
<Redirect to="/library" />
|
||||
)}
|
||||
/>
|
||||
|
||||
<Route path="/settings/about">
|
||||
<About />
|
||||
</Route>
|
||||
<Route path="/settings/categories">
|
||||
<Categories />
|
||||
</Route>
|
||||
<Route path="/settings/backup">
|
||||
<Backup />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<DarkTheme.Provider value={darkThemeContext}>
|
||||
<Settings />
|
||||
</DarkTheme.Provider>
|
||||
</Route>
|
||||
|
||||
{/* Manga Routes */}
|
||||
|
||||
<Route path="/sources/:sourceId/search/">
|
||||
<SearchSingle />
|
||||
</Route>
|
||||
<Route path="/manga/extensions">
|
||||
<MangaExtensions />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/popular/">
|
||||
<SourceMangas popular />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/latest/">
|
||||
<SourceMangas popular={false} />
|
||||
</Route>
|
||||
<Route path="/manga/sources">
|
||||
<MangaSources />
|
||||
</Route>
|
||||
<Route path="/manga/downloads">
|
||||
<DownloadQueue />
|
||||
</Route>
|
||||
<Route path="/manga/:mangaId/chapter/:chapterNum">
|
||||
<></>
|
||||
</Route>
|
||||
<Route path="/manga/:id">
|
||||
<Manga />
|
||||
</Route>
|
||||
<Route path="/library">
|
||||
<Library />
|
||||
</Route>
|
||||
|
||||
{/* Anime Routes */}
|
||||
<Route path="/anime/extensions">
|
||||
<AnimeExtensions />
|
||||
</Route>
|
||||
<Route path="/anime/sources/:sourceId/popular/">
|
||||
<SourceAnimes popular />
|
||||
</Route>
|
||||
<Route path="/anime/sources/:sourceId/latest/">
|
||||
<SourceMangas popular={false} />
|
||||
</Route>
|
||||
<Route path="/anime/sources">
|
||||
<AnimeSources />
|
||||
</Route>
|
||||
<Route path="/anime/:animeId/episode/:episodeIndex">
|
||||
<Player />
|
||||
</Route>
|
||||
<Route path="/anime/:id">
|
||||
<Anime />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Container>
|
||||
<Switch>
|
||||
<Route
|
||||
path="/manga/:mangaId/chapter/:chapterIndex"
|
||||
// passing a key re-mounts the reader when changing chapters
|
||||
render={(props:any) => <Reader key={props.match.params.chapterIndex} />}
|
||||
/>
|
||||
</Switch>
|
||||
</NavbarContext.Provider>
|
||||
</ThemeProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/require-default-props */
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
loading: {
|
||||
margin: '10px auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
shouldRender: boolean | (() => boolean)
|
||||
children?: React.ReactNode
|
||||
component?: string | React.FunctionComponent<any> | React.ComponentClass<any, any>
|
||||
componentProps?: any
|
||||
}
|
||||
|
||||
export default function LoadingPlaceholder(props: IProps) {
|
||||
const {
|
||||
children, shouldRender, component, componentProps,
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
|
||||
const condition = shouldRender instanceof Function ? shouldRender() : shouldRender;
|
||||
|
||||
if (condition) {
|
||||
if (component) {
|
||||
return React.createElement(component, componentProps);
|
||||
}
|
||||
|
||||
if (children) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.loading}>
|
||||
<CircularProgress thickness={5} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 React, { useEffect, useState } from 'react';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
|
||||
interface IProps {
|
||||
src: string
|
||||
alt: string
|
||||
|
||||
imgRef?: React.RefObject<HTMLImageElement>
|
||||
|
||||
spinnerClassName?: string
|
||||
imgClassName?: string
|
||||
|
||||
onImageLoad?: () => void
|
||||
}
|
||||
|
||||
export default function SpinnerImage(props: IProps) {
|
||||
const {
|
||||
src, alt, onImageLoad, imgRef, spinnerClassName, imgClassName,
|
||||
} = props;
|
||||
const [imageSrc, setImagsrc] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
|
||||
img.onload = () => {
|
||||
setImagsrc(src);
|
||||
onImageLoad?.();
|
||||
};
|
||||
|
||||
return () => {
|
||||
img.onload = null;
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
if (imageSrc.length === 0) {
|
||||
return (
|
||||
// <div className={`${classes.image} ${classes.loadingImage}`}>
|
||||
<div className={spinnerClassName}>
|
||||
<CircularProgress thickness={5} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className={imgClassName}
|
||||
ref={imgRef}
|
||||
src={imageSrc}
|
||||
alt={alt}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
SpinnerImage.defaultProps = {
|
||||
spinnerClassName: '',
|
||||
imgClassName: '',
|
||||
onImageLoad: () => {},
|
||||
imgRef: undefined,
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import CollectionsBookmarkIcon from '@material-ui/icons/CollectionsBookmark';
|
||||
import ExploreIcon from '@material-ui/icons/Explore';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
import GetAppIcon from '@material-ui/icons/GetApp';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
list: {
|
||||
width: 250,
|
||||
},
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
drawerOpen: boolean
|
||||
|
||||
setDrawerOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
anchor="left"
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
>
|
||||
<div
|
||||
className={classes.list}
|
||||
role="presentation"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
onKeyDown={() => setDrawerOpen(false)}
|
||||
>
|
||||
<List>
|
||||
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Library">
|
||||
<ListItemIcon>
|
||||
<CollectionsBookmarkIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Library" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/manga/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Extensions">
|
||||
<ListItemIcon>
|
||||
<ExtensionIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Manga Extensions" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/anime/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Extensions">
|
||||
<ListItemIcon>
|
||||
<ExtensionIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Anime Extensions" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/manga/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Sources">
|
||||
<ListItemIcon>
|
||||
<ExploreIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Manga Sources" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/anime/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Sources">
|
||||
<ListItemIcon>
|
||||
<ExploreIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Anime Sources" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/manga/downloads" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Manga Download Queue">
|
||||
<ListItemIcon>
|
||||
<GetAppIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Manga Download Queue" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="settings">
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
</List>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
import Slide, { SlideProps } from '@material-ui/core/Slide';
|
||||
import Snackbar from '@material-ui/core/Snackbar';
|
||||
import MuiAlert, { Color as Severity } from '@material-ui/lab/Alert';
|
||||
|
||||
function removeToast(id: string) {
|
||||
const container = document.querySelector(`#${id}`)!!;
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
|
||||
function Transition(props: SlideProps) {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <Slide {...props} direction="up" />;
|
||||
}
|
||||
|
||||
interface IToastProps{
|
||||
message: string
|
||||
severity: Severity
|
||||
}
|
||||
|
||||
function Toast(props: IToastProps) {
|
||||
const { message, severity } = props;
|
||||
const [open, setOpen] = React.useState(true);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
autoHideDuration={3000}
|
||||
TransitionComponent={Transition}
|
||||
message="I love snacks"
|
||||
>
|
||||
<MuiAlert elevation={6} variant="filled" onClose={handleClose} severity={severity}>
|
||||
{message}
|
||||
</MuiAlert>
|
||||
</Snackbar>
|
||||
);
|
||||
}
|
||||
|
||||
export default function makeToast(message: string, severity: Severity) {
|
||||
const id = Math.floor(Math.random() * 1000);
|
||||
const container = document.createElement('div');
|
||||
container.id = `alert-${id}`;
|
||||
|
||||
document.body.appendChild(container);
|
||||
|
||||
ReactDOM.render(<Toast message={message} severity={severity} />, container);
|
||||
|
||||
setTimeout(() => removeToast(container.id), 3500);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardActionArea from '@material-ui/core/CardActionArea';
|
||||
import CardMedia from '@material-ui/core/CardMedia';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
},
|
||||
wrapper: {
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
},
|
||||
gradient: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(to bottom, transparent, #000000)',
|
||||
opacity: 0.5,
|
||||
},
|
||||
title: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
padding: '0.5em',
|
||||
color: 'white',
|
||||
},
|
||||
image: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
manga: IMangaCard
|
||||
}
|
||||
const AnimeCard = React.forwardRef((props: IProps, ref) => {
|
||||
const {
|
||||
manga: {
|
||||
id, title, thumbnailUrl,
|
||||
},
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
return (
|
||||
<Grid item xs={6} sm={4} md={3} lg={2}>
|
||||
<Link to={`/anime/${id}/`}>
|
||||
<Card className={classes.root} ref={ref}>
|
||||
<CardActionArea>
|
||||
<div className={classes.wrapper}>
|
||||
<CardMedia
|
||||
className={classes.image}
|
||||
component="img"
|
||||
alt={title}
|
||||
image={serverAddress + thumbnailUrl}
|
||||
title={title}
|
||||
/>
|
||||
<div className={classes.gradient} />
|
||||
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
|
||||
</div>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Link>
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
|
||||
export default AnimeCard;
|
||||
@@ -1,257 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 { makeStyles } from '@material-ui/core';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { Theme } from '@material-ui/core/styles';
|
||||
import FavoriteIcon from '@material-ui/icons/Favorite';
|
||||
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
|
||||
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||
import PublicIcon from '@material-ui/icons/Public';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import NavbarContext from 'context/NavbarContext';
|
||||
import client from 'util/client';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
import CategorySelect from 'components/manga/CategorySelect';
|
||||
|
||||
const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
position: 'sticky',
|
||||
top: '64px',
|
||||
left: '0px',
|
||||
width: '50vw',
|
||||
height: 'calc(100vh - 64px)',
|
||||
alignSelf: 'flex-start',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
top: {
|
||||
padding: '10px',
|
||||
// [theme.breakpoints.up('md')]: {
|
||||
// minWidth: '50%',
|
||||
// },
|
||||
},
|
||||
leftRight: {
|
||||
display: 'flex',
|
||||
},
|
||||
leftSide: {
|
||||
'& img': {
|
||||
borderRadius: 4,
|
||||
maxWidth: '100%',
|
||||
minWidth: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
maxWidth: '50%',
|
||||
// [theme.breakpoints.up('md')]: {
|
||||
// minWidth: '100px',
|
||||
// },
|
||||
},
|
||||
rightSide: {
|
||||
marginLeft: 15,
|
||||
maxWidth: '100%',
|
||||
'& span': {
|
||||
fontWeight: '400',
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
fontSize: '1.3em',
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
'& button': {
|
||||
color: inLibrary === 'In Library' ? '#2196f3' : 'inherit',
|
||||
},
|
||||
'& span': {
|
||||
display: 'block',
|
||||
fontSize: '0.85em',
|
||||
},
|
||||
'& a': {
|
||||
textDecoration: 'none',
|
||||
color: '#858585',
|
||||
'& button': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
},
|
||||
bottom: {
|
||||
paddingLeft: '10px',
|
||||
paddingRight: '10px',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
fontSize: '1.2em',
|
||||
// maxWidth: '50%',
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
fontSize: '1.3em',
|
||||
},
|
||||
},
|
||||
description: {
|
||||
'& h4': {
|
||||
marginTop: '1em',
|
||||
marginBottom: 0,
|
||||
},
|
||||
'& p': {
|
||||
textAlign: 'justify',
|
||||
textJustify: 'inter-word',
|
||||
},
|
||||
},
|
||||
genre: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
'& h5': {
|
||||
border: '2px solid #2196f3',
|
||||
borderRadius: '1.13em',
|
||||
marginRight: '1em',
|
||||
marginTop: 0,
|
||||
marginBottom: '10px',
|
||||
padding: '0.3em',
|
||||
color: '#2196f3',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps{
|
||||
manga: IManga
|
||||
}
|
||||
|
||||
function getSourceName(source: ISource) {
|
||||
if (source.name !== null) {
|
||||
return `${source.name} (${source.lang.toLocaleUpperCase()})`;
|
||||
}
|
||||
return source.id;
|
||||
}
|
||||
|
||||
function getValueOrUnknown(val: string) {
|
||||
return val || 'UNKNOWN';
|
||||
}
|
||||
|
||||
export default function AnimeDetails(props: IProps) {
|
||||
const { setAction } = useContext(NavbarContext);
|
||||
|
||||
const { manga } = props;
|
||||
if (manga.genre == null) {
|
||||
manga.genre = '';
|
||||
}
|
||||
const [inLibrary, setInLibrary] = useState<string>(
|
||||
manga.inLibrary ? 'In Library' : 'Add To Library',
|
||||
);
|
||||
|
||||
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (inLibrary === 'In Library') {
|
||||
setAction(
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => setCategoryDialogOpen(true)}
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
<CategorySelect
|
||||
open={categoryDialogOpen}
|
||||
setOpen={setCategoryDialogOpen}
|
||||
mangaId={manga.id}
|
||||
/>
|
||||
</>,
|
||||
|
||||
);
|
||||
} else { setAction(<></>); }
|
||||
}, [inLibrary, categoryDialogOpen]);
|
||||
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
const classes = useStyles(inLibrary)();
|
||||
|
||||
function addToLibrary() {
|
||||
// setInLibrary('adding');
|
||||
client.get(`/api/v1/anime/anime/${manga.id}/library/`).then(() => {
|
||||
setInLibrary('In Library');
|
||||
});
|
||||
}
|
||||
|
||||
function removeFromLibrary() {
|
||||
// setInLibrary('removing');
|
||||
client.delete(`/api/v1/anime/anime/${manga.id}/library/`).then(() => {
|
||||
setInLibrary('Add To Library');
|
||||
});
|
||||
}
|
||||
|
||||
function handleButtonClick() {
|
||||
if (inLibrary === 'Add To Library') {
|
||||
addToLibrary();
|
||||
} else {
|
||||
removeFromLibrary();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.top}>
|
||||
<div className={classes.leftRight}>
|
||||
<div className={classes.leftSide}>
|
||||
<img src={`${serverAddress}${manga.thumbnailUrl}?x=${Math.random()}`} alt="Manga Thumbnail" />
|
||||
</div>
|
||||
<div className={classes.rightSide}>
|
||||
<h1>
|
||||
{manga.title}
|
||||
</h1>
|
||||
<h3>
|
||||
Author:
|
||||
{' '}
|
||||
<span>{getValueOrUnknown(manga.author)}</span>
|
||||
</h3>
|
||||
<h3>
|
||||
Artist:
|
||||
{' '}
|
||||
<span>{getValueOrUnknown(manga.artist)}</span>
|
||||
</h3>
|
||||
<h3>
|
||||
Status:
|
||||
{' '}
|
||||
{manga.status}
|
||||
</h3>
|
||||
<h3>
|
||||
Source:
|
||||
{' '}
|
||||
{getSourceName(manga.source)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.buttons}>
|
||||
<div>
|
||||
<IconButton onClick={() => handleButtonClick()}>
|
||||
{inLibrary === 'In Library' && <FavoriteIcon />}
|
||||
{inLibrary !== 'In Library' && <FavoriteBorderIcon />}
|
||||
<span>{inLibrary}</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
{ /* eslint-disable-next-line react/jsx-no-target-blank */ }
|
||||
<a href={manga.url} target="_blank">
|
||||
<IconButton>
|
||||
<PublicIcon />
|
||||
<span>Open Site</span>
|
||||
</IconButton>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.bottom}>
|
||||
<div className={classes.description}>
|
||||
<h4>About</h4>
|
||||
<p>{manga.description}</p>
|
||||
</div>
|
||||
<div className={classes.genre}>
|
||||
{manga.genre.split(', ').map((g) => <h5 key={g}>{g}</h5>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 React, { useEffect, useRef } from 'react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import AnimeCard from './AnimeCard';
|
||||
|
||||
interface IProps{
|
||||
mangas: IMangaCard[]
|
||||
message?: string
|
||||
hasNextPage: boolean
|
||||
lastPageNum: number
|
||||
setLastPageNum: (lastPageNum: number) => void
|
||||
}
|
||||
|
||||
export default function AnimeGrid(props: IProps) {
|
||||
const {
|
||||
mangas, message, hasNextPage, lastPageNum, setLastPageNum,
|
||||
} = props;
|
||||
let mapped;
|
||||
const lastManga = useRef<HTMLInputElement>();
|
||||
|
||||
const scrollHandler = () => {
|
||||
if (lastManga.current) {
|
||||
const rect = lastManga.current.getBoundingClientRect();
|
||||
if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) {
|
||||
setLastPageNum(lastPageNum + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', scrollHandler, true);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollHandler, true);
|
||||
};
|
||||
}, [hasNextPage, mangas]);
|
||||
|
||||
if (mangas.length === 0) {
|
||||
mapped = <h3>{message}</h3>;
|
||||
} else {
|
||||
mapped = mangas.map((it, idx) => {
|
||||
if (idx === mangas.length - 1) {
|
||||
return <AnimeCard manga={it} ref={lastManga} />;
|
||||
}
|
||||
return <AnimeCard manga={it} />;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
|
||||
{mapped}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
AnimeGrid.defaultProps = {
|
||||
message: 'loading...',
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user