commit
cbc9a26b7d
@ -0,0 +1,115 @@ |
|||||||
|
### JetBrains template |
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider |
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 |
||||||
|
|
||||||
|
# User-specific stuff |
||||||
|
.idea/**/workspace.xml |
||||||
|
.idea/**/tasks.xml |
||||||
|
.idea/**/usage.statistics.xml |
||||||
|
.idea/**/dictionaries |
||||||
|
.idea/**/shelf |
||||||
|
|
||||||
|
# Generated files |
||||||
|
.idea/**/contentModel.xml |
||||||
|
|
||||||
|
# Sensitive or high-churn files |
||||||
|
.idea/**/dataSources/ |
||||||
|
.idea/**/dataSources.ids |
||||||
|
.idea/**/dataSources.local.xml |
||||||
|
.idea/**/sqlDataSources.xml |
||||||
|
.idea/**/dynamic.xml |
||||||
|
.idea/**/uiDesigner.xml |
||||||
|
.idea/**/dbnavigator.xml |
||||||
|
|
||||||
|
# Gradle |
||||||
|
.idea/**/gradle.xml |
||||||
|
.idea/**/libraries |
||||||
|
|
||||||
|
# Gradle and Maven with auto-import |
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files, |
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using |
||||||
|
# auto-import. |
||||||
|
# .idea/artifacts |
||||||
|
# .idea/compiler.xml |
||||||
|
# .idea/jarRepositories.xml |
||||||
|
# .idea/modules.xml |
||||||
|
# .idea/*.iml |
||||||
|
# .idea/modules |
||||||
|
# *.iml |
||||||
|
# *.ipr |
||||||
|
|
||||||
|
# CMake |
||||||
|
cmake-build-*/ |
||||||
|
|
||||||
|
# Mongo Explorer plugin |
||||||
|
.idea/**/mongoSettings.xml |
||||||
|
|
||||||
|
# File-based project format |
||||||
|
*.iws |
||||||
|
|
||||||
|
# IntelliJ |
||||||
|
out/ |
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin |
||||||
|
.idea_modules/ |
||||||
|
|
||||||
|
# JIRA plugin |
||||||
|
atlassian-ide-plugin.xml |
||||||
|
|
||||||
|
# Cursive Clojure plugin |
||||||
|
.idea/replstate.xml |
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ) |
||||||
|
com_crashlytics_export_strings.xml |
||||||
|
crashlytics.properties |
||||||
|
crashlytics-build.properties |
||||||
|
fabric.properties |
||||||
|
|
||||||
|
# Editor-based Rest Client |
||||||
|
.idea/httpRequests |
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file |
||||||
|
.idea/caches/build_file_checksums.ser |
||||||
|
|
||||||
|
### Gradle template |
||||||
|
.gradle |
||||||
|
**/build/ |
||||||
|
!src/**/build/ |
||||||
|
|
||||||
|
# Ignore Gradle GUI config |
||||||
|
gradle-app.setting |
||||||
|
|
||||||
|
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) |
||||||
|
!gradle-wrapper.jar |
||||||
|
|
||||||
|
# Cache of project |
||||||
|
.gradletasknamecache |
||||||
|
|
||||||
|
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 |
||||||
|
# gradle/wrapper/gradle-wrapper.properties |
||||||
|
|
||||||
|
### Kotlin template |
||||||
|
# Compiled class file |
||||||
|
*.class |
||||||
|
|
||||||
|
# Log file |
||||||
|
*.log |
||||||
|
|
||||||
|
# BlueJ files |
||||||
|
*.ctxt |
||||||
|
|
||||||
|
# Mobile Tools for Java (J2ME) |
||||||
|
.mtj.tmp/ |
||||||
|
|
||||||
|
# Package Files # |
||||||
|
*.jar |
||||||
|
*.war |
||||||
|
*.nar |
||||||
|
*.ear |
||||||
|
*.zip |
||||||
|
*.tar.gz |
||||||
|
*.rar |
||||||
|
|
||||||
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml |
||||||
|
hs_err_pid* |
||||||
|
.idea |
@ -0,0 +1,3 @@ |
|||||||
|
# 爬取[takeshobo](https://gammaplus.takeshobo.co.jp/) 网站上的漫画 |
||||||
|
## 服务端:[ktor](https://ktor.io/) |
||||||
|
## 客户端:[Kotlin/JS+React](https://kotlinlang.org/docs/js-get-started.html) |
@ -0,0 +1,125 @@ |
|||||||
|
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack |
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile |
||||||
|
|
||||||
|
plugins { |
||||||
|
val kotlin_version="1.5.20" |
||||||
|
kotlin("multiplatform") version kotlin_version |
||||||
|
kotlin("plugin.serialization") version kotlin_version |
||||||
|
application |
||||||
|
} |
||||||
|
|
||||||
|
val serialization_version:String by project |
||||||
|
val kotlin_version:String by project |
||||||
|
val logback_version:String by project |
||||||
|
val ktor_version:String by project |
||||||
|
val `kotlin-react-version`:String by project |
||||||
|
val `kotlin-styled-version`:String by project |
||||||
|
group = "jp.co.takeshobo" |
||||||
|
version = "1.0-SNAPSHOT" |
||||||
|
|
||||||
|
repositories { |
||||||
|
jcenter() |
||||||
|
mavenCentral() |
||||||
|
maven { url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers") } |
||||||
|
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven") } |
||||||
|
} |
||||||
|
|
||||||
|
kotlin { |
||||||
|
jvm { |
||||||
|
compilations.all { |
||||||
|
kotlinOptions.jvmTarget = "1.8" |
||||||
|
} |
||||||
|
testRuns["test"].executionTask.configure { |
||||||
|
useJUnit() |
||||||
|
} |
||||||
|
withJava() |
||||||
|
} |
||||||
|
js(LEGACY) { |
||||||
|
binaries.executable() |
||||||
|
browser { |
||||||
|
commonWebpackConfig { |
||||||
|
cssSupport.enabled = true |
||||||
|
} |
||||||
|
testTask { |
||||||
|
useKarma { |
||||||
|
useChrome() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
sourceSets { |
||||||
|
val commonMain by getting{ |
||||||
|
dependencies { |
||||||
|
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version") |
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version") |
||||||
|
} |
||||||
|
} |
||||||
|
val commonTest by getting { |
||||||
|
dependencies { |
||||||
|
implementation(kotlin("test")) |
||||||
|
} |
||||||
|
} |
||||||
|
val jvmMain by getting { |
||||||
|
dependencies { |
||||||
|
implementation("io.ktor:ktor-server-netty:$ktor_version") |
||||||
|
implementation("io.ktor:ktor-client-core:$ktor_version") |
||||||
|
implementation("io.ktor:ktor-client-cio:$ktor_version") |
||||||
|
implementation("io.ktor:ktor-html-builder:$ktor_version") |
||||||
|
implementation("io.ktor:ktor-serialization:$ktor_version") |
||||||
|
implementation("io.ktor:ktor-websockets:$ktor_version") |
||||||
|
|
||||||
|
implementation("ch.qos.logback:logback-classic:$logback_version") |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
val jvmTest by getting{ |
||||||
|
dependencies { |
||||||
|
implementation("io.ktor:ktor-server-tests:$ktor_version") |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
val jsMain by getting { |
||||||
|
dependencies { |
||||||
|
implementation("org.jetbrains.kotlin-wrappers:kotlin-react:${`kotlin-react-version`}") |
||||||
|
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:${`kotlin-react-version`}") |
||||||
|
implementation("org.jetbrains.kotlin-wrappers:kotlin-styled:${`kotlin-styled-version`}") |
||||||
|
} |
||||||
|
} |
||||||
|
val jsTest by getting |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
application{ |
||||||
|
mainClass.set("ServerKt") |
||||||
|
} |
||||||
|
|
||||||
|
tasks { |
||||||
|
"run"(JavaExec::class) { |
||||||
|
environment("CHROME_BIN","F:\\ChromeUpdater\\chrome.exe") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
tasks.getByName<KotlinWebpack>("jsBrowserProductionWebpack") { |
||||||
|
outputFileName = "js.js" |
||||||
|
} |
||||||
|
|
||||||
|
tasks.getByName<Jar>("jvmJar") { |
||||||
|
dependsOn(tasks.getByName("jsBrowserProductionWebpack")) |
||||||
|
val jsBrowserProductionWebpack = tasks.getByName<KotlinWebpack>("jsBrowserProductionWebpack") |
||||||
|
from(File(jsBrowserProductionWebpack.destinationDirectory, jsBrowserProductionWebpack.outputFileName)) |
||||||
|
} |
||||||
|
|
||||||
|
tasks.getByName<JavaExec>("run") { |
||||||
|
dependsOn(tasks.getByName<Jar>("jvmJar")) |
||||||
|
classpath(tasks.getByName<Jar>("jvmJar")) |
||||||
|
} |
||||||
|
|
||||||
|
tasks.withType<KotlinCompile> { |
||||||
|
kotlinOptions { |
||||||
|
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
tasks.withType<ProcessResources>{ |
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
kotlin.code.style=official |
||||||
|
kotlin.mpp.enableGranularSourceSetsMetadata=true |
||||||
|
kotlin.native.enableDependencyPropagation=false |
||||||
|
kotlin.js.generate.executable.default=false |
||||||
|
kotlin.mpp.stability.nowarn=true |
||||||
|
serialization_version=1.2.1 |
||||||
|
kotlin_version=1.5.20 |
||||||
|
logback_version=1.2.3 |
||||||
|
ktor_version=1.6.0 |
||||||
|
kotlin-react-version=17.0.2-pre.213-kotlin-1.5.20 |
||||||
|
kotlin-styled-version=5.3.0-pre.213-kotlin-1.5.20 |
||||||
|
systemProp.file.encoding=UTF-8 |
@ -0,0 +1,5 @@ |
|||||||
|
distributionBase=GRADLE_USER_HOME |
||||||
|
distributionPath=wrapper/dists |
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip |
||||||
|
zipStoreBase=GRADLE_USER_HOME |
||||||
|
zipStorePath=wrapper/dists |
@ -0,0 +1,185 @@ |
|||||||
|
#!/usr/bin/env sh |
||||||
|
|
||||||
|
# |
||||||
|
# Copyright 2015 the original author or authors. |
||||||
|
# |
||||||
|
# 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 |
||||||
|
# |
||||||
|
# https://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. |
||||||
|
# |
||||||
|
|
||||||
|
############################################################################## |
||||||
|
## |
||||||
|
## Gradle start up script for UN*X |
||||||
|
## |
||||||
|
############################################################################## |
||||||
|
|
||||||
|
# Attempt to set APP_HOME |
||||||
|
# Resolve links: $0 may be a link |
||||||
|
PRG="$0" |
||||||
|
# Need this for relative symlinks. |
||||||
|
while [ -h "$PRG" ] ; do |
||||||
|
ls=`ls -ld "$PRG"` |
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'` |
||||||
|
if expr "$link" : '/.*' > /dev/null; then |
||||||
|
PRG="$link" |
||||||
|
else |
||||||
|
PRG=`dirname "$PRG"`"/$link" |
||||||
|
fi |
||||||
|
done |
||||||
|
SAVED="`pwd`" |
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null |
||||||
|
APP_HOME="`pwd -P`" |
||||||
|
cd "$SAVED" >/dev/null |
||||||
|
|
||||||
|
APP_NAME="Gradle" |
||||||
|
APP_BASE_NAME=`basename "$0"` |
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' |
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value. |
||||||
|
MAX_FD="maximum" |
||||||
|
|
||||||
|
warn () { |
||||||
|
echo "$*" |
||||||
|
} |
||||||
|
|
||||||
|
die () { |
||||||
|
echo |
||||||
|
echo "$*" |
||||||
|
echo |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false'). |
||||||
|
cygwin=false |
||||||
|
msys=false |
||||||
|
darwin=false |
||||||
|
nonstop=false |
||||||
|
case "`uname`" in |
||||||
|
CYGWIN* ) |
||||||
|
cygwin=true |
||||||
|
;; |
||||||
|
Darwin* ) |
||||||
|
darwin=true |
||||||
|
;; |
||||||
|
MINGW* ) |
||||||
|
msys=true |
||||||
|
;; |
||||||
|
NONSTOP* ) |
||||||
|
nonstop=true |
||||||
|
;; |
||||||
|
esac |
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar |
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM. |
||||||
|
if [ -n "$JAVA_HOME" ] ; then |
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then |
||||||
|
# IBM's JDK on AIX uses strange locations for the executables |
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java" |
||||||
|
else |
||||||
|
JAVACMD="$JAVA_HOME/bin/java" |
||||||
|
fi |
||||||
|
if [ ! -x "$JAVACMD" ] ; then |
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME |
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the |
||||||
|
location of your Java installation." |
||||||
|
fi |
||||||
|
else |
||||||
|
JAVACMD="java" |
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the |
||||||
|
location of your Java installation." |
||||||
|
fi |
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can. |
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then |
||||||
|
MAX_FD_LIMIT=`ulimit -H -n` |
||||||
|
if [ $? -eq 0 ] ; then |
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then |
||||||
|
MAX_FD="$MAX_FD_LIMIT" |
||||||
|
fi |
||||||
|
ulimit -n $MAX_FD |
||||||
|
if [ $? -ne 0 ] ; then |
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD" |
||||||
|
fi |
||||||
|
else |
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" |
||||||
|
fi |
||||||
|
fi |
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock |
||||||
|
if $darwin; then |
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" |
||||||
|
fi |
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java |
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then |
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"` |
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` |
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"` |
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath |
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` |
||||||
|
SEP="" |
||||||
|
for dir in $ROOTDIRSRAW ; do |
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir" |
||||||
|
SEP="|" |
||||||
|
done |
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))" |
||||||
|
# Add a user-defined pattern to the cygpath arguments |
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then |
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" |
||||||
|
fi |
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh |
||||||
|
i=0 |
||||||
|
for arg in "$@" ; do |
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` |
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option |
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition |
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` |
||||||
|
else |
||||||
|
eval `echo args$i`="\"$arg\"" |
||||||
|
fi |
||||||
|
i=`expr $i + 1` |
||||||
|
done |
||||||
|
case $i in |
||||||
|
0) set -- ;; |
||||||
|
1) set -- "$args0" ;; |
||||||
|
2) set -- "$args0" "$args1" ;; |
||||||
|
3) set -- "$args0" "$args1" "$args2" ;; |
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;; |
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; |
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; |
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; |
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; |
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; |
||||||
|
esac |
||||||
|
fi |
||||||
|
|
||||||
|
# Escape application args |
||||||
|
save () { |
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done |
||||||
|
echo " " |
||||||
|
} |
||||||
|
APP_ARGS=`save "$@"` |
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules |
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" |
||||||
|
|
||||||
|
exec "$JAVACMD" "$@" |
@ -0,0 +1,89 @@ |
|||||||
|
@rem |
||||||
|
@rem Copyright 2015 the original author or authors. |
||||||
|
@rem |
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
@rem you may not use this file except in compliance with the License. |
||||||
|
@rem You may obtain a copy of the License at |
||||||
|
@rem |
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
@rem |
||||||
|
@rem Unless required by applicable law or agreed to in writing, software |
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
@rem See the License for the specific language governing permissions and |
||||||
|
@rem limitations under the License. |
||||||
|
@rem |
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off |
||||||
|
@rem ########################################################################## |
||||||
|
@rem |
||||||
|
@rem Gradle startup script for Windows |
||||||
|
@rem |
||||||
|
@rem ########################################################################## |
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell |
||||||
|
if "%OS%"=="Windows_NT" setlocal |
||||||
|
|
||||||
|
set DIRNAME=%~dp0 |
||||||
|
if "%DIRNAME%" == "" set DIRNAME=. |
||||||
|
set APP_BASE_NAME=%~n0 |
||||||
|
set APP_HOME=%DIRNAME% |
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter. |
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi |
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" |
||||||
|
|
||||||
|
@rem Find java.exe |
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome |
||||||
|
|
||||||
|
set JAVA_EXE=java.exe |
||||||
|
%JAVA_EXE% -version >NUL 2>&1 |
||||||
|
if "%ERRORLEVEL%" == "0" goto execute |
||||||
|
|
||||||
|
echo. |
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |
||||||
|
echo. |
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the |
||||||
|
echo location of your Java installation. |
||||||
|
|
||||||
|
goto fail |
||||||
|
|
||||||
|
:findJavaFromJavaHome |
||||||
|
set JAVA_HOME=%JAVA_HOME:"=% |
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe |
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute |
||||||
|
|
||||||
|
echo. |
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% |
||||||
|
echo. |
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the |
||||||
|
echo location of your Java installation. |
||||||
|
|
||||||
|
goto fail |
||||||
|
|
||||||
|
:execute |
||||||
|
@rem Setup the command line |
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar |
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle |
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* |
||||||
|
|
||||||
|
:end |
||||||
|
@rem End local scope for the variables with windows NT shell |
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd |
||||||
|
|
||||||
|
:fail |
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of |
||||||
|
rem the _cmd.exe /c_ return code! |
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 |
||||||
|
exit /b 1 |
||||||
|
|
||||||
|
:mainEnd |
||||||
|
if "%OS%"=="Windows_NT" endlocal |
||||||
|
|
||||||
|
:omega |
@ -0,0 +1,3 @@ |
|||||||
|
|
||||||
|
rootProject.name = "manga" |
||||||
|
|
@ -0,0 +1,37 @@ |
|||||||
|
import kotlinx.serialization.Contextual |
||||||
|
import kotlinx.serialization.SerialName |
||||||
|
import kotlinx.serialization.Serializable |
||||||
|
|
||||||
|
@Serializable |
||||||
|
data class i(val src: String, val width: Int, val height: Int) |
||||||
|
|
||||||
|
@Serializable |
||||||
|
data class resources(val i: i) |
||||||
|
|
||||||
|
@Serializable |
||||||
|
data class area(val href: String, val left: Int, val top: Int, val right: Int, val bottom: Int) |
||||||
|
|
||||||
|
@Serializable |
||||||
|
data class view(val width: Int, val height: Int, val coords: List<String>, val areas: List<area>? = null) |
||||||
|
|
||||||
|
@Serializable |
||||||
|
data class t(@SerialName("ptimg-version") val ptimg_version: Int, |
||||||
|
val resources: resources, val views: List<view>) |
||||||
|
|
||||||
|
@Serializable |
||||||
|
data class ApiResponse<T>(val message:String,@Contextual val body:T?=null) |
||||||
|
|
||||||
|
@Serializable |
||||||
|
data class MessageResponse(val message: String) |
||||||
|
|
||||||
|
@Serializable |
||||||
|
data class UrlResult(val originImagePath:String,val serverImagePath:String, val t:t) |
||||||
|
|
||||||
|
@Serializable |
||||||
|
data class ParseTask(val total:Int,val finish:Int,val percentage:Float) |
||||||
|
|
||||||
|
data class UrlParam(val url:String,val html:String) |
||||||
|
|
||||||
|
const val websocketPath="/webSocket" |
||||||
|
|
||||||
|
const val websiteTitle="朴实无华的漫画解析工具" |
@ -0,0 +1,21 @@ |
|||||||
|
import react.dom.render |
||||||
|
import kotlinx.browser.document |
||||||
|
import kotlinx.browser.window |
||||||
|
import org.w3c.dom.WebSocket |
||||||
|
import org.w3c.dom.events.Event |
||||||
|
|
||||||
|
fun main() { |
||||||
|
window.onload = { |
||||||
|
val webSocket=WebSocket("ws://localhost:8080${websocketPath}") |
||||||
|
webSocket.onopen={event: Event -> console.info("打开连接:${event}") } |
||||||
|
webSocket.onclose={event: Event -> console.info("关闭连接:${event}") } |
||||||
|
webSocket.onerror={event: Event -> console.error("发生错误:${event}") } |
||||||
|
render(document.getElementById("root")) { |
||||||
|
child(Welcome::class) { |
||||||
|
attrs { |
||||||
|
this.webSocket=webSocket |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,308 @@ |
|||||||
|
import kotlinext.js.getOwnPropertyNames |
||||||
|
import kotlinx.browser.document |
||||||
|
import kotlinx.css.head |
||||||
|
import org.khronos.webgl.Uint8Array |
||||||
|
import org.khronos.webgl.set |
||||||
|
import org.w3c.dom.CanvasRenderingContext2D |
||||||
|
import org.w3c.dom.HTMLCanvasElement |
||||||
|
import org.w3c.dom.Image |
||||||
|
import org.w3c.dom.events.Event |
||||||
|
import org.w3c.dom.url.URL |
||||||
|
import org.w3c.files.Blob |
||||||
|
import org.w3c.files.BlobPropertyBag |
||||||
|
import kotlin.js.Promise |
||||||
|
import kotlin.math.ceil |
||||||
|
import kotlin.math.max |
||||||
|
import kotlin.math.min |
||||||
|
import kotlin.math.round |
||||||
|
|
||||||
|
|
||||||
|
data class un(val width: Int, val height: Int) |
||||||
|
|
||||||
|
|
||||||
|
data class coord( |
||||||
|
val resid: String, |
||||||
|
val xsrc: Int, |
||||||
|
val ysrc: Int, |
||||||
|
val width: Int, |
||||||
|
val height: Int, |
||||||
|
val xdest: Int, |
||||||
|
val ydest: Int |
||||||
|
) |
||||||
|
|
||||||
|
data class formatview(val width: Int, val height: Int, val coords: List<coord>, val areas: List<area>? = null) |
||||||
|
|
||||||
|
data class transfer(val index:Int,val coords: List<coord>) |
||||||
|
|
||||||
|
data class n(val width: Int,val height: Int,val transfers:List<transfer>) |
||||||
|
|
||||||
|
data class N( |
||||||
|
val left: Int, val top: Int, val width: Int, val height: Int, |
||||||
|
val bottom: Int = top + height, val right: Int = left + width |
||||||
|
) |
||||||
|
|
||||||
|
object p { |
||||||
|
//speedbinb.js?dmy=016301:formatted:3998 |
||||||
|
fun Rectangle(t: Int, i: Int, n: Int, r: Int): N { |
||||||
|
return N(left = t, top = i, width = n, height = r) |
||||||
|
} |
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:3966 |
||||||
|
fun intersect(t:N,i:N):N?{ |
||||||
|
val n = t.left |
||||||
|
val r = t.left + t.width |
||||||
|
val e = t.top |
||||||
|
val s = t.top + t.height |
||||||
|
val h = i.left |
||||||
|
val u = i.left + i.width |
||||||
|
val o = i.top |
||||||
|
val a = i.top + i.height |
||||||
|
if (n < u && h < r && e < a && o < s) { |
||||||
|
val f = max(n, h) |
||||||
|
val c = max(e, o) |
||||||
|
return N(f,c,min(r, u) - f,min(s, a) - c) |
||||||
|
} |
||||||
|
return null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class e(val Xs: Int = 3, private val Ws: Int = 8, private val Ys: Int = 4, val un: un) { |
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:8848 |
||||||
|
fun Us(t: Int): N { |
||||||
|
val i = this.un.height |
||||||
|
val n = ceil( (i + this.Ys * (this.Xs - 1)) / this.Ws.toDouble()).toInt() |
||||||
|
val r = ceil(t * n / this.Xs.toDouble()).toInt() * this.Ws |
||||||
|
val e = ceil((t + 1) * n / this.Xs.toDouble()).toInt() * this.Ws |
||||||
|
val s = n * this.Ws |
||||||
|
val h = r * i / s |
||||||
|
val u = e * i / s |
||||||
|
val o = e - r |
||||||
|
val a = if (1 == this.Xs) 0 else round(h + (u - h - o) * t / (this.Xs - 1).toDouble()).toInt() |
||||||
|
return p.Rectangle(t = 0, i = a, n = this.un.width, r = o) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
object d { |
||||||
|
|
||||||
|
data class e(val resources: resources,val views: List<formatview>) |
||||||
|
|
||||||
|
lateinit var urlResult: UrlResult |
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:8699 |
||||||
|
private fun RS(t: resources, i: String): coord { |
||||||
|
val n = (Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire(i) |
||||||
|
?: throw IllegalArgumentException("Invalid format for Image Transfer : $i")).groupValues |
||||||
|
val r = n[1] |
||||||
|
if (r !in t.getOwnPropertyNames()) |
||||||
|
throw IllegalArgumentException("resid $r not found.") |
||||||
|
return coord( |
||||||
|
resid = r, xsrc = n[2].toInt(10), ysrc = n[3].toInt(10), width = n[4].toInt(10), |
||||||
|
height = n[5].toInt(10), xdest = n[6].toInt(10), ydest = n[7].toInt(10) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:8632 |
||||||
|
private fun FS():e { |
||||||
|
val t=urlResult.t |
||||||
|
val n = |
||||||
|
t.resources.copy(i = t.resources.i.copy(src =urlResult.originImagePath)) |
||||||
|
return e(resources = n,views = t.views.map { it -> |
||||||
|
formatview(width = it.width, height = it.height, coords = it.coords.map { |
||||||
|
RS(t = n, i = it) |
||||||
|
}, areas = it.areas) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:8604 |
||||||
|
fun Gs():n{ |
||||||
|
val e= FS() |
||||||
|
val u=e.views[0] |
||||||
|
return n(width = u.width,height = u.height,transfers = listOf(transfer(index = 0,coords = u.coords))) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
class ImageLoader(val urlResult: UrlResult){ |
||||||
|
|
||||||
|
private val canvasHtml=createCanvas() |
||||||
|
|
||||||
|
private var imageHeight=0.0 |
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:8766 |
||||||
|
private fun callback(t:un): List<n> { |
||||||
|
d.urlResult=urlResult |
||||||
|
val n=d.Gs() |
||||||
|
// console.info("Gs:") |
||||||
|
// console.info(n) |
||||||
|
val u=e(un=un(width = n.width,height = n.height)) |
||||||
|
val s=n.transfers[0].coords |
||||||
|
val h= mutableListOf<n>() |
||||||
|
repeat(u.Xs){ |
||||||
|
val r=u.Us(t=it) |
||||||
|
// console.info("r:") |
||||||
|
// console.info(r) |
||||||
|
val e= mutableListOf<coord>() |
||||||
|
s.forEach { |
||||||
|
t-> |
||||||
|
// console.info("-------------------") |
||||||
|
// console.info("t:") |
||||||
|
// console.info(t) |
||||||
|
val _i=p.Rectangle(t=t.xdest,i=t.ydest,n=t.width,r=t.height) |
||||||
|
// console.info("p.Rectangle(i):") |
||||||
|
// console.info(_i) |
||||||
|
p.intersect(t=r,i=_i)?.let { |
||||||
|
n-> |
||||||
|
// console.info("p.Rectangle.intersect(n):") |
||||||
|
// console.info(n) |
||||||
|
// console.info("-------------------") |
||||||
|
e.add(coord(resid = t.resid, |
||||||
|
xsrc = t.xsrc+(n.left-t.xdest), |
||||||
|
ysrc= t.ysrc + (n.top - t.ydest), |
||||||
|
width= n.width, |
||||||
|
height= n.height, |
||||||
|
xdest= n.left - r.left, |
||||||
|
ydest = n.top - r.top)) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
// console.info("e.size=${e.size}") |
||||||
|
h.add(n(width = r.width,height = r.height,transfers = listOf(transfer(index = 0,coords = e)))) |
||||||
|
} |
||||||
|
// console.info(h) |
||||||
|
return h.toList() |
||||||
|
} |
||||||
|
|
||||||
|
private fun createCanvas(): HTMLCanvasElement { |
||||||
|
return document.createElement("canvas") as HTMLCanvasElement |
||||||
|
} |
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:7976 |
||||||
|
private fun us(t:n,image:Image){ |
||||||
|
console.info("开始绘制漫画页") |
||||||
|
val canvas:HTMLCanvasElement= createCanvas() |
||||||
|
canvas.apply { |
||||||
|
width=t.width |
||||||
|
height=t.height |
||||||
|
val ctx:CanvasRenderingContext2D= getContext("2d") as CanvasRenderingContext2D |
||||||
|
ctx.clearRect(0.0, 0.0, width.toDouble(), height.toDouble()) |
||||||
|
t.transfers.forEach { |
||||||
|
i-> |
||||||
|
i.coords.forEach { |
||||||
|
t-> |
||||||
|
ctx.drawImage(image = image,sx=t.xsrc.toDouble(),sy=t.ysrc.toDouble(),sw=t.width.toDouble(),sh=t.height.toDouble(), |
||||||
|
dx=t.xdest.toDouble(),dy=t.ydest.toDouble(),dw=t.width.toDouble(),dh=t.height.toDouble()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
canvasToBlob(t=canvas,{blob: Blob -> |
||||||
|
console.info("blob.size:${blob.size}") |
||||||
|
val i=URL.createObjectURL(blob=blob) |
||||||
|
console.info("漫画URL:${i}") |
||||||
|
Image().apply { |
||||||
|
onload={event: Event -> |
||||||
|
canvasHtml.let { |
||||||
|
canvas-> |
||||||
|
val ctx:CanvasRenderingContext2D= canvas.getContext("2d") as CanvasRenderingContext2D |
||||||
|
ctx.drawImage(this,0.0,imageHeight,naturalWidth.toDouble(),naturalHeight.toDouble().apply { |
||||||
|
imageHeight+=this |
||||||
|
console.info("拼接图片imageHeight:${imageHeight}") |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
src=i |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
fun create(){ |
||||||
|
Image().apply { |
||||||
|
onload={event: Event -> |
||||||
|
console.info("拼接图片大小naturalWidth:${naturalWidth},naturalHeight:${naturalHeight}") |
||||||
|
} |
||||||
|
src=canvasHtml.toDataURL() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:3798 |
||||||
|
fun canvasToBlob( |
||||||
|
t:HTMLCanvasElement, callback:(t:Blob)->Unit, n:String="image/jpeg", r: Double =.9){ |
||||||
|
val i=t.toDataURL(type=n,quality = r).split(",")[1] |
||||||
|
console.info("url length:${i.length}") |
||||||
|
val blobTransfer=w(t=i) |
||||||
|
// console.info("w[i]:") |
||||||
|
// console.info(blobTransfer) |
||||||
|
val blob= Blob(blobParts=arrayOf(blobTransfer),options = BlobPropertyBag(type = n)) |
||||||
|
console.info("blob size:${blob.size}") |
||||||
|
callback(blob) |
||||||
|
} |
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:3602 |
||||||
|
fun _m(t: String): Array<Int> { |
||||||
|
val i= arrayOf<Int>() |
||||||
|
repeat(t.length){ |
||||||
|
i[t[it].code]=it |
||||||
|
} |
||||||
|
return i |
||||||
|
} |
||||||
|
|
||||||
|
val m=_m("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") |
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:3608 |
||||||
|
fun w(t:String): Uint8Array { |
||||||
|
val e=t.length |
||||||
|
val s = t.slice(IntRange(e-2,e-1)).split("=").count() - 1 |
||||||
|
val h = 3 * ((e + 3) / 4) - s |
||||||
|
val u = Uint8Array(h) |
||||||
|
var i=0 |
||||||
|
var n=i |
||||||
|
while (n<e){ |
||||||
|
val r=m[t[n].code] shl 18 or (m[t[n + 1].code] shl 12) or (m[t[n + 2].code] shl 6) or m[t[n + 3].code] |
||||||
|
u[i]=(r shr 16 and 255).toByte() |
||||||
|
u[i+1]=(r shr 8 and 255).toByte() |
||||||
|
u[i+2]=(255 and r).toByte() |
||||||
|
i+=3 |
||||||
|
n+=4 |
||||||
|
} |
||||||
|
return u |
||||||
|
} |
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:8020 |
||||||
|
fun rebuild(){ |
||||||
|
hs().then { |
||||||
|
console.info("image(naturalWidth:${it.naturalWidth},naturalHeight=${it.naturalHeight})") |
||||||
|
val f=un(width = it.naturalWidth,height = it.naturalHeight) |
||||||
|
callback(t=f).map { |
||||||
|
t-> |
||||||
|
// val n=t(width = t.width,height = t.height) |
||||||
|
us(t=t,image=it) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//speedbinb.js?dmy=016301:formatted:7949 |
||||||
|
private fun hs(): Promise<Image> { |
||||||
|
return Promise(executor = { resolve, reject -> |
||||||
|
val e=Image() |
||||||
|
// e.crossOrigin="anonymous" |
||||||
|
val t=urlResult.serverImagePath |
||||||
|
e.onload={ |
||||||
|
resolve(e) |
||||||
|
} |
||||||
|
e.onerror = { _: dynamic, _: String, _: Int, _: Int, _: Any? -> |
||||||
|
reject(Error("Failed to load image. : $t")) |
||||||
|
} |
||||||
|
e.onabort = { |
||||||
|
reject(Error("Failed to load image. : $t")) |
||||||
|
} |
||||||
|
e.src=t.apply { console.info("img path:${this}") } |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
@ -0,0 +1,153 @@ |
|||||||
|
import kotlinx.browser.document |
||||||
|
import kotlinx.browser.window |
||||||
|
import kotlinx.css.* |
||||||
|
import kotlinx.html.InputType |
||||||
|
import kotlinx.html.js.onChangeFunction |
||||||
|
import kotlinx.html.js.onClickFunction |
||||||
|
import kotlinx.serialization.decodeFromString |
||||||
|
import kotlinx.serialization.json.Json |
||||||
|
import org.w3c.dom.HTMLInputElement |
||||||
|
import org.w3c.dom.MessageEvent |
||||||
|
import org.w3c.dom.WebSocket |
||||||
|
import org.w3c.dom.events.Event |
||||||
|
import org.w3c.fetch.RequestInit |
||||||
|
import org.w3c.xhr.FormData |
||||||
|
import react.RBuilder |
||||||
|
import react.RComponent |
||||||
|
import react.RProps |
||||||
|
import react.RState |
||||||
|
import react.dom.* |
||||||
|
import styled.css |
||||||
|
import styled.styledDiv |
||||||
|
import styled.styledH1 |
||||||
|
import styled.styledInput |
||||||
|
|
||||||
|
external interface WelcomeProps : RProps { |
||||||
|
var webSocket:WebSocket |
||||||
|
} |
||||||
|
|
||||||
|
data class WelcomeState(var url:String="",var result:String="",var percentage:kotlin.Float=0F) : RState |
||||||
|
|
||||||
|
fun Double.format(digits: Int): String = this.asDynamic().toFixed(digits) |
||||||
|
fun Float.format(digits: Int): String = this.asDynamic().toFixed(digits) |
||||||
|
|
||||||
|
@OptIn(ExperimentalJsExport::class) |
||||||
|
@JsExport |
||||||
|
class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(props) { |
||||||
|
|
||||||
|
init { |
||||||
|
state=WelcomeState() |
||||||
|
props.webSocket.onmessage={messageEvent: MessageEvent -> |
||||||
|
when(val data=messageEvent.data){ |
||||||
|
is String-> { |
||||||
|
if(data.contains("ptimg-version")){ |
||||||
|
val urlResult=Json.decodeFromString<UrlResult>(data) |
||||||
|
console.info("ptimg_version:${urlResult.t.ptimg_version}") |
||||||
|
ImageLoader(urlResult = urlResult).apply { |
||||||
|
rebuild() |
||||||
|
create() |
||||||
|
} |
||||||
|
}else{ |
||||||
|
val task=Json.decodeFromString<ParseTask>(data) |
||||||
|
state.result="解析进度:${task.percentage}%" |
||||||
|
setState(state) |
||||||
|
} |
||||||
|
} |
||||||
|
else->{ |
||||||
|
console.info("unknow data:${data}") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun RBuilder.render() { |
||||||
|
|
||||||
|
styledH1 { |
||||||
|
css{ |
||||||
|
textAlign=TextAlign.center |
||||||
|
} |
||||||
|
+"朴实无华的" |
||||||
|
a { |
||||||
|
attrs { |
||||||
|
href="https://gammaplus.takeshobo.co.jp" |
||||||
|
target="_blank" |
||||||
|
} |
||||||
|
+"漫画" |
||||||
|
} |
||||||
|
+"解析工具" |
||||||
|
} |
||||||
|
|
||||||
|
styledDiv{ |
||||||
|
css{ |
||||||
|
textAlign=TextAlign.center |
||||||
|
} |
||||||
|
styledDiv { |
||||||
|
css { |
||||||
|
width = LinearDimension.fitContent |
||||||
|
margin="0 auto" |
||||||
|
} |
||||||
|
styledInput { |
||||||
|
css { |
||||||
|
width = 100.pct |
||||||
|
} |
||||||
|
attrs{ |
||||||
|
type=InputType.text |
||||||
|
value = state.url |
||||||
|
onChangeFunction = { event -> |
||||||
|
(event.target as HTMLInputElement).let { |
||||||
|
console.info(it.value) |
||||||
|
state.url=it.value |
||||||
|
setState( |
||||||
|
state |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
styledDiv { |
||||||
|
css{ |
||||||
|
visibility=Visibility.hidden |
||||||
|
height=0.px |
||||||
|
paddingLeft=10.px |
||||||
|
} |
||||||
|
+ state.url |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
button { |
||||||
|
attrs { |
||||||
|
onClickFunction={ |
||||||
|
|
||||||
|
val formData=FormData() |
||||||
|
formData.append("url",state.url) |
||||||
|
window.fetch("/api/json", RequestInit(method = "post",body = formData)).then { |
||||||
|
it.text() |
||||||
|
}.then { |
||||||
|
console.info(it) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
+"开始解析" |
||||||
|
} |
||||||
|
|
||||||
|
if(state.result!=""){ |
||||||
|
styledDiv { |
||||||
|
+state.result |
||||||
|
} |
||||||
|
button { |
||||||
|
attrs { |
||||||
|
onClickFunction={ |
||||||
|
state.result="" |
||||||
|
setState(state) |
||||||
|
} |
||||||
|
} |
||||||
|
+"清空消息" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
import kotlinx.css.p |
||||||
|
import kotlinx.serialization.SerialName |
||||||
|
import kotlinx.serialization.Serializable |
||||||
|
import kotlinx.serialization.decodeFromString |
||||||
|
import kotlinx.serialization.encodeToString |
||||||
|
import kotlinx.serialization.json.Json |
||||||
|
import org.khronos.webgl.Uint8Array |
||||||
|
import org.khronos.webgl.set |
||||||
|
|
||||||
|
import kotlin.test.Test |
||||||
|
|
||||||
|
|
||||||
|
class JsTest { |
||||||
|
|
||||||
|
@Serializable |
||||||
|
data class FF(@SerialName("ptimg-version") val c:String) |
||||||
|
|
||||||
|
@Test |
||||||
|
fun testParse(){ |
||||||
|
val c=Json.decodeFromString<t>("{\"ptimg-version\":1,\"resources\":{\"i\":{\"src\":\"0012.jpg\",\"width\":908,\"height\":1264}},\"views\":[{\"width\":844,\"height\":1200,\"coords\":[\"i:4,4+106,150>420,1050\",\"i:118,4+106,150>530,450\",\"i:232,4+106,150>738,0\",\"i:346,4+106,150>526,750\",\"i:460,4+106,150>420,750\",\"i:574,4+106,150>106,300\",\"i:688,4+106,150>318,600\",\"i:802,4+102,150>742,600\",\"i:4,162+106,150>106,450\",\"i:118,162+106,150>526,1050\",\"i:232,162+106,150>0,900\",\"i:346,162+106,150>636,150\",\"i:460,162+102,150>424,900\",\"i:570,162+106,150>0,1050\",\"i:684,162+106,150>106,1050\",\"i:798,162+106,150>0,600\",\"i:4,320+106,150>738,1050\",\"i:118,320+106,150>106,150\",\"i:232,320+106,150>424,600\",\"i:346,320+106,150>420,300\",\"i:460,320+106,150>530,150\",\"i:574,320+106,150>424,0\",\"i:688,320+102,150>742,150\",\"i:798,320+106,150>0,450\",\"i:4,478+106,150>738,750\",\"i:118,478+106,150>632,1050\",\"i:232,478+106,150>106,900\",\"i:346,478+102,150>106,750\",\"i:456,478+106,150>106,600\",\"i:570,478+106,150>424,450\",\"i:684,478+106,150>212,900\",\"i:798,478+106,150>318,450\",\"i:4,636+106,150>632,300\",\"i:118,636+106,150>314,1050\",\"i:232,636+106,150>106,0\",\"i:346,636+106,150>318,0\",\"i:460,636+106,150>314,750\",\"i:574,636+102,150>636,0\",\"i:684,636+106,150>632,900\",\"i:798,636+106,150>738,900\",\"i:4,794+106,150>318,150\",\"i:118,794+106,150>530,600\",\"i:232,794+106,150>212,0\",\"i:346,794+106,150>212,450\",\"i:460,794+106,150>0,150\",\"i:574,794+102,150>636,450\",\"i:684,794+106,150>738,300\",\"i:798,794+106,150>0,750\",\"i:4,952+106,150>208,750\",\"i:118,952+106,150>212,150\",\"i:232,952+106,150>636,600\",\"i:346,952+106,150>212,300\",\"i:460,952+106,150>0,300\",\"i:574,952+102,150>212,1050\",\"i:684,952+106,150>526,900\",\"i:798,952+106,150>424,150\",\"i:4,1110+106,150>530,0\",\"i:118,1110+106,150>526,300\",\"i:232,1110+106,150>632,750\",\"i:346,1110+106,150>738,450\",\"i:460,1110+106,150>212,600\",\"i:574,1110+106,150>0,0\",\"i:688,1110+102,150>318,300\",\"i:798,1110+106,150>318,900\"]}]}") |
||||||
|
println(c.ptimg_version) |
||||||
|
println(c.resources.i) |
||||||
|
println(c.views) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Test |
||||||
|
fun testRS(){ |
||||||
|
|
||||||
|
val f=Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire("i:574,4+106,150>106,600") |
||||||
|
println(f?.groupValues) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun testUnitArray(){ |
||||||
|
ImageLoader(UrlResult(originImagePath = "",serverImagePath = "", |
||||||
|
t=t(ptimg_version = 0,resources = resources(i = i(src = "",width = 0,height = 0)),views = listOf()))) |
||||||
|
.apply { |
||||||
|
m.forEach { |
||||||
|
println("m;${it}") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
package cool.kirito.bili.live.server.plugins |
||||||
|
|
||||||
|
import io.ktor.http.* |
||||||
|
import io.ktor.application.* |
||||||
|
import io.ktor.features.* |
||||||
|
|
||||||
|
fun Application.configureHTTP() { |
||||||
|
install(CORS) { |
||||||
|
method(HttpMethod.Options) |
||||||
|
method(HttpMethod.Put) |
||||||
|
method(HttpMethod.Delete) |
||||||
|
method(HttpMethod.Patch) |
||||||
|
header(HttpHeaders.Authorization) |
||||||
|
header("MyCustomHeader") |
||||||
|
allowCredentials = true |
||||||
|
anyHost() // @TODO: Don't do this in production if possible. Try to limit it. |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
package cool.kirito.bili.live.server.plugins |
||||||
|
|
||||||
|
import io.ktor.features.* |
||||||
|
import org.slf4j.event.* |
||||||
|
import io.ktor.application.* |
||||||
|
import io.ktor.request.* |
||||||
|
|
||||||
|
fun Application.configureMonitoring() { |
||||||
|
install(CallLogging) { |
||||||
|
level = Level.INFO |
||||||
|
filter { call -> call.request.path().startsWith("/") } |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,157 @@ |
|||||||
|
import io.ktor.application.* |
||||||
|
import io.ktor.client.* |
||||||
|
import io.ktor.client.engine.cio.* |
||||||
|
import io.ktor.client.request.* |
||||||
|
import io.ktor.client.statement.* |
||||||
|
import io.ktor.html.* |
||||||
|
import io.ktor.http.* |
||||||
|
import io.ktor.http.cio.websocket.* |
||||||
|
import io.ktor.http.content.* |
||||||
|
import io.ktor.request.* |
||||||
|
import io.ktor.response.* |
||||||
|
import io.ktor.routing.* |
||||||
|
import io.ktor.websocket.* |
||||||
|
import kotlinx.coroutines.channels.Channel |
||||||
|
import kotlinx.coroutines.launch |
||||||
|
import kotlinx.html.HTML |
||||||
|
import kotlinx.serialization.decodeFromString |
||||||
|
import kotlinx.serialization.encodeToString |
||||||
|
import kotlinx.serialization.json.Json |
||||||
|
import java.io.File |
||||||
|
|
||||||
|
|
||||||
|
const val website = "https://gammaplus.takeshobo.co.jp" |
||||||
|
|
||||||
|
var taskChannel = Channel<ParseTask>() |
||||||
|
val urlResultChannel = Channel<UrlResult>() |
||||||
|
val htmlChannel= Channel<UrlParam>() |
||||||
|
|
||||||
|
fun Application.parse(){ |
||||||
|
log.info("初始化解析任务") |
||||||
|
|
||||||
|
val uploadDir = environment.config.property("ktor.deployment.filePath").getString() |
||||||
|
val filePath = environment.classLoader.getResource(uploadDir)?.path.apply { log.info("图片存储目录:${this}") }?:throw IllegalArgumentException("图片存储目录初始化失败") |
||||||
|
|
||||||
|
launch { |
||||||
|
while (true){ |
||||||
|
val resHtml= htmlChannel.receive() |
||||||
|
launch { |
||||||
|
log.info("开始解析:${resHtml.url}") |
||||||
|
val client = HttpClient(CIO) |
||||||
|
|
||||||
|
val titlePrefix="title>" |
||||||
|
val title=Regex("$titlePrefix[\\u4e00-\\u9fa5\\u0800-\\u4e00\\s\\d\\uff00-\\uffef]+").find(resHtml.html)?.value?.replace(titlePrefix,"")?:throw IllegalArgumentException("无法解析漫画标题") |
||||||
|
val romajiPrefix="/manga" |
||||||
|
val romajiTitle=Regex("$romajiPrefix/\\w+").find(resHtml.url)?.value?.replace(romajiPrefix,"")?:throw IllegalArgumentException("无法解析漫画罗马音标题") |
||||||
|
log.info("解析漫画标题") |
||||||
|
val imageDir= File(filePath,romajiTitle).apply { if(!exists()) mkdir() } |
||||||
|
log.info("漫画图片存储到:${imageDir.absolutePath}") |
||||||
|
|
||||||
|
Regex("data/.*.json").findAll(resHtml.html) |
||||||
|
.apply { |
||||||
|
withIndex() |
||||||
|
//TODO .forEach { |
||||||
|
.first().let { |
||||||
|
log.info("开始解析:${it.value.value}") |
||||||
|
val urlPath = "${resHtml.url}/${it.value.value}" |
||||||
|
val jsonRes: HttpResponse = client.get(urlPath) |
||||||
|
if (jsonRes.status == HttpStatusCode.OK) { |
||||||
|
log.info("url:${urlPath} request OK") |
||||||
|
val t:t=Json.decodeFromString(jsonRes.readText()) |
||||||
|
val originImagePath="/data/${t.resources.i.src}" |
||||||
|
val imageUrl="${resHtml.url}${originImagePath}" |
||||||
|
val imgRes:HttpResponse=client.get(imageUrl) |
||||||
|
if(imgRes.status==HttpStatusCode.OK&&imgRes.contentType()==ContentType.Image.JPEG){ |
||||||
|
val filename="${it.index}.jpg" |
||||||
|
val file=File(imageDir,filename).apply { |
||||||
|
writeBytes(imgRes.readBytes()) |
||||||
|
} |
||||||
|
log.info("存储漫画图片:${file.absolutePath}") |
||||||
|
val serverImagePath="/${uploadDir}/${romajiTitle}/${filename}" |
||||||
|
log.info("serverImagePath:${serverImagePath}") |
||||||
|
urlResultChannel.send(UrlResult(originImagePath = originImagePath, t = t, |
||||||
|
serverImagePath = serverImagePath)) |
||||||
|
}else{ |
||||||
|
log.warn("image url:${imageUrl} 响应码${jsonRes.status} 响应类型${imgRes.contentType()}") |
||||||
|
} |
||||||
|
} else { |
||||||
|
log.warn("json url:${urlPath} 响应码:${jsonRes.status}") |
||||||
|
} |
||||||
|
taskChannel.send(ParseTask(total = count(), finish = it.index + 1,percentage = if(count()==it.index+1) 100F else String.format("%.2f",(it.index+1)*100F/count()).toFloat())) |
||||||
|
} |
||||||
|
} |
||||||
|
client.close() |
||||||
|
}.apply { |
||||||
|
invokeOnCompletion { |
||||||
|
log.info("${resHtml.url}解析完成") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
fun Application.configureRouting() { |
||||||
|
|
||||||
|
routing { |
||||||
|
val uploadDir=environment.config.property("ktor.deployment.filePath").getString() |
||||||
|
static(uploadDir) { |
||||||
|
resources(uploadDir) |
||||||
|
} |
||||||
|
|
||||||
|
static("/static") { |
||||||
|
resources() |
||||||
|
} |
||||||
|
|
||||||
|
webSocket(websocketPath) { // websocketSession |
||||||
|
launch { |
||||||
|
while (true) { |
||||||
|
val urlResult = urlResultChannel.receive() |
||||||
|
outgoing.send(Frame.Text(Json.encodeToString(urlResult))) |
||||||
|
} |
||||||
|
} |
||||||
|
launch { |
||||||
|
while (true){ |
||||||
|
val task=taskChannel.receive() |
||||||
|
outgoing.send(Frame.Text(Json.encodeToString(task))) |
||||||
|
} |
||||||
|
} |
||||||
|
for (frame in incoming) { |
||||||
|
when (frame) { |
||||||
|
is Frame.Text -> { |
||||||
|
val text = frame.readText() |
||||||
|
outgoing.send(Frame.Text("YOU SAID: $text")) |
||||||
|
if (text.equals("bye", ignoreCase = true)) { |
||||||
|
close(CloseReason(CloseReason.Codes.NORMAL, "Client said BYE")) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
get("/") { |
||||||
|
call.respondHtml(HttpStatusCode.OK, HTML::index) |
||||||
|
} |
||||||
|
|
||||||
|
post("/api/json") { |
||||||
|
val formParameters = call.receiveParameters() |
||||||
|
val urlParam = formParameters["url"] ?: "" |
||||||
|
log.info("urlParam:${urlParam}") |
||||||
|
if (urlParam.startsWith(website)) { |
||||||
|
val client = HttpClient(CIO) |
||||||
|
val response: HttpResponse = client.get(urlParam) |
||||||
|
if (response.status == HttpStatusCode.OK) { |
||||||
|
val resHtml = response.readText() |
||||||
|
htmlChannel.send(UrlParam(url = urlParam,html = resHtml)) |
||||||
|
call.respond(MessageResponse(message = "开始执行解析任务")) |
||||||
|
} else { |
||||||
|
log.warn("http code:${response.status}") |
||||||
|
call.respond(MessageResponse(message = "请求失败")) |
||||||
|
} |
||||||
|
client.close() |
||||||
|
} else { |
||||||
|
call.respond(MessageResponse(message = "${urlParam}:非法漫画地址")) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
package cool.kirito.bili.live.server.plugins |
||||||
|
|
||||||
|
import io.ktor.features.* |
||||||
|
import io.ktor.application.* |
||||||
|
import io.ktor.serialization.* |
||||||
|
|
||||||
|
fun Application.configureSerialization() { |
||||||
|
install(ContentNegotiation) { |
||||||
|
json() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
package plugins |
||||||
|
|
||||||
|
import io.ktor.application.* |
||||||
|
import io.ktor.websocket.* |
||||||
|
|
||||||
|
fun Application.configureWebSockets() { |
||||||
|
install(WebSockets) |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
import cool.kirito.bili.live.server.plugins.configureHTTP |
||||||
|
import cool.kirito.bili.live.server.plugins.configureMonitoring |
||||||
|
import cool.kirito.bili.live.server.plugins.configureSerialization |
||||||
|
import io.ktor.application.* |
||||||
|
import io.ktor.client.* |
||||||
|
import io.ktor.client.engine.cio.* |
||||||
|
import io.ktor.client.features.* |
||||||
|
import io.ktor.client.request.* |
||||||
|
import io.ktor.client.statement.* |
||||||
|
import io.ktor.features.* |
||||||
|
import io.ktor.html.respondHtml |
||||||
|
import io.ktor.http.* |
||||||
|
import io.ktor.routing.get |
||||||
|
import io.ktor.routing.routing |
||||||
|
import io.ktor.server.engine.embeddedServer |
||||||
|
import io.ktor.server.netty.Netty |
||||||
|
import io.ktor.http.content.resources |
||||||
|
import io.ktor.http.content.static |
||||||
|
import io.ktor.response.* |
||||||
|
import io.ktor.serialization.* |
||||||
|
import kotlinx.html.* |
||||||
|
import kotlinx.serialization.decodeFromString |
||||||
|
import kotlinx.serialization.json.Json |
||||||
|
import plugins.configureWebSockets |
||||||
|
|
||||||
|
fun HTML.index() { |
||||||
|
head { |
||||||
|
title(websiteTitle) |
||||||
|
} |
||||||
|
body { |
||||||
|
div { |
||||||
|
id = "root" |
||||||
|
} |
||||||
|
script(src = "/static/js.js") {} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) |
||||||
|
|
||||||
|
fun Application.module(testing: Boolean = false) { |
||||||
|
configureWebSockets() |
||||||
|
configureRouting() |
||||||
|
configureHTTP() |
||||||
|
configureMonitoring() |
||||||
|
configureSerialization() |
||||||
|
parse() |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
ktor { |
||||||
|
#开发模式 |
||||||
|
development = true |
||||||
|
deployment { |
||||||
|
port = 8080 |
||||||
|
port = ${?PORT} |
||||||
|
mysql = { |
||||||
|
username = root |
||||||
|
password = 123456 |
||||||
|
jdbcUrl = "jdbc:mysql://localhost:3306/csams?serverTimezone=Asia/Shanghai" |
||||||
|
driverClassName = "com.mysql.cj.jdbc.Driver" |
||||||
|
} |
||||||
|
filePath = static/image |
||||||
|
#免重启自动重载classes目录 |
||||||
|
watch = [ classes ] |
||||||
|
} |
||||||
|
application { |
||||||
|
modules = [ |
||||||
|
ServerKt.module, |
||||||
|
] |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
<configuration> |
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
||||||
|
<encoder> |
||||||
|
<pattern>%d{YYYY-MM-dd HH:mm:ss} [%thread] %-5level - %C:%L - %msg%n</pattern> |
||||||
|
<charset>UTF-8</charset> |
||||||
|
</encoder> |
||||||
|
</appender> |
||||||
|
<root level="trace"> |
||||||
|
<appender-ref ref="STDOUT"/> |
||||||
|
</root> |
||||||
|
<logger name="org.eclipse.jetty" level="INFO"/> |
||||||
|
<logger name="io.netty" level="INFO"/> |
||||||
|
<logger name="com.zaxxer.hikari" level="INFO"/> |
||||||
|
</configuration> |
@ -0,0 +1,78 @@ |
|||||||
|
import cool.kirito.bili.live.server.plugins.configureSerialization |
||||||
|
import io.ktor.http.* |
||||||
|
|
||||||
|
import io.ktor.config.* |
||||||
|
import kotlin.test.* |
||||||
|
import io.ktor.server.testing.* |
||||||
|
import kotlinx.coroutines.channels.Channel |
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind |
||||||
|
import kotlinx.serialization.encodeToString |
||||||
|
import kotlinx.serialization.json.Json |
||||||
|
import plugins.configureWebSockets |
||||||
|
|
||||||
|
|
||||||
|
class ApplicationTest { |
||||||
|
@Test |
||||||
|
fun testRoot() { |
||||||
|
withTestApplication({ |
||||||
|
configureWebSockets() |
||||||
|
configureRouting() |
||||||
|
configureSerialization() |
||||||
|
}) { |
||||||
|
handleRequest(HttpMethod.Post, "/").apply { |
||||||
|
// assertEquals(HttpStatusCode.OK, response.status()) |
||||||
|
environment.log.info("返回!!!!!!!!!!!!!!!:${response.content}") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun testParse(){ |
||||||
|
withTestApplication({ |
||||||
|
configureWebSockets() |
||||||
|
configureRouting() |
||||||
|
configureSerialization() |
||||||
|
}) { |
||||||
|
handleRequest(HttpMethod.Post,"/api/json"){ |
||||||
|
addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) |
||||||
|
setBody("url=https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/") |
||||||
|
} |
||||||
|
.apply { |
||||||
|
assertEquals(HttpStatusCode.OK,response.status()) |
||||||
|
environment.log.info("json:${response.content}") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun testRegex(){ |
||||||
|
val s="<div id=\"content\" class=\"pages ptbinb-container\" data-binbsp-direction=\"rtl\" data-binbsp-toc=\"toc\" data-binbsp-recommend=\"../recommend_01/index.html#more[next] ../recommend2/[next]\">\n" + |
||||||
|
"\t\t\t<div data-ptimg=\"data/0001.ptimg.json\" data-binbsp-spread=\"center\" data-binbsp-anchors=\"L_book_000\"></div>\n" + |
||||||
|
"\t\t\t<div data-ptimg=\"data/0002.ptimg.json\" data-binbsp-spread=\"right\"></div>\n" + |
||||||
|
"\t\t\t<div data-ptimg=\"data/0003.ptimg.json\" data-binbsp-spread=\"left\"></div>\n" + |
||||||
|
"\t\t\t<div data-ptimg=\"data/0004.ptimg.json\" data-binbsp-spread=\"right\"></div>\n" + |
||||||
|
"\t\t\t<div data-ptimg=\"data/0005.ptimg.json\" data-binbsp-spread=\"left\"></div>" |
||||||
|
|
||||||
|
Regex("data/.*.json").findAll(s).forEach { |
||||||
|
println(it.value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun testNumber(){ |
||||||
|
println(String.format("%.2f",3*100/22F)) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun testUrl(){ |
||||||
|
println(Regex("/manga/\\w+") |
||||||
|
.find("https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/")?.value) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun testSlice(){ |
||||||
|
val t="sdfsdfsdfsd==" |
||||||
|
println(t.slice(IntRange(t.length-2,t.length-1))) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue