Compare commits
129 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4e8f20b26 | ||
|
|
4531a6b05f | ||
|
|
2ef70d0da0 | ||
|
|
4fe769cbbf | ||
|
|
de068a760e | ||
|
|
a183ff803d | ||
|
|
e84c381423 | ||
|
|
85a7eeeeea | ||
|
|
0d9fb97bbb | ||
|
|
9ed3631c30 | ||
|
|
0a68df6492 | ||
|
|
f3f47d9407 | ||
|
|
b6ff956637 | ||
|
|
aa99c18a1b | ||
|
|
1be5ecac6b | ||
|
|
c8eff885b5 | ||
|
|
e0eb187442 | ||
|
|
0fbce15e41 | ||
|
|
f11dce968e | ||
|
|
df341c777e | ||
|
|
bed6d7d6ab | ||
|
|
97eb85e97c | ||
|
|
4448f61430 | ||
|
|
9beb4ded2e | ||
|
|
fc3a0fa178 | ||
|
|
03e71b3000 | ||
|
|
8503c64f04 | ||
|
|
0dd25faced | ||
|
|
00cf429bd9 | ||
|
|
628d42703f | ||
|
|
f271e61ea2 | ||
|
|
2e11a4907a | ||
|
|
0e19d6c9b2 | ||
|
|
850ac3ea83 | ||
|
|
c3c5761ffa | ||
|
|
0ff91d76b1 | ||
|
|
cd4be5898b | ||
|
|
d80de6fde7 | ||
|
|
193db578f0 | ||
|
|
3abd015505 | ||
|
|
84c536a597 | ||
|
|
480bbd3628 | ||
|
|
b708437a16 | ||
|
|
fcbe107fe7 | ||
|
|
bf3e7d7117 | ||
|
|
f78c66a9f4 | ||
|
|
7e52a2e296 | ||
|
|
4625bb5806 | ||
|
|
d626cc09d5 | ||
|
|
57c4e249cf | ||
|
|
016ce3ff42 | ||
|
|
383baa900c | ||
|
|
551b4cae80 | ||
|
|
0c13ad6869 | ||
|
|
8b2e388a81 | ||
|
|
c34b0f6f0f | ||
|
|
f6f0ed40c1 | ||
|
|
b82ef8695c | ||
|
|
0f4e1a8e0d | ||
|
|
20ddf04614 | ||
|
|
7befa24aff | ||
|
|
93d68d3867 | ||
|
|
9037b41b49 | ||
|
|
02751233f8 | ||
|
|
a57b1d5614 | ||
|
|
adf18341d0 | ||
|
|
bdd2bc8645 | ||
|
|
338b789e62 | ||
|
|
98fda1a53f | ||
|
|
e7debfec46 | ||
|
|
62d0de3ef6 | ||
|
|
ef0f71310b | ||
|
|
052990c4ef | ||
|
|
077d9b976c | ||
|
|
78ba11ca5f | ||
|
|
b690d01243 | ||
|
|
458530e80c | ||
|
|
ddbfd0a201 | ||
|
|
6c13a624a9 | ||
|
|
70452ba7a6 | ||
|
|
14c64299ec | ||
|
|
c2626cdee4 | ||
|
|
52a945d0d9 | ||
|
|
29aefa4197 | ||
|
|
cfe6a814d4 | ||
|
|
9ef7852bab | ||
|
|
0a1e0a2dcf | ||
|
|
5b9a83cbcc | ||
|
|
fc61522955 | ||
|
|
35ee438376 | ||
|
|
8cc89101e7 | ||
|
|
2150d086e0 | ||
|
|
a9a07ddcfa | ||
|
|
32d49833d8 | ||
|
|
2a92d287af | ||
|
|
975b98e4dc | ||
|
|
237d5accc5 | ||
|
|
760194bde8 | ||
|
|
ff0df0d9cc | ||
|
|
dd60a1fdfb | ||
|
|
5a19fb8336 | ||
|
|
51851addc1 | ||
|
|
00c8078642 | ||
|
|
ca54fb6eb0 | ||
|
|
1107cf1a9c | ||
|
|
1dea88a135 | ||
|
|
6fae9e9a30 | ||
|
|
a1c6d87c54 | ||
|
|
80b7293879 | ||
|
|
2f57ee4c83 | ||
|
|
3f8aa744e7 | ||
|
|
fb11149b78 | ||
|
|
8b41c706b6 | ||
|
|
5a61fcf6ee | ||
|
|
c7b3ae7ed1 | ||
|
|
4aea7d08ce | ||
|
|
2f16838e1e | ||
|
|
619730e2ab | ||
|
|
c8aa26e2d9 | ||
|
|
8703fde9b1 | ||
|
|
3051d800bd | ||
|
|
521f3ad809 | ||
|
|
730a3baedc | ||
|
|
26c5e07f04 | ||
|
|
3feae80359 | ||
|
|
24aedfc400 | ||
|
|
aa6cc80172 | ||
|
|
74ed9e9e42 | ||
|
|
2b7b86da96 |
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
|
*.iml
|
||||||
.gradle
|
.idea/
|
||||||
/local.properties
|
misc.xml
|
||||||
/.idea/caches
|
deploymentTargetDropDown.xml
|
||||||
/.idea/libraries
|
render.experimental.xml
|
||||||
/.idea/modules.xml
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.idea/navEditor.xml
|
|
||||||
/.idea/assetWizardSettings.xml
|
|
||||||
.DS_Store
|
|
||||||
/build
|
|
||||||
/captures
|
|
||||||
.externalNativeBuild
|
|
||||||
|
|
||||||
#Github pages
|
# Keystore files
|
||||||
/gh-pages
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
|
||||||
#Private files
|
# Google Services (e.g. APIs or Firebase)
|
||||||
**/google-services.json
|
google-services.json
|
||||||
**/credentials.json
|
|
||||||
|
# Android Profiling
|
||||||
|
*.hprof
|
||||||
|
|||||||
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
@@ -1,5 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<state>
|
|
||||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
|
||||||
</state>
|
|
||||||
</component>
|
|
||||||
6
.idea/compiler.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CompilerConfiguration">
|
|
||||||
<bytecodeTargetLevel target="1.8" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
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
@@ -1,7 +0,0 @@
|
|||||||
<component name="CopyrightManager">
|
|
||||||
<settings>
|
|
||||||
<module2copyright>
|
|
||||||
<element module="Project Files" copyright="GPL" />
|
|
||||||
</module2copyright>
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
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_3a_API_30_x86.avd" />
|
|
||||||
</Key>
|
|
||||||
</deviceKey>
|
|
||||||
</Target>
|
|
||||||
</targetSelectedWithDropDown>
|
|
||||||
<timeTargetWasSelectedWithDropDown value="2021-10-25T00:27:52.904947Z" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
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
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
|
|
||||||
</project>
|
|
||||||
20
.idea/gradle.xml
generated
@@ -1,20 +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="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
@@ -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>
|
|
||||||
85
.idea/jarRepositories.xml
generated
@@ -1,85 +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>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
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
@@ -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>
|
|
||||||
9
.idea/misc.xml
generated
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" 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
@@ -1,3 +0,0 @@
|
|||||||
<component name="DependencyValidationManager">
|
|
||||||
<scope name="Pupil" pattern="file[app]:*/" />
|
|
||||||
</component>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<changelist name="Uncommitted_changes_before_Update_at_10_29_21,_4_40_PM_[Default_Changelist]" date="1635493226384" recycled="true" deleted="true">
|
|
||||||
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/Uncommitted_changes_before_Update_at_10_29_21,_4_40_PM_[Default_Changelist]/shelved.patch" />
|
|
||||||
<option name="DESCRIPTION" value="Uncommitted changes before Update at 10/29/21, 4:40 PM [Default Changelist]" />
|
|
||||||
</changelist>
|
|
||||||
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,8 +2,8 @@
|
|||||||
*Pupil, Hitomi.la viewer for Android*
|
*Pupil, Hitomi.la viewer for Android*
|
||||||
|
|
||||||

|

