Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19450f66a0 | ||
|
|
5b36fd9257 | ||
|
|
a3158d320b | ||
|
|
38494c9fbc | ||
|
|
114158cf73 | ||
|
|
6d108dd7ff | ||
|
|
f36b7f1dbe | ||
|
|
0a22ebd8e9 | ||
|
|
3682eeaf94 | ||
|
|
7df2ae4ba7 | ||
|
|
c9519ec681 | ||
|
|
b146ed684d | ||
|
|
d2787c36d7 | ||
|
|
3ff663114a | ||
|
|
573e62f310 | ||
|
|
f9af670b82 | ||
|
|
bf461475c6 | ||
|
|
bdea6e0cc1 | ||
|
|
57f0ec4e5d | ||
|
|
d663092363 | ||
|
|
edf6188e36 | ||
|
|
f3f3395e68 | ||
|
|
ac9dc347e3 | ||
|
|
8721d85946 | ||
|
|
a0bd1a8738 | ||
|
|
35fdf3e3b0 | ||
|
|
aced8293f1 | ||
|
|
3f516faad8 | ||
|
|
824f7b9602 | ||
|
|
95aeeaa16f | ||
|
|
63f08f0230 | ||
|
|
3b241fe857 | ||
|
|
75bc104f43 | ||
|
|
30afd56324 |
23
.github/workflows/watchdiff.yml
vendored
23
.github/workflows/watchdiff.yml
vendored
@@ -1,23 +0,0 @@
|
||||
# This is a basic workflow that is manually triggered
|
||||
|
||||
name: Watch hitomi.la file changes
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "*/10 * * * *"
|
||||
|
||||
jobs:
|
||||
watchdiff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: watchdiff
|
||||
- name: Download files
|
||||
run: ./fetch.sh
|
||||
- name: Commit and Push
|
||||
id: push
|
||||
run: |
|
||||
git config --global user.name 'Watchdiff bot'
|
||||
git config --global user.email 'tom5079@naver.com'
|
||||
{ git add . && git commit -m "File update" && git push; } | tail -1 | grep -q "nothing to commit"
|
||||
47
.gitignore
vendored
47
.gitignore
vendored
@@ -1,20 +1,33 @@
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
*.apk
|
||||
output.json
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.idea/
|
||||
misc.xml
|
||||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
#Github pages
|
||||
/gh-pages
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
#Private files
|
||||
**/google-services.json
|
||||
**/credentials.json
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
139
.idea/codeStyles/Project.xml
generated
139
.idea/codeStyles/Project.xml
generated
@@ -1,139 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="RIGHT_MARGIN" value="120" />
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="java.util" alias="false" withSubpackages="false" />
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
<package name="java" alias="false" withSubpackages="true" />
|
||||
<package name="javax" alias="false" withSubpackages="true" />
|
||||
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||
<package name="" alias="true" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
5
.idea/codeStyles/codeStyleConfig.xml
generated
@@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/copyright/GPL.xml
generated
6
.idea/copyright/GPL.xml
generated
@@ -1,6 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value=" Pupil, Hitomi.la viewer for Android Copyright (C) &#36;today.year tom5079 This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>." />
|
||||
<option name="myName" value="GPL" />
|
||||
</copyright>
|
||||
</component>
|
||||
7
.idea/copyright/profiles_settings.xml
generated
7
.idea/copyright/profiles_settings.xml
generated
@@ -1,7 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings>
|
||||
<module2copyright>
|
||||
<element module="Project Files" copyright="GPL" />
|
||||
</module2copyright>
|
||||
</settings>
|
||||
</component>
|
||||
17
.idea/deploymentTargetDropDown.xml
generated
17
.idea/deploymentTargetDropDown.xml
generated
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<targetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="$USER_HOME$/.android/avd/Pixel_2_API_31.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2022-02-01T08:00:57.223690Z" />
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/dictionaries/tom50.xml
generated
7
.idea/dictionaries/tom50.xml
generated
@@ -1,7 +0,0 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="tom50">
|
||||
<words>
|
||||
<w>hitomi</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
4
.idea/encodings.xml
generated
4
.idea/encodings.xml
generated
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
|
||||
</project>
|
||||
22
.idea/gradle.xml
generated
22
.idea/gradle.xml
generated
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="/usr/share/java/gradle" />
|
||||
<option name="gradleJvm" value="11" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
6
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="AndroidLintNewerVersionAvailable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
100
.idea/jarRepositories.xml
generated
100
.idea/jarRepositories.xml
generated
@@ -1,100 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven2" />
|
||||
<option name="name" value="maven2" />
|
||||
<option name="url" value="http://guardian.github.com/maven/repo-releases" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven" />
|
||||
<option name="name" value="maven" />
|
||||
<option name="url" value="https://jitpack.io" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenRepo" />
|
||||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven2" />
|
||||
<option name="name" value="maven2" />
|
||||
<option name="url" value="https://guardian.github.com/maven/repo-releases" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven3" />
|
||||
<option name="name" value="maven3" />
|
||||
<option name="url" value="https://s3.amazonaws.com/fabric-artifacts-private/internal-snapshots" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven4" />
|
||||
<option name="name" value="maven4" />
|
||||
<option name="url" value="https://maven.fabric.io/public" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenLocal" />
|
||||
<option name="name" value="MavenLocal" />
|
||||
<option name="url" value="file:/$USER_HOME$/.m2/repository/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenLocal" />
|
||||
<option name="name" value="MavenLocal" />
|
||||
<option name="url" value="file:/$USER_HOME$/.m2/repository" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven3" />
|
||||
<option name="name" value="maven3" />
|
||||
<option name="url" value="https://dl.bintray.com/tom5079/maven" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven3" />
|
||||
<option name="name" value="maven3" />
|
||||
<option name="url" value="http://dl.bintray.com/piasy/maven" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven2" />
|
||||
<option name="name" value="maven2" />
|
||||
<option name="url" value="https://guardian.github.io/maven/repo-releases/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenLocal" />
|
||||
<option name="name" value="MavenLocal" />
|
||||
<option name="url" value="file:$USER_HOME$/.m2/repository/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven2" />
|
||||
<option name="name" value="maven2" />
|
||||
<option name="url" value="https://oss.sonatype.org/content/repositories/snapshots" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven3" />
|
||||
<option name="name" value="maven3" />
|
||||
<option name="url" value="https://maven.mozilla.org/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven3" />
|
||||
<option name="name" value="maven3" />
|
||||
<option name="url" value="https://oss.sonatype.org/content/repositories/snapshots/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/kotlinCodeInsightSettings.xml
generated
7
.idea/kotlinCodeInsightSettings.xml
generated
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinCodeInsightWorkspaceSettings">
|
||||
<option name="addUnambiguousImportsOnTheFly" value="true" />
|
||||
<option name="optimizeImportsOnTheFly" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/kotlinc.xml
generated
6
.idea/kotlinc.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Kotlin2JvmCompilerArguments">
|
||||
<option name="jvmTarget" value="1.8" />
|
||||
</component>
|
||||
</project>
|
||||
20
.idea/misc.xml
generated
20
.idea/misc.xml
generated
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="../../../../layout/custom_preview.xml" value="0.2564814814814815" />
|
||||
<entry key="app/src/main/res/layout/reader_activity.xml" value="0.14351851851851852" />
|
||||
<entry key="app/src/main/res/xml/lock_preferences.xml" value="0.5119791666666667" />
|
||||
<entry key="app/src/main/res/xml/manage_storage_preferences.xml" value="0.2604166666666667" />
|
||||
<entry key="app/src/main/res/xml/root_preferences.xml" value="0.5119791666666667" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
3
.idea/scopes/Pupil.xml
generated
3
.idea/scopes/Pupil.xml
generated
@@ -1,3 +0,0 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<scope name="Pupil" pattern="file[app]:*/" />
|
||||
</component>
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -2,7 +2,7 @@
|
||||
*Pupil, Hitomi.la viewer for Android*
|
||||
|
||||