|
||||||
[](https://github.com/tom5079/Pupil/releases/download/5.1.20/Pupil-v5.1.20.apk)
|
[](https://github.com/tom5079/Pupil/releases/download/5.1.6-hotfix7/Pupil-v5.1.6-hotfix7.apk)
|
||||||
[](https://discord.gg/Stj4b5v)
|
[](https://discord.gg/Stj4b5v)
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||

|

|
||||||
@@ -20,7 +20,7 @@ or Build app yourself
|
|||||||
|
|
||||||
# Contribution
|
# Contribution
|
||||||
|
|
||||||
Any kind of contribution is appreciated. Feel free to leave PR!
|
Any kind of contribution is appriciated. Feel free to leave PR!
|
||||||
|
|
||||||
## Tag Translation
|
## Tag Translation
|
||||||
Head over to [tags branch](https://github.com/tom5079/Pupil/tree/tags)
|
Head over to [tags branch](https://github.com/tom5079/Pupil/tree/tags)
|
||||||
|
|||||||
137
app/build.gradle
@@ -1,137 +0,0 @@
|
|||||||
apply plugin: "com.android.application"
|
|
||||||
apply plugin: "kotlin-android"
|
|
||||||
apply plugin: "kotlin-kapt"
|
|
||||||
apply plugin: "kotlin-parcelize"
|
|
||||||
apply plugin: "kotlinx-serialization"
|
|
||||||
apply plugin: "com.google.android.gms.oss-licenses-plugin"
|
|
||||||
|
|
||||||
if (file("google-services.json").exists()) {
|
|
||||||
logger.lifecycle("Firebase Enabled")
|
|
||||||
apply plugin: "com.google.gms.google-services"
|
|
||||||
apply plugin: "com.google.firebase.crashlytics"
|
|
||||||
apply plugin: "com.google.firebase.firebase-perf"
|
|
||||||
} else {
|
|
||||||
logger.lifecycle("Firebase Disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
ext {
|
|
||||||
okhttp_version = "3.12.12"
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
|
||||||
all {
|
|
||||||
resolutionStrategy {
|
|
||||||
eachDependency { DependencyResolveDetails details ->
|
|
||||||
if (details.requested.group == "com.squareup.okhttp3" && details.requested.name == "okhttp") {
|
|
||||||
// OkHttp drops support before 5.0 since 3.13.0
|
|
||||||
details.useVersion okhttp_version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 30
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "xyz.quaver.pupil"
|
|
||||||
minSdkVersion 16
|
|
||||||
targetSdkVersion 30
|
|
||||||
versionCode 68
|
|
||||||
versionName "5.1.21"
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
defaultConfig.minSdkVersion 21
|
|
||||||
|
|
||||||
minifyEnabled false
|
|
||||||
shrinkResources false
|
|
||||||
|
|
||||||
debuggable true
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
versionNameSuffix "-DEBUG"
|
|
||||||
|
|
||||||
ext.enableCrashlytics = false
|
|
||||||
ext.alwaysUpdateBuildId = false
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
minifyEnabled true
|
|
||||||
shrinkResources true
|
|
||||||
|
|
||||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0"
|
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:1.3.0"
|
|
||||||
implementation "androidx.activity:activity-ktx:1.3.0-beta01"
|
|
||||||
implementation "androidx.fragment:fragment-ktx:1.3.4"
|
|
||||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
|
|
||||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
|
||||||
implementation "androidx.biometric:biometric:1.1.0"
|
|
||||||
implementation "androidx.work:work-runtime-ktx:2.6.0-beta01"
|
|
||||||
|
|
||||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
|
||||||
|
|
||||||
implementation "com.google.android.material:material:1.3.0"
|
|
||||||
|
|
||||||
implementation platform('com.google.firebase:firebase-bom:26.5.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:16.1.7"
|
|
||||||
|
|
||||||
implementation "com.github.clans:fab:1.6.4"
|
|
||||||
|
|
||||||
//implementation "com.quiph.ui:recyclerviewfastscroller:0.2.1"
|
|
||||||
|
|
||||||
implementation 'com.github.piasy:BigImageViewer:1.8.1'
|
|
||||||
implementation 'com.github.piasy:FrescoImageLoader:1.8.1'
|
|
||||||
implementation 'com.github.piasy:FrescoImageViewFactory:1.8.1'
|
|
||||||
|
|
||||||
//noinspection GradleDependency
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
|
||||||
|
|
||||||
implementation "com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2"
|
|
||||||
|
|
||||||
implementation "net.rdrei.android.dirchooser:library:3.2@aar"
|
|
||||||
implementation "com.gu:option:1.3"
|
|
||||||
|
|
||||||
implementation "com.andrognito.patternlockview:patternlockview:1.0.0"
|
|
||||||
//implementation "com.andrognito.pinlockview:pinlockview:2.1.0"
|
|
||||||
|
|
||||||
implementation "ru.noties.markwon:core:3.1.0"
|
|
||||||
|
|
||||||
implementation "xyz.quaver:libpupil:2.1.12"
|
|
||||||
implementation "xyz.quaver:documentfilex:0.7.1"
|
|
||||||
implementation "xyz.quaver:floatingsearchview:1.1.7"
|
|
||||||
|
|
||||||
testImplementation "junit:junit:4.13.1"
|
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
|
||||||
androidTestImplementation "androidx.test:rules:1.3.0"
|
|
||||||
androidTestImplementation "androidx.test:runner:1.3.0"
|
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0"
|
|
||||||
}
|
|
||||||
183
app/build.gradle.kts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import com.google.protobuf.gradle.*
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
kotlin("android")
|
||||||
|
kotlin("kapt")
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
id("kotlinx-serialization")
|
||||||
|
id("com.google.android.gms.oss-licenses-plugin")
|
||||||
|
id("com.google.gms.google-services")
|
||||||
|
id("com.google.firebase.crashlytics")
|
||||||
|
id("com.google.firebase.firebase-perf")
|
||||||
|
id("com.google.protobuf")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = 33
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
storeFile = File(System.getenv("SIGNING_STORE_FILE"))
|
||||||
|
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
|
||||||
|
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
|
||||||
|
keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "xyz.quaver.pupil"
|
||||||
|
minSdk = 21
|
||||||
|
targetSdk = 33
|
||||||
|
versionCode = 600
|
||||||
|
versionName = VERSION
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
getByName("debug") {
|
||||||
|
isDebuggable = true
|
||||||
|
applicationIdSuffix = ".debug"
|
||||||
|
versionNameSuffix = "-DEBUG"
|
||||||
|
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
|
||||||
|
extra.set("enableCrashlytics", false)
|
||||||
|
extra.set("alwaysUpdateBuildId", false)
|
||||||
|
}
|
||||||
|
getByName("release") {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
applicationIdSuffix = ".beta"
|
||||||
|
|
||||||
|
isCrunchPngs = false
|
||||||
|
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = Versions.JETPACK_COMPOSE
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources.excludes.addAll(
|
||||||
|
listOf(
|
||||||
|
"META-INF/AL2.0",
|
||||||
|
"META-INF/LGPL2.1"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
namespace = "xyz.quaver.pupil"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
|
implementation(Kotlin.SERIALIZATION)
|
||||||
|
implementation(Kotlin.COROUTINE)
|
||||||
|
|
||||||
|
implementation("androidx.activity:activity-compose:1.6.1")
|
||||||
|
implementation("androidx.navigation:navigation-compose:2.5.3")
|
||||||
|
|
||||||
|
implementation(JetpackCompose.FOUNDATION)
|
||||||
|
implementation(JetpackCompose.UI)
|
||||||
|
implementation(JetpackCompose.UI_UTIL)
|
||||||
|
implementation(JetpackCompose.UI_TOOLING)
|
||||||
|
implementation(JetpackCompose.ANIMATION)
|
||||||
|
implementation(JetpackCompose.MATERIAL)
|
||||||
|
implementation(JetpackCompose.MATERIAL_ICONS)
|
||||||
|
implementation(JetpackCompose.RUNTIME_LIVEDATA)
|
||||||
|
|
||||||
|
// implementation(JetpackCompose.MARKDOWN)
|
||||||
|
|
||||||
|
implementation(Accompanist.INSETS)
|
||||||
|
implementation(Accompanist.INSETS_UI)
|
||||||
|
implementation(Accompanist.FLOW_LAYOUT)
|
||||||
|
implementation(Accompanist.SYSTEM_UI_CONTROLLER)
|
||||||
|
implementation(Accompanist.DRAWABLE_PAINTER)
|
||||||
|
implementation(Accompanist.APPCOMPAT_THEME)
|
||||||
|
|
||||||
|
implementation("io.coil-kt:coil-compose:2.0.0-rc03")
|
||||||
|
|
||||||
|
implementation(KtorClient.CORE)
|
||||||
|
implementation(KtorClient.OKHTTP)
|
||||||
|
implementation(KtorClient.CONTENT_NEGOTIATION)
|
||||||
|
implementation(KtorClient.SERIALIZATION)
|
||||||
|
|
||||||
|
implementation("androidx.room:room-runtime:2.4.3")
|
||||||
|
annotationProcessor("androidx.room:room-compiler:2.4.3")
|
||||||
|
kapt("androidx.room:room-compiler:2.4.3")
|
||||||
|
implementation("androidx.room:room-ktx:2.4.3")
|
||||||
|
|
||||||
|
implementation("androidx.datastore:datastore:1.0.0")
|
||||||
|
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
||||||
|
|
||||||
|
implementation("org.kodein.di:kodein-di-framework-compose:7.11.0")
|
||||||
|
|
||||||
|
implementation(platform("com.google.firebase:firebase-bom:29.0.3"))
|
||||||
|
implementation("com.google.firebase:firebase-analytics-ktx")
|
||||||
|
implementation("com.google.firebase:firebase-crashlytics-ktx")
|
||||||
|
implementation("com.google.firebase:firebase-perf-ktx")
|
||||||
|
|
||||||
|
implementation("com.google.protobuf:protobuf-javalite:3.19.1")
|
||||||
|
|
||||||
|
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
||||||
|
|
||||||
|
implementation("org.jsoup:jsoup:1.14.3")
|
||||||
|
|
||||||
|
implementation("xyz.quaver.pupil.sources:core:0.0.1-alpha01-DEV29")
|
||||||
|
|
||||||
|
implementation("xyz.quaver:documentfilex:0.7.2")
|
||||||
|
implementation("xyz.quaver:subsampledimage:0.0.1-alpha22-SNAPSHOT")
|
||||||
|
|
||||||
|
implementation("org.kodein.log:kodein-log:0.12.0")
|
||||||
|
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.8.1")
|
||||||
|
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
testImplementation("org.mockito:mockito-inline:4.4.0")
|
||||||
|
testImplementation(KtorClient.TEST)
|
||||||
|
testImplementation(Kotlin.COROUTINE_TEST)
|
||||||
|
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||||
|
androidTestImplementation("androidx.test:rules:1.4.0")
|
||||||
|
androidTestImplementation("androidx.test:runner:1.4.0")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||||
|
androidTestImplementation(KtorClient.TEST)
|
||||||
|
|
||||||
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.1.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = "com.google.protobuf:protoc:3.19.1"
|
||||||
|
}
|
||||||
|
generateProtoTasks {
|
||||||
|
all().forEach { task ->
|
||||||
|
task.builtins {
|
||||||
|
id("java") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task<Exec>("clearAppCache") {
|
||||||
|
commandLine("adb", "shell", "pm", "clear", "xyz.quaver.pupil.debug")
|
||||||
|
}
|
||||||
1
app/credentials.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"installed":{"client_id":"644157827114-rnbcmlqiaqgg295o45kavchnvi3dedbo.apps.googleusercontent.com","project_id":"pupil-1598439316578","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}
|
||||||
24
app/proguard-rules.pro
vendored
@@ -1,6 +1,6 @@
|
|||||||
# Add project specific ProGuard rules here.
|
# Add project specific ProGuard rules here.
|
||||||
# You can control the set of applied configuration files using the
|
# You can control the set of applied configuration files using the
|
||||||
# proguardFiles setting in build.gradle.
|
# proguardFiles setting in build.gradle.kts.
|
||||||
#
|
#
|
||||||
# For more details, see
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
@@ -23,13 +23,23 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
-keepattributes *Annotation*, InnerClasses
|
-keepattributes *Annotation*, InnerClasses
|
||||||
-dontnote kotlinx.serialization.SerializationKt
|
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||||
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; } # <-- change package name to your app's
|
|
||||||
-keepclassmembers class xyz.quaver.pupil.** { # <-- change package name to your app's
|
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||||
|
-keepclassmembers class kotlinx.serialization.json.** {
|
||||||
*** Companion;
|
*** Companion;
|
||||||
}
|
}
|
||||||
-keepclasseswithmembers class xyz.quaver.pupil.** { # <-- change package name to your app's
|
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
|
|
||||||
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
|
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; }
|
||||||
|
-keepclassmembers class xyz.quaver.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class xyz.quaver.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 3,
|
||||||
"artifactType": {
|
"artifactType": {
|
||||||
"type": "APK",
|
"type": "APK",
|
||||||
"kind": "Directory"
|
"kind": "Directory"
|
||||||
},
|
},
|
||||||
"applicationId": "xyz.quaver.pupil",
|
"applicationId": "xyz.quaver.pupil.beta",
|
||||||
"variantName": "release",
|
"variantName": "release",
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
"type": "SINGLE",
|
"type": "SINGLE",
|
||||||
"filters": [],
|
"filters": [],
|
||||||
"versionCode": 68,
|
"attributes": [],
|
||||||
"versionName": "5.1.21",
|
"versionCode": 600,
|
||||||
|
"versionName": "6.0.0-alpha02",
|
||||||
"outputFile": "app-release.apk"
|
"outputFile": "app-release.apk"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"elementType": "File"
|
||||||
}
|
}
|
||||||
@@ -20,8 +20,18 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.google.api.Http
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.engine.okhttp.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="xyz.quaver.pupil">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission-sdk-23 android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||||
<uses-permission-sdk-23 android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||||
<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.VIBRATE"/>
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Pupil"
|
android:name=".Pupil"
|
||||||
@@ -25,15 +21,12 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
tools:replace="android:theme"
|
tools:replace="android:theme"
|
||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
|
||||||
android:value="face" />
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
@@ -41,189 +34,14 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_paths" />
|
android:resource="@xml/file_paths" />
|
||||||
|
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<service android:name=".services.DownloadService"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".receiver.UpdateBroadcastReceiver"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<activity android:name=".ui.LockActivity" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.ReaderActivity"
|
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
|
||||||
android:parentActivityName=".ui.MainActivity">
|
|
||||||
<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="/galleries"
|
|
||||||
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="hitomi.la"
|
|
||||||
android:pathPrefix="/manga"
|
|
||||||
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="hitomi.la"
|
|
||||||
android:pathPrefix="/doujinshi"
|
|
||||||
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="hitomi.la"
|
|
||||||
android:pathPrefix="/cg"
|
|
||||||
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="hitomi.la"
|
|
||||||
android:pathPrefix="/reader"
|
|
||||||
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="hitomi.la"
|
|
||||||
android:pathPrefix="/galleries"
|
|
||||||
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="/manga"
|
|
||||||
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="/doujinshi"
|
|
||||||
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="/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" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ui.SettingsActivity"
|
|
||||||
android:label="@string/settings_title">
|
|
||||||
<tools:validation testUrl="http://ix.io/eer" />
|
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
android:theme="@style/NoActionBarAppTheme">
|
android:theme="@style/NoActionBarAppTheme"
|
||||||
|
android:exported="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -232,17 +50,8 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<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>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -23,120 +23,34 @@ import android.app.Notification
|
|||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.github.piasy.biv.BigImageViewer
|
|
||||||
import com.github.piasy.biv.loader.fresco.FrescoImageLoader
|
|
||||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
import com.google.android.gms.security.ProviderInstaller
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import io.ktor.client.*
|
||||||
import okhttp3.Dispatcher
|
import io.ktor.client.engine.okhttp.*
|
||||||
import okhttp3.Interceptor
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import okhttp3.OkHttpClient
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import okhttp3.Response
|
import org.kodein.di.*
|
||||||
import xyz.quaver.io.FileX
|
import org.kodein.di.android.x.androidXModule
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.sources.core.NetworkCache
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.sources.core.settingsDataStore
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.PupilHttpClient
|
||||||
import xyz.quaver.setClient
|
|
||||||
import java.io.File
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
typealias PupilInterceptor = (Interceptor.Chain) -> Response
|
class Pupil : Application(), DIAware {
|
||||||
|
|
||||||
lateinit var histories: SavedSet<Int>
|
override val di: DI by DI.lazy {
|
||||||
private set
|
import(androidXModule(this@Pupil))
|
||||||
lateinit var favorites: SavedSet<Int>
|
|
||||||
private set
|
|
||||||
lateinit var favoriteTags: SavedSet<Tag>
|
|
||||||
private set
|
|
||||||
lateinit var searchHistory: SavedSet<String>
|
|
||||||
private set
|
|
||||||
|
|
||||||
val interceptors = mutableMapOf<KClass<out Any>, PupilInterceptor>()
|
bind { singleton { NetworkCache(this@Pupil) } }
|
||||||
|
|
||||||
lateinit var clientBuilder: OkHttpClient.Builder
|
bindSingleton { settingsDataStore }
|
||||||
|
|
||||||
var clientHolder: OkHttpClient? = null
|
bind { singleton { PupilHttpClient(OkHttp.create()) } }
|
||||||
val client: OkHttpClient
|
|
||||||
get() = clientHolder ?: clientBuilder.build().also {
|
|
||||||
clientHolder = it
|
|
||||||
setClient(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Pupil : Application() {
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
super.onCreate()
|
||||||
|
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
|
|
||||||
val userID = Preferences["user_id", ""].let { userID ->
|
|
||||||
if (userID.isEmpty()) UUID.randomUUID().toString().also { Preferences["user_id"] = it }
|
|
||||||
else userID
|
|
||||||
}
|
|
||||||
|
|
||||||
FirebaseCrashlytics.getInstance().setUserId(userID)
|
|
||||||
|
|
||||||
val proxyInfo = getProxyInfo()
|
|
||||||
|
|
||||||
clientBuilder = OkHttpClient.Builder()
|
|
||||||
.connectTimeout(0, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(0, TimeUnit.SECONDS)
|
|
||||||
.proxyInfo(proxyInfo)
|
|
||||||
.addInterceptor { chain ->
|
|
||||||
val request = chain.request()
|
|
||||||
val tag = request.tag() ?: return@addInterceptor chain.proceed(request)
|
|
||||||
|
|
||||||
interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request)
|
|
||||||
}.apply {
|
|
||||||
(Preferences.get<String>("max_concurrent_download").toIntOrNull() ?: 0).let {
|
|
||||||
if (it != 0)
|
|
||||||
dispatcher(Dispatcher(Executors.newFixedThreadPool(it)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Preferences.get<String>("download_folder").also {
|
|
||||||
if (it.startsWith("content") && Build.VERSION.SDK_INT > 19)
|
|
||||||
contentResolver.takePersistableUriPermission(
|
|
||||||
Uri.parse(it),
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!FileX(this, it).canWrite())
|
|
||||||
throw Exception()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Preferences.remove("download_folder")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Preferences["reset_secure", false]) {
|
|
||||||
Preferences["security_mode"] = false
|
|
||||||
Preferences["reset_secure"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0)
|
|
||||||
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0)
|
|
||||||
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
|
|
||||||
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
|
|
||||||
|
|
||||||
favoriteTags.filter { it.tag.contains('_') }.forEach {
|
|
||||||
favoriteTags.remove(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
if (BuildConfig.DEBUG)
|
|
||||||
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)*/
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProviderInstaller.installIfNeeded(this)
|
ProviderInstaller.installIfNeeded(this)
|
||||||
@@ -146,8 +60,6 @@ class Pupil : Application() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
BigImageViewer.initialize(FrescoImageLoader.with(this))
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
@@ -171,21 +83,6 @@ class Pupil : Application() {
|
|||||||
enableVibration(true)
|
enableVibration(true)
|
||||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.createNotificationChannel(NotificationChannel("import", getString(R.string.channel_update), NotificationManager.IMPORTANCE_LOW).apply {
|
|
||||||
description = getString(R.string.channel_update_description)
|
|
||||||
enableLights(false)
|
|
||||||
enableVibration(false)
|
|
||||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppCompatDelegate.setDefaultNightMode(when (Preferences.get<Boolean>("dark_mode")) {
|
|
||||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
|
||||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
|
||||||
})
|
|
||||||
|
|
||||||
super.onCreate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|
||||||
import com.daimajia.swipe.SwipeLayout
|
|
||||||
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
|
||||||
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
|
|
||||||
import com.github.piasy.biv.loader.ImageLoader
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import xyz.quaver.hitomi.getGallery
|
|
||||||
import xyz.quaver.hitomi.getGalleryInfo
|
|
||||||
import xyz.quaver.hitomi.getReader
|
|
||||||
import xyz.quaver.io.util.getChild
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.databinding.GalleryblockItemBinding
|
|
||||||
import xyz.quaver.pupil.favoriteTags
|
|
||||||
import xyz.quaver.pupil.favorites
|
|
||||||
import xyz.quaver.pupil.types.Tag
|
|
||||||
import xyz.quaver.pupil.ui.view.ProgressCard
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
|
||||||
|
|
||||||
var updateAll = true
|
|
||||||
var thin: Boolean = Preferences["thin"]
|
|
||||||
|
|
||||||
inner class GalleryViewHolder(val binding: GalleryblockItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
private var galleryID: Int = 0
|
|
||||||
|
|
||||||
init {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
while (updateAll) {
|
|
||||||
updateProgress(itemView.context)
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateProgress(context: Context) = CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
with(binding.galleryblockCard) {
|
|
||||||
val imageList = Cache.getInstance(context, galleryID).metadata.imageList
|
|
||||||
|
|
||||||
if (imageList == null) {
|
|
||||||
max = 0
|
|
||||||
return@with
|
|
||||||
}
|
|
||||||
|
|
||||||
progress = imageList.count { it != null }
|
|
||||||
max = imageList.size
|
|
||||||
|
|
||||||
this@GalleryViewHolder.binding.galleryblockId.setOnClickListener {
|
|
||||||
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
|
||||||
ClipData.newPlainText("gallery_id", galleryID.toString())
|
|
||||||
)
|
|
||||||
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
type = if (!imageList.contains(null)) {
|
|
||||||
val downloadManager = DownloadManager.getInstance(context)
|
|
||||||
|
|
||||||
if (downloadManager.getDownloadFolder(galleryID) == null)
|
|
||||||
ProgressCard.Type.CACHE
|
|
||||||
else
|
|
||||||
ProgressCard.Type.DOWNLOAD
|
|
||||||
} else
|
|
||||||
ProgressCard.Type.LOADING
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(galleryID: Int) {
|
|
||||||
this.galleryID = galleryID
|
|
||||||
updateProgress(itemView.context)
|
|
||||||
|
|
||||||
val cache = Cache.getInstance(itemView.context, galleryID)
|
|
||||||
|
|
||||||
val galleryBlock = runBlocking {
|
|
||||||
cache.getGalleryBlock()
|
|
||||||
} ?: return
|
|
||||||
|
|
||||||
val resources = itemView.context.resources
|
|
||||||
val languages = resources.getStringArray(R.array.languages).map {
|
|
||||||
it.split("|").let { split ->
|
|
||||||
Pair(split[0], split[1])
|
|
||||||
}
|
|
||||||
}.toMap()
|
|
||||||
|
|
||||||
val artists = galleryBlock.artists
|
|
||||||
val series = galleryBlock.series
|
|
||||||
|
|
||||||
binding.galleryblockThumbnail.apply {
|
|
||||||
setOnClickListener {
|
|
||||||
itemView.performClick()
|
|
||||||
}
|
|
||||||
setOnLongClickListener {
|
|
||||||
itemView.performLongClick()
|
|
||||||
}
|
|
||||||
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
|
|
||||||
setImageLoaderCallback(object: ImageLoader.Callback {
|
|
||||||
override fun onFail(error: Exception?) {
|
|
||||||
Cache.getInstance(context, galleryID).let { cache ->
|
|
||||||
cache.cacheFolder.getChild(".thumbnail").let { if (it.exists()) it.delete() }
|
|
||||||
cache.downloadFolder?.getChild(".thumbnail")?.let { if (it.exists()) it.delete() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCacheHit(imageType: Int, image: File?) {}
|
|
||||||
override fun onCacheMiss(imageType: Int, image: File?) {}
|
|
||||||
override fun onFinish() {}
|
|
||||||
override fun onProgress(progress: Int) {}
|
|
||||||
override fun onStart() {}
|
|
||||||
override fun onSuccess(image: File?) {}
|
|
||||||
})
|
|
||||||
ssiv?.recycle()
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
cache.getThumbnail().let { launch(Dispatchers.Main) {
|
|
||||||
showImage(it)
|
|
||||||
} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.galleryblockTitle.text = galleryBlock.title
|
|
||||||
with(binding.galleryblockArtist) {
|
|
||||||
text = artists.joinToString { it.wordCapitalize() }
|
|
||||||
visibility = when {
|
|
||||||
artists.isNotEmpty() -> View.VISIBLE
|
|
||||||
else -> View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val gallery = runCatching {
|
|
||||||
getGallery(galleryID)
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
if (gallery?.groups?.isNotEmpty() != true)
|
|
||||||
return@launch
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
text = context.getString(
|
|
||||||
R.string.galleryblock_artist_with_group,
|
|
||||||
artists.joinToString { it.wordCapitalize() },
|
|
||||||
gallery.groups.joinToString { it.wordCapitalize() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
with(binding.galleryblockSeries) {
|
|
||||||
text =
|
|
||||||
resources.getString(
|
|
||||||
R.string.galleryblock_series,
|
|
||||||
series.joinToString(", ") { it.wordCapitalize() })
|
|
||||||
visibility = when {
|
|
||||||
series.isNotEmpty() -> View.VISIBLE
|
|
||||||
else -> View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.galleryblockType.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
|
|
||||||
with(binding.galleryblockLanguage) {
|
|
||||||
text =
|
|
||||||
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
|
|
||||||
visibility = when {
|
|
||||||
galleryBlock.language.isNotEmpty() -> View.VISIBLE
|
|
||||||
else -> View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.galleryblockTagGroup) {
|
|
||||||
onClickListener = {
|
|
||||||
onChipClickedHandler.forEach { callback ->
|
|
||||||
callback.invoke(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.clear()
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
tags.addAll(
|
|
||||||
galleryBlock.relatedTags.sortedBy {
|
|
||||||
val tag = Tag.parse(it)
|
|
||||||
|
|
||||||
if (favoriteTags.contains(tag))
|
|
||||||
-1
|
|
||||||
else
|
|
||||||
when(Tag.parse(it).area) {
|
|
||||||
"female" -> 0
|
|
||||||
"male" -> 1
|
|
||||||
else -> 2
|
|
||||||
}
|
|
||||||
}.map {
|
|
||||||
Tag.parse(it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.galleryblockId.text = galleryBlock.id.toString()
|
|
||||||
binding.galleryblockPagecount.text = "-"
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val pageCount = kotlin.runCatching {
|
|
||||||
getGalleryInfo(galleryBlock.id).files.size
|
|
||||||
}.getOrNull() ?: return@launch
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
binding.galleryblockPagecount.text = itemView.context.getString(R.string.galleryblock_pagecount, pageCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.galleryblockFavorite) {
|
|
||||||
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
|
|
||||||
setOnClickListener {
|
|
||||||
when {
|
|
||||||
favorites.contains(galleryBlock.id) -> {
|
|
||||||
favorites.remove(galleryBlock.id)
|
|
||||||
|
|
||||||
setImageResource(R.drawable.ic_star_empty)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
favorites.add(galleryBlock.id)
|
|
||||||
|
|
||||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
|
|
||||||
this ?: return@apply
|
|
||||||
|
|
||||||
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
|
|
||||||
override fun onAnimationEnd(drawable: Drawable?) {
|
|
||||||
setImageResource(R.drawable.ic_star_filled)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
start()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Make some views invisible to make it thinner
|
|
||||||
if (thin) {
|
|
||||||
binding.galleryblockTagGroup.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
|
||||||
var onDownloadClickedHandler: ((Int) -> Unit)? = null
|
|
||||||
var onDeleteClickedHandler: ((Int) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
return GalleryViewHolder(GalleryblockItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
if (holder is GalleryViewHolder) {
|
|
||||||
val galleryID = galleries[position]
|
|
||||||
|
|
||||||
holder.bind(galleryID)
|
|
||||||
|
|
||||||
holder.binding.galleryblockCard.binding.download.setOnClickListener {
|
|
||||||
onDownloadClickedHandler?.invoke(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.binding.galleryblockCard.binding.delete.setOnClickListener {
|
|
||||||
onDeleteClickedHandler?.invoke(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
mItemManger.bindView(holder.binding.root, position)
|
|
||||||
|
|
||||||
holder.binding.galleryblockCard.binding.swipeLayout.addSwipeListener(object: SwipeLayout.SwipeListener {
|
|
||||||
override fun onStartOpen(layout: SwipeLayout?) {
|
|
||||||
mItemManger.closeAllExcept(layout)
|
|
||||||
|
|
||||||
holder.binding.galleryblockCard.binding.download.text =
|
|
||||||
if (DownloadManager.getInstance(holder.binding.root.context).isDownloading(galleryID))
|
|
||||||
holder.binding.root.context.getString(android.R.string.cancel)
|
|
||||||
else
|
|
||||||
holder.binding.root.context.getString(R.string.main_download)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClose(layout: SwipeLayout?) {}
|
|
||||||
override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {}
|
|
||||||
override fun onOpen(layout: SwipeLayout?) {}
|
|
||||||
override fun onStartClose(layout: SwipeLayout?) {}
|
|
||||||
override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = galleries.size
|
|
||||||
|
|
||||||
override fun getSwipeLayoutResourceId(position: Int) = R.id.swipe_layout
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import android.net.Uri
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco
|
|
||||||
import com.facebook.drawee.controller.BaseControllerListener
|
|
||||||
import com.facebook.drawee.drawable.ScalingUtils
|
|
||||||
import com.facebook.drawee.interfaces.DraweeController
|
|
||||||
import com.facebook.drawee.view.SimpleDraweeView
|
|
||||||
import com.facebook.imagepipeline.image.ImageInfo
|
|
||||||
import com.github.piasy.biv.view.BigImageView
|
|
||||||
import com.github.piasy.biv.view.ImageShownCallback
|
|
||||||
import com.github.piasy.biv.view.ImageViewFactory
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import xyz.quaver.hitomi.GalleryInfo
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.databinding.ReaderItemBinding
|
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
class ReaderAdapter(
|
|
||||||
private val activity: ReaderActivity,
|
|
||||||
private val galleryID: Int
|
|
||||||
) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
|
||||||
var galleryInfo: GalleryInfo? = null
|
|
||||||
|
|
||||||
var isFullScreen = false
|
|
||||||
|
|
||||||
var onItemClickListener : (() -> (Unit))? = null
|
|
||||||
|
|
||||||
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
init {
|
|
||||||
with (binding.image) {
|
|
||||||
setImageViewFactory(FrescoImageViewFactory().apply {
|
|
||||||
updateView = { imageInfo ->
|
|
||||||
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
|
||||||
dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setImageShownCallback(object : ImageShownCallback {
|
|
||||||
override fun onMainImageShown() {
|
|
||||||
binding.image.mainView.let { v ->
|
|
||||||
when (v) {
|
|
||||||
is SubsamplingScaleImageView ->
|
|
||||||
if (!isFullScreen) binding.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onThumbnailShown() {}
|
|
||||||
})
|
|
||||||
|
|
||||||
setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
|
|
||||||
setOnClickListener {
|
|
||||||
onItemClickListener?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.setOnClickListener {
|
|
||||||
onItemClickListener?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(position: Int) {
|
|
||||||
if (cache == null)
|
|
||||||
cache = Cache.getInstance(itemView.context, galleryID)
|
|
||||||
|
|
||||||
if (!isFullScreen) {
|
|
||||||
binding.root.setBackgroundResource(R.drawable.reader_item_boundary)
|
|
||||||
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
|
||||||
height = 0
|
|
||||||
dimensionRatio =
|
|
||||||
"${galleryInfo!!.files[position].width}:${galleryInfo!!.files[position].height}"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
|
||||||
height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
|
||||||
dimensionRatio = null
|
|
||||||
}
|
|
||||||
binding.root.background = null
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.readerIndex.text = (position+1).toString()
|
|
||||||
|
|
||||||
val image = cache!!.getImage(position)
|
|
||||||
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
|
|
||||||
|
|
||||||
if (progress?.isInfinite() == true && image != null) {
|
|
||||||
binding.progressGroup.visibility = View.INVISIBLE
|
|
||||||
binding.image.showImage(image.uri)
|
|
||||||
} else {
|
|
||||||
binding.progressGroup.visibility = View.VISIBLE
|
|
||||||
binding.readerItemProgressbar.progress =
|
|
||||||
if (progress?.isInfinite() == true)
|
|
||||||
100
|
|
||||||
else
|
|
||||||
progress?.roundToInt() ?: 0
|
|
||||||
|
|
||||||
clear()
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
delay(1000)
|
|
||||||
notifyItemChanged(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear() {
|
|
||||||
binding.image.mainView.let {
|
|
||||||
when (it) {
|
|
||||||
is SubsamplingScaleImageView ->
|
|
||||||
it.recycle()
|
|
||||||
is SimpleDraweeView ->
|
|
||||||
it.controller = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
return ViewHolder(ReaderItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cache: Cache? = null
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
holder.bind(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = galleryInfo?.files?.size ?: 0
|
|
||||||
|
|
||||||
override fun onViewRecycled(holder: ViewHolder) {
|
|
||||||
holder.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class FrescoImageViewFactory : ImageViewFactory() {
|
|
||||||
var updateView: ((ImageInfo) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun createAnimatedImageView(
|
|
||||||
context: Context, imageType: Int,
|
|
||||||
initScaleType: Int
|
|
||||||
): View {
|
|
||||||
val view = SimpleDraweeView(context)
|
|
||||||
view.hierarchy.actualImageScaleType = scaleType(initScaleType)
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadAnimatedContent(
|
|
||||||
view: View, imageType: Int,
|
|
||||||
imageFile: File
|
|
||||||
) {
|
|
||||||
if (view is SimpleDraweeView) {
|
|
||||||
val controller: DraweeController = Fresco.newDraweeControllerBuilder()
|
|
||||||
.setUri(Uri.parse("file://" + imageFile.absolutePath))
|
|
||||||
.setAutoPlayAnimations(true)
|
|
||||||
.setControllerListener(object: BaseControllerListener<ImageInfo>() {
|
|
||||||
override fun onIntermediateImageSet(id: String?, imageInfo: ImageInfo?) {
|
|
||||||
imageInfo?.let { updateView?.invoke(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
|
|
||||||
imageInfo?.let { updateView?.invoke(it) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
view.controller = controller
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createThumbnailView(
|
|
||||||
context: Context,
|
|
||||||
scaleType: ImageView.ScaleType, willLoadFromNetwork: Boolean
|
|
||||||
): View {
|
|
||||||
return if (willLoadFromNetwork) {
|
|
||||||
val thumbnailView = SimpleDraweeView(context)
|
|
||||||
thumbnailView.hierarchy.actualImageScaleType = scaleType(scaleType)
|
|
||||||
thumbnailView
|
|
||||||
} else {
|
|
||||||
super.createThumbnailView(context, scaleType, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadThumbnailContent(view: View, thumbnail: Uri) {
|
|
||||||
if (view is SimpleDraweeView) {
|
|
||||||
val controller: DraweeController = Fresco.newDraweeControllerBuilder()
|
|
||||||
.setUri(thumbnail)
|
|
||||||
.build()
|
|
||||||
view.controller = controller
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scaleType(value: Int): ScalingUtils.ScaleType {
|
|
||||||
return when (value) {
|
|
||||||
BigImageView.INIT_SCALE_TYPE_CENTER -> ScalingUtils.ScaleType.CENTER
|
|
||||||
BigImageView.INIT_SCALE_TYPE_CENTER_CROP -> ScalingUtils.ScaleType.CENTER_CROP
|
|
||||||
BigImageView.INIT_SCALE_TYPE_CENTER_INSIDE -> ScalingUtils.ScaleType.CENTER_INSIDE
|
|
||||||
BigImageView.INIT_SCALE_TYPE_FIT_END -> ScalingUtils.ScaleType.FIT_END
|
|
||||||
BigImageView.INIT_SCALE_TYPE_FIT_START -> ScalingUtils.ScaleType.FIT_START
|
|
||||||
BigImageView.INIT_SCALE_TYPE_FIT_XY -> ScalingUtils.ScaleType.FIT_XY
|
|
||||||
BigImageView.INIT_SCALE_TYPE_FIT_CENTER -> ScalingUtils.ScaleType.FIT_CENTER
|
|
||||||
else -> ScalingUtils.ScaleType.FIT_CENTER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scaleType(scaleType: ImageView.ScaleType): ScalingUtils.ScaleType {
|
|
||||||
return when (scaleType) {
|
|
||||||
ImageView.ScaleType.CENTER -> ScalingUtils.ScaleType.CENTER
|
|
||||||
ImageView.ScaleType.CENTER_CROP -> ScalingUtils.ScaleType.CENTER_CROP
|
|
||||||
ImageView.ScaleType.CENTER_INSIDE -> ScalingUtils.ScaleType.CENTER_INSIDE
|
|
||||||
ImageView.ScaleType.FIT_END -> ScalingUtils.ScaleType.FIT_END
|
|
||||||
ImageView.ScaleType.FIT_START -> ScalingUtils.ScaleType.FIT_START
|
|
||||||
ImageView.ScaleType.FIT_XY -> ScalingUtils.ScaleType.FIT_XY
|
|
||||||
ImageView.ScaleType.FIT_CENTER -> ScalingUtils.ScaleType.FIT_CENTER
|
|
||||||
else -> ScalingUtils.ScaleType.FIT_CENTER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.github.piasy.biv.view.BigImageView
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
|
|
||||||
class ThumbnailAdapter(var thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
class ViewHolder(val view: BigImageView) : RecyclerView.ViewHolder(view) {
|
|
||||||
fun clear() {
|
|
||||||
view.ssiv?.recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
return ViewHolder(BigImageView(parent.context).apply {
|
|
||||||
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
holder.view.showImage(Uri.parse(thumbnails[position]))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = thumbnails.size
|
|
||||||
|
|
||||||
override fun onViewRecycled(holder: ViewHolder) {
|
|
||||||
holder.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
class ThumbnailPageAdapter(private val thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailPageAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
class ViewHolder(val view: RecyclerView) : RecyclerView.ViewHolder(view)
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
return ViewHolder(RecyclerView(parent.context).apply {
|
|
||||||
val layoutManager = GridLayoutManager(parent.context, 3)
|
|
||||||
val adapter = ThumbnailAdapter(listOf())
|
|
||||||
|
|
||||||
this.layoutManager = layoutManager
|
|
||||||
this.adapter = adapter
|
|
||||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
(holder.view.adapter as ThumbnailAdapter).apply {
|
|
||||||
thumbnails = this@ThumbnailPageAdapter.thumbnails.slice(9*position until min(9*position+9, this@ThumbnailPageAdapter.thumbnails.size))
|
|
||||||
notifyDataSetChanged()
|
|
||||||
|
|
||||||
(holder.view.layoutManager as GridLayoutManager).scrollToPosition(8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = if (thumbnails.isEmpty()) 0 else thumbnails.size/9 + if (thumbnails.size%9 != 0) 1 else 0
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.receiver
|
|
||||||
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class UpdateBroadcastReceiver : BroadcastReceiver() {
|
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
context ?: return
|
|
||||||
|
|
||||||
when (intent?.action) {
|
|
||||||
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
|
||||||
|
|
||||||
// Validate download
|
|
||||||
val downloadID: Long = Preferences["update_download_id"]
|
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
||||||
|
|
||||||
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -2) != downloadID)
|
|
||||||
return
|
|
||||||
|
|
||||||
// Get target uri
|
|
||||||
|
|
||||||
val query = DownloadManager.Query()
|
|
||||||
.setFilterById(downloadID)
|
|
||||||
|
|
||||||
val uri = downloadManager.query(query).use { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))?.let {
|
|
||||||
val uri = Uri.parse(it)
|
|
||||||
|
|
||||||
when (uri.scheme) {
|
|
||||||
"file" ->
|
|
||||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!)
|
|
||||||
)
|
|
||||||
"content" -> uri
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
null
|
|
||||||
} ?: return
|
|
||||||
|
|
||||||
// Build Notification
|
|
||||||
|
|
||||||
val notificationManager = NotificationManagerCompat.from(context)
|
|
||||||
|
|
||||||
val pendingIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(context, "update")
|
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
.setContentTitle(context.getText(R.string.update_download_completed))
|
|
||||||
.setContentText(context.getText(R.string.update_download_completed_description))
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
notificationManager.notify(R.id.notification_id_update, notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.services
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.app.TaskStackBuilder
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.Callback
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import okio.*
|
|
||||||
import xyz.quaver.pupil.*
|
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
|
||||||
import xyz.quaver.pupil.util.cleanCache
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import xyz.quaver.pupil.util.ellipsize
|
|
||||||
import xyz.quaver.pupil.util.normalizeID
|
|
||||||
import xyz.quaver.pupil.util.requestBuilders
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import kotlin.math.ceil
|
|
||||||
import kotlin.math.log10
|
|
||||||
|
|
||||||
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
|
|
||||||
class DownloadService : Service() {
|
|
||||||
data class Tag(val galleryID: Int, val index: Int, val startId: Int? = null)
|
|
||||||
|
|
||||||
//region Notification
|
|
||||||
private val notificationManager by lazy {
|
|
||||||
NotificationManagerCompat.from(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val serviceNotification by lazy {
|
|
||||||
NotificationCompat.Builder(this, "downloader")
|
|
||||||
.setContentTitle(getString(R.string.downloader_running))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setOngoing(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val notification = ConcurrentHashMap<Int, NotificationCompat.Builder?>()
|
|
||||||
|
|
||||||
private fun initNotification(galleryID: Int) {
|
|
||||||
val intent = Intent(this, ReaderActivity::class.java)
|
|
||||||
.putExtra("galleryID", galleryID)
|
|
||||||
|
|
||||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
|
||||||
addNextIntentWithParentStack(intent)
|
|
||||||
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
|
||||||
val action =
|
|
||||||
NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
|
||||||
PendingIntent.getService(
|
|
||||||
this,
|
|
||||||
R.id.notification_download_cancel_action.normalizeID(),
|
|
||||||
Intent(this, DownloadService::class.java)
|
|
||||||
.putExtra(KEY_COMMAND, COMMAND_CANCEL)
|
|
||||||
.putExtra(KEY_ID, galleryID),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT),
|
|
||||||
).build()
|
|
||||||
|
|
||||||
notification[galleryID] = NotificationCompat.Builder(this, "download").apply {
|
|
||||||
setContentTitle(getString(R.string.reader_loading))
|
|
||||||
setContentText(getString(R.string.reader_notification_text))
|
|
||||||
setSmallIcon(R.drawable.ic_notification)
|
|
||||||
setContentIntent(pendingIntent)
|
|
||||||
addAction(action)
|
|
||||||
setProgress(0, 0, true)
|
|
||||||
setOngoing(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
notify(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
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 (isCompleted(galleryID)) {
|
|
||||||
notification
|
|
||||||
.setContentText(getString(R.string.reader_notification_complete))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
.setOngoing(false)
|
|
||||||
.mActions.clear()
|
|
||||||
|
|
||||||
notificationManager.cancel(galleryID)
|
|
||||||
} else
|
|
||||||
notification
|
|
||||||
.setProgress(max, progress, false)
|
|
||||||
.setContentText("$progress/$max")
|
|
||||||
|
|
||||||
if (DownloadManager.getInstance(this).getDownloadFolder(galleryID) != null || galleryID == priority)
|
|
||||||
notification.let { notificationManager.notify(galleryID, it.build()) }
|
|
||||||
else
|
|
||||||
notificationManager.cancel(galleryID)
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region ProgressListener
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private val progressListener: ProgressListener = { (galleryID, index), bytesRead, contentLength, done ->
|
|
||||||
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
|
|
||||||
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ProgressResponseBody(
|
|
||||||
val tag: Any?,
|
|
||||||
val responseBody: ResponseBody,
|
|
||||||
val progressListener : ProgressListener
|
|
||||||
) : ResponseBody() {
|
|
||||||
private var bufferedSource : BufferedSource? = null
|
|
||||||
|
|
||||||
override fun contentLength() = responseBody.contentLength()
|
|
||||||
override fun contentType() = responseBody.contentType()
|
|
||||||
|
|
||||||
override fun source(): BufferedSource {
|
|
||||||
if (bufferedSource == null)
|
|
||||||
bufferedSource = Okio.buffer(source(responseBody.source()))
|
|
||||||
|
|
||||||
return bufferedSource!!
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun source(source: Source) = object: ForwardingSource(source) {
|
|
||||||
var totalBytesRead = 0L
|
|
||||||
|
|
||||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
|
||||||
val bytesRead = super.read(sink, byteCount)
|
|
||||||
|
|
||||||
totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
|
|
||||||
progressListener.invoke(tag as Tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
|
||||||
|
|
||||||
return bytesRead
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val interceptor: PupilInterceptor = { chain ->
|
|
||||||
val request = chain.request()
|
|
||||||
val response = chain.proceed(request)
|
|
||||||
|
|
||||||
response.newBuilder()
|
|
||||||
.body(response.body()?.let {
|
|
||||||
ProgressResponseBody(request.tag(), it, progressListener)
|
|
||||||
}).build()
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Downloader
|
|
||||||
/**
|
|
||||||
* KEY
|
|
||||||
* primary galleryID
|
|
||||||
* secondary index
|
|
||||||
* PRIMARY VALUE
|
|
||||||
* MutableList -> Download in progress
|
|
||||||
* null -> Loading / Gallery doesn't exist
|
|
||||||
* SECONDARY VALUE
|
|
||||||
* 0 <= value < 100 -> Download in progress
|
|
||||||
* Float.POSITIVE_INFINITY -> Download completed
|
|
||||||
*/
|
|
||||||
val progress = ConcurrentHashMap<Int, MutableList<Float>>()
|
|
||||||
var priority = 0
|
|
||||||
|
|
||||||
fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
|
|
||||||
|
|
||||||
private val callback = object: Callback {
|
|
||||||
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
|
||||||
FirebaseCrashlytics.getInstance().recordException(e)
|
|
||||||
|
|
||||||
if (e.message?.contains("cancel", true) == false) {
|
|
||||||
val galleryID = (call.request().tag() as Tag).galleryID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
|
||||||
val (galleryID, index, startId) = call.request().tag() as Tag
|
|
||||||
val ext = call.request().url().encodedPath().split('.').last()
|
|
||||||
|
|
||||||
kotlin.runCatching {
|
|
||||||
val image = response.also { if (it.code() != 200) throw IOException("$galleryID $index ${response.request().url()} CODE ${it.code()}") }.body()?.use { it.bytes() } ?: throw Exception("Response null")
|
|
||||||
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
kotlin.runCatching {
|
|
||||||
Cache.getInstance(this@DownloadService, galleryID).putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image)
|
|
||||||
}.onSuccess {
|
|
||||||
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
|
|
||||||
notify(galleryID)
|
|
||||||
|
|
||||||
if (isCompleted(galleryID)) {
|
|
||||||
if (DownloadManager.getInstance(this@DownloadService)
|
|
||||||
.getDownloadFolder(galleryID) != null)
|
|
||||||
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
|
|
||||||
|
|
||||||
startId?.let { stopSelf(it) }
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
FirebaseCrashlytics.getInstance().recordException(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
FirebaseCrashlytics.getInstance().recordException(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancel(startId: Int? = null) {
|
|
||||||
client.dispatcher().queuedCalls().filter {
|
|
||||||
it.request().tag() is Tag
|
|
||||||
}.forEach {
|
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
|
||||||
it.cancel()
|
|
||||||
}
|
|
||||||
client.dispatcher().runningCalls().filter {
|
|
||||||
it.request().tag() is Tag
|
|
||||||
}.forEach {
|
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
|
||||||
it.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.clear()
|
|
||||||
notification.clear()
|
|
||||||
notificationManager.cancelAll()
|
|
||||||
|
|
||||||
startId?.let { stopSelf(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancel(galleryID: Int, startId: Int? = null) {
|
|
||||||
client.dispatcher().queuedCalls().filter {
|
|
||||||
(it.request().tag() as? Tag)?.galleryID == galleryID
|
|
||||||
}.forEach {
|
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
|
||||||
it.cancel()
|
|
||||||
}
|
|
||||||
client.dispatcher().runningCalls().filter {
|
|
||||||
(it.request().tag() as? Tag)?.galleryID == galleryID
|
|
||||||
}.forEach {
|
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
|
||||||
it.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.remove(galleryID)
|
|
||||||
notification.remove(galleryID)
|
|
||||||
notificationManager.cancel(galleryID)
|
|
||||||
|
|
||||||
startId?.let { stopSelf(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun delete(galleryID: Int, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
cancel(galleryID)
|
|
||||||
DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
|
|
||||||
Cache.delete(this@DownloadService, galleryID)
|
|
||||||
|
|
||||||
startId?.let { stopSelf(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
if (DownloadManager.getInstance(this@DownloadService).isDownloading(galleryID))
|
|
||||||
return@launch
|
|
||||||
|
|
||||||
cleanCache(this@DownloadService)
|
|
||||||
|
|
||||||
val cache = Cache.getInstance(this@DownloadService, galleryID)
|
|
||||||
|
|
||||||
initNotification(galleryID)
|
|
||||||
|
|
||||||
val galleryInfo = cache.getGalleryInfo()
|
|
||||||
|
|
||||||
// Gallery doesn't exist
|
|
||||||
if (galleryInfo == null) {
|
|
||||||
delete(galleryID)
|
|
||||||
progress[galleryID] = mutableListOf()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
histories.add(galleryID)
|
|
||||||
|
|
||||||
progress[galleryID] = MutableList(galleryInfo.files.size) { 0F }
|
|
||||||
|
|
||||||
cache.metadata.imageList?.let {
|
|
||||||
it.forEachIndexed { index, image ->
|
|
||||||
progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCompleted(galleryID)) {
|
|
||||||
if (DownloadManager.getInstance(this@DownloadService)
|
|
||||||
.getDownloadFolder(galleryID) != null )
|
|
||||||
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
|
|
||||||
|
|
||||||
notificationManager.cancel(galleryID)
|
|
||||||
startId?.let { stopSelf(it) }
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
notification[galleryID]?.setContentTitle(galleryInfo.title?.ellipsize(30))
|
|
||||||
notify(galleryID)
|
|
||||||
|
|
||||||
val queued = mutableSetOf<Int>()
|
|
||||||
|
|
||||||
if (priority) {
|
|
||||||
client.dispatcher().queuedCalls().forEach {
|
|
||||||
val queuedID = (it.request().tag() as? Tag)?.galleryID ?: return@forEach
|
|
||||||
|
|
||||||
if (queued.add(queuedID))
|
|
||||||
cancel(queuedID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
galleryInfo.requestBuilders.forEachIndexed { index, it ->
|
|
||||||
if (progress[galleryID]?.get(index)?.isInfinite() == false) {
|
|
||||||
val request = it.tag(Tag(galleryID, index, startId)).build()
|
|
||||||
client.newCall(request).enqueue(callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
queued.forEach { download(it) }
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val KEY_COMMAND = "COMMAND" // String
|
|
||||||
const val KEY_ID = "ID" // Int
|
|
||||||
const val KEY_PRIORITY = "PRIORITY" // Boolean
|
|
||||||
|
|
||||||
const val COMMAND_DOWNLOAD = "DOWNLOAD"
|
|
||||||
const val COMMAND_CANCEL = "CANCEL"
|
|
||||||
const val COMMAND_DELETE = "DELETE"
|
|
||||||
|
|
||||||
private fun command(context: Context, extras: Intent.() -> Unit) {
|
|
||||||
ContextCompat.startForegroundService(context, Intent(context, DownloadService::class.java).apply(extras))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun download(context: Context, galleryID: Int, priority: Boolean = false) {
|
|
||||||
command(context) {
|
|
||||||
putExtra(KEY_COMMAND, COMMAND_DOWNLOAD)
|
|
||||||
putExtra(KEY_PRIORITY, priority)
|
|
||||||
putExtra(KEY_ID, galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancel(context: Context, galleryID: Int? = null) {
|
|
||||||
command(context) {
|
|
||||||
putExtra(KEY_COMMAND, COMMAND_CANCEL)
|
|
||||||
galleryID?.let { putExtra(KEY_ID, it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun delete(context: Context, galleryID: Int) {
|
|
||||||
command(context) {
|
|
||||||
putExtra(KEY_COMMAND, COMMAND_DELETE)
|
|
||||||
putExtra(KEY_ID, galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
|
||||||
|
|
||||||
when (intent?.getStringExtra(KEY_COMMAND)) {
|
|
||||||
COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0)
|
|
||||||
download(it, intent.getBooleanExtra(KEY_PRIORITY, false), startId)
|
|
||||||
}
|
|
||||||
COMMAND_CANCEL -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) cancel(it, startId) else cancel(startId = startId) }
|
|
||||||
COMMAND_DELETE -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) delete(it, startId) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Binder : android.os.Binder() {
|
|
||||||
val service = this@DownloadService
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binder = Binder()
|
|
||||||
override fun onBind(p0: Intent?) = binder
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
|
||||||
interceptors[Tag::class] = interceptor
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
interceptors.remove(Tag::class)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
108
app/src/main/java/xyz/quaver/pupil/sources/LocalSources.kt
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import dalvik.system.PathClassLoader
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import xyz.quaver.pupil.sources.core.Source
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberLocalSourceList(context: Context = LocalContext.current): State<List<SourceEntry>> = produceState(emptyList()) {
|
||||||
|
while (true) {
|
||||||
|
value = loadSourceList(context)
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadSource(context: Context, sourceEntry: SourceEntry): Source = coroutineScope {
|
||||||
|
sourceCacheMutex.withLock {
|
||||||
|
sourceCache[sourceEntry.packageName] ?: run {
|
||||||
|
val classLoader = PathClassLoader(sourceEntry.sourceDir, null, context.classLoader)
|
||||||
|
|
||||||
|
Class.forName("${sourceEntry.packagePath}${sourceEntry.sourcePath}", false, classLoader)
|
||||||
|
.getConstructor(Application::class.java)
|
||||||
|
.newInstance(context.applicationContext) as Source
|
||||||
|
}.also { sourceCache[sourceEntry.packageName] = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SOURCES_FEATURE = "pupil.sources"
|
||||||
|
private const val SOURCES_PACKAGE_PREFIX = "xyz.quaver.pupil.sources"
|
||||||
|
private const val SOURCES_PATH = "pupil.sources.path"
|
||||||
|
|
||||||
|
private val PackageInfo.isSourceFeatureEnabled
|
||||||
|
get() = this.reqFeatures.orEmpty().any { it.name == SOURCES_FEATURE }
|
||||||
|
|
||||||
|
private fun loadSource(context: Context, packageInfo: PackageInfo): List<SourceEntry> {
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
|
||||||
|
val applicationInfo = packageInfo.applicationInfo
|
||||||
|
|
||||||
|
val packageName = packageManager.getApplicationLabel(applicationInfo).toString().substringAfter("[Pupil] ")
|
||||||
|
val packagePath = packageInfo.packageName
|
||||||
|
|
||||||
|
val icon = packageManager.getApplicationIcon(applicationInfo)
|
||||||
|
|
||||||
|
val version = packageInfo.versionName
|
||||||
|
|
||||||
|
return packageInfo
|
||||||
|
.applicationInfo
|
||||||
|
.metaData
|
||||||
|
?.getString(SOURCES_PATH)
|
||||||
|
?.split(';')
|
||||||
|
?.map { source ->
|
||||||
|
val (sourceName, sourcePath) = source.split(':', limit = 2)
|
||||||
|
SourceEntry(
|
||||||
|
packageName,
|
||||||
|
packagePath,
|
||||||
|
sourceName,
|
||||||
|
sourcePath,
|
||||||
|
applicationInfo.sourceDir,
|
||||||
|
icon,
|
||||||
|
version
|
||||||
|
)
|
||||||
|
}.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sourceCacheMutex = Mutex()
|
||||||
|
private val sourceCache = mutableMapOf<String, Source>()
|
||||||
|
|
||||||
|
private fun loadSourceList(context: Context): List<SourceEntry> {
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
|
||||||
|
val packages = packageManager.getInstalledPackages(
|
||||||
|
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_META_DATA
|
||||||
|
)
|
||||||
|
|
||||||
|
return packages.flatMap { packageInfo ->
|
||||||
|
if (packageInfo.isSourceFeatureEnabled)
|
||||||
|
loadSource(context, packageInfo)
|
||||||
|
else
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/src/main/java/xyz/quaver/pupil/sources/RemoteSources.kt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2022 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.kodein.di.compose.localDI
|
||||||
|
import org.kodein.di.compose.rememberInstance
|
||||||
|
import org.kodein.di.direct
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import xyz.quaver.pupil.util.PupilHttpClient
|
||||||
|
import xyz.quaver.pupil.util.RemoteSourceInfo
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberRemoteSourceList(client: PupilHttpClient = localDI().direct.instance()) = produceState<Map<String, RemoteSourceInfo>?>(null) {
|
||||||
|
while (true) {
|
||||||
|
value = client.getRemoteSourceList()
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/src/main/java/xyz/quaver/pupil/sources/common.kt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2022 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
|
||||||
|
data class SourceEntry(
|
||||||
|
val packageName: String,
|
||||||
|
val packagePath: String,
|
||||||
|
val sourceName: String,
|
||||||
|
val sourcePath: String,
|
||||||
|
val sourceDir: String,
|
||||||
|
val icon: Drawable,
|
||||||
|
val version: String
|
||||||
|
)
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.types
|
|
||||||
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
|
||||||
import xyz.quaver.hitomi.Suggestion
|
|
||||||
import xyz.quaver.pupil.util.translations
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion {
|
|
||||||
constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n)
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val body =
|
|
||||||
if (translations[s] != null)
|
|
||||||
"${translations[s]} ($s)"
|
|
||||||
else
|
|
||||||
s
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
class Suggestion(override val body: String) : SearchSuggestion
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
class NoResultSuggestion(override val body: String) : SearchSuggestion
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
class LoadingSuggestion(override val body: String) : SearchSuggestion
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
@Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY")
|
|
||||||
class FavoriteHistorySwitch(override val body: String) : SearchSuggestion
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.types
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) {
|
|
||||||
companion object {
|
|
||||||
fun parse(tag: String) : Tag {
|
|
||||||
if (tag.firstOrNull() == '-') {
|
|
||||||
tag.substring(1).split(Regex(":"), 2).let {
|
|
||||||
return when(it.size) {
|
|
||||||
2 -> Tag(it[0], it[1], true)
|
|
||||||
else -> Tag(null, tag, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tag.split(Regex(":"), 2).let {
|
|
||||||
return when(it.size) {
|
|
||||||
2 -> Tag(it[0], it[1])
|
|
||||||
else -> Tag(null, tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return (if (isNegative) "-" else "") + when(area) {
|
|
||||||
null -> tag
|
|
||||||
else -> "$area:$tag"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toQuery(): String {
|
|
||||||
return toString().replace(' ', '_')
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other !is Tag)
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (other.area == area && other.tag == tag)
|
|
||||||
return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode() = toString().hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Tags(val tags: MutableSet<Tag> = mutableSetOf()) : MutableSet<Tag> by tags {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun parse(tags: String) : Tags {
|
|
||||||
return Tags(
|
|
||||||
tags.split(' ').map {
|
|
||||||
if (it.isNotEmpty())
|
|
||||||
Tag.parse(it)
|
|
||||||
else
|
|
||||||
null
|
|
||||||
}.filterNotNull().toMutableSet()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun contains(element: String): Boolean {
|
|
||||||
tags.forEach {
|
|
||||||
if (it.toString() == element)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun add(element: String): Boolean {
|
|
||||||
return tags.add(Tag.parse(element))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun remove(element: String) {
|
|
||||||
tags.filter { it.toString() == element }.forEach {
|
|
||||||
tags.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeByArea(area: String, isNegative: Boolean? = null) {
|
|
||||||
tags.filter { it.area == area && (if(isNegative == null) true else (it.isNegative == isNegative)) }.forEach {
|
|
||||||
tags.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return tags.joinToString(" ") { it.toString() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.PersistableBundle
|
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.util.LockManager
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.normalizeID
|
|
||||||
|
|
||||||
open class BaseActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private var locked: Boolean = true
|
|
||||||
|
|
||||||
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == Activity.RESULT_OK)
|
|
||||||
locked = false
|
|
||||||
else
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
|
||||||
super.onCreate(savedInstanceState, persistentState)
|
|
||||||
|
|
||||||
locked = !LockManager(this).locks.isNullOrEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
if (Preferences["security_mode"])
|
|
||||||
window.setFlags(
|
|
||||||
WindowManager.LayoutParams.FLAG_SECURE,
|
|
||||||
WindowManager.LayoutParams.FLAG_SECURE)
|
|
||||||
else
|
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
|
||||||
|
|
||||||
if (locked)
|
|
||||||
lockLauncher.launch(Intent(this, LockActivity::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.view.animation.AnimationUtils
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.biometric.BiometricPrompt
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.andrognito.patternlockview.PatternLockView
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.databinding.LockActivityBinding
|
|
||||||
import xyz.quaver.pupil.ui.fragment.PINLockFragment
|
|
||||||
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
|
|
||||||
import xyz.quaver.pupil.util.Lock
|
|
||||||
import xyz.quaver.pupil.util.LockManager
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
|
|
||||||
private var lastUnlocked = 0L
|
|
||||||
class LockActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var lockManager: LockManager
|
|
||||||
private var mode: String? = null
|
|
||||||
|
|
||||||
private lateinit var binding: LockActivityBinding
|
|
||||||
|
|
||||||
private val patternLockFragment = PatternLockFragment().apply {
|
|
||||||
var lastPass = ""
|
|
||||||
onPatternDrawn = {
|
|
||||||
when(mode) {
|
|
||||||
null -> {
|
|
||||||
val result = lockManager.check(it)
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
lastUnlocked = System.currentTimeMillis()
|
|
||||||
setResult(Activity.RESULT_OK)
|
|
||||||
finish()
|
|
||||||
} else
|
|
||||||
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
|
||||||
}
|
|
||||||
"add_lock" -> {
|
|
||||||
if (lastPass.isEmpty()) {
|
|
||||||
lastPass = it
|
|
||||||
|
|
||||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
|
||||||
} else {
|
|
||||||
if (lastPass == it) {
|
|
||||||
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
|
||||||
lastPass = ""
|
|
||||||
|
|
||||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pinLockFragment = PINLockFragment().apply {
|
|
||||||
var lastPass = ""
|
|
||||||
onPINEntered = {
|
|
||||||
when(mode) {
|
|
||||||
null -> {
|
|
||||||
val result = lockManager.check(it)
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
lastUnlocked = System.currentTimeMillis()
|
|
||||||
setResult(Activity.RESULT_OK)
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
|
||||||
setAnimationListener(object: Animation.AnimationListener {
|
|
||||||
override fun onAnimationEnd(animation: Animation?) {
|
|
||||||
binding.pinLockView.resetPinLockView()
|
|
||||||
binding.pinLockView.isEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationStart(animation: Animation?) {
|
|
||||||
binding.pinLockView.isEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation?) {
|
|
||||||
// Do Nothing
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"add_lock" -> {
|
|
||||||
if (lastPass.isEmpty()) {
|
|
||||||
lastPass = it
|
|
||||||
|
|
||||||
binding.pinLockView.resetPinLockView()
|
|
||||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
|
||||||
} else {
|
|
||||||
if (lastPass == it) {
|
|
||||||
LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it))
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
|
||||||
setAnimationListener(object: Animation.AnimationListener {
|
|
||||||
override fun onAnimationEnd(animation: Animation?) {
|
|
||||||
binding.pinLockView.resetPinLockView()
|
|
||||||
binding.pinLockView.isEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationStart(animation: Animation?) {
|
|
||||||
binding.pinLockView.isEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation?) {
|
|
||||||
// Do Nothing
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
lastPass = ""
|
|
||||||
|
|
||||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showBiometricPrompt() {
|
|
||||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
||||||
.setTitle(getText(R.string.settings_lock_fingerprint_prompt))
|
|
||||||
.setSubtitle(getText(R.string.settings_lock_fingerprint_prompt_subtitle))
|
|
||||||
.setNegativeButtonText(getText(android.R.string.cancel))
|
|
||||||
.setConfirmationRequired(false)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val biometricPrompt = BiometricPrompt(this, ContextCompat.getMainExecutor(this),
|
|
||||||
object : BiometricPrompt.AuthenticationCallback() {
|
|
||||||
override fun onAuthenticationSucceeded(
|
|
||||||
result: BiometricPrompt.AuthenticationResult) {
|
|
||||||
super.onAuthenticationSucceeded(result)
|
|
||||||
lastUnlocked = System.currentTimeMillis()
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Displays the "log in" prompt.
|
|
||||||
biometricPrompt.authenticate(promptInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = LockActivityBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
lockManager = try {
|
|
||||||
LockManager(this)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
AlertDialog.Builder(this).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.lock_corrupted)
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mode = intent.getStringExtra("mode")
|
|
||||||
val force = intent.getBooleanExtra("force", false)
|
|
||||||
|
|
||||||
when(mode) {
|
|
||||||
null -> {
|
|
||||||
if (lockManager.isEmpty()) {
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (System.currentTimeMillis() - lastUnlocked < 5*60*1000 && !force) {
|
|
||||||
lastUnlocked = System.currentTimeMillis()
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
Preferences["lock_fingerprint"]
|
|
||||||
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
|
||||||
) {
|
|
||||||
binding.fingerprintBtn.apply {
|
|
||||||
isEnabled = true
|
|
||||||
setOnClickListener {
|
|
||||||
showBiometricPrompt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showBiometricPrompt()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.patternBtn.apply {
|
|
||||||
isEnabled = lockManager.contains(Lock.Type.PATTERN)
|
|
||||||
setOnClickListener {
|
|
||||||
supportFragmentManager.beginTransaction().replace(
|
|
||||||
R.id.lock_content, patternLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.pinBtn.apply {
|
|
||||||
isEnabled = lockManager.contains(Lock.Type.PIN)
|
|
||||||
setOnClickListener {
|
|
||||||
supportFragmentManager.beginTransaction().replace(
|
|
||||||
R.id.lock_content, pinLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.passwordBtn.isEnabled = false
|
|
||||||
|
|
||||||
when (lockManager.locks!!.first().type) {
|
|
||||||
Lock.Type.PIN -> {
|
|
||||||
|
|
||||||
supportFragmentManager.beginTransaction().add(
|
|
||||||
R.id.lock_content, pinLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
Lock.Type.PATTERN -> {
|
|
||||||
supportFragmentManager.beginTransaction().add(
|
|
||||||
R.id.lock_content, patternLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
else -> return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"add_lock" -> {
|
|
||||||
binding.patternBtn.isEnabled = false
|
|
||||||
binding.pinBtn.isEnabled = false
|
|
||||||
binding.fingerprintBtn.isEnabled = false
|
|
||||||
binding.passwordBtn.isEnabled = false
|
|
||||||
|
|
||||||
when(intent.getStringExtra("type")!!) {
|
|
||||||
"pattern" -> {
|
|
||||||
binding.patternBtn.isEnabled = true
|
|
||||||
supportFragmentManager.beginTransaction().add(
|
|
||||||
R.id.lock_content, patternLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
"pin" -> {
|
|
||||||
binding.pinBtn.isEnabled = true
|
|
||||||
supportFragmentManager.beginTransaction().add(
|
|
||||||
R.id.lock_content, pinLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -18,822 +18,103 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputType
|
import androidx.activity.ComponentActivity
|
||||||
import android.text.util.Linkify
|
import androidx.activity.compose.BackHandler
|
||||||
import android.view.KeyEvent
|
import androidx.activity.compose.setContent
|
||||||
import android.view.MenuItem
|
import androidx.compose.animation.Crossfade
|
||||||
import android.view.View
|
import androidx.compose.material.MaterialTheme
|
||||||
import android.view.animation.DecelerateInterpolator
|
import androidx.compose.runtime.*
|
||||||
import android.widget.EditText
|
import androidx.compose.ui.graphics.Color
|
||||||
import android.widget.TextView
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.navigation.compose.composable
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.core.view.ViewCompat
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import com.google.android.material.navigation.NavigationView
|
import kotlinx.coroutines.launch
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import org.kodein.di.*
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import org.kodein.di.android.closestDI
|
||||||
import kotlinx.coroutines.*
|
import org.kodein.di.android.subDI
|
||||||
import xyz.quaver.floatingsearchview.FloatingSearchView
|
import org.kodein.di.compose.rememberInstance
|
||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.floatingsearchview.util.view.MenuView
|
import xyz.quaver.pupil.sources.core.Source
|
||||||
import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
import xyz.quaver.pupil.sources.core.util.LocalActivity
|
||||||
import xyz.quaver.hitomi.doSearch
|
import xyz.quaver.pupil.sources.loadSource
|
||||||
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
import xyz.quaver.pupil.ui.theme.PupilTheme
|
||||||
import xyz.quaver.hitomi.getSuggestionsForQuery
|
import xyz.quaver.pupil.util.PupilHttpClient
|
||||||
import xyz.quaver.pupil.*
|
import xyz.quaver.pupil.util.Release
|
||||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
|
||||||
import xyz.quaver.pupil.databinding.MainActivityBinding
|
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
|
||||||
import xyz.quaver.pupil.types.*
|
|
||||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
|
|
||||||
import xyz.quaver.pupil.ui.dialog.GalleryDialog
|
|
||||||
import xyz.quaver.pupil.ui.view.MainView
|
|
||||||
import xyz.quaver.pupil.ui.view.ProgressCard
|
|
||||||
import xyz.quaver.pupil.util.ItemClickSupport
|
|
||||||
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.restore
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
import kotlin.math.*
|
|
||||||
|
|
||||||
class MainActivity :
|
class MainActivity : ComponentActivity(), DIAware {
|
||||||
BaseActivity(),
|
override val di by closestDI()
|
||||||
NavigationView.OnNavigationItemSelectedListener
|
|
||||||
{
|
|
||||||
|
|
||||||
enum class Mode {
|
|
||||||
SEARCH,
|
|
||||||
HISTORY,
|
|
||||||
DOWNLOAD,
|
|
||||||
FAVORITE
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class SortMode {
|
|
||||||
NEWEST,
|
|
||||||
POPULAR
|
|
||||||
}
|
|
||||||
|
|
||||||
private val galleries = ArrayList<Int>()
|
|
||||||
|
|
||||||
private var query = ""
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
with(findViewById<SearchInputView>(R.id.search_bar_text)) {
|
|
||||||
if (text.toString() != value)
|
|
||||||
setText(query, TextView.BufferType.EDITABLE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var queryStack = mutableListOf<String>()
|
|
||||||
|
|
||||||
private var mode = Mode.SEARCH
|
|
||||||
private var sortMode = SortMode.NEWEST
|
|
||||||
|
|
||||||
private var galleryIDs: Deferred<List<Int>>? = null
|
|
||||||
private var totalItems = 0
|
|
||||||
private var loadingJob: Job? = null
|
|
||||||
private var currentPage = 0
|
|
||||||
|
|
||||||
private lateinit var binding: MainActivityBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = MainActivityBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
if (intent.action == Intent.ACTION_VIEW) {
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
intent.dataString?.let { url ->
|
|
||||||
restore(url,
|
setContent {
|
||||||
onFailure = {
|
PupilTheme {
|
||||||
Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||||
}, onSuccess = {
|
val navController = rememberNavController()
|
||||||
Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
|
|
||||||
|
val systemUiController = rememberSystemUiController()
|
||||||
|
val useDarkIcons = MaterialTheme.colors.isLight
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val client: PupilHttpClient by rememberInstance()
|
||||||
|
|
||||||
|
val latestRelease by produceState<Release?>(null) {
|
||||||
|
value = null //client.latestRelease()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Preferences["download_folder", ""].isEmpty())
|
var dismissUpdate by remember { mutableStateOf(false) }
|
||||||
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
|
|
||||||
|
|
||||||
|
SideEffect {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
systemUiController.setSystemBarsColor(
|
||||||
!Preferences["download_folder_ignore_warning", false] &&
|
color = Color.Transparent,
|
||||||
ContextCompat.getExternalFilesDirs(this, null).filterNotNull().map { Uri.fromFile(it).toString() }
|
darkIcons = useDarkIcons
|
||||||
.contains(Preferences["download_folder", ""])
|
)
|
||||||
) {
|
|
||||||
AlertDialog.Builder(this)
|
|
||||||
.setTitle(R.string.warning)
|
|
||||||
.setMessage(R.string.unaccessible_download_folder)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
|
|
||||||
}.setNegativeButton(R.string.ignore) { _, _ ->
|
|
||||||
Preferences["download_folder_ignore_warning"] = true
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
initView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
checkUpdate(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
override fun onBackPressed() {
|
|
||||||
when {
|
|
||||||
binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(GravityCompat.START)
|
|
||||||
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
|
|
||||||
query = queryStack.last()
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
else -> super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
|
|
||||||
(binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
|
||||||
val perPage = Preferences["per_page", "25"].toInt()
|
|
||||||
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
|
|
||||||
|
|
||||||
return when(keyCode) {
|
|
||||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
|
||||||
if (currentPage > 0) {
|
|
||||||
runOnUiThread {
|
|
||||||
currentPage--
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
true
|
latestRelease?.let { release ->
|
||||||
}
|
UpdateAlertDialog(
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
show = !dismissUpdate && release.version != BuildConfig.VERSION_NAME,
|
||||||
if (currentPage < maxPage) {
|
release = release,
|
||||||
runOnUiThread {
|
onDismiss = { dismissUpdate = true }
|
||||||
currentPage++
|
)
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
true
|
NavHost(navController, "main") {
|
||||||
}
|
composable("main") {
|
||||||
else -> super.onKeyDown(keyCode, event)
|
var source by remember { mutableStateOf<Source?>(null) }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
BackHandler(
|
||||||
binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
enabled = source != null
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
) {
|
||||||
// -height of the search view < translationY < 0
|
source = null
|
||||||
binding.contents.searchview.translationY =
|
}
|
||||||
min(
|
|
||||||
max(
|
|
||||||
binding.contents.searchview.translationY - dy,
|
|
||||||
-binding.contents.searchview.binding.querySection.root.height.toFloat()
|
|
||||||
), 0F)
|
|
||||||
|
|
||||||
if (dy > 0)
|
Crossfade(source) { _source ->
|
||||||
binding.contents.fab.hideMenuButton(true)
|
if (_source == null)
|
||||||
else if (dy < 0)
|
SourceSelector {
|
||||||
binding.contents.fab.showMenuButton(true)
|
coroutineScope.launch {
|
||||||
}
|
source = loadSource(application, it)
|
||||||
})
|
}
|
||||||
|
}
|
||||||
Linkify.addLinks(binding.contents.noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) })
|
else {
|
||||||
|
CompositionLocalProvider(LocalActivity provides this@MainActivity) {
|
||||||
//NavigationView
|
_source.Entry()
|
||||||
binding.navView.setNavigationItemSelectedListener(this)
|
}
|
||||||
|
|
||||||
with(binding.contents.cancelFab) {
|
|
||||||
setImageResource(R.drawable.cancel)
|
|
||||||
setOnClickListener {
|
|
||||||
DownloadService.cancel(this@MainActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.contents.jumpFab) {
|
|
||||||
setImageResource(R.drawable.ic_jump)
|
|
||||||
setOnClickListener {
|
|
||||||
val perPage = Preferences["per_page", "25"].toInt()
|
|
||||||
val editText = EditText(context)
|
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
|
||||||
setView(editText)
|
|
||||||
setTitle(R.string.main_jump_title)
|
|
||||||
setMessage(getString(
|
|
||||||
R.string.main_jump_message,
|
|
||||||
currentPage+1,
|
|
||||||
ceil(totalItems / perPage.toDouble()).roundToInt()
|
|
||||||
))
|
|
||||||
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1
|
|
||||||
|
|
||||||
runOnUiThread {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.contents.randomFab) {
|
|
||||||
setImageResource(R.drawable.shuffle_variant)
|
|
||||||
setOnClickListener {
|
|
||||||
runBlocking {
|
|
||||||
withTimeoutOrNull(100) {
|
|
||||||
galleryIDs?.await()
|
|
||||||
}
|
|
||||||
}.let {
|
|
||||||
if (it?.isEmpty() == false) {
|
|
||||||
val galleryID = it.random()
|
|
||||||
|
|
||||||
GalleryDialog(this@MainActivity, galleryID).apply {
|
|
||||||
onChipClickedHandler.add {
|
|
||||||
runOnUiThread {
|
|
||||||
query = it.toQuery()
|
|
||||||
currentPage = 0
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
}
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.contents.idFab) {
|
|
||||||
setImageResource(R.drawable.numeric)
|
|
||||||
setOnClickListener {
|
|
||||||
val editText = EditText(context).apply {
|
|
||||||
inputType = InputType.TYPE_CLASS_NUMBER
|
|
||||||
}
|
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
|
||||||
setView(editText)
|
|
||||||
setTitle(R.string.main_open_gallery_by_id)
|
|
||||||
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton
|
|
||||||
|
|
||||||
GalleryDialog(this@MainActivity, galleryID).apply {
|
|
||||||
onChipClickedHandler.add {
|
|
||||||
runOnUiThread {
|
|
||||||
query = it.toQuery()
|
|
||||||
currentPage = 0
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.contents.view) {
|
|
||||||
setOnPageTurnListener(object: MainView.OnPageTurnListener {
|
|
||||||
override fun onPrev(page: Int) {
|
|
||||||
currentPage--
|
|
||||||
|
|
||||||
// disable pageturn until the contents are loaded
|
|
||||||
setCurrentPage(1, false)
|
|
||||||
|
|
||||||
ViewCompat.animate(binding.contents.searchview)
|
|
||||||
.setDuration(100)
|
|
||||||
.setInterpolator(DecelerateInterpolator())
|
|
||||||
.translationY(0F)
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNext(page: Int) {
|
|
||||||
currentPage++
|
|
||||||
|
|
||||||
// disable pageturn until the contents are loaded
|
|
||||||
setCurrentPage(1, false)
|
|
||||||
|
|
||||||
ViewCompat.animate(binding.contents.searchview)
|
|
||||||
.setDuration(100)
|
|
||||||
.setInterpolator(DecelerateInterpolator())
|
|
||||||
.translationY(0F)
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSearchBar()
|
|
||||||
setupRecyclerView()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
private fun setupRecyclerView() {
|
|
||||||
with(binding.contents.recyclerview) {
|
|
||||||
adapter = GalleryBlockAdapter(galleries).apply {
|
|
||||||
onChipClickedHandler.add {
|
|
||||||
runOnUiThread {
|
|
||||||
query = it.toQuery()
|
|
||||||
currentPage = 0
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onDownloadClickedHandler = { position ->
|
|
||||||
val galleryID = galleries[position]
|
|
||||||
|
|
||||||
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
|
|
||||||
DownloadService.cancel(this@MainActivity, galleryID)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
DownloadManager.getInstance(context).addDownloadFolder(galleryID)
|
|
||||||
DownloadService.download(this@MainActivity, galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAllItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeleteClickedHandler = { position ->
|
|
||||||
val galleryID = galleries[position]
|
|
||||||
DownloadService.delete(this@MainActivity, galleryID)
|
|
||||||
|
|
||||||
histories.remove(galleryID)
|
|
||||||
|
|
||||||
if (this@MainActivity.mode != Mode.SEARCH)
|
|
||||||
runOnUiThread {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAllItems()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ItemClickSupport.addTo(this).apply {
|
|
||||||
onItemClickListener = listener@{ _, position, v ->
|
|
||||||
if (v !is ProgressCard)
|
|
||||||
return@listener
|
|
||||||
|
|
||||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
|
||||||
intent.putExtra("galleryID", galleries[position])
|
|
||||||
|
|
||||||
//TODO: Maybe sprinkling some transitions will be nice :D
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemLongClickListener = listener@{ _, position, v ->
|
|
||||||
if (v !is ProgressCard)
|
|
||||||
return@listener false
|
|
||||||
|
|
||||||
val galleryID = galleries.getOrNull(position) ?: return@listener true
|
|
||||||
|
|
||||||
GalleryDialog(this@MainActivity, galleryID).apply {
|
|
||||||
onChipClickedHandler.add {
|
|
||||||
runOnUiThread {
|
|
||||||
query = it.toQuery()
|
|
||||||
currentPage = 0
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isFavorite = false
|
|
||||||
private val defaultSuggestions: List<SearchSuggestion>
|
|
||||||
get() = when {
|
|
||||||
isFavorite -> {
|
|
||||||
favoriteTags.map {
|
|
||||||
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
|
|
||||||
} + FavoriteHistorySwitch(getString(R.string.search_show_histories))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
searchHistory.map {
|
|
||||||
Suggestion(it)
|
|
||||||
}.takeLast(10) + FavoriteHistorySwitch(getString(R.string.search_show_tags))
|
|
||||||
}
|
|
||||||
}.reversed()
|
|
||||||
|
|
||||||
private var suggestionJob : Job? = null
|
|
||||||
private fun setupSearchBar() {
|
|
||||||
with(binding.contents.searchview) {
|
|
||||||
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
|
|
||||||
override fun onMenuOpened() {
|
|
||||||
(this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuClosed() {
|
|
||||||
//Do Nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post {
|
|
||||||
findViewById<MenuView>(R.id.menu_view).menuItems.firstOrNull {
|
|
||||||
(it as MenuItem).itemId == R.id.main_menu_thin
|
|
||||||
}?.let {
|
|
||||||
(it as MenuItem).isChecked = Preferences["thin"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onHistoryDeleteClickedListener = {
|
|
||||||
searchHistory.remove(it)
|
|
||||||
swapSuggestions(defaultSuggestions)
|
|
||||||
}
|
|
||||||
onFavoriteHistorySwitchClickListener = {
|
|
||||||
isFavorite = !isFavorite
|
|
||||||
swapSuggestions(defaultSuggestions)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMenuItemClickListener = {
|
|
||||||
onActionMenuItemSelected(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
onQueryChangeListener = lambda@{ _, query ->
|
|
||||||
this@MainActivity.query = query
|
|
||||||
|
|
||||||
suggestionJob?.cancel()
|
|
||||||
|
|
||||||
if (query.isEmpty() or query.endsWith(' ')) {
|
|
||||||
swapSuggestions(defaultSuggestions)
|
|
||||||
|
|
||||||
return@lambda
|
|
||||||
}
|
|
||||||
|
|
||||||
swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString())))
|
|
||||||
|
|
||||||
val currentQuery = query.split(" ").last()
|
|
||||||
.replace(Regex("^-"), "")
|
|
||||||
.replace('_', ' ')
|
|
||||||
|
|
||||||
suggestionJob = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val suggestions = kotlin.runCatching {
|
|
||||||
getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }.toMutableList()
|
|
||||||
}.getOrElse { mutableListOf() }
|
|
||||||
|
|
||||||
suggestions.filter {
|
|
||||||
val tag = "${it.n}:${it.s.replace(Regex("\\s"), "_")}"
|
|
||||||
favoriteTags.contains(Tag.parse(tag))
|
|
||||||
}.reversed().forEach {
|
|
||||||
suggestions.remove(it)
|
|
||||||
suggestions.add(0, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
swapSuggestions(if (suggestions.isNotEmpty()) suggestions else listOf(NoResultSuggestion(getText(R.string.main_no_result).toString())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener {
|
|
||||||
override fun onFocus() {
|
|
||||||
if (query.isEmpty() or query.endsWith(' '))
|
|
||||||
swapSuggestions(defaultSuggestions)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFocusCleared() {
|
|
||||||
suggestionJob?.cancel()
|
|
||||||
|
|
||||||
runOnUiThread {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
currentPage = 0
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attachNavigationDrawerToMenuButton(this@MainActivity.binding.drawer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onActionMenuItemSelected(item: MenuItem?) {
|
|
||||||
when(item?.itemId) {
|
|
||||||
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
|
|
||||||
R.id.main_menu_thin -> {
|
|
||||||
val thin = !item.isChecked
|
|
||||||
|
|
||||||
item.isChecked = thin
|
|
||||||
binding.contents.recyclerview.apply {
|
|
||||||
(adapter as GalleryBlockAdapter).apply {
|
|
||||||
this.thin = thin
|
|
||||||
|
|
||||||
Preferences["thin"] = thin
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter = adapter // Force to redraw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
R.id.main_menu_sort_newest -> {
|
|
||||||
sortMode = SortMode.NEWEST
|
|
||||||
item.isChecked = true
|
|
||||||
|
|
||||||
runOnUiThread {
|
|
||||||
currentPage = 0
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
R.id.main_menu_sort_popular -> {
|
|
||||||
sortMode = SortMode.POPULAR
|
|
||||||
item.isChecked = true
|
|
||||||
|
|
||||||
runOnUiThread {
|
|
||||||
currentPage = 0
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
|
||||||
runOnUiThread {
|
|
||||||
binding.drawer.closeDrawers()
|
|
||||||
|
|
||||||
when(item.itemId) {
|
|
||||||
R.id.main_drawer_home -> {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
currentPage = 0
|
|
||||||
query = ""
|
|
||||||
queryStack.clear()
|
|
||||||
mode = Mode.SEARCH
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
R.id.main_drawer_history -> {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
currentPage = 0
|
|
||||||
query = ""
|
|
||||||
queryStack.clear()
|
|
||||||
mode = Mode.HISTORY
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
R.id.main_drawer_downloads -> {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
currentPage = 0
|
|
||||||
query = ""
|
|
||||||
queryStack.clear()
|
|
||||||
mode = Mode.DOWNLOAD
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
R.id.main_drawer_favorite -> {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
currentPage = 0
|
|
||||||
query = ""
|
|
||||||
queryStack.clear()
|
|
||||||
mode = Mode.FAVORITE
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
R.id.main_drawer_help -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
|
|
||||||
}
|
|
||||||
R.id.main_drawer_github -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github))))
|
|
||||||
}
|
|
||||||
R.id.main_drawer_homepage -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page))))
|
|
||||||
}
|
|
||||||
R.id.main_drawer_email -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
|
|
||||||
}
|
|
||||||
R.id.main_drawer_kakaotalk -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelFetch() {
|
|
||||||
galleryIDs?.cancel()
|
|
||||||
loadingJob?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
galleries.clear()
|
|
||||||
|
|
||||||
with(binding.contents.recyclerview.adapter as GalleryBlockAdapter?) {
|
|
||||||
this ?: return@with
|
|
||||||
|
|
||||||
this.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.contents.noresult.visibility = View.INVISIBLE
|
|
||||||
binding.contents.progressbar.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchGalleries(query: String, sortMode: SortMode) {
|
|
||||||
val defaultQuery: String = Preferences["default_query"]
|
|
||||||
|
|
||||||
if (query.isNotBlank())
|
|
||||||
searchHistory.add(query)
|
|
||||||
|
|
||||||
if (query != queryStack.lastOrNull()) {
|
|
||||||
queryStack.remove(query)
|
|
||||||
queryStack.add(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.isNotEmpty() && mode != Mode.SEARCH) {
|
|
||||||
Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply {
|
|
||||||
setAction(android.R.string.ok) {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
currentPage = 0
|
|
||||||
mode = Mode.SEARCH
|
|
||||||
queryStack.clear()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
galleryIDs = null
|
|
||||||
|
|
||||||
if (galleryIDs?.isActive == true)
|
|
||||||
return
|
|
||||||
|
|
||||||
galleryIDs = CoroutineScope(Dispatchers.IO).async {
|
|
||||||
when(mode) {
|
|
||||||
Mode.SEARCH -> {
|
|
||||||
when {
|
|
||||||
query.isEmpty() and defaultQuery.isEmpty() -> {
|
|
||||||
when(sortMode) {
|
|
||||||
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
|
|
||||||
else -> getGalleryIDsFromNozomi(null, "index", "all")
|
|
||||||
}.also {
|
|
||||||
totalItems = it.size
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
|
composable("settings") {
|
||||||
totalItems = it.size
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode.HISTORY -> {
|
|
||||||
when {
|
|
||||||
query.isEmpty() -> {
|
|
||||||
histories.reversed().also {
|
|
||||||
totalItems = it.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val result = doSearch(query).sorted()
|
|
||||||
histories.reversed().filter { result.binarySearch(it) >= 0 }.also {
|
|
||||||
totalItems = it.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Mode.DOWNLOAD -> {
|
|
||||||
val downloads = DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList()
|
|
||||||
|
|
||||||
when {
|
|
||||||
query.isEmpty() -> downloads.reversed().also {
|
|
||||||
totalItems = it.size
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val result = doSearch(query).sorted()
|
|
||||||
downloads.reversed().filter { result.binarySearch(it) >= 0 }.also {
|
|
||||||
totalItems = it.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Mode.FAVORITE -> {
|
|
||||||
when {
|
|
||||||
query.isEmpty() -> favorites.reversed().also {
|
|
||||||
totalItems = it.size
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val result = doSearch(query).sorted()
|
|
||||||
favorites.reversed().filter { result.binarySearch(it) >= 0 }.also {
|
|
||||||
totalItems = it.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.toList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadBlocks() {
|
|
||||||
val perPage = Preferences["per_page", "25"].toInt()
|
|
||||||
|
|
||||||
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val galleryIDs = try {
|
|
||||||
galleryIDs!!.await().also {
|
|
||||||
if (it.isEmpty())
|
|
||||||
throw Exception("No result")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
|
|
||||||
if (e !is CancellationException)
|
|
||||||
FirebaseCrashlytics.getInstance().recordException(e)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
binding.contents.noresult.visibility = View.VISIBLE
|
|
||||||
binding.contents.progressbar.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
binding.contents.view.setCurrentPage(currentPage + 1, galleryIDs.size > (currentPage+1)*perPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks ->
|
|
||||||
for (chunk in chunks)
|
|
||||||
chunk.map { galleryID ->
|
|
||||||
async {
|
|
||||||
Cache.getInstance(this@MainActivity, galleryID).getGalleryBlock()?.let {
|
|
||||||
galleryID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.forEach {
|
|
||||||
it.await()?.also {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
binding.contents.progressbar.hide()
|
|
||||||
|
|
||||||
galleries.add(it)
|
|
||||||
binding.contents.recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,606 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.view.*
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.view.animation.AnticipateInterpolator
|
|
||||||
import android.view.animation.OvershootInterpolator
|
|
||||||
import android.view.animation.TranslateAnimation
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.PagerSnapHelper
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
||||||
import com.google.mlkit.vision.face.Face
|
|
||||||
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
|
||||||
import xyz.quaver.pupil.databinding.NumberpickerDialogBinding
|
|
||||||
import xyz.quaver.pupil.databinding.ReaderActivityBinding
|
|
||||||
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.closeCamera
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import xyz.quaver.pupil.util.startCamera
|
|
||||||
|
|
||||||
class ReaderActivity : BaseActivity() {
|
|
||||||
|
|
||||||
private var galleryID = 0
|
|
||||||
private var currentPage = 0
|
|
||||||
|
|
||||||
private var isScroll = true
|
|
||||||
private var isFullscreen = false
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
|
|
||||||
(binding.recyclerview.adapter as ReaderAdapter).isFullScreen = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var cache: Cache
|
|
||||||
var downloader: DownloadService? = null
|
|
||||||
private val conn = object: ServiceConnection {
|
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
|
||||||
downloader = (service as DownloadService.Binder).service.also {
|
|
||||||
it.priority = 0
|
|
||||||
|
|
||||||
if (!it.progress.containsKey(galleryID))
|
|
||||||
DownloadService.download(this@ReaderActivity, galleryID, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
|
||||||
downloader = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val snapHelper = PagerSnapHelper()
|
|
||||||
private var menu: Menu? = null
|
|
||||||
|
|
||||||
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
|
||||||
if (isGranted)
|
|
||||||
toggleCamera()
|
|
||||||
else
|
|
||||||
AlertDialog.Builder(this)
|
|
||||||
.setTitle(R.string.error)
|
|
||||||
.setMessage(R.string.camera_denied)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Eye {
|
|
||||||
LEFT,
|
|
||||||
RIGHT
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cameraEnabled = false
|
|
||||||
private var eyeType: Eye? = null
|
|
||||||
private var eyeTime: Long = 0L
|
|
||||||
|
|
||||||
private lateinit var binding: ReaderActivityBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = ReaderActivityBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
title = getString(R.string.reader_loading)
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
|
||||||
|
|
||||||
handleIntent(intent)
|
|
||||||
cache = Cache.getInstance(this, galleryID)
|
|
||||||
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
|
|
||||||
|
|
||||||
if (galleryID == 0) {
|
|
||||||
onBackPressed()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
initDownloadListener()
|
|
||||||
initView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
|
||||||
super.onNewIntent(intent)
|
|
||||||
handleIntent(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleIntent(intent: Intent) {
|
|
||||||
if (intent.action == Intent.ACTION_VIEW) {
|
|
||||||
val uri = intent.data
|
|
||||||
val lastPathSegment = uri?.lastPathSegment
|
|
||||||
if (uri != null && lastPathSegment != null) {
|
|
||||||
galleryID = 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
galleryID = intent.getIntExtra("galleryID", 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.reader, menu)
|
|
||||||
|
|
||||||
with(menu?.findItem(R.id.reader_menu_favorite)) {
|
|
||||||
this ?: return@with
|
|
||||||
|
|
||||||
if (favorites.contains(galleryID))
|
|
||||||
(icon as Animatable).start()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.menu = menu
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when(item.itemId) {
|
|
||||||
R.id.reader_menu_page_indicator -> {
|
|
||||||
// TODO: Switch to DialogFragment
|
|
||||||
val binding = NumberpickerDialogBinding.inflate(layoutInflater, binding.root, false)
|
|
||||||
|
|
||||||
with(binding.numberPicker) {
|
|
||||||
minValue = 1
|
|
||||||
maxValue = cache.metadata.galleryInfo?.files?.size ?: 0
|
|
||||||
value = currentPage
|
|
||||||
}
|
|
||||||
val dialog = AlertDialog.Builder(this).apply {
|
|
||||||
setView(binding.root)
|
|
||||||
}.create()
|
|
||||||
binding.okButton.setOnClickListener {
|
|
||||||
(this@ReaderActivity.binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(binding.numberPicker.value-1, 0)
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.show()
|
|
||||||
}
|
|
||||||
R.id.reader_menu_favorite -> {
|
|
||||||
val id = galleryID
|
|
||||||
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
|
|
||||||
|
|
||||||
if (favorites.contains(id)) {
|
|
||||||
favorites.remove(id)
|
|
||||||
favorite.icon = AnimatedVectorDrawableCompat.create(this, R.drawable.avd_star)
|
|
||||||
} else {
|
|
||||||
favorites.add(id)
|
|
||||||
(favorite.icon as Animatable).start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
|
|
||||||
|
|
||||||
if (cameraEnabled)
|
|
||||||
startCamera(this, cameraCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
closeCamera()
|
|
||||||
|
|
||||||
if (downloader != null)
|
|
||||||
unbindService(conn)
|
|
||||||
|
|
||||||
downloader?.priority = galleryID
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
|
|
||||||
update = false
|
|
||||||
|
|
||||||
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
|
|
||||||
DownloadService.cancel(this, galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (isScroll and !isFullscreen)
|
|
||||||
super.onBackPressed()
|
|
||||||
|
|
||||||
if (isFullscreen) {
|
|
||||||
isFullscreen = false
|
|
||||||
fullscreen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isScroll) {
|
|
||||||
isScroll = true
|
|
||||||
scrollMode(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
|
||||||
//currentPage is 1-based
|
|
||||||
return when(keyCode) {
|
|
||||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-2, 0)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onKeyDown(keyCode, event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var update = true
|
|
||||||
private fun initDownloadListener() {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
while (update) {
|
|
||||||
delay(1000)
|
|
||||||
|
|
||||||
val downloader = downloader ?: continue
|
|
||||||
|
|
||||||
if (!downloader.progress.containsKey(galleryID)) //loading
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (downloader.progress[galleryID]?.isEmpty() == true) { //Gallery not found
|
|
||||||
update = false
|
|
||||||
Snackbar
|
|
||||||
.make(binding.root, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
|
||||||
.show()
|
|
||||||
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.downloadProgressbar.max = binding.recyclerview.adapter?.itemCount ?: 0
|
|
||||||
binding.downloadProgressbar.progress =
|
|
||||||
downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
|
|
||||||
|
|
||||||
if (title == getString(R.string.reader_loading)) {
|
|
||||||
val galleryInfo = cache.metadata.galleryInfo
|
|
||||||
|
|
||||||
if (galleryInfo != null) {
|
|
||||||
with(binding.recyclerview.adapter as ReaderAdapter) {
|
|
||||||
this.galleryInfo = galleryInfo
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
title = galleryInfo.title
|
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title =
|
|
||||||
"$currentPage/${galleryInfo.files.size}"
|
|
||||||
|
|
||||||
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.hitomi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloader.isCompleted(galleryID)) { //Download finished
|
|
||||||
binding.downloadProgressbar.visibility = View.GONE
|
|
||||||
|
|
||||||
animateDownloadFAB(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
with(binding.recyclerview) {
|
|
||||||
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
|
|
||||||
onItemClickListener = {
|
|
||||||
if (isScroll) {
|
|
||||||
isScroll = false
|
|
||||||
isFullscreen = true
|
|
||||||
|
|
||||||
scrollMode(false)
|
|
||||||
fullscreen(true)
|
|
||||||
} else {
|
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0) //Moves to next page because currentPage is 1-based indexing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
|
||||||
|
|
||||||
if (dy < 0)
|
|
||||||
binding.fab.showMenuButton(true)
|
|
||||||
else if (dy > 0)
|
|
||||||
binding.fab.hideMenuButton(true)
|
|
||||||
|
|
||||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
|
||||||
|
|
||||||
if (layoutManager.findFirstVisibleItemPosition() == -1)
|
|
||||||
return
|
|
||||||
currentPage = layoutManager.findFirstVisibleItemPosition()+1
|
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.downloadFab) {
|
|
||||||
animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button
|
|
||||||
|
|
||||||
setOnClickListener {
|
|
||||||
val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
|
|
||||||
|
|
||||||
if (downloadManager.isDownloading(galleryID)) {
|
|
||||||
downloadManager.deleteDownloadFolder(galleryID)
|
|
||||||
animateDownloadFAB(false)
|
|
||||||
} else {
|
|
||||||
downloadManager.addDownloadFolder(galleryID)
|
|
||||||
DownloadService.download(context, galleryID, true)
|
|
||||||
animateDownloadFAB(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.retryFab) {
|
|
||||||
setImageResource(R.drawable.refresh)
|
|
||||||
setOnClickListener {
|
|
||||||
DownloadService.download(context, galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.autoFab) {
|
|
||||||
setImageResource(R.drawable.eye_white)
|
|
||||||
setOnClickListener {
|
|
||||||
when {
|
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
|
|
||||||
toggleCamera()
|
|
||||||
}
|
|
||||||
Build.VERSION.SDK_INT >= 23 && shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
|
|
||||||
AlertDialog.Builder(this@ReaderActivity)
|
|
||||||
.setTitle(R.string.warning)
|
|
||||||
.setMessage(R.string.camera_denied)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
else ->
|
|
||||||
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.fullscreenFab) {
|
|
||||||
setImageResource(R.drawable.ic_fullscreen)
|
|
||||||
setOnClickListener {
|
|
||||||
isFullscreen = true
|
|
||||||
fullscreen(isFullscreen)
|
|
||||||
|
|
||||||
binding.fab.close(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fullscreen(isFullscreen: Boolean) {
|
|
||||||
with(window.attributes) {
|
|
||||||
if (isFullscreen) {
|
|
||||||
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
|
||||||
supportActionBar?.hide()
|
|
||||||
binding.fab.visibility = View.INVISIBLE
|
|
||||||
binding.scroller.let {
|
|
||||||
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
|
||||||
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
|
||||||
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb_horizontal)
|
|
||||||
it.fastScrollDirection = RecyclerViewFastScroller.FastScrollDirection.HORIZONTAL
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
|
||||||
supportActionBar?.show()
|
|
||||||
binding.fab.visibility = View.VISIBLE
|
|
||||||
binding.scroller.let {
|
|
||||||
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
|
||||||
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
|
||||||
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb)
|
|
||||||
it.fastScrollDirection = RecyclerViewFastScroller.FastScrollDirection.VERTICAL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.attributes = this
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.recyclerview.adapter = binding.recyclerview.adapter // Force to redraw
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scrollMode(isScroll: Boolean) {
|
|
||||||
if (isScroll) {
|
|
||||||
snapHelper.attachToRecyclerView(null)
|
|
||||||
binding.recyclerview.layoutManager = LinearLayoutManager(this)
|
|
||||||
} else {
|
|
||||||
snapHelper.attachToRecyclerView(binding.recyclerview)
|
|
||||||
binding.recyclerview.layoutManager = object: LinearLayoutManager(this, HORIZONTAL, Preferences["rtl", false]) {
|
|
||||||
override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
|
|
||||||
extraLayoutSpace.fill(600)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animateDownloadFAB(animate: Boolean) {
|
|
||||||
with(binding.downloadFab) {
|
|
||||||
if (animate) {
|
|
||||||
val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
|
|
||||||
|
|
||||||
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
|
|
||||||
override fun onAnimationEnd(drawable: Drawable?) {
|
|
||||||
if (downloader?.isCompleted(galleryID) == true) // If download is finished, stop animating
|
|
||||||
post {
|
|
||||||
setImageResource(R.drawable.ic_download)
|
|
||||||
labelText = getString(R.string.reader_fab_download_cancel)
|
|
||||||
}
|
|
||||||
else // Or continue animate
|
|
||||||
post {
|
|
||||||
icon.start()
|
|
||||||
labelText = getString(R.string.reader_fab_download_cancel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setImageDrawable(icon)
|
|
||||||
icon?.start()
|
|
||||||
} else {
|
|
||||||
setImageResource(R.drawable.ic_download)
|
|
||||||
labelText = getString(R.string.reader_fab_download)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val cameraCallback: (List<Face>) -> Unit = callback@{ faces ->
|
|
||||||
binding.eyeCard.dot.let {
|
|
||||||
it.visibility = View.VISIBLE
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
delay(50)
|
|
||||||
it.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (faces.size != 1)
|
|
||||||
ContextCompat.getDrawable(this, R.drawable.eye_off).let {
|
|
||||||
with(binding.eyeCard) {
|
|
||||||
leftEye.setImageDrawable(it)
|
|
||||||
rightEye.setImageDrawable(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
return@callback
|
|
||||||
}
|
|
||||||
|
|
||||||
val (left, right) = Pair(
|
|
||||||
faces[0].rightEyeOpenProbability?.let { it > 0.4 } == true,
|
|
||||||
faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true
|
|
||||||
)
|
|
||||||
|
|
||||||
with(binding.eyeCard) {
|
|
||||||
leftEye.setImageDrawable(
|
|
||||||
ContextCompat.getDrawable(
|
|
||||||
leftEye.context,
|
|
||||||
if (left) R.drawable.eye else R.drawable.eye_closed
|
|
||||||
)
|
|
||||||
)
|
|
||||||
rightEye.setImageDrawable(
|
|
||||||
ContextCompat.getDrawable(
|
|
||||||
rightEye.context,
|
|
||||||
if (right) R.drawable.eye else R.drawable.eye_closed
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
|
||||||
// Both closed / opened
|
|
||||||
!left.xor(right) -> {
|
|
||||||
eyeType = null
|
|
||||||
eyeTime = 0L
|
|
||||||
}
|
|
||||||
!left -> {
|
|
||||||
if (eyeType != Eye.LEFT) {
|
|
||||||
eyeType = Eye.LEFT
|
|
||||||
eyeTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
!right -> {
|
|
||||||
if (eyeType != Eye.RIGHT) {
|
|
||||||
eyeType = Eye.RIGHT
|
|
||||||
eyeTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eyeType != null && System.currentTimeMillis() - eyeTime > 100) {
|
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager).let {
|
|
||||||
it.scrollToPositionWithOffset(when(eyeType!!) {
|
|
||||||
Eye.RIGHT -> {
|
|
||||||
if (it.reverseLayout) currentPage - 2 else currentPage
|
|
||||||
}
|
|
||||||
Eye.LEFT -> {
|
|
||||||
if (it.reverseLayout) currentPage else currentPage - 2
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
eyeTime = System.currentTimeMillis() + 500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleCamera() {
|
|
||||||
val eyes = binding.eyeCard.root
|
|
||||||
when (camera) {
|
|
||||||
null -> {
|
|
||||||
binding.autoFab.labelText = getString(R.string.reader_fab_auto_cancel)
|
|
||||||
binding.autoFab.setImageResource(R.drawable.eye_off_white)
|
|
||||||
eyes.apply {
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
TranslateAnimation(0F, 0F, -100F, 0F).apply {
|
|
||||||
duration = 500
|
|
||||||
fillAfter = false
|
|
||||||
interpolator = OvershootInterpolator()
|
|
||||||
}.let { startAnimation(it) }
|
|
||||||
}
|
|
||||||
startCamera(this, cameraCallback)
|
|
||||||
cameraEnabled = true
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
binding.autoFab.labelText = getString(R.string.reader_fab_auto)
|
|
||||||
binding.autoFab.setImageResource(R.drawable.eye_white)
|
|
||||||
eyes.apply {
|
|
||||||
TranslateAnimation(0F, 0F, 0F, -100F).apply {
|
|
||||||
duration = 500
|
|
||||||
fillAfter = false
|
|
||||||
interpolator = AnticipateInterpolator()
|
|
||||||
setAnimationListener(object: Animation.AnimationListener {
|
|
||||||
override fun onAnimationStart(p0: Animation?) {}
|
|
||||||
override fun onAnimationRepeat(p0: Animation?) {}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animation?) {
|
|
||||||
eyes.visibility = View.GONE
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}.let { startAnimation(it) }
|
|
||||||
}
|
|
||||||
closeCamera()
|
|
||||||
cameraEnabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.MenuItem
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
|
||||||
|
|
||||||
class SettingsActivity : BaseActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.settings_activity)
|
|
||||||
supportFragmentManager
|
|
||||||
.beginTransaction()
|
|
||||||
.replace(R.id.settings, SettingsFragment())
|
|
||||||
.commit()
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
android.R.id.home -> onBackPressed()
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
352
app/src/main/java/xyz/quaver/pupil/ui/SourceSelector.kt
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.DownloadDone
|
||||||
|
import androidx.compose.material.icons.filled.Explore
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.capitalize
|
||||||
|
import androidx.compose.ui.text.intl.Locale
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
|
import com.google.accompanist.insets.LocalWindowInsets
|
||||||
|
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||||
|
import com.google.accompanist.insets.systemBarsPadding
|
||||||
|
import com.google.accompanist.insets.ui.BottomNavigation
|
||||||
|
import com.google.accompanist.insets.ui.Scaffold
|
||||||
|
import com.google.accompanist.insets.ui.TopAppBar
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.compose.localDI
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import xyz.quaver.pupil.sources.SourceEntry
|
||||||
|
import xyz.quaver.pupil.sources.rememberLocalSourceList
|
||||||
|
import xyz.quaver.pupil.sources.rememberRemoteSourceList
|
||||||
|
import xyz.quaver.pupil.util.PupilHttpClient
|
||||||
|
import xyz.quaver.pupil.util.RemoteSourceInfo
|
||||||
|
import xyz.quaver.pupil.util.launchApkInstaller
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
private sealed class SourceSelectorScreen(val route: String, val icon: ImageVector) {
|
||||||
|
object Local: SourceSelectorScreen("local", Icons.Default.DownloadDone)
|
||||||
|
object Explore: SourceSelectorScreen("explore", Icons.Default.Explore)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sourceSelectorScreens = listOf(
|
||||||
|
SourceSelectorScreen.Local,
|
||||||
|
SourceSelectorScreen.Explore
|
||||||
|
)
|
||||||
|
|
||||||
|
private val RemoteSourceInfo.apkUrl: String
|
||||||
|
get() = "https://github.com/tom5079/PupilSources/releases/download/$name-$version/$projectName-release.apk"
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadApkActionState(override val di: DI) : DIAware {
|
||||||
|
private val app: Application by instance()
|
||||||
|
private val client: PupilHttpClient by instance()
|
||||||
|
|
||||||
|
var progress by mutableStateOf<Float?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
suspend fun download(url: String): File? = withContext(Dispatchers.IO) {
|
||||||
|
progress = 0f
|
||||||
|
|
||||||
|
val file = File.createTempFile("pupil", ".apk", File(app.cacheDir, "apks").also {
|
||||||
|
it.mkdirs()
|
||||||
|
})
|
||||||
|
|
||||||
|
client.downloadFile(url, file).collect { progress = it }
|
||||||
|
|
||||||
|
if (progress == Float.POSITIVE_INFINITY) file else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
progress = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberDownloadApkActionState(di: DI = localDI()) = remember { DownloadApkActionState(di) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DownloadApkAction(
|
||||||
|
state: DownloadApkActionState,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
state.progress?.let { progress ->
|
||||||
|
Box(
|
||||||
|
Modifier.padding(12.dp, 0.dp)
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
progress.isFinite() && progress > 0f ->
|
||||||
|
CircularProgressIndicator(progress, modifier = Modifier.size(24.dp))
|
||||||
|
else ->
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
} ?: content()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SourceListItem(icon: @Composable (Modifier) -> Unit = { }, name: String, version: String, actions: @Composable () -> Unit = { }) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
elevation = 4.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
icon(Modifier.size(48.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(name.capitalize(Locale.current))
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalContentAlpha provides 0.5f) {
|
||||||
|
Text(
|
||||||
|
"v$version",
|
||||||
|
style = MaterialTheme.typography.caption
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Local(onSource: (SourceEntry) -> Unit) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val localSourceList by rememberLocalSourceList()
|
||||||
|
val remoteSourceList by rememberRemoteSourceList()
|
||||||
|
|
||||||
|
if (localSourceList.isEmpty()) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Column(
|
||||||
|
Modifier.align(Alignment.Center),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(LocalContentAlpha provides 0.5f) {
|
||||||
|
Text("(´∇`)", style = MaterialTheme.typography.h2)
|
||||||
|
}
|
||||||
|
Text("No sources found!\nLet's go download one.", textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn {
|
||||||
|
items(localSourceList) { source ->
|
||||||
|
val actionState = rememberDownloadApkActionState()
|
||||||
|
|
||||||
|
SourceListItem(
|
||||||
|
icon = { modifier ->
|
||||||
|
Image(
|
||||||
|
rememberDrawablePainter(source.icon),
|
||||||
|
contentDescription = "source icon",
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
},
|
||||||
|
source.sourceName,
|
||||||
|
source.version
|
||||||
|
) {
|
||||||
|
DownloadApkAction(actionState) {
|
||||||
|
val remoteSource = remoteSourceList?.get(source.packageName)
|
||||||
|
if (remoteSource != null && remoteSource.version != source.version) {
|
||||||
|
TextButton(onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val file = actionState.download(remoteSource.apkUrl)!! // TODO("Handle error")
|
||||||
|
context.launchApkInstaller(file)
|
||||||
|
actionState.reset()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("UPDATE")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TextButton(
|
||||||
|
onClick = { onSource(source) }
|
||||||
|
) {
|
||||||
|
Text("GO")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Explore() {
|
||||||
|
val localSourceList by rememberLocalSourceList()
|
||||||
|
val localSources by derivedStateOf {
|
||||||
|
localSourceList.associateBy {
|
||||||
|
it.packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val remoteSources by rememberRemoteSourceList()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
if (remoteSources == null)
|
||||||
|
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
||||||
|
else
|
||||||
|
LazyColumn {
|
||||||
|
items(remoteSources?.values?.toList().orEmpty()) { sourceInfo ->
|
||||||
|
val actionState = rememberDownloadApkActionState()
|
||||||
|
|
||||||
|
SourceListItem(
|
||||||
|
icon = { modifier ->
|
||||||
|
AsyncImage(
|
||||||
|
"https://raw.githubusercontent.com/tom5079/PupilSources/master/${sourceInfo.projectName}/src/main/res/mipmap-xxxhdpi/ic_launcher.png",
|
||||||
|
contentDescription = "source icon",
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
},
|
||||||
|
sourceInfo.name,
|
||||||
|
sourceInfo.version
|
||||||
|
) {
|
||||||
|
DownloadApkAction(actionState) {
|
||||||
|
if (localSources[sourceInfo.name]?.version != sourceInfo.version) {
|
||||||
|
TextButton(onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val file = actionState.download(sourceInfo.apkUrl)!! // TODO("Handle exception")
|
||||||
|
context.launchApkInstaller(file)
|
||||||
|
actionState.reset()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("UPDATE")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (sourceInfo.name in localSources) {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(
|
||||||
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||||
|
Uri.fromParts("package", localSources[sourceInfo.name]!!.packagePath, null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else coroutineScope.launch {
|
||||||
|
val file = actionState.download(sourceInfo.apkUrl)!! // TODO("Handle exception")
|
||||||
|
context.launchApkInstaller(file)
|
||||||
|
actionState.reset()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
if (sourceInfo.name !in localSources) Icons.Default.Download
|
||||||
|
else Icons.Outlined.Info,
|
||||||
|
contentDescription = "download"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SourceSelector(onSource: (SourceEntry) -> Unit) {
|
||||||
|
val bottomNavController = rememberNavController()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text("Pupil")
|
||||||
|
},
|
||||||
|
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.statusBars)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
BottomNavigation(
|
||||||
|
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
|
||||||
|
) {
|
||||||
|
val navBackStackEntry by bottomNavController.currentBackStackEntryAsState()
|
||||||
|
val currentDestination = navBackStackEntry?.destination
|
||||||
|
|
||||||
|
sourceSelectorScreens.forEach { screen ->
|
||||||
|
BottomNavigationItem(
|
||||||
|
icon = { Icon(screen.icon, contentDescription = screen.route) },
|
||||||
|
label = { Text(screen.route) },
|
||||||
|
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
|
||||||
|
onClick = {
|
||||||
|
bottomNavController.navigate(screen.route) {
|
||||||
|
popUpTo(bottomNavController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { contentPadding ->
|
||||||
|
NavHost(bottomNavController, startDestination = "local", modifier = Modifier
|
||||||
|
.systemBarsPadding(top = false, bottom = false)
|
||||||
|
.padding(contentPadding)) {
|
||||||
|
composable(SourceSelectorScreen.Local.route) { Local(onSource) }
|
||||||
|
composable(SourceSelectorScreen.Explore.route) { Explore() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
95
app/src/main/java/xyz/quaver/pupil/ui/UpdateDialog.kt
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2022 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.kodein.di.compose.onDIContext
|
||||||
|
import xyz.quaver.pupil.util.Release
|
||||||
|
import xyz.quaver.pupil.util.launchApkInstaller
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UpdateAlertDialog(
|
||||||
|
show: Boolean,
|
||||||
|
release: Release,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val state = rememberDownloadApkActionState()
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
Dialog(onDismissRequest = { if (state.progress == null) onDismiss() }) {
|
||||||
|
Card {
|
||||||
|
val progress = state.progress
|
||||||
|
|
||||||
|
if (progress != null) {
|
||||||
|
if (progress.isFinite() && progress > 0)
|
||||||
|
LinearProgressIndicator(progress)
|
||||||
|
else
|
||||||
|
LinearProgressIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.padding(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 0.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Update Available",
|
||||||
|
style = MaterialTheme.typography.h6
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(release.releaseNotes.getOrElse(Locale.getDefault()) { release.releaseNotes[Locale.ENGLISH]!! })
|
||||||
|
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss, enabled = progress == null) {
|
||||||
|
Text("DISMISS")
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val file = state.download(release.apkUrl)!! // TODO("Handle exception")
|
||||||
|
context.launchApkInstaller(file)
|
||||||
|
state.reset()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = progress == null
|
||||||
|
) {
|
||||||
|
Text("UPDATE")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding
|
|
||||||
import xyz.quaver.pupil.types.Tags
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
|
|
||||||
class DefaultQueryDialog : DialogFragment() {
|
|
||||||
|
|
||||||
private val languages: Map<String, String> by lazy {
|
|
||||||
requireContext().resources.getStringArray(R.array.languages).map {
|
|
||||||
it.split("|").let { split ->
|
|
||||||
Pair(split[0], split[1])
|
|
||||||
}
|
|
||||||
}.toMap()
|
|
||||||
}
|
|
||||||
private val reverseLanguages: Map<String, String> by lazy {
|
|
||||||
languages.entries.associate { (k, v) -> v to k }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val excludeBL = "-male:yaoi"
|
|
||||||
private val excludeGuro = listOf("-female:guro", "-male:guro")
|
|
||||||
private val excludeLoli = listOf("-female:loli", "-male:shota")
|
|
||||||
|
|
||||||
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
|
|
||||||
|
|
||||||
private var _binding: DefaultQueryDialogBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
val tags = Tags.parse(
|
|
||||||
Preferences["default_query"]
|
|
||||||
)
|
|
||||||
|
|
||||||
with(binding.languageSelector) {
|
|
||||||
adapter =
|
|
||||||
ArrayAdapter(
|
|
||||||
context,
|
|
||||||
android.R.layout.simple_spinner_dropdown_item,
|
|
||||||
arrayListOf(
|
|
||||||
context.getString(R.string.default_query_dialog_language_selector_none)
|
|
||||||
).apply {
|
|
||||||
addAll(languages.values)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (tags.any { it.area == "language" && !it.isNegative }) {
|
|
||||||
val tag = languages[tags.first { it.area == "language" }.tag]
|
|
||||||
if (tag != null) {
|
|
||||||
setSelection(
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(adapter as ArrayAdapter<String>).getPosition(tag)
|
|
||||||
)
|
|
||||||
tags.removeByArea("language", false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.BLCheckbox) {
|
|
||||||
isChecked = tags.contains(excludeBL)
|
|
||||||
if (tags.contains(excludeBL))
|
|
||||||
tags.remove(excludeBL)
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.guroCheckbox) {
|
|
||||||
isChecked = excludeGuro.all { tags.contains(it) }
|
|
||||||
if (excludeGuro.all { tags.contains(it) })
|
|
||||||
excludeGuro.forEach {
|
|
||||||
tags.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.loliCheckbox) {
|
|
||||||
isChecked = excludeLoli.all { tags.contains(it) }
|
|
||||||
if (excludeLoli.all { tags.contains(it) })
|
|
||||||
excludeLoli.forEach {
|
|
||||||
tags.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.edittext) {
|
|
||||||
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
|
|
||||||
addTextChangedListener(object : TextWatcher {
|
|
||||||
override fun beforeTextChanged(
|
|
||||||
s: CharSequence?,
|
|
||||||
start: Int,
|
|
||||||
count: Int,
|
|
||||||
after: Int
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
||||||
|
|
||||||
override fun afterTextChanged(s: Editable?) {
|
|
||||||
s ?: return
|
|
||||||
|
|
||||||
if (s.any { it.isUpperCase() })
|
|
||||||
s.replace(
|
|
||||||
0,
|
|
||||||
s.length,
|
|
||||||
s.toString().toLowerCase(java.util.Locale.getDefault())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
_binding = DefaultQueryDialogBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
initView()
|
|
||||||
|
|
||||||
return AlertDialog.Builder(requireContext()).apply {
|
|
||||||
setTitle(R.string.default_query_dialog_title)
|
|
||||||
setView(binding.root)
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
val newTags = Tags.parse(binding.edittext.text.toString())
|
|
||||||
|
|
||||||
with(binding.languageSelector) {
|
|
||||||
if (selectedItemPosition != 0)
|
|
||||||
newTags.add("language:${reverseLanguages[selectedItem]}")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binding.BLCheckbox.isChecked)
|
|
||||||
newTags.add(excludeBL)
|
|
||||||
|
|
||||||
if (binding.guroCheckbox.isChecked)
|
|
||||||
excludeGuro.forEach { tag ->
|
|
||||||
newTags.add(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binding.loliCheckbox.isChecked)
|
|
||||||
excludeLoli.forEach { tag ->
|
|
||||||
newTags.add(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
onPositiveButtonClickListener?.invoke(newTags)
|
|
||||||
}
|
|
||||||
}.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.widget.addTextChangedListener
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.databinding.DownloadFolderNameDialogBinding
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.formatDownloadFolder
|
|
||||||
import xyz.quaver.pupil.util.formatDownloadFolderTest
|
|
||||||
import xyz.quaver.pupil.util.formatMap
|
|
||||||
|
|
||||||
class DownloadFolderNameDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var _binding: DownloadFolderNameDialogBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
_binding = DownloadFolderNameDialogBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
initView()
|
|
||||||
|
|
||||||
return Dialog(requireContext()).apply {
|
|
||||||
setContentView(binding.root)
|
|
||||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
val galleryID = Cache.instances.let { if (it.size == 0) 1199708 else it.keys.elementAt((0 until it.size).random()) }
|
|
||||||
val galleryBlock = runBlocking {
|
|
||||||
Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
|
|
||||||
binding.edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
|
|
||||||
binding.edittext.addTextChangedListener {
|
|
||||||
binding.message.text = requireContext().getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
|
|
||||||
}
|
|
||||||
binding.okButton.setOnClickListener {
|
|
||||||
val newValue = binding.edittext.text.toString()
|
|
||||||
|
|
||||||
if ((newValue as? String)?.contains("/") != false) {
|
|
||||||
Snackbar.make(binding.root, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
Preferences["download_folder_name"] = binding.edittext.text.toString()
|
|
||||||
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.toFile
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.databinding.DownloadLocationDialogBinding
|
|
||||||
import xyz.quaver.pupil.databinding.DownloadLocationItemBinding
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.byteToString
|
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class DownloadLocationDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var _binding: DownloadLocationDialogBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val entries = mutableMapOf<File?, DownloadLocationItemBinding>()
|
|
||||||
|
|
||||||
private val requestDownloadFolderLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
val context = context ?: return@registerForActivityResult
|
|
||||||
val dialog = dialog ?: return@registerForActivityResult
|
|
||||||
|
|
||||||
it.data?.data?.also { uri ->
|
|
||||||
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
|
||||||
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
|
||||||
|
|
||||||
if (kotlin.runCatching { FileX(context, uri).canWrite() }.getOrDefault(false)) {
|
|
||||||
entries[null]?.locationAvailable?.text = uri.toFile(context)?.canonicalPath
|
|
||||||
Preferences["download_folder"] = uri.toString()
|
|
||||||
} else {
|
|
||||||
Snackbar.make(
|
|
||||||
dialog.window!!.decorView.rootView,
|
|
||||||
R.string.settings_download_folder_not_writable,
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
|
|
||||||
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
|
||||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
|
||||||
entries[key]!!.button.isChecked = true
|
|
||||||
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val downloadFolder = DownloadManager.getInstance(context ?: return@registerForActivityResult).downloadFolder.canonicalPath
|
|
||||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
|
||||||
if (key == null)
|
|
||||||
entries[key]!!.locationAvailable.text = downloadFolder
|
|
||||||
else {
|
|
||||||
entries[null]!!.button.isChecked = false
|
|
||||||
entries[key]!!.button.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val requestDownloadFolderOldLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
val context = context ?: return@registerForActivityResult
|
|
||||||
val dialog = dialog ?: return@registerForActivityResult
|
|
||||||
|
|
||||||
if (it.resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
|
||||||
val directory = it.data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
|
||||||
|
|
||||||
if (!File(directory).canWrite()) {
|
|
||||||
Snackbar.make(
|
|
||||||
dialog.window!!.decorView.rootView,
|
|
||||||
R.string.settings_download_folder_not_writable,
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
|
|
||||||
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
|
||||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
|
||||||
entries[key]!!.button.isChecked = true
|
|
||||||
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
entries[null]?.locationAvailable?.text = directory
|
|
||||||
Preferences["download_folder"] = File(directory).toURI().toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
val externalFilesDirs = ContextCompat.getExternalFilesDirs(requireContext(), null)
|
|
||||||
|
|
||||||
externalFilesDirs.forEachIndexed { index, dir ->
|
|
||||||
dir ?: return@forEachIndexed
|
|
||||||
|
|
||||||
DownloadLocationItemBinding.inflate(layoutInflater, binding.root, true).apply {
|
|
||||||
locationType.text = requireContext().getString(when (index) {
|
|
||||||
0 -> R.string.settings_download_folder_internal
|
|
||||||
else -> R.string.settings_download_folder_removable
|
|
||||||
})
|
|
||||||
locationAvailable.text = requireContext().getString(
|
|
||||||
R.string.settings_download_folder_available,
|
|
||||||
byteToString(dir.freeSpace)
|
|
||||||
)
|
|
||||||
root.setOnClickListener {
|
|
||||||
entries.values.forEach { entry ->
|
|
||||||
entry.button.isChecked = false
|
|
||||||
}
|
|
||||||
button.performClick()
|
|
||||||
Preferences["download_folder"] = dir.toUri().toString()
|
|
||||||
}
|
|
||||||
entries[dir] = this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadLocationItemBinding.inflate(layoutInflater, binding.root, true).apply {
|
|
||||||
locationType.text = requireContext().getString(R.string.settings_download_folder_custom)
|
|
||||||
root.setOnClickListener {
|
|
||||||
entries.values.forEach { entry ->
|
|
||||||
entry.button.isChecked = false
|
|
||||||
}
|
|
||||||
button.performClick()
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
|
||||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
requestDownloadFolderLauncher.launch(intent)
|
|
||||||
} else { // Can't use SAF on old Androids!
|
|
||||||
val config = DirectoryChooserConfig.builder()
|
|
||||||
.newDirectoryName("Pupil")
|
|
||||||
.allowNewDirectoryNameModification(true)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
|
|
||||||
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
requestDownloadFolderOldLauncher.launch(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entries[null] = this
|
|
||||||
}
|
|
||||||
|
|
||||||
val downloadFolder = DownloadManager.getInstance(requireContext()).downloadFolder.canonicalPath
|
|
||||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
|
||||||
entries[key]!!.button.isChecked = true
|
|
||||||
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
_binding = DownloadLocationDialogBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
initView()
|
|
||||||
|
|
||||||
return AlertDialog.Builder(requireContext()).apply {
|
|
||||||
setTitle(R.string.settings_download_folder)
|
|
||||||
setView(binding.root)
|
|
||||||
setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
|
|
||||||
if (Preferences["download_folder", ""].isEmpty())
|
|
||||||
Preferences["download_folder"] = context.getExternalFilesDir(null)?.toUri()?.toString() ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
isCancelable = false
|
|
||||||
}.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout.LayoutParams
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import xyz.quaver.hitomi.Gallery
|
|
||||||
import xyz.quaver.hitomi.getGallery
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
|
||||||
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
|
||||||
import xyz.quaver.pupil.databinding.*
|
|
||||||
import xyz.quaver.pupil.favoriteTags
|
|
||||||
import xyz.quaver.pupil.types.Tag
|
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
|
||||||
import xyz.quaver.pupil.ui.view.TagChip
|
|
||||||
import xyz.quaver.pupil.util.ItemClickSupport
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(context) {
|
|
||||||
|
|
||||||
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
|
||||||
|
|
||||||
private lateinit var binding: GalleryDialogBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = GalleryDialogBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
window?.attributes.apply {
|
|
||||||
this ?: return@apply
|
|
||||||
|
|
||||||
width = LayoutParams.MATCH_PARENT
|
|
||||||
height = LayoutParams.MATCH_PARENT
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.fab) {
|
|
||||||
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right))
|
|
||||||
setOnClickListener {
|
|
||||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
|
||||||
putExtra("galleryID", galleryID)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
val gallery = getGallery(galleryID)
|
|
||||||
|
|
||||||
launch (Dispatchers.Main) {
|
|
||||||
binding.progressbar.visibility = View.GONE
|
|
||||||
binding.title.text = gallery.title
|
|
||||||
binding.artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
|
|
||||||
|
|
||||||
with(binding.type) {
|
|
||||||
text = gallery.type.wordCapitalize()
|
|
||||||
setOnClickListener {
|
|
||||||
gallery.type.let {
|
|
||||||
when (it) {
|
|
||||||
"artist CG" -> "artistcg"
|
|
||||||
"game CG" -> "gamecg"
|
|
||||||
else -> it
|
|
||||||
}
|
|
||||||
}.let {
|
|
||||||
onChipClickedHandler.forEach { handler ->
|
|
||||||
handler.invoke(Tag("type", it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.cover.showImage(Uri.parse(gallery.cover))
|
|
||||||
|
|
||||||
addDetails(gallery)
|
|
||||||
addThumbnails(gallery)
|
|
||||||
addRelated(gallery)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Snackbar.make(binding.root, R.string.unable_to_connect, Snackbar.LENGTH_INDEFINITE).apply {
|
|
||||||
if (Locale.getDefault().language == "ko")
|
|
||||||
setAction(context.getText(R.string.https_text)) {
|
|
||||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.https))))
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addDetails(gallery: Gallery) {
|
|
||||||
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
|
||||||
type.setText(R.string.gallery_details)
|
|
||||||
|
|
||||||
listOf(
|
|
||||||
R.string.gallery_artists,
|
|
||||||
R.string.gallery_groups,
|
|
||||||
R.string.gallery_language,
|
|
||||||
R.string.gallery_series,
|
|
||||||
R.string.gallery_characters,
|
|
||||||
R.string.gallery_tags
|
|
||||||
).zip(
|
|
||||||
listOf(
|
|
||||||
gallery.artists.map { Tag("artist", it) },
|
|
||||||
gallery.groups.map { Tag("group", it) },
|
|
||||||
listOf(gallery.language).map { Tag("language", it) },
|
|
||||||
gallery.series.map { Tag("series", it) },
|
|
||||||
gallery.characters.map { Tag("character", it) },
|
|
||||||
gallery.tags.sortedBy {
|
|
||||||
val tag = Tag.parse(it)
|
|
||||||
|
|
||||||
if (favoriteTags.contains(tag))
|
|
||||||
-1
|
|
||||||
else
|
|
||||||
when(Tag.parse(it).area) {
|
|
||||||
"female" -> 0
|
|
||||||
"male" -> 1
|
|
||||||
else -> 2
|
|
||||||
}
|
|
||||||
}.map {
|
|
||||||
Tag.parse(it).let { tag ->
|
|
||||||
when {
|
|
||||||
tag.area != null -> tag
|
|
||||||
else -> Tag("tag", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).filter {
|
|
||||||
(_, content) -> content.isNotEmpty()
|
|
||||||
}.forEach { (title, content) ->
|
|
||||||
GalleryDialogTagsBinding.inflate(layoutInflater, contents, true).apply {
|
|
||||||
type.setText(title)
|
|
||||||
|
|
||||||
content.forEach { tag ->
|
|
||||||
tags.addView(
|
|
||||||
TagChip(context, tag).apply {
|
|
||||||
setOnClickListener {
|
|
||||||
onChipClickedHandler.forEach { handler ->
|
|
||||||
handler.invoke(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addThumbnails(gallery: Gallery) {
|
|
||||||
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
|
||||||
type.setText(R.string.gallery_thumbnails)
|
|
||||||
|
|
||||||
val pager = ViewPager2(context).apply {
|
|
||||||
adapter = ThumbnailPageAdapter(gallery.thumbnails)
|
|
||||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
contents.addView(
|
|
||||||
pager,
|
|
||||||
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: Change to direct allocation
|
|
||||||
GalleryDialogDotindicatorBinding.inflate(layoutInflater, contents, true).apply {
|
|
||||||
dotindicator.setViewPager2(pager)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addRelated(gallery: Gallery) {
|
|
||||||
val galleries = mutableListOf<Int>()
|
|
||||||
|
|
||||||
val adapter = GalleryBlockAdapter(galleries).apply {
|
|
||||||
onChipClickedHandler.add { tag ->
|
|
||||||
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
|
|
||||||
handler.invoke(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
|
||||||
type.setText(R.string.gallery_related)
|
|
||||||
|
|
||||||
contents.addView(RecyclerView(context).apply {
|
|
||||||
layoutManager = LinearLayoutManager(context)
|
|
||||||
this.adapter = adapter
|
|
||||||
|
|
||||||
ItemClickSupport.addTo(this).apply {
|
|
||||||
onItemClickListener = { _, position, _ ->
|
|
||||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
|
||||||
putExtra("galleryID", galleries[position])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onItemLongClickListener = { _, position, _ ->
|
|
||||||
GalleryDialog(context, galleries[position]).apply {
|
|
||||||
onChipClickedHandler.add { tag ->
|
|
||||||
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
gallery.related.forEach { galleryID ->
|
|
||||||
Cache.getInstance(context, galleryID).getGalleryBlock()?.let {
|
|
||||||
galleries.add(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.client
|
|
||||||
import xyz.quaver.pupil.clientBuilder
|
|
||||||
import xyz.quaver.pupil.clientHolder
|
|
||||||
import xyz.quaver.pupil.databinding.ProxyDialogBinding
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.ProxyInfo
|
|
||||||
import xyz.quaver.pupil.util.getProxyInfo
|
|
||||||
import xyz.quaver.pupil.util.proxyInfo
|
|
||||||
import java.net.Proxy
|
|
||||||
|
|
||||||
class ProxyDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var _binding: ProxyDialogBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
_binding = ProxyDialogBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
initView()
|
|
||||||
|
|
||||||
return AlertDialog.Builder(requireContext()).apply {
|
|
||||||
setView(binding.root)
|
|
||||||
}.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
val proxyInfo = getProxyInfo()
|
|
||||||
|
|
||||||
val enabler = { enable: Boolean ->
|
|
||||||
binding.addr.isEnabled = enable
|
|
||||||
binding.port.isEnabled = enable
|
|
||||||
binding.username.isEnabled = enable
|
|
||||||
binding.password.isEnabled = enable
|
|
||||||
|
|
||||||
if (!enable) {
|
|
||||||
binding.addr.text = null
|
|
||||||
binding.port.text = null
|
|
||||||
binding.username.text = null
|
|
||||||
binding.password.text = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.typeSelector) {
|
|
||||||
adapter = ArrayAdapter(
|
|
||||||
context,
|
|
||||||
android.R.layout.simple_spinner_dropdown_item,
|
|
||||||
context.resources.getStringArray(R.array.proxy_type)
|
|
||||||
)
|
|
||||||
|
|
||||||
setSelection(proxyInfo.type.ordinal)
|
|
||||||
|
|
||||||
onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
||||||
enabler.invoke(position != 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.addr.setText(proxyInfo.host)
|
|
||||||
binding.port.setText(proxyInfo.port?.toString())
|
|
||||||
binding.username.setText(proxyInfo.username)
|
|
||||||
binding.password.setText(proxyInfo.password)
|
|
||||||
|
|
||||||
enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT)
|
|
||||||
|
|
||||||
binding.cancelButton.setOnClickListener {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.okButton.setOnClickListener {
|
|
||||||
val type = Proxy.Type.values()[binding.typeSelector.selectedItemPosition]
|
|
||||||
val addr = binding.addr.text?.toString()
|
|
||||||
val port = binding.port.text?.toString()?.toIntOrNull()
|
|
||||||
val username = binding.username.text?.toString()
|
|
||||||
val password = binding.password.text?.toString()
|
|
||||||
|
|
||||||
if (type != Proxy.Type.DIRECT) {
|
|
||||||
if (addr == null || addr.isEmpty())
|
|
||||||
binding.addr.error = requireContext().getText(R.string.proxy_dialog_error)
|
|
||||||
if (port == null)
|
|
||||||
binding.port.error = requireContext().getText(R.string.proxy_dialog_error)
|
|
||||||
|
|
||||||
if (addr == null || addr.isEmpty() || port == null)
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
ProxyInfo(type, addr, port, username, password).let {
|
|
||||||
Preferences["proxy"] = Json.encodeToString(it)
|
|
||||||
|
|
||||||
clientBuilder
|
|
||||||
.proxyInfo(it)
|
|
||||||
clientHolder = null
|
|
||||||
client
|
|
||||||
}
|
|
||||||
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.fragment
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.ui.LockActivity
|
|
||||||
import xyz.quaver.pupil.util.Lock
|
|
||||||
import xyz.quaver.pupil.util.LockManager
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
|
|
||||||
class LockSettingsFragment : PreferenceFragmentCompat() {
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
val lockManager = LockManager(requireContext())
|
|
||||||
|
|
||||||
findPreference<Preference>("lock_pattern")?.summary =
|
|
||||||
if (lockManager.contains(Lock.Type.PATTERN))
|
|
||||||
getString(R.string.settings_lock_enabled)
|
|
||||||
else
|
|
||||||
""
|
|
||||||
|
|
||||||
findPreference<Preference>("lock_pin")?.summary =
|
|
||||||
if (lockManager.contains(Lock.Type.PIN))
|
|
||||||
getString(R.string.settings_lock_enabled)
|
|
||||||
else
|
|
||||||
""
|
|
||||||
|
|
||||||
if (lockManager.isEmpty()) {
|
|
||||||
(findPreference<Preference>("lock_fingerprint") as SwitchPreferenceCompat).isChecked = false
|
|
||||||
|
|
||||||
Preferences["lock_fingerprint"] = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
|
|
||||||
|
|
||||||
with(findPreference<Preference>("lock_pattern")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
if (LockManager(requireContext()).contains(Lock.Type.PATTERN))
|
|
||||||
summary = getString(R.string.settings_lock_enabled)
|
|
||||||
|
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
val lockManager = LockManager(requireContext())
|
|
||||||
|
|
||||||
if (lockManager.contains(Lock.Type.PATTERN)) {
|
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_lock_remove_message)
|
|
||||||
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
lockManager.remove(Lock.Type.PATTERN)
|
|
||||||
onResume()
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
} else {
|
|
||||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
|
||||||
putExtra("mode", "add_lock")
|
|
||||||
putExtra("type", "pattern")
|
|
||||||
}
|
|
||||||
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(findPreference<Preference>("lock_pin")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
if (LockManager(requireContext()).contains(Lock.Type.PIN))
|
|
||||||
summary = getString(R.string.settings_lock_enabled)
|
|
||||||
|
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
val lockManager = LockManager(requireContext())
|
|
||||||
|
|
||||||
if (lockManager.contains(Lock.Type.PIN)) {
|
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_lock_remove_message)
|
|
||||||
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
lockManager.remove(Lock.Type.PIN)
|
|
||||||
onResume()
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
} else {
|
|
||||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
|
||||||
putExtra("mode", "add_lock")
|
|
||||||
putExtra("type", "pin")
|
|
||||||
}
|
|
||||||
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(findPreference<Preference>("lock_fingerprint")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
this as SwitchPreferenceCompat
|
|
||||||
|
|
||||||
if (newValue == true && LockManager(requireContext()).isEmpty()) {
|
|
||||||
isChecked = false
|
|
||||||
|
|
||||||
Toast.makeText(requireContext(), R.string.settings_lock_fingerprint_without_lock, Toast.LENGTH_SHORT).show()
|
|
||||||
} else
|
|
||||||
isChecked = newValue as Boolean
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.fragment
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import okhttp3.*
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.client
|
|
||||||
import xyz.quaver.pupil.favorites
|
|
||||||
import xyz.quaver.pupil.util.restore
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class ManageFavoritesFragment : PreferenceFragmentCompat() {
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey)
|
|
||||||
|
|
||||||
initPreferences()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initPreferences() {
|
|
||||||
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()
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
|
||||||
if (response.code() != 200) {
|
|
||||||
response.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent(Intent.ACTION_SEND).apply {
|
|
||||||
type = "text/plain"
|
|
||||||
putExtra(Intent.EXTRA_TEXT, response.body()?.use { it.string() }?.replace("\n", ""))
|
|
||||||
}.let {
|
|
||||||
getContext()?.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.fragment
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.deleteRecursively
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.histories
|
|
||||||
import xyz.quaver.pupil.util.byteToString
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
|
|
||||||
|
|
||||||
private var job: Job? = null
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey)
|
|
||||||
|
|
||||||
initPreferences()
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_clear_cache_alert_message)
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
if (dir.exists())
|
|
||||||
dir.deleteRecursively()
|
|
||||||
|
|
||||||
Cache.instances.clear()
|
|
||||||
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
var size = 0L
|
|
||||||
|
|
||||||
dir.walk().forEach {
|
|
||||||
size += it.length()
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
"delete_downloads" -> {
|
|
||||||
val dir = DownloadManager.getInstance(context).downloadFolder
|
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_clear_downloads_alert_message)
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
job?.cancel()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
summary = context.getString(R.string.settings_storage_usage_loading)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dir.exists())
|
|
||||||
dir.listFiles()?.forEach {
|
|
||||||
when (it) {
|
|
||||||
is FileX -> it.deleteRecursively()
|
|
||||||
else -> it.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
job = launch {
|
|
||||||
var size = 0L
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
|
||||||
}
|
|
||||||
dir.walk().forEach {
|
|
||||||
size += it.length()
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
"clear_history" -> {
|
|
||||||
AlertDialog.Builder(context).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_clear_history_alert_message)
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
histories.clear()
|
|
||||||
summary = context.getString(R.string.settings_clear_history_summary, histories.size)
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initPreferences() {
|
|
||||||
val context = context ?: return
|
|
||||||
|
|
||||||
with(findPreference<Preference>("delete_cache")) {
|
|
||||||
this ?: return@with
|
|
||||||
|
|
||||||
val dir = File(context.cacheDir, "imageCache")
|
|
||||||
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
var size = 0L
|
|
||||||
|
|
||||||
dir.walk().forEach {
|
|
||||||
size += it.length()
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@ManageStorageFragment
|
|
||||||
}
|
|
||||||
|
|
||||||
with(findPreference<Preference>("delete_downloads")) {
|
|
||||||
this ?: return@with
|
|
||||||
|
|
||||||
val dir = DownloadManager.getInstance(context).downloadFolder
|
|
||||||
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
|
||||||
job?.cancel()
|
|
||||||
job = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
var size = 0L
|
|
||||||
|
|
||||||
dir.walk().forEach {
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
|
||||||
}
|
|
||||||
|
|
||||||
size += it.length()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@ManageStorageFragment
|
|
||||||
}
|
|
||||||
|
|
||||||
with(findPreference<Preference>("clear_history")) {
|
|
||||||
this ?: return@with
|
|
||||||
|
|
||||||
summary = context.getString(R.string.settings_clear_history_summary, histories.size)
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@ManageStorageFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
job?.cancel()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.fragment
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.andrognito.pinlockview.PinLockListener
|
|
||||||
import xyz.quaver.pupil.databinding.PinLockFragmentBinding
|
|
||||||
|
|
||||||
class PINLockFragment : Fragment() {
|
|
||||||
|
|
||||||
private var _binding: PinLockFragmentBinding? = null
|
|
||||||
val binding get() = _binding!!
|
|
||||||
|
|
||||||
var onPINEntered: ((String) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
_binding = PinLockFragmentBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
binding.pinLockView.attachIndicatorDots(binding.indicatorDots)
|
|
||||||
binding.pinLockView.setPinLockListener(object: PinLockListener {
|
|
||||||
override fun onComplete(p0: String?) {
|
|
||||||
onPINEntered?.invoke(p0 ?: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEmpty() {}
|
|
||||||
override fun onPinChange(p0: Int, p1: String?) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.fragment
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.andrognito.patternlockview.PatternLockView
|
|
||||||
import com.andrognito.patternlockview.listener.PatternLockViewListener
|
|
||||||
import com.andrognito.patternlockview.utils.PatternLockUtils
|
|
||||||
import xyz.quaver.pupil.databinding.PatternLockFragmentBinding
|
|
||||||
|
|
||||||
class PatternLockFragment : Fragment() {
|
|
||||||
|
|
||||||
private var _binding: PatternLockFragmentBinding? = null
|
|
||||||
val binding get() = _binding!!
|
|
||||||
|
|
||||||
var onPatternDrawn: ((String) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = PatternLockFragmentBinding.inflate(inflater, container, false)
|
|
||||||
binding.patternLockView.addPatternLockListener(object: PatternLockViewListener {
|
|
||||||
override fun onComplete(pattern: MutableList<PatternLockView.Dot>?) {
|
|
||||||
val password = PatternLockUtils.patternToMD5(binding.patternLockView, pattern)
|
|
||||||
onPatternDrawn?.invoke(password)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {}
|
|
||||||
override fun onProgress(progressPattern: MutableList<PatternLockView.Dot>?) {}
|
|
||||||
override fun onStarted() {}
|
|
||||||
})
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.fragment
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.*
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.preference.*
|
|
||||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.Dispatcher
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.getChild
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.client
|
|
||||||
import xyz.quaver.pupil.clientBuilder
|
|
||||||
import xyz.quaver.pupil.clientHolder
|
|
||||||
import xyz.quaver.pupil.ui.LockActivity
|
|
||||||
import xyz.quaver.pupil.ui.SettingsActivity
|
|
||||||
import xyz.quaver.pupil.ui.dialog.*
|
|
||||||
import xyz.quaver.pupil.util.*
|
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
class SettingsFragment :
|
|
||||||
PreferenceFragmentCompat(),
|
|
||||||
Preference.OnPreferenceClickListener,
|
|
||||||
Preference.OnPreferenceChangeListener,
|
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
|
||||||
|
|
||||||
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
parentFragmentManager
|
|
||||||
.beginTransaction()
|
|
||||||
.replace(R.id.settings, LockSettingsFragment())
|
|
||||||
.addToBackStack("Lock")
|
|
||||||
.commitAllowingStateLoss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
val lockManager = LockManager(requireContext())
|
|
||||||
|
|
||||||
findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) {
|
|
||||||
getString(R.string.settings_lock_none)
|
|
||||||
} else {
|
|
||||||
lockManager.locks?.joinToString(", ") {
|
|
||||||
when(it.type) {
|
|
||||||
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
|
|
||||||
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
|
|
||||||
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceClick(preference: Preference?): Boolean {
|
|
||||||
with (preference) {
|
|
||||||
this ?: return false
|
|
||||||
|
|
||||||
when (key) {
|
|
||||||
"app_version" -> {
|
|
||||||
checkUpdate(activity as SettingsActivity, true)
|
|
||||||
}
|
|
||||||
"download_folder" -> {
|
|
||||||
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
|
|
||||||
}
|
|
||||||
"default_query" -> {
|
|
||||||
DefaultQueryDialog().apply {
|
|
||||||
onPositiveButtonClickListener = { newTags ->
|
|
||||||
Preferences["default_query"] = newTags.toString()
|
|
||||||
summary = newTags.toString()
|
|
||||||
}
|
|
||||||
}.show(parentFragmentManager, "Default Query Dialog")
|
|
||||||
}
|
|
||||||
"app_lock" -> {
|
|
||||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
|
||||||
putExtra("force", true)
|
|
||||||
}
|
|
||||||
lockLauncher.launch(intent)
|
|
||||||
}
|
|
||||||
"proxy" -> {
|
|
||||||
ProxyDialogFragment().show(parentFragmentManager, "Proxy Dialog")
|
|
||||||
}
|
|
||||||
"user_id" -> {
|
|
||||||
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
|
||||||
ClipData.newPlainText("user_id", Preferences.get<String>("user_id"))
|
|
||||||
)
|
|
||||||
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean {
|
|
||||||
with (preference) {
|
|
||||||
this ?: return false
|
|
||||||
|
|
||||||
when (key) {
|
|
||||||
"tag_translation" -> {
|
|
||||||
updateTranslations()
|
|
||||||
}
|
|
||||||
"nomedia" -> {
|
|
||||||
val create = (newValue as? Boolean) ?: return false
|
|
||||||
|
|
||||||
return kotlin.runCatching {
|
|
||||||
val nomedia = DownloadManager.getInstance(context).downloadFolder.getChild(".nomedia")
|
|
||||||
|
|
||||||
if (create)
|
|
||||||
nomedia.createNewFile()
|
|
||||||
else
|
|
||||||
nomedia.delete()
|
|
||||||
}.getOrDefault(false)
|
|
||||||
}
|
|
||||||
"dark_mode" -> {
|
|
||||||
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
|
|
||||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
|
||||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
|
||||||
key ?: return
|
|
||||||
|
|
||||||
with(findPreference<Preference>(key)) {
|
|
||||||
this ?: return
|
|
||||||
|
|
||||||
when (key) {
|
|
||||||
"proxy" -> {
|
|
||||||
summary = context?.let { getProxyInfo().type.name }
|
|
||||||
}
|
|
||||||
"download_folder" -> {
|
|
||||||
summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath
|
|
||||||
}
|
|
||||||
"download_folder_name" -> {
|
|
||||||
summary = Preferences["download_folder_name", "[-id-] -title-"]
|
|
||||||
}
|
|
||||||
"max_concurrent_download" -> {
|
|
||||||
val newValue = Preferences.get<String>(key).toIntOrNull() ?: 0
|
|
||||||
|
|
||||||
if (newValue == 0)
|
|
||||||
clientBuilder.dispatcher(Dispatcher())
|
|
||||||
else
|
|
||||||
clientBuilder.dispatcher((Dispatcher(Executors.newFixedThreadPool(newValue))))
|
|
||||||
|
|
||||||
clientHolder = null
|
|
||||||
client
|
|
||||||
}
|
|
||||||
else -> return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
|
||||||
|
|
||||||
Preferences.registerOnSharedPreferenceChangeListener(this)
|
|
||||||
|
|
||||||
initPreferences()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
Preferences.unregisterOnSharedPreferenceChangeListener(this)
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initPreferences() {
|
|
||||||
for (i in 0 until preferenceScreen.preferenceCount) {
|
|
||||||
|
|
||||||
preferenceScreen.getPreference(i).run {
|
|
||||||
if (this is PreferenceCategory)
|
|
||||||
(0 until preferenceCount).map { getPreference(it) }
|
|
||||||
else
|
|
||||||
listOf(this)
|
|
||||||
}.forEach { preference ->
|
|
||||||
with (preference) with@{
|
|
||||||
|
|
||||||
when (key) {
|
|
||||||
"app_version" -> {
|
|
||||||
val manager = requireContext().packageManager
|
|
||||||
val info = manager.getPackageInfo(requireContext().packageName, 0)
|
|
||||||
summary = requireContext().getString(R.string.settings_app_version_description, info.versionName)
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"download_folder_name" -> {
|
|
||||||
summary = Preferences["download_folder_name", "[-id-] -title-"]
|
|
||||||
|
|
||||||
setOnPreferenceClickListener {
|
|
||||||
DownloadFolderNameDialogFragment().show(requireActivity().supportFragmentManager, "Download Location Dialog")
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"download_folder" -> {
|
|
||||||
summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"nomedia" -> {
|
|
||||||
(this as SwitchPreferenceCompat).isChecked = kotlin.runCatching {
|
|
||||||
DownloadManager.getInstance(context).downloadFolder.getChild(".nomedia").exists()
|
|
||||||
}.getOrDefault(false)
|
|
||||||
|
|
||||||
onPreferenceChangeListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"default_query" -> {
|
|
||||||
summary = Preferences.get<String>("default_query")
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"app_lock" -> {
|
|
||||||
val lockManager = LockManager(requireContext())
|
|
||||||
summary =
|
|
||||||
if (lockManager.locks.isNullOrEmpty()) {
|
|
||||||
getString(R.string.settings_lock_none)
|
|
||||||
} else {
|
|
||||||
lockManager.locks?.joinToString(", ") {
|
|
||||||
when (it.type) {
|
|
||||||
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
|
|
||||||
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
|
|
||||||
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"proxy" -> {
|
|
||||||
summary = getProxyInfo().type.name
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"tag_translation" -> {
|
|
||||||
this as ListPreference
|
|
||||||
|
|
||||||
isEnabled = false
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
kotlin.runCatching {
|
|
||||||
val languages = getAvailableLanguages().distinct().toTypedArray()
|
|
||||||
|
|
||||||
entries = languages.map { Locale(it).let { loc -> loc.getDisplayLanguage(loc) } }.toTypedArray()
|
|
||||||
entryValues = languages
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
isEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPreferenceChangeListener = this@SettingsFragment
|
|
||||||
|
|
||||||
}
|
|
||||||
"dark_mode" -> {
|
|
||||||
onPreferenceChangeListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"old_import_galleries" -> {
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"user_id" -> {
|
|
||||||
summary = Preferences.get<String>("user_id")
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"oss" -> {
|
|
||||||
setOnPreferenceClickListener {
|
|
||||||
context?.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
28
app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val LightBlue300 = Color(0xFF4FC3F7)
|
||||||
|
val LightBlue700 = Color(0xFF0288D1)
|
||||||
|
val Pink600 = Color(0xFFD81B60)
|
||||||
|
val Blue700 = Color(0xFF1976D2)
|
||||||
|
val GreenA700 = Color(0xFF00C853)
|
||||||
|
val Orange500 = Color(0xFFFF9800)
|
||||||
29
app/src/main/java/xyz/quaver/pupil/ui/theme/Shape.kt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Shapes
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
val Shapes = Shapes(
|
||||||
|
small = RoundedCornerShape(4.dp),
|
||||||
|
medium = RoundedCornerShape(4.dp),
|
||||||
|
large = RoundedCornerShape(0.dp)
|
||||||
|
)
|
||||||
57
app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.contentColorFor
|
||||||
|
import androidx.compose.material.darkColors
|
||||||
|
import androidx.compose.material.lightColors
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
private val DarkColorPalette = darkColors(
|
||||||
|
primary = LightBlue300,
|
||||||
|
primaryVariant = LightBlue700,
|
||||||
|
secondary = Pink600,
|
||||||
|
onPrimary = Color.White,
|
||||||
|
onSecondary = Color.White
|
||||||
|
)
|
||||||
|
private val LightColorPalette = lightColors(
|
||||||
|
primary = LightBlue300,
|
||||||
|
primaryVariant = LightBlue700,
|
||||||
|
secondary = Pink600,
|
||||||
|
onPrimary = Color.White,
|
||||||
|
onSecondary = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PupilTheme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val colors = if (darkTheme) DarkColorPalette else LightColorPalette
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colors = colors,
|
||||||
|
typography = Typography,
|
||||||
|
shapes = Shapes,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
33
app/src/main/java/xyz/quaver/pupil/ui/theme/Type.kt
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material.Typography
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
val Typography = Typography(
|
||||||
|
body1 = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.view
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.PorterDuff
|
|
||||||
import android.graphics.PorterDuffColorFilter
|
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|
||||||
import xyz.quaver.floatingsearchview.FloatingSearchView
|
|
||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
|
||||||
import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.favoriteTags
|
|
||||||
import xyz.quaver.pupil.types.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
|
||||||
FloatingSearchView(context, attrs),
|
|
||||||
FloatingSearchView.OnSearchListener,
|
|
||||||
TextWatcher
|
|
||||||
{
|
|
||||||
private val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
|
||||||
|
|
||||||
var onHistoryDeleteClickedListener: ((String) -> Unit)? = null
|
|
||||||
var onFavoriteHistorySwitchClickListener: (() -> Unit)? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or searchInputView.imeOptions
|
|
||||||
|
|
||||||
searchInputView.addTextChangedListener(this)
|
|
||||||
onSearchListener = this
|
|
||||||
onBindSuggestionCallback = { binding, item, itemPosition ->
|
|
||||||
onBindSuggestion(binding.root, binding.leftIcon, binding.body, item, itemPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun afterTextChanged(s: Editable?) {
|
|
||||||
s ?: return
|
|
||||||
|
|
||||||
if (s.any { it.isUpperCase() })
|
|
||||||
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
|
|
||||||
when (searchSuggestion) {
|
|
||||||
is TagSuggestion -> {
|
|
||||||
val tag = "${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")}"
|
|
||||||
with(searchInputView.text!!) {
|
|
||||||
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ') + 1, length)
|
|
||||||
|
|
||||||
if (!this.contains(tag))
|
|
||||||
append("$tag ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Suggestion -> {
|
|
||||||
with(searchInputView.text!!) {
|
|
||||||
clear()
|
|
||||||
append(searchSuggestion.body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is FavoriteHistorySwitch -> onFavoriteHistorySwitchClickListener?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSearchAction(currentQuery: String?) {}
|
|
||||||
|
|
||||||
fun onBindSuggestion(
|
|
||||||
suggestionView: View?,
|
|
||||||
leftIcon: ImageView?,
|
|
||||||
textView: TextView?,
|
|
||||||
item: SearchSuggestion?,
|
|
||||||
itemPosition: Int
|
|
||||||
) {
|
|
||||||
when(item) {
|
|
||||||
is TagSuggestion -> {
|
|
||||||
val tag = "${item.n}:${item.s}"
|
|
||||||
|
|
||||||
leftIcon?.setImageDrawable(
|
|
||||||
ResourcesCompat.getDrawable(
|
|
||||||
resources,
|
|
||||||
when(item.n) {
|
|
||||||
"female" -> R.drawable.gender_female
|
|
||||||
"male" -> R.drawable.gender_male
|
|
||||||
"language" -> R.drawable.translate
|
|
||||||
"group" -> R.drawable.account_group
|
|
||||||
"character" -> R.drawable.account_star
|
|
||||||
"series" -> R.drawable.book_open
|
|
||||||
"artist" -> R.drawable.brush
|
|
||||||
else -> R.drawable.tag
|
|
||||||
},
|
|
||||||
context.theme)
|
|
||||||
)
|
|
||||||
|
|
||||||
with(suggestionView?.findViewById<ImageView>(R.id.right_icon)) {
|
|
||||||
this ?: return@with
|
|
||||||
|
|
||||||
if (favoriteTags.contains(Tag.parse(tag)))
|
|
||||||
setImageResource(R.drawable.ic_star_filled)
|
|
||||||
else
|
|
||||||
setImageResource(R.drawable.ic_star_empty)
|
|
||||||
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
rotation = 0f
|
|
||||||
|
|
||||||
isEnabled = true
|
|
||||||
isClickable = true
|
|
||||||
|
|
||||||
setOnClickListener {
|
|
||||||
val tag = Tag.parse(tag)
|
|
||||||
|
|
||||||
if (favoriteTags.contains(tag)) {
|
|
||||||
setImageResource(R.drawable.ic_star_empty)
|
|
||||||
favoriteTags.remove(tag)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setImageDrawable(
|
|
||||||
AnimatedVectorDrawableCompat.create(context,
|
|
||||||
R.drawable.avd_star
|
|
||||||
))
|
|
||||||
(drawable as Animatable).start()
|
|
||||||
|
|
||||||
favoriteTags.add(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.t > 0) {
|
|
||||||
(suggestionView as? LinearLayout)?.let {
|
|
||||||
val count = it.findViewById<TextView>(R.id.count)
|
|
||||||
if (count == null)
|
|
||||||
it.addView(
|
|
||||||
LayoutInflater.from(context).inflate(R.layout.suggestion_count, suggestionView, false)
|
|
||||||
.apply {
|
|
||||||
this as TextView
|
|
||||||
|
|
||||||
text = item.t.toString()
|
|
||||||
}, 2
|
|
||||||
)
|
|
||||||
else
|
|
||||||
count.text = item.t.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is FavoriteHistorySwitch -> {
|
|
||||||
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.swap_horizontal, context.theme))
|
|
||||||
}
|
|
||||||
is Suggestion -> {
|
|
||||||
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.history, context.theme))
|
|
||||||
|
|
||||||
with(suggestionView?.findViewById<ImageView>(R.id.right_icon)) {
|
|
||||||
this ?: return@with
|
|
||||||
|
|
||||||
setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.delete, context.theme))
|
|
||||||
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
rotation = 0f
|
|
||||||
|
|
||||||
isEnabled = true
|
|
||||||
isClickable = true
|
|
||||||
|
|
||||||
setOnClickListener {
|
|
||||||
onHistoryDeleteClickedListener?.invoke(item.body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is LoadingSuggestion -> {
|
|
||||||
leftIcon?.setImageDrawable(CircularProgressDrawable(context).also {
|
|
||||||
it.setStyle(CircularProgressDrawable.DEFAULT)
|
|
||||||
it.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN)
|
|
||||||
it.start()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
is NoResultSuggestion -> {
|
|
||||||
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.close, context.theme))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,462 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.view;
|
|
||||||
|
|
||||||
import android.animation.ValueAnimator;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.os.Vibrator;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.animation.DecelerateInterpolator;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.core.view.NestedScrollingChild;
|
|
||||||
import androidx.core.view.NestedScrollingChildHelper;
|
|
||||||
import androidx.core.view.NestedScrollingParent;
|
|
||||||
import androidx.core.view.NestedScrollingParentHelper;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import androidx.core.widget.TextViewCompat;
|
|
||||||
|
|
||||||
import xyz.quaver.pupil.R;
|
|
||||||
|
|
||||||
@SuppressWarnings("NullableProblems")
|
|
||||||
public class MainView extends ViewGroup implements NestedScrollingChild, NestedScrollingParent {
|
|
||||||
|
|
||||||
private static final int PAGE_TURN_LAYOUT_SIZE = 48;
|
|
||||||
private static final int PAGE_TURN_ANIM_DURATION = 500;
|
|
||||||
private static final int PREV_OFFSET = 64;
|
|
||||||
private static final int RIPPLE_GIVE = 4;
|
|
||||||
|
|
||||||
private final float adjustedPageTurnLayoutSize;
|
|
||||||
private final float adjustedPrevOffset;
|
|
||||||
private final float adjustedRippleGive;
|
|
||||||
|
|
||||||
final private NestedScrollingParentHelper mNestedScrollingParentHelper;
|
|
||||||
final private NestedScrollingChildHelper mNestedScrollingChildHelper;
|
|
||||||
|
|
||||||
final private Vibrator mVibrator;
|
|
||||||
|
|
||||||
private View mTarget;
|
|
||||||
|
|
||||||
private TextView mPrev;
|
|
||||||
private TextView mNext;
|
|
||||||
|
|
||||||
private final Paint mRipplePaint = new Paint();
|
|
||||||
private final Rect mRippleBound = new Rect();
|
|
||||||
|
|
||||||
private int mRippleSize = 0;
|
|
||||||
private final int mRippleTargetSize;
|
|
||||||
private final ValueAnimator mRippleAnimator = new ValueAnimator();
|
|
||||||
|
|
||||||
private int mCurrentOverScroll = 0;
|
|
||||||
|
|
||||||
private int mCurrentPage = 1;
|
|
||||||
private boolean mShowPrev;
|
|
||||||
private boolean mShowNext;
|
|
||||||
|
|
||||||
private OnPageTurnListener mOnPageTurnListener;
|
|
||||||
|
|
||||||
public MainView(@NonNull Context context) {
|
|
||||||
this(context, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MainView(@NonNull Context context, AttributeSet attr) {
|
|
||||||
this(context, attr, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MainView(@NonNull Context context, AttributeSet attr, int defStyle) {
|
|
||||||
super(context, attr, defStyle);
|
|
||||||
|
|
||||||
setWillNotDraw(false);
|
|
||||||
|
|
||||||
DisplayMetrics metrics = getResources().getDisplayMetrics();
|
|
||||||
|
|
||||||
adjustedPageTurnLayoutSize = PAGE_TURN_LAYOUT_SIZE * metrics.density;
|
|
||||||
adjustedPrevOffset = PREV_OFFSET * metrics.density;
|
|
||||||
adjustedRippleGive = RIPPLE_GIVE * metrics.density;
|
|
||||||
|
|
||||||
mRippleTargetSize = metrics.widthPixels;
|
|
||||||
|
|
||||||
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
|
|
||||||
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
|
|
||||||
|
|
||||||
mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
|
|
||||||
|
|
||||||
mRippleAnimator.addUpdateListener(animation -> {
|
|
||||||
mRippleSize = (int) animation.getAnimatedValue();
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
mRippleAnimator.setDuration(PAGE_TURN_ANIM_DURATION);
|
|
||||||
|
|
||||||
initPageTurnView();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCurrentPage(int currentPage, boolean showNext) {
|
|
||||||
mCurrentPage = currentPage;
|
|
||||||
|
|
||||||
mShowPrev = currentPage > 1;
|
|
||||||
mShowNext = showNext;
|
|
||||||
|
|
||||||
mPrev.setText(getContext().getString(R.string.main_move_to_page, mCurrentPage-1));
|
|
||||||
mNext.setText(getContext().getString(R.string.main_move_to_page, mCurrentPage+1));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnPageTurnListener(OnPageTurnListener listener) {
|
|
||||||
mOnPageTurnListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initPageTurnView() {
|
|
||||||
TextView prev = new TextView(getContext());
|
|
||||||
TextView next = new TextView(getContext());
|
|
||||||
|
|
||||||
prev.setGravity(Gravity.CENTER_VERTICAL);
|
|
||||||
next.setGravity(Gravity.CENTER_VERTICAL);
|
|
||||||
|
|
||||||
prev.setCompoundDrawablesWithIntrinsicBounds(R.drawable.navigate_prev, 0, 0, 0);
|
|
||||||
next.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.navigate_next, 0);
|
|
||||||
|
|
||||||
TextViewCompat.setCompoundDrawableTintList(prev, AppCompatResources.getColorStateList(getContext(), R.color.colorAccent));
|
|
||||||
TextViewCompat.setCompoundDrawableTintList(next, AppCompatResources.getColorStateList(getContext(), R.color.colorAccent));
|
|
||||||
|
|
||||||
prev.setVisibility(View.INVISIBLE);
|
|
||||||
next.setVisibility(View.INVISIBLE);
|
|
||||||
|
|
||||||
mPrev = prev;
|
|
||||||
mNext = next;
|
|
||||||
|
|
||||||
addView(mPrev);
|
|
||||||
addView(mNext);
|
|
||||||
|
|
||||||
setCurrentPage(1, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureTarget() {
|
|
||||||
if (mTarget == null) {
|
|
||||||
for (int i = 0; i < getChildCount(); i++) {
|
|
||||||
View child = getChildAt(i);
|
|
||||||
|
|
||||||
if (!child.equals(mNext) && !child.equals(mPrev)) {
|
|
||||||
mTarget = child;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
||||||
final int width = getMeasuredWidth();
|
|
||||||
final int height = getMeasuredHeight();
|
|
||||||
|
|
||||||
if (getChildCount() == 0)
|
|
||||||
return;
|
|
||||||
if (mTarget == null)
|
|
||||||
ensureTarget();
|
|
||||||
if (mTarget == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
mTarget.layout(
|
|
||||||
getPaddingLeft(),
|
|
||||||
getPaddingTop(),
|
|
||||||
width - getPaddingRight(),
|
|
||||||
height - getPaddingBottom()
|
|
||||||
);
|
|
||||||
|
|
||||||
final int prevWidth = mPrev.getMeasuredWidth();
|
|
||||||
mPrev.layout(
|
|
||||||
width / 2 - prevWidth / 2,
|
|
||||||
getPaddingTop() + (int) adjustedPrevOffset,
|
|
||||||
width / 2 + prevWidth / 2,
|
|
||||||
getPaddingTop() + (int) adjustedPrevOffset + mPrev.getMeasuredHeight()
|
|
||||||
);
|
|
||||||
|
|
||||||
final int nextWidth = mNext.getMeasuredWidth();
|
|
||||||
mNext.layout(
|
|
||||||
width / 2 - nextWidth / 2,
|
|
||||||
height - getPaddingBottom() - mNext.getMeasuredHeight(),
|
|
||||||
width / 2 + nextWidth / 2,
|
|
||||||
height - getPaddingBottom()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
||||||
if (mTarget == null)
|
|
||||||
ensureTarget();
|
|
||||||
if (mTarget == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
mTarget.measure(
|
|
||||||
MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY),
|
|
||||||
MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)
|
|
||||||
);
|
|
||||||
|
|
||||||
mPrev.measure(
|
|
||||||
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST),
|
|
||||||
MeasureSpec.makeMeasureSpec((int) adjustedPageTurnLayoutSize, MeasureSpec.EXACTLY)
|
|
||||||
);
|
|
||||||
|
|
||||||
mNext.measure(
|
|
||||||
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST),
|
|
||||||
MeasureSpec.makeMeasureSpec((int) adjustedPageTurnLayoutSize, MeasureSpec.EXACTLY)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDraw(Canvas canvas) {
|
|
||||||
super.onDraw(canvas);
|
|
||||||
|
|
||||||
if (mCurrentOverScroll == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (mCurrentOverScroll > 0) {
|
|
||||||
mRippleBound.set(
|
|
||||||
getPaddingLeft(),
|
|
||||||
(int) (getPaddingTop() - adjustedRippleGive),
|
|
||||||
getMeasuredWidth() - getPaddingRight(),
|
|
||||||
(int) (getPaddingTop() + adjustedPrevOffset + mPrev.getMeasuredHeight() + adjustedRippleGive)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mCurrentOverScroll < 0) {
|
|
||||||
final int height = getMeasuredHeight();
|
|
||||||
mRippleBound.set(
|
|
||||||
getPaddingLeft(),
|
|
||||||
(int) (height - getPaddingBottom() - mNext.getMeasuredHeight() - adjustedRippleGive),
|
|
||||||
getMeasuredWidth() - getPaddingRight(),
|
|
||||||
height - getPaddingBottom()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
mRipplePaint.reset();
|
|
||||||
mRipplePaint.setStyle(Paint.Style.FILL);
|
|
||||||
|
|
||||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
|
||||||
|
|
||||||
switch (currentNightMode) {
|
|
||||||
case Configuration.UI_MODE_NIGHT_YES:
|
|
||||||
mRipplePaint.setColor(ContextCompat.getColor(getContext(), R.color.material_light_blue_700));
|
|
||||||
break;
|
|
||||||
case Configuration.UI_MODE_NIGHT_NO:
|
|
||||||
mRipplePaint.setColor(ContextCompat.getColor(getContext(), R.color.material_light_blue_300));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawCircle(
|
|
||||||
(mRippleBound.left + mRippleBound.right) / 2F,
|
|
||||||
mCurrentOverScroll > 0 ? mRippleBound.bottom : mRippleBound.top,
|
|
||||||
mRippleSize,
|
|
||||||
mRipplePaint
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onOverscroll(int overscroll) {
|
|
||||||
if (mTarget == null)
|
|
||||||
ensureTarget();
|
|
||||||
if (mTarget == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
mCurrentOverScroll = overscroll;
|
|
||||||
|
|
||||||
if (overscroll > 0) {
|
|
||||||
mPrev.setVisibility(View.VISIBLE);
|
|
||||||
mNext.setVisibility(View.INVISIBLE);
|
|
||||||
} else if (overscroll < 0) {
|
|
||||||
mPrev.setVisibility(View.INVISIBLE);
|
|
||||||
mNext.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
mPrev.setVisibility(View.INVISIBLE);
|
|
||||||
mNext.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.abs(overscroll) >= adjustedPageTurnLayoutSize) {
|
|
||||||
if (!mRippleAnimator.isStarted() && mRippleSize != mRippleTargetSize) {
|
|
||||||
mVibrator.vibrate(10);
|
|
||||||
|
|
||||||
mRippleAnimator.setIntValues(mRippleSize, mRippleTargetSize);
|
|
||||||
mRippleAnimator.start();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!mRippleAnimator.isStarted() && mRippleSize != 0) {
|
|
||||||
mRippleAnimator.setIntValues(mRippleSize, 0);
|
|
||||||
mRippleAnimator.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float clippedOverScrollTop = (overscroll > 0 ? 1 : -1) * Math.min(Math.abs(overscroll), adjustedPageTurnLayoutSize);
|
|
||||||
mTarget.setTranslationY(clippedOverScrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onOverscrollEnd(int overscroll) {
|
|
||||||
if (mTarget == null)
|
|
||||||
ensureTarget();
|
|
||||||
if (mTarget == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
mRippleAnimator.cancel();
|
|
||||||
mRippleAnimator.setIntValues(mRippleSize, 0);
|
|
||||||
mRippleAnimator.start();
|
|
||||||
|
|
||||||
mPrev.setVisibility(View.INVISIBLE);
|
|
||||||
mNext.setVisibility(View.INVISIBLE);
|
|
||||||
|
|
||||||
ViewCompat.animate(mTarget)
|
|
||||||
.setDuration(PAGE_TURN_ANIM_DURATION)
|
|
||||||
.setInterpolator(new DecelerateInterpolator())
|
|
||||||
.translationY(0);
|
|
||||||
|
|
||||||
if (Math.abs(overscroll) > adjustedPageTurnLayoutSize && mOnPageTurnListener != null) {
|
|
||||||
if (overscroll > 0)
|
|
||||||
mOnPageTurnListener.onPrev(mCurrentPage-1);
|
|
||||||
if (overscroll < 0)
|
|
||||||
mOnPageTurnListener.onNext(mCurrentPage+1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NestedScrollingParent
|
|
||||||
|
|
||||||
private int mTotalUnconsumed = 0;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
|
|
||||||
return isEnabled() && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNestedScrollAccepted(View child, View target, int axes) {
|
|
||||||
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
|
|
||||||
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
|
|
||||||
|
|
||||||
mTotalUnconsumed = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
|
|
||||||
if (mTotalUnconsumed != 0 && dy > 0 == mTotalUnconsumed > 0) {
|
|
||||||
if (Math.abs(dy) > Math.abs(mTotalUnconsumed)) {
|
|
||||||
consumed[1] = dy - mTotalUnconsumed;
|
|
||||||
mTotalUnconsumed = 0;
|
|
||||||
} else {
|
|
||||||
mTotalUnconsumed -= dy;
|
|
||||||
consumed[1] = dy;
|
|
||||||
}
|
|
||||||
|
|
||||||
onOverscroll(mTotalUnconsumed);
|
|
||||||
}
|
|
||||||
|
|
||||||
final int[] parentConsumed = new int[2];
|
|
||||||
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
|
|
||||||
consumed[0] += parentConsumed[0];
|
|
||||||
consumed[1] += parentConsumed[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
|
|
||||||
final int[] mParentOffsetInWindow = new int[2];
|
|
||||||
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
|
|
||||||
|
|
||||||
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
|
|
||||||
|
|
||||||
if (mTotalUnconsumed == 0 && ((dy < 0 && !mShowPrev) || (dy > 0 && !mShowNext)))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (dy != 0) {
|
|
||||||
mTotalUnconsumed -= dy;
|
|
||||||
onOverscroll(mTotalUnconsumed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStopNestedScroll(View child) {
|
|
||||||
mNestedScrollingParentHelper.onStopNestedScroll(child);
|
|
||||||
|
|
||||||
if (Math.abs(mTotalUnconsumed) > 0) {
|
|
||||||
onOverscrollEnd(mTotalUnconsumed);
|
|
||||||
mTotalUnconsumed = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
stopNestedScroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
// NestedScrollingChild
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setNestedScrollingEnabled(boolean enabled) {
|
|
||||||
mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isNestedScrollingEnabled() {
|
|
||||||
return mNestedScrollingChildHelper.isNestedScrollingEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean startNestedScroll(int axes) {
|
|
||||||
return mNestedScrollingChildHelper.startNestedScroll(axes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stopNestedScroll() {
|
|
||||||
mNestedScrollingChildHelper.stopNestedScroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasNestedScrollingParent() {
|
|
||||||
return mNestedScrollingChildHelper.hasNestedScrollingParent();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
|
|
||||||
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
|
|
||||||
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
|
|
||||||
return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
|
|
||||||
return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnPageTurnListener {
|
|
||||||
void onPrev(int page);
|
|
||||||
void onNext(int page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package xyz.quaver.pupil.ui.view
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.cardview.widget.CardView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.databinding.ProgressCardViewBinding
|
|
||||||
|
|
||||||
class ProgressCard @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, defStyle: Int = R.attr.cardViewStyle) : CardView(context, attr, defStyle) {
|
|
||||||
|
|
||||||
enum class Type {
|
|
||||||
LOADING,
|
|
||||||
CACHE,
|
|
||||||
DOWNLOAD
|
|
||||||
}
|
|
||||||
|
|
||||||
var type: Type = Type.LOADING
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
|
|
||||||
when (field) {
|
|
||||||
Type.LOADING -> R.color.colorAccent
|
|
||||||
Type.CACHE -> R.color.material_blue_700
|
|
||||||
Type.DOWNLOAD -> R.color.material_green_a700
|
|
||||||
}.let {
|
|
||||||
val color = ContextCompat.getColor(context, it)
|
|
||||||
DrawableCompat.setTint(binding.progressbar.progressDrawable, color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var progress: Int
|
|
||||||
get() = binding.progressbar.progress
|
|
||||||
set(value) {
|
|
||||||
binding.progressbar.progress = value
|
|
||||||
}
|
|
||||||
var max: Int
|
|
||||||
get() = binding.progressbar.max
|
|
||||||
set(value) {
|
|
||||||
binding.progressbar.max = value
|
|
||||||
|
|
||||||
binding.progressbar.visibility =
|
|
||||||
if (value == 0)
|
|
||||||
GONE
|
|
||||||
else
|
|
||||||
VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
val binding = ProgressCardViewBinding.inflate(LayoutInflater.from(context), this)
|
|
||||||
|
|
||||||
init {
|
|
||||||
binding.content.setOnClickListener {
|
|
||||||
performClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.content.setOnLongClickListener {
|
|
||||||
performLongClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
|
|
||||||
if (childCount == 0)
|
|
||||||
super.addView(child, index, params)
|
|
||||||
else
|
|
||||||
binding.content.addView(child, index, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.view
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.google.android.material.chip.Chip
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.favoriteTags
|
|
||||||
import xyz.quaver.pupil.types.Tag
|
|
||||||
import xyz.quaver.pupil.util.translations
|
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
|
||||||
class TagChip(context: Context, _tag: Tag) : Chip(context) {
|
|
||||||
|
|
||||||
val tag: Tag =
|
|
||||||
_tag.let {
|
|
||||||
when {
|
|
||||||
it.area != null -> it
|
|
||||||
else -> Tag("tag", _tag.tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val languages = context.resources.getStringArray(R.array.languages).map {
|
|
||||||
it.split("|").let { split ->
|
|
||||||
Pair(split[0], split[1])
|
|
||||||
}
|
|
||||||
}.toMap()
|
|
||||||
|
|
||||||
init {
|
|
||||||
when(tag.area) {
|
|
||||||
"male" -> {
|
|
||||||
setChipBackgroundColorResource(R.color.material_blue_700)
|
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
|
||||||
setCloseIconTintResource(android.R.color.white)
|
|
||||||
chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_male_white)
|
|
||||||
}
|
|
||||||
"female" -> {
|
|
||||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
|
||||||
setCloseIconTintResource(android.R.color.white)
|
|
||||||
chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_female_white)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (favoriteTags.contains(tag))
|
|
||||||
setChipBackgroundColorResource(R.color.material_orange_500)
|
|
||||||
|
|
||||||
isCloseIconVisible = true
|
|
||||||
closeIcon = ContextCompat.getDrawable(context,
|
|
||||||
if (favoriteTags.contains(tag))
|
|
||||||
R.drawable.ic_star_filled
|
|
||||||
else
|
|
||||||
R.drawable.ic_star_empty
|
|
||||||
)
|
|
||||||
|
|
||||||
setOnCloseIconClickListener {
|
|
||||||
if (favoriteTags.contains(tag)) {
|
|
||||||
favoriteTags.remove(tag)
|
|
||||||
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_empty)
|
|
||||||
|
|
||||||
when(tag.area) {
|
|
||||||
"male" -> setChipBackgroundColorResource(R.color.material_blue_700)
|
|
||||||
"female" -> setChipBackgroundColorResource(R.color.material_pink_600)
|
|
||||||
else -> chipBackgroundColor = null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
favoriteTags.add(tag)
|
|
||||||
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_filled)
|
|
||||||
setChipBackgroundColorResource(R.color.material_orange_500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
text = when (tag.area) {
|
|
||||||
"language" -> languages[tag.tag]
|
|
||||||
else -> (translations[tag.tag] ?: tag.tag).wordCapitalize()
|
|
||||||
}
|
|
||||||
|
|
||||||
setEnsureMinTouchTargetSize(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.view
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.TypedArray
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.android.material.chip.Chip
|
|
||||||
import com.google.android.material.chip.ChipGroup
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.types.Tag
|
|
||||||
import xyz.quaver.pupil.types.Tags
|
|
||||||
|
|
||||||
class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, attrStyle: Int = R.attr.chipGroupStyle, val tags: Tags = Tags()) : ChipGroup(context, attr, attrStyle), MutableSet<Tag> by tags {
|
|
||||||
|
|
||||||
object Defaults {
|
|
||||||
const val maxChipSize = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
var maxChipSize: Int = Defaults.maxChipSize
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val moreView = Chip(context).apply {
|
|
||||||
text = "…"
|
|
||||||
|
|
||||||
setEnsureMinTouchTargetSize(false)
|
|
||||||
|
|
||||||
setOnClickListener {
|
|
||||||
removeView(this)
|
|
||||||
|
|
||||||
for (i in maxChipSize until tags.size) {
|
|
||||||
val tag = tags.elementAt(i)
|
|
||||||
|
|
||||||
addView(TagChip(context, tag).apply {
|
|
||||||
setOnClickListener {
|
|
||||||
onClickListener?.invoke(tag)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var onClickListener: ((Tag) -> Unit)? = null
|
|
||||||
|
|
||||||
private fun applyAttributes(attr: TypedArray) {
|
|
||||||
maxChipSize = attr.getInt(R.styleable.TagChipGroup_maxTag, Defaults.maxChipSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var refreshJob: Job? = null
|
|
||||||
fun refresh() {
|
|
||||||
refreshJob?.cancel()
|
|
||||||
this.removeAllViews()
|
|
||||||
|
|
||||||
refreshJob = CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
tags.take(maxChipSize).map {
|
|
||||||
CoroutineScope(Dispatchers.Default).async {
|
|
||||||
TagChip(context, it).apply {
|
|
||||||
setOnClickListener {
|
|
||||||
onClickListener?.invoke(this.tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.forEach {
|
|
||||||
addView(it.await())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxChipSize > 0 && tags.size > maxChipSize)
|
|
||||||
addView(moreView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
applyAttributes(context.obtainStyledAttributes(attr, R.styleable.TagChipGroup))
|
|
||||||
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
|
|
||||||
class ItemClickSupport(private val recyclerView: RecyclerView) {
|
|
||||||
|
|
||||||
var onItemClickListener: ((RecyclerView, Int, View) -> Unit)? = null
|
|
||||||
var onItemLongClickListener: ((RecyclerView, Int, View) -> Boolean)? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
recyclerView.apply {
|
|
||||||
setTag(R.id.item_click_support, this)
|
|
||||||
addOnChildAttachStateChangeListener(object: RecyclerView.OnChildAttachStateChangeListener {
|
|
||||||
override fun onChildViewAttachedToWindow(view: View) {
|
|
||||||
onItemClickListener?.let { listener ->
|
|
||||||
view.setOnClickListener {
|
|
||||||
recyclerView.getChildViewHolder(view).let { holder ->
|
|
||||||
listener.invoke(recyclerView, holder.adapterPosition, view)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onItemLongClickListener?.let { listener ->
|
|
||||||
view.setOnLongClickListener {
|
|
||||||
recyclerView.getChildViewHolder(view).let { holder ->
|
|
||||||
listener.invoke(recyclerView, holder.adapterPosition, view)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChildViewDetachedFromWindow(view: View) {
|
|
||||||
// Do Nothing
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun detach() {
|
|
||||||
recyclerView.apply {
|
|
||||||
clearOnChildAttachStateChangeListeners()
|
|
||||||
setTag(R.id.item_click_support, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun addTo(view: RecyclerView) = view.let { removeFrom(it); ItemClickSupport(it) }
|
|
||||||
fun removeFrom(view: RecyclerView) = (view.tag as? ItemClickSupport)?.detach()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
lateinit var preferences: SharedPreferences
|
|
||||||
|
|
||||||
object Preferences: SharedPreferences by preferences {
|
|
||||||
|
|
||||||
val defMap = mapOf(
|
|
||||||
String::class to "",
|
|
||||||
Int::class to -1,
|
|
||||||
Long::class to -1L,
|
|
||||||
Boolean::class to false,
|
|
||||||
Set::class to emptySet<Any>()
|
|
||||||
)
|
|
||||||
|
|
||||||
operator fun set(key: String, value: String) = edit().putString(key, value).apply()
|
|
||||||
operator fun set(key: String, value: Int) = edit().putInt(key, value).apply()
|
|
||||||
operator fun set(key: String, value: Long) = edit().putLong(key, value).apply()
|
|
||||||
operator fun set(key: String, value: Boolean) = edit().putBoolean(key, value).apply()
|
|
||||||
operator fun set(key: String, value: Set<String>) = edit().putStringSet(key, value).apply()
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
inline operator fun <reified T: Any> get(key: String, defaultVal: T = defMap[T::class] as T): T = (all[key] as? T) ?: defaultVal
|
|
||||||
|
|
||||||
fun remove(key: String) {
|
|
||||||
edit().remove(key).apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
156
app/src/main/java/xyz/quaver/pupil/util/PupilHttpClient.kt
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2022 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.engine.*
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RemoteSourceInfo(
|
||||||
|
val projectName: String,
|
||||||
|
val name: String,
|
||||||
|
val version: String
|
||||||
|
)
|
||||||
|
|
||||||
|
class Release(
|
||||||
|
val version: String,
|
||||||
|
val apkUrl: String,
|
||||||
|
val releaseNotes: Map<Locale, String>
|
||||||
|
)
|
||||||
|
|
||||||
|
private val localeMap = mapOf(
|
||||||
|
"한국어" to Locale.KOREAN,
|
||||||
|
"日本語" to Locale.JAPANESE,
|
||||||
|
"English" to Locale.ENGLISH
|
||||||
|
)
|
||||||
|
|
||||||
|
class PupilHttpClient(engine: HttpClientEngine) {
|
||||||
|
private val httpClient = HttpClient(engine) {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a list of available sources from PupilSources repository.
|
||||||
|
* Returns empty map when exception occurs
|
||||||
|
*/
|
||||||
|
suspend fun getRemoteSourceList(): Map<String, RemoteSourceInfo> = withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
httpClient.get("https://tom5079.github.io/PupilSources/versions.json").body<Map<String, RemoteSourceInfo>>()
|
||||||
|
}.getOrDefault(emptyMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads specific file from :url to :dest.
|
||||||
|
* Returns flow that emits progress.
|
||||||
|
* when value emitted by flow {
|
||||||
|
* in 0f .. 1f -> downloading
|
||||||
|
* POSITIVE_INFINITY -> download finised
|
||||||
|
* NEGATIVE_INFINITY -> exception occured
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
fun downloadFile(url: String, dest: File) = flow {
|
||||||
|
runCatching {
|
||||||
|
httpClient.prepareGet(url).execute { response ->
|
||||||
|
val channel = response.bodyAsChannel()
|
||||||
|
val contentLength = response.contentLength() ?: -1
|
||||||
|
var readBytes = 0f
|
||||||
|
|
||||||
|
dest.outputStream().use { outputStream ->
|
||||||
|
while (!channel.isClosedForRead) {
|
||||||
|
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||||
|
while (!packet.isEmpty) {
|
||||||
|
val bytes = packet.readBytes()
|
||||||
|
outputStream.write(bytes)
|
||||||
|
|
||||||
|
readBytes += bytes.size
|
||||||
|
emit(readBytes / contentLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(Float.POSITIVE_INFINITY)
|
||||||
|
}.onFailure {
|
||||||
|
emit(Float.NEGATIVE_INFINITY)
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Latest application release info from Github API.
|
||||||
|
* Returns null when exception occurs.
|
||||||
|
*/
|
||||||
|
suspend fun latestRelease(beta: Boolean = true): Release? = withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val releases = Json.parseToJsonElement(
|
||||||
|
httpClient.get("https://api.github.com/repos/tom5079/Pupil/releases").bodyAsText()
|
||||||
|
).jsonArray
|
||||||
|
|
||||||
|
val latestRelease = releases.first { release ->
|
||||||
|
beta || !release.jsonObject["prerelease"]!!.jsonPrimitive.boolean
|
||||||
|
}.jsonObject
|
||||||
|
|
||||||
|
val version = latestRelease["tag_name"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
|
val apkUrl = latestRelease["assets"]!!.jsonArray.first { asset ->
|
||||||
|
val name = asset.jsonObject["name"]!!.jsonPrimitive.content
|
||||||
|
name.startsWith("Pupil-v") && name.endsWith(".apk")
|
||||||
|
}.jsonObject["browser_download_url"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
|
val releaseNotes: Map<Locale, String> = buildMap {
|
||||||
|
val body = latestRelease["body"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
|
var locale: Locale? = null
|
||||||
|
val stringBuilder = StringBuilder()
|
||||||
|
body.lineSequence().forEach { line ->
|
||||||
|
localeMap[line.drop(3)]?.let { newLocale ->
|
||||||
|
if (locale != null) {
|
||||||
|
put(locale!!, stringBuilder.deleteCharAt(stringBuilder.length-1).toString())
|
||||||
|
stringBuilder.clear()
|
||||||
|
}
|
||||||
|
locale = newLocale
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locale != null) stringBuilder.appendLine(line)
|
||||||
|
}
|
||||||
|
put(locale!!, stringBuilder.deleteCharAt(stringBuilder.length-1).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
Release(version, apkUrl, releaseNotes)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.serializer
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
val serializer: KSerializer<List<T>>
|
|
||||||
get() = ListSerializer(serializer(any::class.java) as KSerializer<T>)
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (!file.exists()) {
|
|
||||||
file.parentFile?.mkdirs()
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun load() {
|
|
||||||
set.clear()
|
|
||||||
kotlin.runCatching {
|
|
||||||
Json.decodeFromString(serializer, file.readText())
|
|
||||||
}.onSuccess {
|
|
||||||
set.addAll(it)
|
|
||||||
}.onFailure {
|
|
||||||
FirebaseCrashlytics.getInstance().recordException(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
fun save() {
|
|
||||||
file.writeText(Json.encodeToString(serializer, set.toList()))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun add(element: T): Boolean {
|
|
||||||
set.remove(element)
|
|
||||||
|
|
||||||
return set.add(element).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun addAll(elements: Collection<T>): Boolean {
|
|
||||||
set.removeAll(elements)
|
|
||||||
|
|
||||||
return set.addAll(elements).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun remove(element: T): Boolean {
|
|
||||||
return set.remove(element).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun clear() {
|
|
||||||
set.clear()
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:Suppress("DEPRECATION", "Recycle")
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.ImageFormat
|
|
||||||
import android.graphics.SurfaceTexture
|
|
||||||
import android.hardware.Camera
|
|
||||||
import android.view.Surface
|
|
||||||
import android.view.WindowManager
|
|
||||||
import com.google.android.gms.tasks.Task
|
|
||||||
import com.google.mlkit.vision.common.InputImage
|
|
||||||
import com.google.mlkit.vision.face.Face
|
|
||||||
import com.google.mlkit.vision.face.FaceDetection
|
|
||||||
import com.google.mlkit.vision.face.FaceDetectorOptions
|
|
||||||
|
|
||||||
/** Check if this device has a camera */
|
|
||||||
private fun Context.checkCameraHardware() =
|
|
||||||
this.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
|
||||||
|
|
||||||
private fun openFrontCamera() : Pair<Camera?, Int> {
|
|
||||||
var camera: Camera? = null
|
|
||||||
var cameraID: Int = -1
|
|
||||||
|
|
||||||
val cameraInfo = Camera.CameraInfo()
|
|
||||||
|
|
||||||
for (i in 0 until Camera.getNumberOfCameras()) {
|
|
||||||
Camera.getCameraInfo(i, cameraInfo)
|
|
||||||
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
|
|
||||||
runCatching { Camera.open(i) }.getOrNull()?.let { camera = it; cameraID = i }
|
|
||||||
|
|
||||||
if (camera != null) break
|
|
||||||
}
|
|
||||||
|
|
||||||
return Pair(camera, cameraID)
|
|
||||||
}
|
|
||||||
|
|
||||||
val orientations = mapOf(
|
|
||||||
Surface.ROTATION_0 to 0,
|
|
||||||
Surface.ROTATION_90 to 90,
|
|
||||||
Surface.ROTATION_180 to 180,
|
|
||||||
Surface.ROTATION_270 to 270,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun getRotation(context: Context, cameraID: Int): Int {
|
|
||||||
val cameraRotation = Camera.CameraInfo().also { Camera.getCameraInfo(cameraID, it) }.orientation
|
|
||||||
val rotation = orientations[(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.rotation] ?: error("")
|
|
||||||
|
|
||||||
return (cameraRotation + rotation) % 360
|
|
||||||
}
|
|
||||||
|
|
||||||
var camera: Camera? = null
|
|
||||||
var surfaceTexture: SurfaceTexture? = null
|
|
||||||
private val detector = FaceDetection.getClient(
|
|
||||||
FaceDetectorOptions.Builder()
|
|
||||||
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
private var process: Task<List<Face>>? = null
|
|
||||||
|
|
||||||
fun startCamera(context: Context, callback: (List<Face>) -> Unit) {
|
|
||||||
if (camera != null) closeCamera()
|
|
||||||
|
|
||||||
val cameraID = openFrontCamera().let { (cam, cameraID) ->
|
|
||||||
cam ?: return
|
|
||||||
camera = cam
|
|
||||||
cameraID
|
|
||||||
}
|
|
||||||
|
|
||||||
with (camera!!) {
|
|
||||||
parameters = parameters.apply {
|
|
||||||
setPreviewSize(640, 480)
|
|
||||||
previewFormat = ImageFormat.NV21
|
|
||||||
}
|
|
||||||
|
|
||||||
setPreviewTexture(surfaceTexture ?: SurfaceTexture(0).also {
|
|
||||||
surfaceTexture = it
|
|
||||||
})
|
|
||||||
startPreview()
|
|
||||||
setPreviewCallback { bytes, _ ->
|
|
||||||
if (process?.isComplete == false)
|
|
||||||
return@setPreviewCallback
|
|
||||||
|
|
||||||
val rotation = getRotation(context, cameraID)
|
|
||||||
|
|
||||||
val image = InputImage.fromByteArray(bytes, 640, 480, rotation, InputImage.IMAGE_FORMAT_NV21)
|
|
||||||
process = detector.process(image)
|
|
||||||
.addOnSuccessListener(callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeCamera() {
|
|
||||||
camera?.setPreviewCallback(null)
|
|
||||||
camera?.stopPreview()
|
|
||||||
surfaceTexture?.release()
|
|
||||||
surfaceTexture = null
|
|
||||||
camera?.release()
|
|
||||||
camera = null
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util.downloader
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.net.Uri
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Request
|
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.GalleryInfo
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.*
|
|
||||||
import xyz.quaver.pupil.client
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class OldGalleryBlock(
|
|
||||||
val code: String,
|
|
||||||
val id: Int,
|
|
||||||
val galleryUrl: String,
|
|
||||||
val thumbnails: List<String>,
|
|
||||||
val title: String,
|
|
||||||
val artists: List<String>,
|
|
||||||
val series: List<String>,
|
|
||||||
val type: String,
|
|
||||||
val language: String,
|
|
||||||
val relatedTags: List<String>
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class OldReader(val code: String, val galleryInfo: GalleryInfo)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class OldMetadata(
|
|
||||||
var galleryBlock: OldGalleryBlock? = null,
|
|
||||||
var reader: OldReader? = null,
|
|
||||||
var imageList: MutableList<String?>? = null
|
|
||||||
) {
|
|
||||||
fun copy(): OldMetadata = OldMetadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Metadata(
|
|
||||||
var galleryBlock: GalleryBlock? = null,
|
|
||||||
var galleryInfo: GalleryInfo? = null,
|
|
||||||
var imageList: MutableList<String?>? = null
|
|
||||||
) {
|
|
||||||
constructor(old: OldMetadata) : this(old.galleryBlock?.let { galleryBlock -> GalleryBlock(
|
|
||||||
galleryBlock.id,
|
|
||||||
galleryBlock.galleryUrl,
|
|
||||||
galleryBlock.thumbnails,
|
|
||||||
galleryBlock.title,
|
|
||||||
galleryBlock.artists,
|
|
||||||
galleryBlock.series,
|
|
||||||
galleryBlock.type,
|
|
||||||
galleryBlock.language,
|
|
||||||
galleryBlock.relatedTags) },
|
|
||||||
old.reader?.galleryInfo,
|
|
||||||
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>()
|
|
||||||
|
|
||||||
fun getInstance(context: Context, galleryID: Int) =
|
|
||||||
instances[galleryID] ?: synchronized(this) {
|
|
||||||
instances[galleryID] ?: Cache(context, galleryID).also { instances.put(galleryID, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun delete(context: Context, galleryID: Int) {
|
|
||||||
File(context.cacheDir, "imageCache/$galleryID").deleteRecursively()
|
|
||||||
instances.remove(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
cacheFolder.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata = kotlin.runCatching {
|
|
||||||
findFile(".metadata")?.readText()?.let { metadata ->
|
|
||||||
kotlin.runCatching {
|
|
||||||
Json.decodeFromString<Metadata>(metadata)
|
|
||||||
}.getOrElse {
|
|
||||||
Metadata(Json.decodeFromString<OldMetadata>(metadata))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.getOrNull() ?: Metadata()
|
|
||||||
|
|
||||||
val downloadFolder: FileX?
|
|
||||||
get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID)
|
|
||||||
|
|
||||||
val cacheFolder: FileX
|
|
||||||
get() = FileX(this, cacheDir, "imageCache/$galleryID").also {
|
|
||||||
if (!it.exists())
|
|
||||||
it.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findFile(fileName: String): FileX? =
|
|
||||||
downloadFolder?.let { downloadFolder -> downloadFolder.getChild(fileName).let {
|
|
||||||
if (it.exists()) it else null
|
|
||||||
} } ?: cacheFolder.getChild(fileName).let {
|
|
||||||
if (it.exists()) it else null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
fun setMetadata(change: (Metadata) -> Unit) {
|
|
||||||
change.invoke(metadata)
|
|
||||||
|
|
||||||
val file = cacheFolder.getChild(".metadata")
|
|
||||||
|
|
||||||
kotlin.runCatching {
|
|
||||||
if (!file.exists()) {
|
|
||||||
file.createNewFile()
|
|
||||||
}
|
|
||||||
file.writeText(Json.encodeToString(metadata))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGalleryBlock(): GalleryBlock? {
|
|
||||||
return metadata.galleryBlock
|
|
||||||
?: withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
xyz.quaver.hitomi.getGalleryBlock(galleryID).also {
|
|
||||||
setMetadata { metadata -> metadata.galleryBlock = it }
|
|
||||||
}
|
|
||||||
} catch (e: Exception) { return@withContext null }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
suspend fun getThumbnail(): Uri =
|
|
||||||
findFile(".thumbnail")?.uri
|
|
||||||
?: getGalleryBlock()?.thumbnails?.firstOrNull()?.let { withContext(Dispatchers.IO) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(it)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() }
|
|
||||||
}.getOrNull()?.let { thumbnail -> kotlin.runCatching {
|
|
||||||
cacheFolder.getChild(".thumbnail").also {
|
|
||||||
if (!it.exists())
|
|
||||||
it.createNewFile()
|
|
||||||
|
|
||||||
it.writeBytes(thumbnail)
|
|
||||||
}
|
|
||||||
}.getOrNull()?.uri }
|
|
||||||
} } ?: Uri.EMPTY
|
|
||||||
|
|
||||||
suspend fun getGalleryInfo(): GalleryInfo? {
|
|
||||||
|
|
||||||
return metadata.galleryInfo
|
|
||||||
?: withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
xyz.quaver.hitomi.getGalleryInfo(galleryID).also {
|
|
||||||
setMetadata { metadata ->
|
|
||||||
metadata.galleryInfo = it
|
|
||||||
|
|
||||||
if (metadata.imageList == null)
|
|
||||||
metadata.imageList = MutableList(it.files.size) { null }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getImage(index: Int): FileX? =
|
|
||||||
metadata.imageList?.getOrNull(index)?.let { findFile(it) }
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
fun putImage(index: Int, fileName: String, data: ByteArray) {
|
|
||||||
val file = cacheFolder.getChild(fileName)
|
|
||||||
|
|
||||||
if (!file.exists())
|
|
||||||
file.createNewFile()
|
|
||||||
file.writeBytes(data)
|
|
||||||
setMetadata { metadata -> metadata.imageList!![index] = fileName }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val lock = ConcurrentHashMap<Int, Mutex>()
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val downloadFolder = downloadFolder ?: return@launch
|
|
||||||
|
|
||||||
if (lock[galleryID]?.isLocked == true)
|
|
||||||
return@launch
|
|
||||||
|
|
||||||
(lock[galleryID] ?: Mutex().also { lock[galleryID] = it }).withLock {
|
|
||||||
val cacheMetadata = cacheFolder.getChild(".metadata")
|
|
||||||
val downloadMetadata = downloadFolder.getChild(".metadata")
|
|
||||||
|
|
||||||
if (!cacheMetadata.exists())
|
|
||||||
return@launch
|
|
||||||
|
|
||||||
if (cacheMetadata.exists()) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
if (!downloadMetadata.exists())
|
|
||||||
downloadMetadata.createNewFile()
|
|
||||||
|
|
||||||
downloadMetadata.writeText(Json.encodeToString(metadata))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val cacheThumbnail = cacheFolder.getChild(".thumbnail")
|
|
||||||
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
|
|
||||||
|
|
||||||
if (cacheThumbnail.exists()) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
if (!downloadThumbnail.exists())
|
|
||||||
downloadThumbnail.createNewFile()
|
|
||||||
|
|
||||||
downloadThumbnail.outputStream()?.use { target -> target.channel.truncate(0L); cacheThumbnail.inputStream()?.use { source ->
|
|
||||||
source.copyTo(target)
|
|
||||||
} }
|
|
||||||
cacheThumbnail.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata.imageList?.forEach { imageName ->
|
|
||||||
imageName ?: return@forEach
|
|
||||||
val target = downloadFolder.getChild(imageName)
|
|
||||||
val source = cacheFolder.getChild(imageName)
|
|
||||||
|
|
||||||
if (!source.exists())
|
|
||||||
return@forEach
|
|
||||||
|
|
||||||
kotlin.runCatching {
|
|
||||||
if (!target.exists())
|
|
||||||
target.createNewFile()
|
|
||||||
|
|
||||||
target.outputStream()?.use { target -> target.channel.truncate(0L); source.inputStream()?.use { source ->
|
|
||||||
source.copyTo(target)
|
|
||||||
} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheFolder.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util.downloader
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Call
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.*
|
|
||||||
import xyz.quaver.pupil.client
|
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.formatDownloadFolder
|
|
||||||
|
|
||||||
class DownloadManager private constructor(context: Context) : ContextWrapper(context) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@Volatile private var instance: DownloadManager? = null
|
|
||||||
|
|
||||||
fun getInstance(context: Context) =
|
|
||||||
instance ?: synchronized(this) {
|
|
||||||
instance ?: DownloadManager(context).also { instance = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
|
|
||||||
|
|
||||||
val downloadFolder: FileX
|
|
||||||
get() = {
|
|
||||||
kotlin.runCatching {
|
|
||||||
FileX(this, Preferences.get<String>("download_folder"))
|
|
||||||
}.getOrElse {
|
|
||||||
Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
|
|
||||||
defaultDownloadFolder
|
|
||||||
}
|
|
||||||
}.invoke()
|
|
||||||
|
|
||||||
private var prevDownloadFolder: FileX? = null
|
|
||||||
private var downloadFolderMapInstance: MutableMap<Int, String>? = null
|
|
||||||
val downloadFolderMap: MutableMap<Int, String>
|
|
||||||
@Synchronized
|
|
||||||
get() {
|
|
||||||
if (prevDownloadFolder != downloadFolder) {
|
|
||||||
prevDownloadFolder = downloadFolder
|
|
||||||
downloadFolderMapInstance = {
|
|
||||||
val file = downloadFolder.getChild(".download")
|
|
||||||
|
|
||||||
val data = if (file.exists())
|
|
||||||
kotlin.runCatching {
|
|
||||||
file.readText()?.let { Json.decodeFromString<MutableMap<Int, String>>(it) }
|
|
||||||
}.onFailure { file.delete() }.getOrNull()
|
|
||||||
else
|
|
||||||
null
|
|
||||||
|
|
||||||
data ?: {
|
|
||||||
file.createNewFile()
|
|
||||||
mutableMapOf<Int, String>()
|
|
||||||
}.invoke()
|
|
||||||
}.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
return downloadFolderMapInstance ?: mutableMapOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun isDownloading(galleryID: Int): Boolean {
|
|
||||||
val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
|
|
||||||
|
|
||||||
return downloadFolderMap.containsKey(galleryID)
|
|
||||||
&& client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun getDownloadFolder(galleryID: Int): FileX? =
|
|
||||||
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun addDownloadFolder(galleryID: Int) {
|
|
||||||
val name = runBlocking {
|
|
||||||
Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
|
|
||||||
}?.formatDownloadFolder() ?: return
|
|
||||||
|
|
||||||
val folder = downloadFolder.getChild(name)
|
|
||||||
|
|
||||||
if (folder.exists())
|
|
||||||
return
|
|
||||||
|
|
||||||
folder.mkdir()
|
|
||||||
|
|
||||||
downloadFolderMap[galleryID] = folder.name
|
|
||||||
|
|
||||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
|
|
||||||
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun deleteDownloadFolder(galleryID: Int) {
|
|
||||||
downloadFolderMap[galleryID]?.let {
|
|
||||||
kotlin.runCatching {
|
|
||||||
downloadFolder.getChild(it).deleteRecursively()
|
|
||||||
downloadFolderMap.remove(galleryID)
|
|
||||||
|
|
||||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
|
|
||||||
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import xyz.quaver.pupil.histories
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
val mutex = Mutex()
|
|
||||||
fun cleanCache(context: Context) = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
if (mutex.isLocked) return@launch
|
|
||||||
|
|
||||||
mutex.withLock {
|
|
||||||
val cacheFolder = File(context.cacheDir, "imageCache")
|
|
||||||
val downloadManager = DownloadManager.getInstance(context)
|
|
||||||
|
|
||||||
val limit = (Preferences.get<String>("cache_limit").toLongOrNull() ?: 0L)*1024*1024*1024
|
|
||||||
|
|
||||||
if (limit == 0L) return@withLock
|
|
||||||
|
|
||||||
val cacheSize = {
|
|
||||||
var size = 0L
|
|
||||||
|
|
||||||
cacheFolder.walk().forEach {
|
|
||||||
size += it.length()
|
|
||||||
}
|
|
||||||
|
|
||||||
size
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cacheSize.invoke() > limit)
|
|
||||||
while (cacheSize.invoke() > limit/2) {
|
|
||||||
val caches = cacheFolder.list() ?: return@withLock
|
|
||||||
|
|
||||||
synchronized(histories) {
|
|
||||||
(histories.firstOrNull {
|
|
||||||
caches.contains(it.toString()) && !downloadManager.isDownloading(it)
|
|
||||||
} ?: return@withLock).let {
|
|
||||||
Cache.delete(context, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.io.File
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
fun hash(password: String): String {
|
|
||||||
val bytes = password.toByteArray()
|
|
||||||
val md = MessageDigest.getInstance("SHA-256")
|
|
||||||
|
|
||||||
return md.digest(bytes).fold("") { str, it -> str + "%02x".format(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ret1: SHA-256 Hash
|
|
||||||
// Ret2: Hash salt
|
|
||||||
fun hashWithSalt(password: String): Pair<String, String> {
|
|
||||||
val salt = (0 until 12).map { source.random() }.joinToString()
|
|
||||||
|
|
||||||
return Pair(hash(password+salt), salt)
|
|
||||||
}
|
|
||||||
|
|
||||||
const val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Lock(val type: Type, val hash: String, val salt: String) {
|
|
||||||
|
|
||||||
enum class Type {
|
|
||||||
PATTERN,
|
|
||||||
PIN,
|
|
||||||
PASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun generate(type: Type, password: String): Lock {
|
|
||||||
val (hash, salt) = hashWithSalt(password)
|
|
||||||
return Lock(type, hash, salt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun match(password: String): Boolean {
|
|
||||||
return hash(password+salt) == hash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LockManager(base: Context): ContextWrapper(base) {
|
|
||||||
|
|
||||||
var locks: ArrayList<Lock>? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun load() {
|
|
||||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
|
||||||
|
|
||||||
if (!lock.exists()) {
|
|
||||||
lock.createNewFile()
|
|
||||||
lock.writeText("[]")
|
|
||||||
}
|
|
||||||
|
|
||||||
locks = Json.decodeFromString(lock.readText())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun save() {
|
|
||||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
|
||||||
|
|
||||||
if (!lock.exists())
|
|
||||||
lock.createNewFile()
|
|
||||||
|
|
||||||
lock.writeText(Json.encodeToString(locks?.toList() ?: listOf()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun add(lock: Lock) {
|
|
||||||
remove(lock.type)
|
|
||||||
locks?.add(lock)
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun remove(type: Lock.Type) {
|
|
||||||
locks?.removeAll { it.type == type }
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun check(password: String): Boolean? {
|
|
||||||
return locks?.any {
|
|
||||||
it.match(password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEmpty(): Boolean {
|
|
||||||
return locks.isNullOrEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isNotEmpty(): Boolean = !isEmpty()
|
|
||||||
|
|
||||||
fun contains(type: Lock.Type): Boolean {
|
|
||||||
return locks?.any { it.type == type } ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import kotlinx.serialization.json.*
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.GalleryInfo
|
|
||||||
import xyz.quaver.hitomi.getReferer
|
|
||||||
import xyz.quaver.hitomi.imageUrlFromImage
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
fun String.wordCapitalize() : String {
|
|
||||||
val result = ArrayList<String>()
|
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
for (word in this.split(" "))
|
|
||||||
result.add(word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() })
|
|
||||||
|
|
||||||
return result.joinToString(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
private val suffix = listOf(
|
|
||||||
"B",
|
|
||||||
"kB",
|
|
||||||
"MB",
|
|
||||||
"GB",
|
|
||||||
"TB" //really?
|
|
||||||
)
|
|
||||||
|
|
||||||
fun byteToString(byte: Long, precision : Int = 1) : String {
|
|
||||||
var size = byte.toDouble(); var suffixIndex = 0
|
|
||||||
|
|
||||||
while (size >= 1024) {
|
|
||||||
size /= 1024
|
|
||||||
suffixIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
return "%.${precision}f ${suffix[suffixIndex]}".format(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert android generated ID to requestCode
|
|
||||||
* to prevent java.lang.IllegalArgumentException: Can only use lower 16 bits for requestCode
|
|
||||||
*
|
|
||||||
* https://stackoverflow.com/questions/38072322/generate-16-bit-unique-ids-in-android-for-startactivityforresult
|
|
||||||
*/
|
|
||||||
fun Int.normalizeID() = this.and(0xFFFF)
|
|
||||||
|
|
||||||
fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
|
|
||||||
proxy(proxyInfo.proxy())
|
|
||||||
proxyInfo.authenticator()?.let {
|
|
||||||
proxyAuthenticator(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val formatMap = mapOf<String, GalleryBlock.() -> (String)>(
|
|
||||||
"-id-" to { id.toString() },
|
|
||||||
"-title-" to { title },
|
|
||||||
"-artist-" to { artists.joinToString() }
|
|
||||||
// TODO
|
|
||||||
)
|
|
||||||
/**
|
|
||||||
* Formats download folder name with given Metadata
|
|
||||||
*/
|
|
||||||
fun GalleryBlock.formatDownloadFolder(): String =
|
|
||||||
Preferences["download_folder_name", "[-id-] -title-"].let {
|
|
||||||
formatMap.entries.fold(it) { str, (k, v) ->
|
|
||||||
str.replace(k, v.invoke(this), true)
|
|
||||||
}
|
|
||||||
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
|
|
||||||
|
|
||||||
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
|
|
||||||
format.let {
|
|
||||||
formatMap.entries.fold(it) { str, (k, v) ->
|
|
||||||
str.replace(k, v.invoke(this), true)
|
|
||||||
}
|
|
||||||
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
|
|
||||||
|
|
||||||
val GalleryInfo.requestBuilders: List<Request.Builder>
|
|
||||||
get() {
|
|
||||||
val galleryID = this.id ?: 0
|
|
||||||
val lowQuality = Preferences["low_quality", true]
|
|
||||||
|
|
||||||
return this.files.map {
|
|
||||||
Request.Builder()
|
|
||||||
.url(imageUrlFromImage(galleryID, it, !lowQuality))
|
|
||||||
.header("Referer", "https://hitomi.la/")
|
|
||||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun String.ellipsize(n: Int): String =
|
|
||||||
if (this.length > n)
|
|
||||||
this.slice(0 until n) + "…"
|
|
||||||
else
|
|
||||||
this
|
|
||||||
|
|
||||||
operator fun JsonElement.get(index: Int) =
|
|
||||||
this.jsonArray[index]
|
|
||||||
|
|
||||||
operator fun JsonElement.get(tag: String) =
|
|
||||||
this.jsonObject[tag]
|
|
||||||
|
|
||||||
fun JsonElement.getOrNull(tag: String) = kotlin.runCatching {
|
|
||||||
this.jsonObject.getOrDefault(tag, null)
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
val JsonElement.content
|
|
||||||
get() = this.jsonPrimitive.contentOrNull
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Authenticator
|
|
||||||
import okhttp3.Credentials
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.Proxy
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ProxyInfo(
|
|
||||||
val type: Proxy.Type,
|
|
||||||
val host: String? = null,
|
|
||||||
val port: Int? = null,
|
|
||||||
val username: String? = null,
|
|
||||||
val password: String? = null
|
|
||||||
) {
|
|
||||||
fun proxy() : Proxy {
|
|
||||||
return if (host.isNullOrBlank() || port == null)
|
|
||||||
return Proxy.NO_PROXY
|
|
||||||
else
|
|
||||||
Proxy(type, InetSocketAddress.createUnresolved(host, port))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun authenticator(): Authenticator? = if (username.isNullOrBlank() || password.isNullOrBlank()) null else
|
|
||||||
Authenticator { _, response ->
|
|
||||||
val credential = Credentials.basic(username, password)
|
|
||||||
|
|
||||||
response.request().newBuilder()
|
|
||||||
.header("Proxy-Authorization", credential)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getProxyInfo(): ProxyInfo =
|
|
||||||
Json.decodeFromString(Preferences["proxy", Json.encodeToString(ProxyInfo(Proxy.Type.DIRECT))])
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.Request
|
|
||||||
import xyz.quaver.pupil.client
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private val filesURL = "https://api.github.com/repos/tom5079/Pupil/git/trees/tags"
|
|
||||||
private val contentURL = "https://raw.githubusercontent.com/tom5079/Pupil/tags/"
|
|
||||||
|
|
||||||
var translations: Map<String, String> = run {
|
|
||||||
updateTranslations()
|
|
||||||
emptyMap()
|
|
||||||
}
|
|
||||||
private set
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
fun updateTranslations() = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
translations = emptyMap()
|
|
||||||
kotlin.runCatching {
|
|
||||||
translations = Json.decodeFromString<Map<String, String>>(client.newCall(
|
|
||||||
Request.Builder()
|
|
||||||
.url(contentURL + "${Preferences["tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json")
|
|
||||||
.build()
|
|
||||||
).execute().also { if (it.code() != 200) return@launch }.body()?.use { it.string() } ?: return@launch).filterValues { it.isNotEmpty() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAvailableLanguages(): List<String> {
|
|
||||||
val languages = Locale.getISOLanguages()
|
|
||||||
|
|
||||||
val json = Json.parseToJsonElement(client.newCall(
|
|
||||||
Request.Builder()
|
|
||||||
.url(filesURL)
|
|
||||||
.build()
|
|
||||||
).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.string() } ?: return emptyList())
|
|
||||||
|
|
||||||
return listOf("en") + (json["tree"]?.jsonArray?.mapNotNull {
|
|
||||||
val name = it["path"]?.jsonPrimitive?.content?.takeWhile { c -> c != '.' }
|
|
||||||
|
|
||||||
languages.firstOrNull { code -> code.equals(name, ignoreCase = true) }
|
|
||||||
} ?: emptyList())
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.webkit.URLUtil
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
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 java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
fun getReleases(url: String) : JsonArray {
|
|
||||||
return try {
|
|
||||||
URL(url).readText().let {
|
|
||||||
Json.parseToJsonElement(it).jsonArray
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
JsonArray(emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkUpdate(url: String) : JsonObject? {
|
|
||||||
val releases = getReleases(url)
|
|
||||||
|
|
||||||
if (releases.isEmpty())
|
|
||||||
return null
|
|
||||||
|
|
||||||
return releases.firstOrNull {
|
|
||||||
Preferences["beta"] || it.jsonObject["prerelease"]?.jsonPrimitive?.booleanOrNull == false
|
|
||||||
}?.let {
|
|
||||||
if (it.jsonObject["tag_name"]?.jsonPrimitive?.contentOrNull == BuildConfig.VERSION_NAME)
|
|
||||||
null
|
|
||||||
else
|
|
||||||
it.jsonObject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getApkUrl(releases: JsonObject) : String? {
|
|
||||||
return releases["assets"]?.jsonArray?.firstOrNull {
|
|
||||||
Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.jsonPrimitive?.contentOrNull ?: "")
|
|
||||||
}.let {
|
|
||||||
it?.jsonObject?.get("browser_download_url")?.jsonPrimitive?.contentOrNull
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkUpdate(context: Context, force: Boolean = false) {
|
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
|
|
||||||
|
|
||||||
if (!force && ignoreUpdateUntil > System.currentTimeMillis())
|
|
||||||
return
|
|
||||||
|
|
||||||
fun extractReleaseNote(update: JsonObject, locale: Locale) : String {
|
|
||||||
val markdown = update["body"]!!.jsonPrimitive.content
|
|
||||||
|
|
||||||
val target = when(locale.language) {
|
|
||||||
"ko" -> "한국어"
|
|
||||||
"ja" -> "日本語"
|
|
||||||
else -> "English"
|
|
||||||
}
|
|
||||||
|
|
||||||
val releaseNote = Regex("^# Release Note.+$")
|
|
||||||
val language = Regex("^## $target$")
|
|
||||||
val end = Regex("^#.+$")
|
|
||||||
|
|
||||||
var releaseNoteFlag = false
|
|
||||||
var languageFlag = false
|
|
||||||
|
|
||||||
val result = StringBuilder()
|
|
||||||
|
|
||||||
for(line in markdown.lines()) {
|
|
||||||
if (releaseNote.matches(line)) {
|
|
||||||
releaseNoteFlag = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (releaseNoteFlag) {
|
|
||||||
if (language.matches(line)) {
|
|
||||||
languageFlag = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (languageFlag) {
|
|
||||||
if (end.matches(line))
|
|
||||||
break
|
|
||||||
|
|
||||||
result.append(line+"\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.getString(R.string.update_release_note, update["tag_name"]?.jsonPrimitive?.contentOrNull, result.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
val update =
|
|
||||||
checkUpdate(context.getString(R.string.release_url)) ?: return@launch
|
|
||||||
|
|
||||||
val url = getApkUrl(update) ?: return@launch
|
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(context).apply {
|
|
||||||
setTitle(R.string.update_title)
|
|
||||||
val msg = extractReleaseNote(update, Locale.getDefault())
|
|
||||||
setMessage(Markwon.create(context).toMarkdown(msg))
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
||||||
|
|
||||||
//Cancel any download queued before
|
|
||||||
|
|
||||||
val id: Long = Preferences["update_download_id"]
|
|
||||||
|
|
||||||
if (id != -1L)
|
|
||||||
downloadManager.remove(id)
|
|
||||||
|
|
||||||
val target = File(context.getExternalFilesDir(null), "Pupil.apk").also {
|
|
||||||
it.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = DownloadManager.Request(Uri.parse(url))
|
|
||||||
.setTitle(context.getText(R.string.update_notification_description))
|
|
||||||
.setDestinationUri(Uri.fromFile(target))
|
|
||||||
|
|
||||||
downloadManager.enqueue(request).also {
|
|
||||||
Preferences["update_download_id"] = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setNegativeButton(if (force) android.R.string.cancel else R.string.ignore) { _, _ ->
|
|
||||||
if (!force)
|
|
||||||
preferences.edit()
|
|
||||||
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
dialog.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<Int>) -> Unit)? = null) {
|
|
||||||
if (!URLUtil.isValidUrl(url)) {
|
|
||||||
onFailure?.invoke(IllegalArgumentException())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.get()
|
|
||||||
.build()
|
|
||||||
|
|
||||||
client.newCall(request).enqueue(object: Callback {
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
|
||||||
onFailure?.invoke(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}.onFailure { onFailure?.invoke(it) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Pupil, Hitomi.la viewer for Android
|
* Pupil, Hitomi.la viewer for Android
|
||||||
* Copyright (C) 2019 tom5079
|
* Copyright (C) 2022 tom5079
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -16,31 +16,23 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:Suppress("UNUSED_VARIABLE", "IncorrectScope")
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
package xyz.quaver.pupil
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
/**
|
fun Context.launchApkInstaller(file: File) {
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
val uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", file)
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import kotlinx.serialization.*
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
import kotlinx.serialization.json.Json
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||||
import org.junit.Test
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
import java.lang.reflect.ParameterizedType
|
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import kotlin.reflect.KClass
|
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||||
import kotlin.reflect.KType
|
|
||||||
import kotlin.reflect.typeOf
|
|
||||||
|
|
||||||
class ExampleUnitTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test() {
|
|
||||||
val a = mutableSetOf<Int>()
|
|
||||||
|
|
||||||
print(a::class.java.methods.firstOrNull { it.name == "add" }?.genericParameterTypes?.firstOrNull() as? ParameterizedType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
startActivity(intent)
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
|
||||||
~ Copyright (C) 2020 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/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:duration="300"
|
|
||||||
android:fromXDelta="0"
|
|
||||||
android:interpolator="@anim/shake_cycle"
|
|
||||||
android:toXDelta="10" />
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
|
||||||
~ Copyright (C) 2020 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/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:cycles="3" />
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
|
||||||
~ Copyright (C) 2020 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/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_enabled="false" android:color="@android:color/darker_gray"/>
|
|
||||||
<item android:color="@color/colorPrimary"/>
|
|
||||||
</selector>
|
|
||||||
|
Before Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 145 B |
|
Before Width: | Height: | Size: 571 B |
|
Before Width: | Height: | Size: 279 B |
|
Before Width: | Height: | Size: 255 B |
|
Before Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 235 B |
|
Before Width: | Height: | Size: 99 B |
|
Before Width: | Height: | Size: 345 B |
|
Before Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 183 B |
|
Before Width: | Height: | Size: 352 B |
|
Before Width: | Height: | Size: 383 B |
|
Before Width: | Height: | Size: 125 B |
|
Before Width: | Height: | Size: 676 B |