|
||||
[](https://github.com/tom5079/Pupil/releases/download/5.3.3/Pupil-v5.3.3.apk)
|
||||
[](https://github.com/tom5079/Pupil/releases/download/5.3.13/Pupil-v5.3.13.apk)
|
||||
[](https://discord.gg/Stj4b5v)
|
||||
|
||||
# Features
|
||||
|
||||
@@ -32,13 +32,13 @@ configurations {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
defaultConfig {
|
||||
applicationId "xyz.quaver.pupil"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 31
|
||||
compileSdk 34
|
||||
targetSdkVersion 34
|
||||
versionCode 69
|
||||
versionName "5.3.4"
|
||||
versionName "5.3.13"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
@@ -79,13 +79,14 @@ android {
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||
implementation "androidx.activity:activity-ktx:1.4.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.0"
|
||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
|
||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||
@@ -94,15 +95,15 @@ dependencies {
|
||||
|
||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||
|
||||
implementation "com.google.android.material:material:1.5.0"
|
||||
implementation "com.google.android.material:material:1.11.0"
|
||||
|
||||
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
||||
implementation platform('com.google.firebase:firebase-bom:32.7.0')
|
||||
implementation "com.google.firebase:firebase-analytics-ktx"
|
||||
implementation "com.google.firebase:firebase-crashlytics-ktx"
|
||||
implementation "com.google.firebase:firebase-perf-ktx"
|
||||
|
||||
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
|
||||
implementation "com.google.android.gms:play-services-mlkit-face-detection:17.0.0"
|
||||
implementation "com.google.android.gms:play-services-oss-licenses:17.0.1"
|
||||
implementation "com.google.android.gms:play-services-mlkit-face-detection:17.1.0"
|
||||
|
||||
implementation "com.github.clans:fab:1.6.4"
|
||||
|
||||
@@ -128,15 +129,11 @@ dependencies {
|
||||
|
||||
implementation "org.jsoup:jsoup:1.14.3"
|
||||
|
||||
implementation ("app.cash.zipline:zipline:1.0.0-SNAPSHOT") {
|
||||
exclude group: "com.squareup.okio", module: "okio"
|
||||
}
|
||||
|
||||
implementation "xyz.quaver:documentfilex:0.7.2"
|
||||
implementation "xyz.quaver:floatingsearchview:1.1.7"
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1"
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
||||
androidTestImplementation "androidx.test:rules:1.4.0"
|
||||
androidTestImplementation "androidx.test:runner:1.4.0"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 69,
|
||||
"versionName": "5.3.4",
|
||||
"versionName": "5.3.12",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
@@ -45,7 +47,8 @@
|
||||
</provider>
|
||||
|
||||
<service android:name=".services.DownloadService"
|
||||
android:exported="false"/>
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse" />
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.UpdateBroadcastReceiver"
|
||||
@@ -61,165 +64,107 @@
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:parentActivityName=".ui.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/galleries"
|
||||
android:scheme="http" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="*.hasha.in"/>
|
||||
<data android:pathPrefix="/reader"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/manga"
|
||||
android:scheme="http" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="hitomi.la"/>
|
||||
<data android:pathPrefix="/galleries"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/doujinshi"
|
||||
android:scheme="http" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="hitomi.la" />
|
||||
<data android:pathPrefix="/manga" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/cg"
|
||||
android:scheme="http" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="hitomi.la" />
|
||||
<data android:pathPrefix="/doujinshi" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="http" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="hitomi.la" />
|
||||
<data android:pathPrefix="/cg" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/galleries"
|
||||
android:scheme="https" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="hitomi.la" />
|
||||
<data android:pathPrefix="/imageset" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/manga"
|
||||
android:scheme="https" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="hitomi.la" />
|
||||
<data android:pathPrefix="/reader" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/doujinshi"
|
||||
android:scheme="https" />
|
||||
<data android:scheme="http" />
|
||||
<data android:host="e-hentai.org" />
|
||||
<data android:pathPrefix="/g" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/cg"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hiyobi.me"
|
||||
android:scheme="http"
|
||||
android:pathPrefix="/reader" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hiyobi.me"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="e-hentai.org"
|
||||
android:pathPrefix="/g"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="e-hentai.org"
|
||||
android:pathPrefix="/g"
|
||||
android:scheme="https" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="e-hentai.org" />
|
||||
<data android:pathPrefix="/g" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.SettingsActivity"
|
||||
android:label="@string/settings_title">
|
||||
<tools:validation testUrl="http://ix.io/eer" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
@@ -232,17 +177,6 @@
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:scheme="http"
|
||||
android:host="ix.io"
|
||||
android:pathPattern="/..*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
||||
</application>
|
||||
|
||||
@@ -30,7 +30,6 @@ import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import app.cash.zipline.QuickJs
|
||||
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
|
||||
import com.github.piasy.biv.BigImageViewer
|
||||
import com.github.piasy.biv.loader.fresco.FrescoImageLoader
|
||||
@@ -51,9 +50,15 @@ import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
typealias PupilInterceptor = (Interceptor.Chain) -> Response
|
||||
@@ -77,10 +82,35 @@ val client: OkHttpClient
|
||||
clientHolder = it
|
||||
}
|
||||
|
||||
private var version = ""
|
||||
var runtimeReady = false
|
||||
private set
|
||||
lateinit var runtime: QuickJs
|
||||
fun getSSLContext(context: Context): SSLContext {
|
||||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
keyStore.load(null, null)
|
||||
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
|
||||
val certificate = context.resources.openRawResource(R.raw.isrgrootx1).use {
|
||||
certificateFactory.generateCertificate(it)
|
||||
}
|
||||
|
||||
keyStore.setCertificateEntry("isrgrootx1", certificate)
|
||||
|
||||
val defaultTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
defaultTrustManagerFactory.init(null as KeyStore?)
|
||||
|
||||
defaultTrustManagerFactory.trustManagers.filterIsInstance(X509TrustManager::class.java).forEach { trustManager ->
|
||||
trustManager.acceptedIssuers.forEach { acceptedIssuer ->
|
||||
keyStore.setCertificateEntry(acceptedIssuer.subjectDN.name, acceptedIssuer)
|
||||
}
|
||||
}
|
||||
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustManagerFactory.init(keyStore)
|
||||
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(null, trustManagerFactory.trustManagers, SecureRandom())
|
||||
|
||||
return sslContext
|
||||
}
|
||||
|
||||
class Pupil : Application() {
|
||||
companion object {
|
||||
@@ -88,35 +118,6 @@ class Pupil : Application() {
|
||||
private set
|
||||
}
|
||||
|
||||
init {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
runtime = QuickJs.create()
|
||||
}
|
||||
while (true) {
|
||||
kotlin.runCatching {
|
||||
val newVersion = URL("https://tom5079.github.io/PupilSources/hitomi.html.ver").readText()
|
||||
|
||||
if (version != newVersion) {
|
||||
runtimeReady = false
|
||||
evaluationContext.cancelChildren()
|
||||
kotlin.runCatching {
|
||||
URL("https://tom5079.github.io/PupilSources/assets/js/gg.js").readText()
|
||||
}.getOrNull()?.also { gg ->
|
||||
withContext(Dispatchers.Main) {
|
||||
runtime.evaluate(gg)
|
||||
version = newVersion
|
||||
runtimeReady = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delay(10000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
instance = this
|
||||
|
||||
@@ -135,7 +136,8 @@ class Pupil : Application() {
|
||||
val proxyInfo = getProxyInfo()
|
||||
|
||||
clientBuilder = OkHttpClient.Builder()
|
||||
.connectTimeout(0, TimeUnit.SECONDS)
|
||||
// .connectTimeout(0, TimeUnit.SECONDS)
|
||||
.sslSocketFactory(getSSLContext(this).socketFactory)
|
||||
.readTimeout(0, TimeUnit.SECONDS)
|
||||
.proxyInfo(proxyInfo)
|
||||
.addInterceptor { chain ->
|
||||
|
||||
@@ -17,16 +17,23 @@
|
||||
package xyz.quaver.pupil.hitomi
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.datetime.Clock.System.now
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.runtime
|
||||
import xyz.quaver.pupil.runtimeReady
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
const val protocol = "https:"
|
||||
|
||||
@@ -131,19 +138,73 @@ const val nozomiextension = ".nozomi"
|
||||
val evaluationContext = Dispatchers.Main + Job()
|
||||
|
||||
object gg {
|
||||
private var lastRetrieval: Long? = null
|
||||
|
||||
suspend fun m(g: Int): Int = withContext(evaluationContext) {
|
||||
while (!runtimeReady) delay(1000)
|
||||
runtime.evaluate("gg.m($g)").toString().toInt()
|
||||
}
|
||||
suspend fun b(): String = withContext(evaluationContext) {
|
||||
while (!runtimeReady) delay(1000)
|
||||
runtime.evaluate("gg.b").toString()
|
||||
private val mutex = Mutex()
|
||||
|
||||
private var mDefault = 0
|
||||
private val mMap = mutableMapOf<Int, Int>()
|
||||
|
||||
private var b = ""
|
||||
|
||||
@OptIn(ExperimentalTime::class, ExperimentalCoroutinesApi::class)
|
||||
private suspend fun refresh() = withContext(Dispatchers.IO) {
|
||||
mutex.withLock {
|
||||
if (lastRetrieval == null || (lastRetrieval!! + 60000) < System.currentTimeMillis()) {
|
||||
val ggjs: String = suspendCancellableCoroutine { continuation ->
|
||||
val call = client.newCall(Request.Builder().url("https://ltn.hitomi.la/gg.js").build())
|
||||
|
||||
call.enqueue(object: Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
if (continuation.isCancelled) return
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
|
||||
suspend fun s(h: String): String = withContext(evaluationContext) {
|
||||
while (!runtimeReady) delay(1000)
|
||||
runtime.evaluate("gg.s('$h')").toString()
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (!call.isCanceled) {
|
||||
response.body()?.use {
|
||||
continuation.resume(it.string()) {
|
||||
call.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
call.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
mDefault = Regex("var o = (\\d)").find(ggjs)!!.groupValues[1].toInt()
|
||||
val o = Regex("o = (\\d); break;").find(ggjs)!!.groupValues[1].toInt()
|
||||
|
||||
mMap.clear()
|
||||
Regex("case (\\d+):").findAll(ggjs).forEach {
|
||||
val case = it.groupValues[1].toInt()
|
||||
mMap[case] = o
|
||||
}
|
||||
|
||||
b = Regex("b: '(.+)'").find(ggjs)!!.groupValues[1]
|
||||
|
||||
lastRetrieval = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun m(g: Int): Int {
|
||||
refresh()
|
||||
|
||||
return mMap[g] ?: mDefault
|
||||
}
|
||||
|
||||
suspend fun b(): String {
|
||||
refresh()
|
||||
return b
|
||||
}
|
||||
fun s(h: String): String {
|
||||
val m = Regex("(..)(.)$").find(h)
|
||||
return m!!.groupValues.let { it[2]+it[1] }.toInt(16).toString(10)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -166,11 +166,7 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<
|
||||
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
||||
}
|
||||
|
||||
val bytes = try {
|
||||
URL(nozomiAddress).readBytes()
|
||||
} catch (e: Exception) {
|
||||
return emptySet()
|
||||
}
|
||||
val bytes = URL(nozomiAddress).readBytes()
|
||||
|
||||
val nozomi = mutableSetOf<Int>()
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
@@ -100,13 +101,15 @@ class DownloadService : Service() {
|
||||
notify(galleryID)
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@SuppressLint("RestrictedApi", "MissingPermission")
|
||||
private fun notify(galleryID: Int) {
|
||||
val max = progress[galleryID]?.size ?: 0
|
||||
val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0
|
||||
|
||||
val notification = notification[galleryID] ?: return
|
||||
|
||||
if (!checkNotificationEnabled(this)) return
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
notification
|
||||
.setContentText(getString(R.string.reader_notification_complete))
|
||||
@@ -168,19 +171,26 @@ class DownloadService : Service() {
|
||||
private val interceptor: PupilInterceptor = { chain ->
|
||||
val request = chain.request()
|
||||
|
||||
var response = chain.proceed(request)
|
||||
var limit = 5
|
||||
var response = kotlin.runCatching {
|
||||
chain.proceed(request)
|
||||
}.getOrNull()
|
||||
var limit = 10
|
||||
|
||||
while (!response.isSuccessful) {
|
||||
if (response.code() == 503) {
|
||||
while (response?.isSuccessful != true) {
|
||||
if (response?.code() == 503) {
|
||||
Thread.sleep(200)
|
||||
} else if (--limit > 0)
|
||||
} else if (--limit < 0)
|
||||
break
|
||||
|
||||
response = chain.proceed(request)
|
||||
response = kotlin.runCatching {
|
||||
chain.proceed(request)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
response.newBuilder()
|
||||
if (response == null)
|
||||
response = chain.proceed(request)
|
||||
|
||||
response!!.newBuilder()
|
||||
.body(response.body()?.let {
|
||||
ProgressResponseBody(request.tag(), it, progressListener)
|
||||
}).build()
|
||||
@@ -207,6 +217,7 @@ class DownloadService : Service() {
|
||||
private val callback = object: Callback {
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
Log.d("PUPILD", "ONFAILURE ${call.request().tag()}, ${e}")
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
|
||||
if (e.message?.contains("cancel", true) == false) {
|
||||
@@ -215,6 +226,7 @@ class DownloadService : Service() {
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
Log.d("PUPILD", "ONRESPONSE ${call.request().tag()}")
|
||||
val (galleryID, index, startId) = call.request().tag() as Tag
|
||||
val ext = call.request().url().encodedPath().split('.').last()
|
||||
|
||||
@@ -394,7 +406,11 @@ class DownloadService : Service() {
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
|
||||
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
||||
} else {
|
||||
startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
}
|
||||
|
||||
when (intent?.getStringExtra(KEY_COMMAND)) {
|
||||
COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0)
|
||||
@@ -415,7 +431,11 @@ class DownloadService : Service() {
|
||||
override fun onBind(p0: Intent?) = binder
|
||||
|
||||
override fun onCreate() {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
|
||||
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
||||
} else {
|
||||
startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
}
|
||||
interceptors[Tag::class] = interceptor
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,13 @@ import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
@@ -61,7 +64,9 @@ import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.checkUpdate
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.requestNotificationPermission
|
||||
import xyz.quaver.pupil.util.restore
|
||||
import xyz.quaver.pupil.util.showNotificationPermissionExplanationDialog
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
@@ -107,6 +112,12 @@ class MainActivity :
|
||||
|
||||
private lateinit var binding: MainActivityBinding
|
||||
|
||||
private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (!isGranted) {
|
||||
showNotificationPermissionExplanationDialog(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = MainActivityBinding.inflate(layoutInflater)
|
||||
@@ -118,12 +129,14 @@ class MainActivity :
|
||||
onFailure = {
|
||||
Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
||||
}, onSuccess = {
|
||||
Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
|
||||
Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
requestNotificationPermission(this, requestNotificationPermssionLauncher, false) {}
|
||||
|
||||
if (Preferences["download_folder", ""].isEmpty())
|
||||
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
|
||||
|
||||
@@ -392,6 +405,10 @@ class MainActivity :
|
||||
onDownloadClickedHandler = { position ->
|
||||
val galleryID = galleries[position]
|
||||
|
||||
requestNotificationPermission(
|
||||
this@MainActivity,
|
||||
requestNotificationPermssionLauncher
|
||||
) {
|
||||
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
|
||||
DownloadService.cancel(this@MainActivity, galleryID)
|
||||
}
|
||||
@@ -399,6 +416,7 @@ class MainActivity :
|
||||
DownloadManager.getInstance(context).addDownloadFolder(galleryID)
|
||||
DownloadService.download(this@MainActivity, galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
closeAllItems()
|
||||
}
|
||||
@@ -477,6 +495,20 @@ class MainActivity :
|
||||
private var suggestionJob : Job? = null
|
||||
private fun setupSearchBar() {
|
||||
with(binding.contents.searchview) {
|
||||
val scrollSuggestionToTop = {
|
||||
with(binding.suggestionSection.suggestionsList) {
|
||||
MainScope().launch {
|
||||
withTimeout(1000) {
|
||||
val layoutManager = layoutManager as LinearLayoutManager
|
||||
while (layoutManager.findLastVisibleItemPosition() != adapter?.itemCount?.minus(1)) {
|
||||
layoutManager.scrollToPosition(adapter?.itemCount?.minus(1) ?: 0)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
|
||||
override fun onMenuOpened() {
|
||||
(this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
|
||||
@@ -502,6 +534,7 @@ class MainActivity :
|
||||
onFavoriteHistorySwitchClickListener = {
|
||||
isFavorite = !isFavorite
|
||||
swapSuggestions(defaultSuggestions)
|
||||
scrollSuggestionToTop()
|
||||
}
|
||||
|
||||
onMenuItemClickListener = {
|
||||
@@ -515,6 +548,7 @@ class MainActivity :
|
||||
|
||||
if (query.isEmpty() or query.endsWith(' ')) {
|
||||
swapSuggestions(defaultSuggestions)
|
||||
scrollSuggestionToTop()
|
||||
|
||||
return@lambda
|
||||
}
|
||||
@@ -546,8 +580,10 @@ class MainActivity :
|
||||
|
||||
onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener {
|
||||
override fun onFocus() {
|
||||
if (query.isEmpty() or query.endsWith(' '))
|
||||
if (query.isEmpty() or query.endsWith(' ')) {
|
||||
swapSuggestions(defaultSuggestions)
|
||||
scrollSuggestionToTop()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFocusCleared() {
|
||||
|
||||
@@ -57,9 +57,12 @@ import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.camera
|
||||
import xyz.quaver.pupil.util.checkNotificationEnabled
|
||||
import xyz.quaver.pupil.util.closeCamera
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.requestNotificationPermission
|
||||
import xyz.quaver.pupil.util.showNotificationPermissionExplanationDialog
|
||||
import xyz.quaver.pupil.util.startCamera
|
||||
|
||||
class ReaderActivity : BaseActivity() {
|
||||
@@ -117,6 +120,12 @@ class ReaderActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ReaderActivityBinding
|
||||
|
||||
private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (!isGranted) {
|
||||
showNotificationPermissionExplanationDialog(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ReaderActivityBinding.inflate(layoutInflater)
|
||||
@@ -148,10 +157,11 @@ class ReaderActivity : BaseActivity() {
|
||||
val uri = intent.data
|
||||
val lastPathSegment = uri?.lastPathSegment
|
||||
if (uri != null && lastPathSegment != null) {
|
||||
galleryID = when (uri.host) {
|
||||
galleryID = if (uri.host?.endsWith("hasha.in") == true) {
|
||||
lastPathSegment?.toInt() ?: 0
|
||||
} else when (uri.host) {
|
||||
"hitomi.la" ->
|
||||
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
||||
"hiyobi.me" -> lastPathSegment.toInt()
|
||||
"e-hentai.org" -> uri.pathSegments[1].toInt()
|
||||
else -> 0
|
||||
}
|
||||
@@ -360,6 +370,10 @@ class ReaderActivity : BaseActivity() {
|
||||
animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button
|
||||
|
||||
setOnClickListener {
|
||||
requestNotificationPermission(
|
||||
this@ReaderActivity,
|
||||
requestNotificationPermssionLauncher
|
||||
) {
|
||||
val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
|
||||
|
||||
if (downloadManager.isDownloading(galleryID)) {
|
||||
@@ -372,6 +386,7 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with(binding.retryFab) {
|
||||
setImageResource(R.drawable.refresh)
|
||||
|
||||
@@ -18,25 +18,72 @@
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import okhttp3.*
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.readText
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.favoriteTags
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.get
|
||||
import xyz.quaver.pupil.util.restore
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ManageFavoritesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val requestBackupFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val uri = result.data?.data ?: return@registerForActivityResult
|
||||
val context = context ?: return@registerForActivityResult
|
||||
val view = view ?: return@registerForActivityResult
|
||||
|
||||
val backupData = runCatching {
|
||||
FileX(context, uri).readText()?.let { Json.parseToJsonElement(it) }
|
||||
}.getOrNull() ?: run{
|
||||
Snackbar.make(view, context.getString(R.string.error), Toast.LENGTH_LONG).show()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val newFavorites = backupData["favorites"]?.let { Json.decodeFromJsonElement<List<Int>>(it) }.orEmpty()
|
||||
val newFavoriteTags = backupData["favorite_tags"]?.let { Json.decodeFromJsonElement<List<Tag>>(it) }.orEmpty()
|
||||
|
||||
favorites.addAll(newFavorites)
|
||||
favoriteTags.addAll(newFavoriteTags)
|
||||
|
||||
Snackbar.make(view, context.getString(R.string.settings_restore_success, newFavorites.size + newFavoriteTags.size), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey)
|
||||
|
||||
@@ -47,57 +94,44 @@ class ManageFavoritesFragment : PreferenceFragmentCompat() {
|
||||
val context = context ?: return
|
||||
|
||||
findPreference<Preference>("backup")?.setOnPreferenceClickListener {
|
||||
val request = Request.Builder()
|
||||
.url(context.getString(R.string.backup_url))
|
||||
.post(
|
||||
FormBody.Builder()
|
||||
.add("f:1", File(ContextCompat.getDataDir(context), "favorites.json").readText())
|
||||
.build()
|
||||
).build()
|
||||
val favorites = runCatching {
|
||||
Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites.json").readText())
|
||||
}.getOrNull()
|
||||
val favoriteTags = kotlin.runCatching {
|
||||
Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites_tags.json").readText())
|
||||
}.getOrNull()
|
||||
|
||||
client.newCall(request).enqueue(object: Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
val view = view ?: return
|
||||
Snackbar.make(view, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
||||
val favoriteJson = buildJsonObject {
|
||||
favorites?.let {
|
||||
put("favorites", it)
|
||||
}
|
||||
favoriteTags?.let {
|
||||
put("favorite_tags", it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (response.code() != 200) {
|
||||
response.close()
|
||||
return
|
||||
val backupFile = File(context.filesDir, "pupil-backup.json").also {
|
||||
it.writeText(favoriteJson.toString())
|
||||
}
|
||||
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, response.body()?.use { it.string() }?.replace("\n", ""))
|
||||
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", backupFile)
|
||||
setDataAndType(uri, "application/json")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
}.let {
|
||||
getContext()?.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
|
||||
context.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
true
|
||||
}
|
||||
findPreference<Preference>("restore")?.setOnPreferenceClickListener {
|
||||
val editText = EditText(context).apply {
|
||||
setText(context.getString(R.string.backup_url), TextView.BufferType.EDITABLE)
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.settings_restore_title)
|
||||
.setView(editText)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
restore(editText.text.toString(),
|
||||
onFailure = onFailure@{
|
||||
val view = view ?: return@onFailure
|
||||
Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show()
|
||||
}, onSuccess = onSuccess@{
|
||||
val view = view ?: return@onSuccess
|
||||
Snackbar.make(view, context.getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
|
||||
})
|
||||
}.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// Do Nothing
|
||||
}.show()
|
||||
requestBackupFileLauncher.launch(intent)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -61,12 +61,10 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
||||
initPreferences()
|
||||
}
|
||||
|
||||
override fun onPreferenceClick(preference: Preference?): Boolean {
|
||||
override fun onPreferenceClick(preference: Preference): Boolean {
|
||||
val context = context ?: return false
|
||||
|
||||
with(preference) {
|
||||
this ?: return false
|
||||
|
||||
when (key) {
|
||||
"delete_cache" -> {
|
||||
val dir = File(context.cacheDir, "imageCache")
|
||||
@@ -119,7 +117,9 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
||||
if (!metadataFile.exists()) return@forEach
|
||||
|
||||
val metadata = metadataFile.readText()?.let {
|
||||
runCatching {
|
||||
json.decodeFromString<Metadata>(it)
|
||||
}.getOrNull()
|
||||
} ?: return@forEach
|
||||
|
||||
val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach
|
||||
|
||||
@@ -80,10 +80,8 @@ class SettingsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceClick(preference: Preference?): Boolean {
|
||||
override fun onPreferenceClick(preference: Preference): Boolean {
|
||||
with (preference) {
|
||||
this ?: return false
|
||||
|
||||
when (key) {
|
||||
"app_version" -> {
|
||||
checkUpdate(activity as SettingsActivity, true)
|
||||
@@ -122,10 +120,8 @@ class SettingsFragment :
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean {
|
||||
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
|
||||
with (preference) {
|
||||
this ?: return false
|
||||
|
||||
when (key) {
|
||||
"tag_translation" -> {
|
||||
updateTranslations()
|
||||
@@ -163,7 +159,7 @@ class SettingsFragment :
|
||||
|
||||
when (key) {
|
||||
"proxy" -> {
|
||||
summary = context?.let { getProxyInfo().type.name }
|
||||
summary = context.let { getProxyInfo().type.name }
|
||||
}
|
||||
"download_folder" -> {
|
||||
summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath
|
||||
@@ -300,7 +296,7 @@ class SettingsFragment :
|
||||
}
|
||||
"oss" -> {
|
||||
setOnPreferenceClickListener {
|
||||
context?.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
|
||||
context.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,14 @@ import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.hitomi.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Serializable
|
||||
data class OldReader(
|
||||
val code: String,
|
||||
val galleryInfo: OldGalleryInfo
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OldGalleryInfo(
|
||||
val language_localname: String? = null,
|
||||
@@ -63,7 +68,7 @@ data class OldGalleryFiles(
|
||||
@Serializable
|
||||
data class OldMetadata(
|
||||
var galleryBlock: GalleryBlock? = null,
|
||||
var reader: OldGalleryInfo? = null,
|
||||
var reader: OldReader? = null,
|
||||
var imageList: MutableList<String?>? = null
|
||||
) {
|
||||
fun copy(): OldMetadata = OldMetadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
||||
@@ -75,12 +80,36 @@ data class Metadata(
|
||||
var galleryInfo: GalleryInfo? = null,
|
||||
var imageList: MutableList<String?>? = null
|
||||
) {
|
||||
constructor(old: OldMetadata) : this(old.galleryBlock, getGalleryInfo(old.galleryBlock?.id ?: throw Exception()), old.imageList)
|
||||
constructor(old: OldMetadata) : this(
|
||||
old.galleryBlock,
|
||||
old.reader?.galleryInfo?.let { oldGalleryInfo ->
|
||||
GalleryInfo(
|
||||
oldGalleryInfo.id.toString(),
|
||||
oldGalleryInfo.title ?: "",
|
||||
null,
|
||||
oldGalleryInfo.language,
|
||||
oldGalleryInfo.type ?: "",
|
||||
oldGalleryInfo.date ?: "",
|
||||
files = oldGalleryInfo.files.map {
|
||||
GalleryFiles(
|
||||
it.width,
|
||||
it.hash,
|
||||
it.haswebp,
|
||||
it.name,
|
||||
it.height,
|
||||
it.hasavif,
|
||||
it.hasavifsmalltn
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
old.imageList
|
||||
)
|
||||
|
||||
fun copy(): Metadata = Metadata(galleryBlock, galleryInfo, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
||||
}
|
||||
|
||||
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
|
||||
|
||||
companion object {
|
||||
val instances = ConcurrentHashMap<Int, Cache>()
|
||||
|
||||
@@ -103,7 +132,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
|
||||
var metadata = kotlin.runCatching {
|
||||
findFile(".metadata")?.readText()?.let { metadata ->
|
||||
kotlin.runCatching {
|
||||
json.decodeFromString<Metadata>(metadata)
|
||||
Json.decodeFromString<Metadata>(metadata)
|
||||
}.getOrElse {
|
||||
Metadata(json.decodeFromString<OldMetadata>(metadata))
|
||||
}
|
||||
|
||||
@@ -18,11 +18,21 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.serialization.json.*
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.hitomi.GalleryBlock
|
||||
import xyz.quaver.pupil.hitomi.GalleryInfo
|
||||
import xyz.quaver.pupil.hitomi.imageUrlFromImage
|
||||
@@ -133,3 +143,30 @@ fun JsonElement.getOrNull(tag: String) = kotlin.runCatching {
|
||||
|
||||
val JsonElement.content
|
||||
get() = this.jsonPrimitive.contentOrNull
|
||||
|
||||
fun checkNotificationEnabled(context: Context) =
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
fun showNotificationPermissionExplanationDialog(context: Context) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.notification_denied)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
fun requestNotificationPermission(
|
||||
activity: Activity,
|
||||
requestPermissionLauncher: ActivityResultLauncher<String>,
|
||||
showRationale: Boolean = true,
|
||||
ifGranted: () -> Unit,
|
||||
) {
|
||||
when {
|
||||
checkNotificationEnabled(activity) -> ifGranted()
|
||||
showRationale && ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.POST_NOTIFICATIONS) ->
|
||||
showNotificationPermissionExplanationDialog(activity)
|
||||
else ->
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
@@ -27,17 +27,14 @@ import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.*
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import ru.noties.markwon.Markwon
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.*
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
@@ -138,6 +135,11 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
||||
val msg = extractReleaseNote(update, Locale.getDefault())
|
||||
setMessage(Markwon.create(context).toMarkdown(msg))
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (!checkNotificationEnabled(context)) {
|
||||
showNotificationPermissionExplanationDialog(context)
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
//Cancel any download queued before
|
||||
@@ -173,7 +175,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<Int>) -> Unit)? = null) {
|
||||
fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((Int) -> Unit)? = null) {
|
||||
if (!URLUtil.isValidUrl(url)) {
|
||||
onFailure?.invoke(IllegalArgumentException())
|
||||
return
|
||||
@@ -191,9 +193,20 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
kotlin.runCatching {
|
||||
Json.decodeFromString<List<Int>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let {
|
||||
favorites.addAll(it)
|
||||
onSuccess?.invoke(it)
|
||||
val data = Json.parseToJsonElement(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]")
|
||||
|
||||
when (data) {
|
||||
is JsonArray -> favorites.addAll(data.map { it.jsonPrimitive.int })
|
||||
is JsonObject -> {
|
||||
val newFavorites = data["favorites"]?.let { Json.decodeFromJsonElement<List<Int>>(it) }.orEmpty()
|
||||
val newFavoriteTags = data["favorite_tags"]?.let { Json.decodeFromJsonElement<List<Tag>>(it) }.orEmpty()
|
||||
|
||||
favorites.addAll(newFavorites)
|
||||
favoriteTags.addAll(newFavoriteTags)
|
||||
|
||||
onSuccess?.invoke(favorites.size + favoriteTags.size)
|
||||
}
|
||||
else -> error("data is neither JsonArray or JsonObject")
|
||||
}
|
||||
}.onFailure { onFailure?.invoke(it) }
|
||||
}
|
||||
|
||||
31
app/src/main/res/raw/isrgrootx1.pem
Normal file
31
app/src/main/res/raw/isrgrootx1.pem
Normal file
@@ -0,0 +1,31 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
||||
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
||||
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
||||
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
||||
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
||||
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
||||
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
||||
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
||||
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
||||
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
||||
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
||||
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
||||
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
||||
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
||||
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
||||
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
||||
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
||||
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
||||
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
||||
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
||||
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
||||
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
||||
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
||||
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
||||
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
||||
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -22,6 +22,7 @@
|
||||
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
|
||||
<string name="settings_clear_history_summary">履歴数: %1$d</string>
|
||||
<string name="main_drawer_history">履歴</string>
|
||||
<string name="notification_denied">通知を無効にするとバックグラウンドダウンロード及びアプリのアップデート機能が使用不可になります。</string>
|
||||
<string name="main_drawer_home">トップ</string>
|
||||
<string name="update_release_note"># リリースノート(v%1$s)\n%2$s</string>
|
||||
<string name="settings_security_mode_title">セキュリティーモード</string>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
||||
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
|
||||
<string name="main_drawer_history">기록</string>
|
||||
<string name="notification_denied">백그라운드 다운로드를 위해서는 알림을 활성화할 필요가 있습니다. 알림을 비활성화하면 백그라운드 다운로드와 앱 업데이트 기능을 사용할 수 없습니다.</string>
|
||||
<string name="main_drawer_home">홈</string>
|
||||
<string name="update_release_note"># 릴리즈 노트(v%1$s)\n%2$s</string>
|
||||
<string name="settings_security_mode_summary">최근 앱 목록 창에서 앱 화면을 보이지 않게 합니다</string>
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
|
||||
<string name="unaccessible_download_folder">From Android 11 and above, current Download folder cannot be accessed by outside apps. Would you like to change the download folder?</string>
|
||||
|
||||
<string name="notification_denied">Notification permission is required for background downloads. If you deny notifications from this app, in-app update and background download will be disabled.</string>
|
||||
|
||||
<string name="main_drawer_home">Home</string>
|
||||
<string name="main_drawer_history">History</string>
|
||||
<string name="main_drawer_downloads">Downloads</string>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
-->
|
||||
|
||||
<paths>
|
||||
<external-path name="external" path="/"/>
|
||||
<external-files-path name="files" path="/"/>
|
||||
<external-path name="external" path="."/>
|
||||
<external-files-path name="files" path="."/>
|
||||
<files-path name="files" path="." />
|
||||
</paths>
|
||||
10
build.gradle
10
build.gradle
@@ -6,16 +6,16 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.0'
|
||||
classpath 'com.android.tools.build:gradle:7.3.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath "com.google.gms:google-services:4.3.10"
|
||||
classpath "com.google.gms:google-services:4.3.15"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
classpath "com.google.firebase:firebase-crashlytics-gradle:2.8.1"
|
||||
classpath "com.google.firebase:perf-plugin:1.4.1"
|
||||
classpath "com.google.android.gms:oss-licenses-plugin:0.10.4"
|
||||
classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.9"
|
||||
classpath "com.google.firebase:perf-plugin:1.4.2"
|
||||
classpath "com.google.android.gms:oss-licenses-plugin:0.10.6"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,4 +20,4 @@ kotlin.code.style=official
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
|
||||
kotlin_version=1.6.10
|
||||
kotlin_version=1.9.0
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||
|
||||
Reference in New Issue
Block a user