Compare commits
181 Commits
5.1.2-hotf
...
source
| 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 | ||
|
|
9f9a4c81b3 | ||
|
|
d567b30f4b | ||
|
|
6d7c4ce0ab | ||
|
|
e062b8f9e9 | ||
|
|
08403b7a4e | ||
|
|
c6ed5d35e7 | ||
|
|
dba3460b60 | ||
|
|
f07f624fcf | ||
|
|
48ff2f328f | ||
|
|
9ae2423a40 | ||
|
|
2bc3c78c75 | ||
|
|
18e9fe75fb | ||
|
|
880a741a44 | ||
|
|
2c6ddcc64b | ||
|
|
8f2e757b77 | ||
|
|
ff177955b3 | ||
|
|
8bb8066a98 | ||
|
|
2747ddbf65 | ||
|
|
b939e9424d | ||
|
|
fb9dea5d1e | ||
|
|
da4d5d711b | ||
|
|
331cbec5f1 | ||
|
|
7f02284285 | ||
|
|
ac2c3a6d97 | ||
|
|
c3bc80fec6 | ||
|
|
09779a0710 | ||
|
|
e82c6ef866 | ||
|
|
861ae9be64 | ||
|
|
96108bc1ec | ||
|
|
016f217db0 | ||
|
|
0688294f18 | ||
|
|
9ad008255d | ||
|
|
4c5a862dd6 | ||
|
|
b165a2308f | ||
|
|
8757b08cd2 | ||
|
|
3800543fba | ||
|
|
02ef60c818 | ||
|
|
88f3b30266 | ||
|
|
9203dc0112 | ||
|
|
4c683bec68 | ||
|
|
0cfd1eb453 | ||
|
|
19744dab37 | ||
|
|
12d58e5aa7 | ||
|
|
e46dd37a26 | ||
|
|
49c3ebc36b | ||
|
|
11e9bc2235 | ||
|
|
3029b3bf0e | ||
|
|
9a6c6f67ce | ||
|
|
a6ed0baef2 | ||
|
|
d3b43d80da | ||
|
|
46d4316d49 | ||
|
|
ade2864351 |
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/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="Pupil" copyright="GPL" />
|
|
||||||
</module2copyright>
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
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="PLATFORM" />
|
|
||||||
<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>
|
|
||||||
75
.idea/jarRepositories.xml
generated
@@ -1,75 +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>
|
|
||||||
</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" 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>
|
|
||||||
12
.idea/runConfigurations.xml
generated
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RunConfigurationProducerService">
|
|
||||||
<option name="ignoredProducers">
|
|
||||||
<set>
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
3
.idea/scopes/Pupil.xml
generated
@@ -1,3 +0,0 @@
|
|||||||
<component name="DependencyValidationManager">
|
|
||||||
<scope name="Pupil" pattern="file[app]:*/" />
|
|
||||||
</component>
|
|
||||||
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
*Pupil, Hitomi.la viewer for Android*
|
*Pupil, Hitomi.la viewer for Android*
|
||||||
|
|
||||||

|

|
||||||
[](https://github.com/tom5079/Pupil/releases/download/5.1.1-hotfix3/Pupil-v5.1.2.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
|
||||||
@@ -21,3 +21,6 @@ or Build app yourself
|
|||||||
# Contribution
|
# Contribution
|
||||||
|
|
||||||
Any kind of contribution is appriciated. Feel free to leave PR!
|
Any kind of contribution is appriciated. Feel free to leave PR!
|
||||||
|
|
||||||
|
## Tag Translation
|
||||||
|
Head over to [tags branch](https://github.com/tom5079/Pupil/tree/tags)
|
||||||
|
|||||||
141
app/build.gradle
@@ -1,141 +0,0 @@
|
|||||||
apply plugin: "com.android.application"
|
|
||||||
apply plugin: "kotlin-android"
|
|
||||||
apply plugin: "kotlin-kapt"
|
|
||||||
apply plugin: "kotlin-android-extensions"
|
|
||||||
apply plugin: "kotlinx-serialization"
|
|
||||||
apply plugin: "com.google.android.gms.oss-licenses-plugin"
|
|
||||||
|
|
||||||
if (file("google-services.json").exists() && file("src/debug/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 61
|
|
||||||
versionName "5.1.2"
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
minifyEnabled true
|
|
||||||
shrinkResources true
|
|
||||||
|
|
||||||
debuggable true
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
versionNameSuffix "-DEBUG"
|
|
||||||
|
|
||||||
buildConfigField("Boolean", "CENSOR", "false")
|
|
||||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
|
||||||
|
|
||||||
ext.enableCrashlytics = false
|
|
||||||
ext.alwaysUpdateBuildId = false
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
minifyEnabled true
|
|
||||||
shrinkResources true
|
|
||||||
|
|
||||||
buildConfigField("Boolean", "CENSOR", "false")
|
|
||||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
buildToolsVersion = "29.0.3"
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0-RC2"
|
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
|
||||||
implementation "androidx.activity:activity-ktx:1.2.0-alpha08"
|
|
||||||
implementation "androidx.fragment:fragment-ktx:1.3.0-alpha08"
|
|
||||||
implementation "androidx.preference:preference:1.1.1"
|
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.0.1"
|
|
||||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
|
||||||
implementation "androidx.biometric:biometric:1.0.1"
|
|
||||||
implementation "androidx.work:work-runtime-ktx:2.4.0"
|
|
||||||
|
|
||||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
|
||||||
|
|
||||||
implementation "com.google.android.material:material:1.3.0-alpha02"
|
|
||||||
|
|
||||||
implementation "com.google.firebase:firebase-core:17.5.0"
|
|
||||||
implementation "com.google.firebase:firebase-analytics:17.5.0"
|
|
||||||
implementation "com.google.firebase:firebase-crashlytics:17.2.1"
|
|
||||||
implementation "com.google.firebase:firebase-perf:19.0.8"
|
|
||||||
|
|
||||||
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
|
|
||||||
implementation "com.google.android.gms:play-services-mlkit-face-detection:16.1.1"
|
|
||||||
|
|
||||||
implementation "com.github.clans:fab:1.6.4"
|
|
||||||
|
|
||||||
//implementation "com.quiph.ui:recyclerviewfastscroller:0.2.1"
|
|
||||||
|
|
||||||
implementation 'com.github.piasy:BigImageViewer:1.6.7'
|
|
||||||
implementation 'com.github.piasy:FrescoImageLoader:1.6.7'
|
|
||||||
implementation 'com.github.piasy:FrescoImageViewFactory:1.6.7'
|
|
||||||
|
|
||||||
//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:1.7.2"
|
|
||||||
implementation "xyz.quaver:documentfilex:0.3"
|
|
||||||
implementation "xyz.quaver:floatingsearchview:1.0.7"
|
|
||||||
|
|
||||||
testImplementation "junit:junit:4.13"
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
androidExtensions {
|
|
||||||
experimental = true
|
|
||||||
}
|
|
||||||
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"]}}
|
||||||
40
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
|
||||||
@@ -22,30 +22,24 @@
|
|||||||
|
|
||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
|
||||||
-keep class * extends com.bumptech.glide.module.AppGlideModule {
|
|
||||||
<init>(...);
|
|
||||||
}
|
|
||||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
|
||||||
**[] $VALUES;
|
|
||||||
public *;
|
|
||||||
}
|
|
||||||
-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
|
|
||||||
*** rewind();
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
|
||||||
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
|
|
||||||
|
|
||||||
-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 { *; }
|
||||||
-keep class xyz.quaver.pupil.util.Preferences
|
-keepclassmembers class xyz.quaver.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class xyz.quaver.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"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": [],
|
||||||
"properties": [],
|
"attributes": [],
|
||||||
"versionCode": 61,
|
"versionCode": 600,
|
||||||
"versionName": "5.1.2",
|
"versionName": "6.0.0-alpha02",
|
||||||
"enabled": true,
|
|
||||||
"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,18 +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 android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="21" />
|
<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.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<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"
|
||||||
@@ -24,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">
|
||||||
@@ -40,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" />
|
||||||
@@ -231,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>
|
||||||
@@ -18,112 +18,39 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
import android.app.*
|
import android.app.Application
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
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 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.analytics.FirebaseAnalytics
|
import io.ktor.client.*
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
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.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
DownloadManager.getInstance(this).migrate()
|
|
||||||
}
|
|
||||||
} 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"), "")
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG)
|
|
||||||
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProviderInstaller.installIfNeeded(this)
|
ProviderInstaller.installIfNeeded(this)
|
||||||
@@ -133,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
|
||||||
|
|
||||||
@@ -158,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,400 +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.Drawable
|
|
||||||
import android.util.SparseBooleanArray
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.cardview.widget.CardView
|
|
||||||
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.android.synthetic.main.item_galleryblock.view.*
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import xyz.quaver.hitomi.getGallery
|
|
||||||
import xyz.quaver.hitomi.getReader
|
|
||||||
import xyz.quaver.io.util.getChild
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.favoriteTags
|
|
||||||
import xyz.quaver.pupil.favorites
|
|
||||||
import xyz.quaver.pupil.types.Tag
|
|
||||||
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 {
|
|
||||||
|
|
||||||
enum class ViewType {
|
|
||||||
NEXT,
|
|
||||||
GALLERY,
|
|
||||||
PREV
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateAll = true
|
|
||||||
var thin: Boolean = Preferences["thin"]
|
|
||||||
|
|
||||||
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
private var galleryID: Int = 0
|
|
||||||
|
|
||||||
init {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
while (updateAll) {
|
|
||||||
updateProgress(view.context)
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateProgress(context: Context) {
|
|
||||||
val cache = Cache.getInstance(context, galleryID)
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
if (cache.metadata.reader == null) {
|
|
||||||
view.galleryblock_progressbar_layout.visibility = View.GONE
|
|
||||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
with(view.galleryblock_progressbar) {
|
|
||||||
val imageList = cache.metadata.imageList!!
|
|
||||||
|
|
||||||
progress = imageList.count { it != null }
|
|
||||||
max = imageList.size
|
|
||||||
|
|
||||||
with(view.galleryblock_progressbar_layout) {
|
|
||||||
if (visibility == View.GONE)
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!imageList.contains(null)) {
|
|
||||||
val downloadManager = DownloadManager.getInstance(context)
|
|
||||||
|
|
||||||
if (completeFlag.get(galleryID, false)) {
|
|
||||||
with(view.galleryblock_progress_complete) {
|
|
||||||
setImageResource(
|
|
||||||
if (downloadManager.getDownloadFolder(galleryID) != null)
|
|
||||||
R.drawable.ic_progressbar
|
|
||||||
else R.drawable.ic_progressbar_cache
|
|
||||||
)
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
with(view.galleryblock_progress_complete) {
|
|
||||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context,
|
|
||||||
if (downloadManager.getDownloadFolder(galleryID) != null)
|
|
||||||
R.drawable.ic_progressbar_complete
|
|
||||||
else R.drawable.ic_progressbar_complete_cache
|
|
||||||
).apply {
|
|
||||||
this?.start()
|
|
||||||
})
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
completeFlag.put(galleryID, true)
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(galleryID: Int) {
|
|
||||||
this.galleryID = galleryID
|
|
||||||
|
|
||||||
val cache = Cache.getInstance(view.context, galleryID)
|
|
||||||
|
|
||||||
val galleryBlock = runBlocking {
|
|
||||||
cache.getGalleryBlock()
|
|
||||||
} ?: return
|
|
||||||
|
|
||||||
with(view) {
|
|
||||||
val resources = 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
|
|
||||||
|
|
||||||
galleryblock_thumbnail.apply {
|
|
||||||
setOnClickListener {
|
|
||||||
view.performClick()
|
|
||||||
}
|
|
||||||
setOnLongClickListener {
|
|
||||||
view.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)
|
|
||||||
} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
galleryblock_title.text = galleryBlock.title
|
|
||||||
with(galleryblock_artist) {
|
|
||||||
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(galleryblock_series) {
|
|
||||||
text =
|
|
||||||
resources.getString(
|
|
||||||
R.string.galleryblock_series,
|
|
||||||
series.joinToString(", ") { it.wordCapitalize() })
|
|
||||||
visibility = when {
|
|
||||||
series.isNotEmpty() -> View.VISIBLE
|
|
||||||
else -> View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
galleryblock_type.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
|
|
||||||
with(galleryblock_language) {
|
|
||||||
text =
|
|
||||||
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
|
|
||||||
visibility = when {
|
|
||||||
galleryBlock.language.isNotEmpty() -> View.VISIBLE
|
|
||||||
else -> View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(galleryblock_tag_group) {
|
|
||||||
onClickListener = {
|
|
||||||
onChipClickedHandler.forEach { callback ->
|
|
||||||
callback.invoke(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.clear()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
galleryblock_id.text = galleryBlock.id.toString()
|
|
||||||
galleryblock_pagecount.text = "-"
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val pageCount = kotlin.runCatching {
|
|
||||||
getReader(galleryBlock.id).galleryInfo.files.size
|
|
||||||
}.getOrNull() ?: return@launch
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
galleryblock_pagecount.text = context.getString(R.string.galleryblock_pagecount, pageCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(galleryblock_favorite) {
|
|
||||||
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) {
|
|
||||||
galleryblock_tag_group.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class NextViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
|
|
||||||
class PrevViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
|
|
||||||
|
|
||||||
class ViewHolderFactory {
|
|
||||||
companion object {
|
|
||||||
fun getLayoutID(type: Int): Int {
|
|
||||||
return when(ViewType.values()[type]) {
|
|
||||||
ViewType.NEXT -> R.layout.item_next
|
|
||||||
ViewType.PREV -> R.layout.item_prev
|
|
||||||
ViewType.GALLERY -> R.layout.item_galleryblock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val completeFlag = SparseBooleanArray()
|
|
||||||
|
|
||||||
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
|
||||||
var onDownloadClickedHandler: ((Int) -> Unit)? = null
|
|
||||||
var onDeleteClickedHandler: ((Int) -> Unit)? = null
|
|
||||||
|
|
||||||
var showNext = false
|
|
||||||
var showPrev = false
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
fun getViewHolder(type: Int, view: View): RecyclerView.ViewHolder {
|
|
||||||
return when(ViewType.values()[type]) {
|
|
||||||
ViewType.NEXT -> NextViewHolder(view as LinearLayout)
|
|
||||||
ViewType.PREV -> PrevViewHolder(view as LinearLayout)
|
|
||||||
ViewType.GALLERY -> GalleryViewHolder(view as CardView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getViewHolder(
|
|
||||||
viewType,
|
|
||||||
LayoutInflater.from(parent.context).inflate(
|
|
||||||
ViewHolderFactory.getLayoutID(viewType),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
if (holder is GalleryViewHolder) {
|
|
||||||
val galleryID = galleries[position-(if (showPrev) 1 else 0)]
|
|
||||||
|
|
||||||
holder.bind(galleryID)
|
|
||||||
|
|
||||||
with(holder.view.galleryblock_primary) {
|
|
||||||
setOnClickListener {
|
|
||||||
holder.view.performClick()
|
|
||||||
}
|
|
||||||
setOnLongClickListener {
|
|
||||||
holder.view.performLongClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.view.galleryblock_download.setOnClickListener {
|
|
||||||
onDownloadClickedHandler?.invoke(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.view.galleryblock_delete.setOnClickListener {
|
|
||||||
onDeleteClickedHandler?.invoke(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
mItemManger.bindView(holder.view, position)
|
|
||||||
|
|
||||||
holder.view.galleryblock_swipe_layout.addSwipeListener(object: SwipeLayout.SwipeListener {
|
|
||||||
override fun onStartOpen(layout: SwipeLayout?) {
|
|
||||||
mItemManger.closeAllExcept(layout)
|
|
||||||
|
|
||||||
holder.view.galleryblock_download.text =
|
|
||||||
if (DownloadManager.getInstance(holder.view.context).isDownloading(galleryID))
|
|
||||||
holder.view.context.getString(android.R.string.cancel)
|
|
||||||
else
|
|
||||||
holder.view.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 +
|
|
||||||
(if (showNext) 1 else 0) +
|
|
||||||
(if (showPrev) 1 else 0)
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
|
||||||
return when {
|
|
||||||
showPrev && position == 0 -> ViewType.PREV
|
|
||||||
showNext && position == galleries.size+(if (showPrev) 1 else 0) -> ViewType.NEXT
|
|
||||||
else -> ViewType.GALLERY
|
|
||||||
}.ordinal
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSwipeLayoutResourceId(position: Int) = R.id.galleryblock_swipe_layout
|
|
||||||
}
|
|
||||||
@@ -1,86 +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.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import kotlinx.android.synthetic.main.item_mirrors.view.*
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
|
||||||
|
|
||||||
val mirrors = context.resources.getStringArray(R.array.mirrors).map {
|
|
||||||
it.split('|').let { split ->
|
|
||||||
Pair(split.first(), split.last())
|
|
||||||
}
|
|
||||||
}.toMap()
|
|
||||||
|
|
||||||
val list = mirrors.keys.toMutableList().apply {
|
|
||||||
Preferences.get<String>("mirrors")
|
|
||||||
.split(">")
|
|
||||||
.reversed()
|
|
||||||
.forEach {
|
|
||||||
if (this.contains(it)) {
|
|
||||||
this.remove(it)
|
|
||||||
this.add(0, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onItemMove : ((Int, Int) -> Unit) = { from, to ->
|
|
||||||
Collections.swap(list, from, to)
|
|
||||||
notifyItemMoved(from, to)
|
|
||||||
onItemMoved?.invoke(list)
|
|
||||||
}
|
|
||||||
var onStartDrag : ((ViewHolder) -> Unit)? = null
|
|
||||||
var onItemMoved : ((List<String>) -> (Unit))? = null
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
with(holder.view) {
|
|
||||||
mirror_name.text = mirrors[list.elementAt(position)]
|
|
||||||
mirror_button.setOnTouchListener { _, event ->
|
|
||||||
if (event.action == MotionEvent.ACTION_DOWN)
|
|
||||||
onStartDrag?.invoke(holder)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
return LayoutInflater.from(parent.context).inflate(
|
|
||||||
R.layout.item_mirrors, parent, false
|
|
||||||
).let {
|
|
||||||
ViewHolder(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = mirrors.size
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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.DiscretePathEffect
|
|
||||||
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.android.synthetic.main.item_reader.view.*
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import xyz.quaver.hitomi.Reader
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
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 reader: Reader? = null
|
|
||||||
|
|
||||||
var isFullScreen = false
|
|
||||||
|
|
||||||
var onItemClickListener : (() -> (Unit))? = null
|
|
||||||
|
|
||||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
fun clear() {
|
|
||||||
view.image.mainView.let {
|
|
||||||
when (it) {
|
|
||||||
is SubsamplingScaleImageView ->
|
|
||||||
it.recycle()
|
|
||||||
is SimpleDraweeView ->
|
|
||||||
it.controller = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
return LayoutInflater.from(parent.context).inflate(
|
|
||||||
R.layout.item_reader, parent, false
|
|
||||||
).let {
|
|
||||||
with(it) {
|
|
||||||
image.setImageViewFactory(FrescoImageViewFactory().apply {
|
|
||||||
updateView = { imageInfo ->
|
|
||||||
it.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
|
||||||
dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
image.setImageShownCallback(object : ImageShownCallback {
|
|
||||||
override fun onMainImageShown() {
|
|
||||||
it.image.mainView.let { v ->
|
|
||||||
when (v) {
|
|
||||||
is SubsamplingScaleImageView ->
|
|
||||||
if (!isFullScreen) it.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onThumbnailShown() {}
|
|
||||||
})
|
|
||||||
image.setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
|
|
||||||
image.setOnClickListener {
|
|
||||||
this.performClick()
|
|
||||||
}
|
|
||||||
setOnClickListener {
|
|
||||||
onItemClickListener?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewHolder(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cache: Cache? = null
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
holder.view as ConstraintLayout
|
|
||||||
|
|
||||||
if (cache == null)
|
|
||||||
cache = Cache.getInstance(holder.view.context, galleryID)
|
|
||||||
|
|
||||||
if (!isFullScreen) {
|
|
||||||
holder.view.setBackgroundResource(R.drawable.reader_item_boundary)
|
|
||||||
holder.view.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
|
||||||
height = 0
|
|
||||||
dimensionRatio =
|
|
||||||
"${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
holder.view.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
holder.view.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
|
||||||
height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
|
||||||
dimensionRatio = null
|
|
||||||
}
|
|
||||||
holder.view.background = null
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.view.reader_index.text = (position+1).toString()
|
|
||||||
|
|
||||||
val image = cache!!.getImage(position)
|
|
||||||
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
|
|
||||||
|
|
||||||
if (progress?.isInfinite() == true && image != null) {
|
|
||||||
holder.view.progress_group.visibility = View.INVISIBLE
|
|
||||||
holder.view.image.showImage(image.uri)
|
|
||||||
} else {
|
|
||||||
holder.view.progress_group.visibility = View.VISIBLE
|
|
||||||
holder.view.reader_item_progressbar.progress =
|
|
||||||
if (progress?.isInfinite() == true)
|
|
||||||
100
|
|
||||||
else
|
|
||||||
progress?.roundToInt() ?: 0
|
|
||||||
|
|
||||||
holder.clear()
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
delay(1000)
|
|
||||||
notifyItemChanged(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = reader?.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,425 +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 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.PupilInterceptor
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.client
|
|
||||||
import xyz.quaver.pupil.interceptors
|
|
||||||
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.put(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)
|
|
||||||
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()
|
|
||||||
var response = chain.proceed(request)
|
|
||||||
|
|
||||||
var retry = 5
|
|
||||||
while (!response.isSuccessful && retry > 0) {
|
|
||||||
response = chain.proceed(request)
|
|
||||||
retry--
|
|
||||||
}
|
|
||||||
|
|
||||||
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>>()
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (e.message?.contains("cancel", true) == false) {
|
|
||||||
val galleryID = (call.request().tag() as Tag).galleryID
|
|
||||||
|
|
||||||
// Retry
|
|
||||||
cancel(galleryID)
|
|
||||||
download(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() }.body()?.use { it.bytes() } ?: throw Exception()
|
|
||||||
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 {
|
|
||||||
cancel(galleryID)
|
|
||||||
download(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(galleryID)
|
|
||||||
|
|
||||||
startId?.let { stopSelf(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
cleanCache(this@DownloadService)
|
|
||||||
|
|
||||||
if (progress.containsKey(galleryID))
|
|
||||||
cancel(galleryID)
|
|
||||||
|
|
||||||
val cache = Cache.getInstance(this@DownloadService, galleryID)
|
|
||||||
|
|
||||||
initNotification(galleryID)
|
|
||||||
|
|
||||||
val reader = cache.getReader()
|
|
||||||
|
|
||||||
// Gallery doesn't exist
|
|
||||||
if (reader == null) {
|
|
||||||
delete(galleryID)
|
|
||||||
progress[galleryID] = mutableListOf()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
progress[galleryID] = MutableList(reader.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(reader.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.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,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.types
|
|
||||||
|
|
||||||
import kotlinx.android.parcel.IgnoredOnParcel
|
|
||||||
import kotlinx.android.parcel.Parcelize
|
|
||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
|
||||||
import xyz.quaver.hitomi.Suggestion
|
|
||||||
|
|
||||||
@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 = 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,279 +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 kotlinx.android.synthetic.main.activity_lock.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_pin_lock.*
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
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 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
|
|
||||||
lock_pattern_view.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 {
|
|
||||||
lock_pattern_view.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 {
|
|
||||||
indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
|
||||||
setAnimationListener(object: Animation.AnimationListener {
|
|
||||||
override fun onAnimationEnd(animation: Animation?) {
|
|
||||||
pin_lock_view.resetPinLockView()
|
|
||||||
pin_lock_view.isEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationStart(animation: Animation?) {
|
|
||||||
pin_lock_view.isEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation?) {
|
|
||||||
// Do Nothing
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"add_lock" -> {
|
|
||||||
if (lastPass.isEmpty()) {
|
|
||||||
lastPass = it
|
|
||||||
|
|
||||||
pin_lock_view.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 {
|
|
||||||
indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
|
||||||
setAnimationListener(object: Animation.AnimationListener {
|
|
||||||
override fun onAnimationEnd(animation: Animation?) {
|
|
||||||
pin_lock_view.resetPinLockView()
|
|
||||||
pin_lock_view.isEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationStart(animation: Animation?) {
|
|
||||||
pin_lock_view.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)
|
|
||||||
setContentView(R.layout.activity_lock)
|
|
||||||
|
|
||||||
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
|
|
||||||
) {
|
|
||||||
lock_fingerprint.apply {
|
|
||||||
isEnabled = true
|
|
||||||
setOnClickListener {
|
|
||||||
showBiometricPrompt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showBiometricPrompt()
|
|
||||||
}
|
|
||||||
|
|
||||||
lock_pattern.apply {
|
|
||||||
isEnabled = lockManager.contains(Lock.Type.PATTERN)
|
|
||||||
setOnClickListener {
|
|
||||||
supportFragmentManager.beginTransaction().replace(
|
|
||||||
R.id.lock_content, patternLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lock_pin.apply {
|
|
||||||
isEnabled = lockManager.contains(Lock.Type.PIN)
|
|
||||||
setOnClickListener {
|
|
||||||
supportFragmentManager.beginTransaction().replace(
|
|
||||||
R.id.lock_content, pinLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lock_password.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" -> {
|
|
||||||
lock_pattern.isEnabled = false
|
|
||||||
lock_pin.isEnabled = false
|
|
||||||
lock_fingerprint.isEnabled = false
|
|
||||||
lock_password.isEnabled = false
|
|
||||||
|
|
||||||
when(intent.getStringExtra("type")!!) {
|
|
||||||
"pattern" -> {
|
|
||||||
lock_pattern.isEnabled = true
|
|
||||||
supportFragmentManager.beginTransaction().add(
|
|
||||||
R.id.lock_content, patternLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
"pin" -> {
|
|
||||||
lock_pin.isEnabled = true
|
|
||||||
supportFragmentManager.beginTransaction().add(
|
|
||||||
R.id.lock_content, pinLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,611 +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 android.widget.Toast
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
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.android.synthetic.main.activity_reader.*
|
|
||||||
import kotlinx.android.synthetic.main.activity_reader.view.*
|
|
||||||
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
|
||||||
import kotlinx.android.synthetic.main.reader_eye_card.view.*
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import xyz.quaver.Code
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
|
||||||
import xyz.quaver.pupil.favorites
|
|
||||||
import xyz.quaver.pupil.histories
|
|
||||||
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
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
|
|
||||||
class ReaderActivity : BaseActivity() {
|
|
||||||
|
|
||||||
private var galleryID = 0
|
|
||||||
private var currentPage = 0
|
|
||||||
|
|
||||||
private var isScroll = true
|
|
||||||
private var isFullscreen = false
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
|
|
||||||
(reader_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 {
|
|
||||||
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
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_reader)
|
|
||||||
|
|
||||||
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 -> {
|
|
||||||
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
|
|
||||||
with(view.dialog_number_picker) {
|
|
||||||
minValue = 1
|
|
||||||
maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0
|
|
||||||
value = currentPage
|
|
||||||
}
|
|
||||||
val dialog = AlertDialog.Builder(this).apply {
|
|
||||||
setView(view)
|
|
||||||
}.create()
|
|
||||||
view.dialog_ok.setOnClickListener {
|
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(view.dialog_number_picker.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)
|
|
||||||
|
|
||||||
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
|
|
||||||
DownloadService.cancel(this, galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
|
|
||||||
update = false
|
|
||||||
}
|
|
||||||
|
|
||||||
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 -> {
|
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage-2, 0)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
|
||||||
(reader_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(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
|
||||||
.show()
|
|
||||||
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
histories.add(galleryID)
|
|
||||||
|
|
||||||
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
|
||||||
reader_download_progressbar.progress =
|
|
||||||
downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
|
|
||||||
|
|
||||||
if (title == getString(R.string.reader_loading)) {
|
|
||||||
val reader = cache.metadata.reader
|
|
||||||
|
|
||||||
if (reader != null) {
|
|
||||||
with(reader_recyclerview.adapter as ReaderAdapter) {
|
|
||||||
this.reader = reader
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
title = reader.galleryInfo.title
|
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title =
|
|
||||||
"$currentPage/${reader.galleryInfo.files.size}"
|
|
||||||
|
|
||||||
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(
|
|
||||||
this@ReaderActivity,
|
|
||||||
when (reader.code) {
|
|
||||||
Code.HITOMI -> R.drawable.hitomi
|
|
||||||
Code.HIYOBI -> R.drawable.ic_hiyobi
|
|
||||||
else -> android.R.color.transparent
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloader.isCompleted(galleryID)) { //Download finished
|
|
||||||
reader_download_progressbar.visibility = View.GONE
|
|
||||||
|
|
||||||
animateDownloadFAB(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
with(reader_recyclerview) {
|
|
||||||
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
|
|
||||||
onItemClickListener = {
|
|
||||||
if (isScroll) {
|
|
||||||
isScroll = false
|
|
||||||
isFullscreen = true
|
|
||||||
|
|
||||||
scrollMode(false)
|
|
||||||
fullscreen(true)
|
|
||||||
} else {
|
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //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)
|
|
||||||
this@ReaderActivity.reader_fab.showMenuButton(true)
|
|
||||||
else if (dy > 0)
|
|
||||||
this@ReaderActivity.reader_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(reader_fab_download) {
|
|
||||||
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(reader_fab_retry) {
|
|
||||||
setImageResource(R.drawable.refresh)
|
|
||||||
setOnClickListener {
|
|
||||||
DownloadService.download(context, galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(reader_fab_auto) {
|
|
||||||
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(reader_fab_fullscreen) {
|
|
||||||
setImageResource(R.drawable.ic_fullscreen)
|
|
||||||
setOnClickListener {
|
|
||||||
isFullscreen = true
|
|
||||||
fullscreen(isFullscreen)
|
|
||||||
|
|
||||||
this@ReaderActivity.reader_fab.close(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fullscreen(isFullscreen: Boolean) {
|
|
||||||
with(window.attributes) {
|
|
||||||
if (isFullscreen) {
|
|
||||||
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
|
||||||
supportActionBar?.hide()
|
|
||||||
this@ReaderActivity.reader_fab.visibility = View.INVISIBLE
|
|
||||||
this@ReaderActivity.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()
|
|
||||||
this@ReaderActivity.reader_fab.visibility = View.VISIBLE
|
|
||||||
this@ReaderActivity.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
|
|
||||||
}
|
|
||||||
|
|
||||||
reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scrollMode(isScroll: Boolean) {
|
|
||||||
if (isScroll) {
|
|
||||||
snapHelper.attachToRecyclerView(null)
|
|
||||||
reader_recyclerview.layoutManager = LinearLayoutManager(this)
|
|
||||||
} else {
|
|
||||||
snapHelper.attachToRecyclerView(reader_recyclerview)
|
|
||||||
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, Preferences["rtl", false])
|
|
||||||
}
|
|
||||||
|
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animateDownloadFAB(animate: Boolean) {
|
|
||||||
with(reader_fab_download) {
|
|
||||||
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 ->
|
|
||||||
eye_card.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(eye_card) {
|
|
||||||
left_eye.setImageDrawable(it)
|
|
||||||
right_eye.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(eye_card) {
|
|
||||||
left_eye.setImageDrawable(
|
|
||||||
ContextCompat.getDrawable(
|
|
||||||
context,
|
|
||||||
if (left) R.drawable.eye else R.drawable.eye_closed
|
|
||||||
)
|
|
||||||
)
|
|
||||||
right_eye.setImageDrawable(
|
|
||||||
ContextCompat.getDrawable(
|
|
||||||
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) {
|
|
||||||
(this@ReaderActivity.reader_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 = this@ReaderActivity.eye_card
|
|
||||||
when (camera) {
|
|
||||||
null -> {
|
|
||||||
reader_fab_auto.labelText = getString(R.string.reader_fab_auto_cancel)
|
|
||||||
reader_fab_auto.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 -> {
|
|
||||||
reader_fab_auto.labelText = getString(R.string.reader_fab_auto)
|
|
||||||
reader_fab_auto.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,165 +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.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import kotlinx.android.synthetic.main.dialog_default_query.*
|
|
||||||
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.types.Tags
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
|
|
||||||
class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|
||||||
|
|
||||||
private val languages = context.resources.getStringArray(R.array.languages).map {
|
|
||||||
it.split("|").let { split ->
|
|
||||||
Pair(split[0], split[1])
|
|
||||||
}
|
|
||||||
}.toMap()
|
|
||||||
private val reverseLanguages = 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
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
setTitle(R.string.default_query_dialog_title)
|
|
||||||
setView(build())
|
|
||||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
|
|
||||||
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
|
|
||||||
|
|
||||||
with(default_query_dialog_language_selector) {
|
|
||||||
if (selectedItemPosition != 0)
|
|
||||||
newTags.add("language:${reverseLanguages[selectedItem]}")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (default_query_dialog_BL_checkbox.isChecked)
|
|
||||||
newTags.add(excludeBL)
|
|
||||||
|
|
||||||
if (default_query_dialog_guro_checkbox.isChecked)
|
|
||||||
excludeGuro.forEach { tag ->
|
|
||||||
newTags.add(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (default_query_dialog_loli_checkbox.isChecked)
|
|
||||||
excludeLoli.forEach { tag ->
|
|
||||||
newTags.add(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
onPositiveButtonClickListener?.invoke(newTags)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
private fun build() : View {
|
|
||||||
val tags = Tags.parse(
|
|
||||||
Preferences["default_query"]
|
|
||||||
)
|
|
||||||
|
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
|
||||||
|
|
||||||
with(view.default_query_dialog_language_selector) {
|
|
||||||
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(view.default_query_dialog_BL_checkbox) {
|
|
||||||
isChecked = tags.contains(excludeBL)
|
|
||||||
if (tags.contains(excludeBL))
|
|
||||||
tags.remove(excludeBL)
|
|
||||||
}
|
|
||||||
|
|
||||||
with(view.default_query_dialog_guro_checkbox) {
|
|
||||||
isChecked = excludeGuro.all { tags.contains(it) }
|
|
||||||
if (excludeGuro.all { tags.contains(it) })
|
|
||||||
excludeGuro.forEach {
|
|
||||||
tags.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(view.default_query_dialog_loli_checkbox) {
|
|
||||||
isChecked = excludeLoli.all { tags.contains(it) }
|
|
||||||
if (excludeLoli.all { tags.contains(it) })
|
|
||||||
excludeLoli.forEach {
|
|
||||||
tags.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(view.default_query_dialog_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())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,74 +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.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.widget.addTextChangedListener
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
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() {
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
private fun build(): View {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
return layoutInflater.inflate(R.layout.dialog_download_folder_name, null).apply {
|
|
||||||
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
|
|
||||||
edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
|
|
||||||
edittext.addTextChangedListener {
|
|
||||||
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
|
|
||||||
}
|
|
||||||
ok_button.setOnClickListener {
|
|
||||||
val newValue = edittext.text.toString()
|
|
||||||
|
|
||||||
if ((newValue as? String)?.contains("/") != false) {
|
|
||||||
Snackbar.make(this, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
Preferences["download_folder_name"] = edittext.text.toString()
|
|
||||||
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
|
||||||
Dialog(requireContext()).apply {
|
|
||||||
setContentView(build())
|
|
||||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,196 +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.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
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 kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
|
|
||||||
import kotlinx.android.synthetic.main.item_download_folder.view.*
|
|
||||||
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.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.byteToString
|
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import xyz.quaver.pupil.util.migrate
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class DownloadLocationDialogFragment : DialogFragment() {
|
|
||||||
private val entries = mutableMapOf<File?, View>()
|
|
||||||
|
|
||||||
private val requestDownloadFolderLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
val activity = activity ?: return@registerForActivityResult
|
|
||||||
val context = context ?: return@registerForActivityResult
|
|
||||||
val dialog = dialog ?: return@registerForActivityResult
|
|
||||||
|
|
||||||
it.data?.data?.also { uri ->
|
|
||||||
val takeFlags: Int =
|
|
||||||
activity.intent.flags and
|
|
||||||
(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]?.location_available?.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]!!.location_available.text = downloadFolder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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]!!.location_available.text = downloadFolder
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
entries[null]?.location_available?.text = directory
|
|
||||||
Preferences["download_folder"] = File(directory).toURI().toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
private fun build() : View? {
|
|
||||||
val context = context ?: return null
|
|
||||||
|
|
||||||
val view = layoutInflater.inflate(R.layout.dialog_download_folder, null) as LinearLayout
|
|
||||||
|
|
||||||
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
|
||||||
|
|
||||||
externalFilesDirs.forEachIndexed { index, dir ->
|
|
||||||
dir ?: return@forEachIndexed
|
|
||||||
|
|
||||||
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
|
|
||||||
location_type.text = context.getString(when (index) {
|
|
||||||
0 -> R.string.settings_download_folder_internal
|
|
||||||
else -> R.string.settings_download_folder_removable
|
|
||||||
})
|
|
||||||
location_available.text = context.getString(
|
|
||||||
R.string.settings_download_folder_available,
|
|
||||||
byteToString(dir.freeSpace)
|
|
||||||
)
|
|
||||||
setOnClickListener {
|
|
||||||
entries.values.forEach {
|
|
||||||
it.button.isChecked = false
|
|
||||||
}
|
|
||||||
button.performClick()
|
|
||||||
Preferences["download_folder"] = dir.toUri().toString()
|
|
||||||
}
|
|
||||||
entries[dir] = this
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
|
|
||||||
location_type.text = context.getString(R.string.settings_download_folder_custom)
|
|
||||||
setOnClickListener {
|
|
||||||
entries.values.forEach {
|
|
||||||
it.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(context).downloadFolder.canonicalPath
|
|
||||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
|
||||||
entries[key]!!.button.isChecked = true
|
|
||||||
if (key == null) entries[key]!!.location_available.text = downloadFolder
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val builder = AlertDialog.Builder(requireContext())
|
|
||||||
|
|
||||||
builder
|
|
||||||
.setTitle(R.string.settings_download_folder)
|
|
||||||
.setView(build())
|
|
||||||
.setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
|
|
||||||
if (Preferences["download_folder", ""].isEmpty())
|
|
||||||
Preferences["download_folder"] = context?.getExternalFilesDir(null)?.toUri()?.toString() ?: ""
|
|
||||||
|
|
||||||
DownloadManager.getInstance(requireContext()).migrate()
|
|
||||||
}
|
|
||||||
|
|
||||||
isCancelable = false
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,273 +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.LayoutInflater
|
|
||||||
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.android.synthetic.main.dialog_gallery.*
|
|
||||||
import kotlinx.android.synthetic.main.dialog_gallery_details.view.*
|
|
||||||
import kotlinx.android.synthetic.main.dialog_gallery_dotindicator.view.*
|
|
||||||
import kotlinx.android.synthetic.main.item_gallery_details.view.*
|
|
||||||
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.favoriteTags
|
|
||||||
import xyz.quaver.pupil.histories
|
|
||||||
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))>()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.dialog_gallery)
|
|
||||||
|
|
||||||
window?.attributes.apply {
|
|
||||||
this ?: return@apply
|
|
||||||
|
|
||||||
width = LayoutParams.MATCH_PARENT
|
|
||||||
height = LayoutParams.MATCH_PARENT
|
|
||||||
}
|
|
||||||
|
|
||||||
with(gallery_fab) {
|
|
||||||
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right))
|
|
||||||
setOnClickListener {
|
|
||||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
|
||||||
putExtra("galleryID", galleryID)
|
|
||||||
})
|
|
||||||
histories.add(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
val gallery = getGallery(galleryID)
|
|
||||||
|
|
||||||
gallery_cover.post {
|
|
||||||
gallery_progressbar.visibility = View.GONE
|
|
||||||
gallery_title.text = gallery.title
|
|
||||||
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
|
|
||||||
|
|
||||||
with(gallery_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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gallery_cover.showImage(Uri.parse(gallery.cover))
|
|
||||||
|
|
||||||
addDetails(gallery)
|
|
||||||
addThumbnails(gallery)
|
|
||||||
addRelated(gallery)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Snackbar.make(gallery_layout, 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) {
|
|
||||||
val inflater = LayoutInflater.from(context)
|
|
||||||
|
|
||||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
|
||||||
gallery_details.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) ->
|
|
||||||
inflater.inflate(R.layout.item_gallery_details, gallery_details_contents, false).apply {
|
|
||||||
gallery_details_type.setText(title)
|
|
||||||
|
|
||||||
content.forEach { tag ->
|
|
||||||
gallery_details_tags.addView(
|
|
||||||
TagChip(context, tag).apply {
|
|
||||||
setOnClickListener {
|
|
||||||
onChipClickedHandler.forEach { handler ->
|
|
||||||
handler.invoke(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.let {
|
|
||||||
gallery_details_contents.addView(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.let {
|
|
||||||
gallery_contents.addView(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addThumbnails(gallery: Gallery) {
|
|
||||||
val inflater = LayoutInflater.from(context)
|
|
||||||
|
|
||||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
|
||||||
gallery_details.setText(R.string.gallery_thumbnails)
|
|
||||||
|
|
||||||
val pager = ViewPager2(context).apply {
|
|
||||||
adapter = ThumbnailPageAdapter(gallery.thumbnails)
|
|
||||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
gallery_details_contents.addView(
|
|
||||||
pager,
|
|
||||||
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
||||||
)
|
|
||||||
|
|
||||||
LayoutInflater.from(context).inflate(R.layout.dialog_gallery_dotindicator, gallery_details_contents)
|
|
||||||
|
|
||||||
gallery_dotindicator.setViewPager2(pager)
|
|
||||||
}.let {
|
|
||||||
gallery_contents.addView(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addRelated(gallery: Gallery) {
|
|
||||||
val inflater = LayoutInflater.from(context)
|
|
||||||
val galleries = ArrayList<Int>()
|
|
||||||
|
|
||||||
val adapter = GalleryBlockAdapter(galleries).apply {
|
|
||||||
onChipClickedHandler.add { tag ->
|
|
||||||
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
|
|
||||||
handler.invoke(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
|
||||||
gallery_details.setText(R.string.gallery_related)
|
|
||||||
|
|
||||||
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])
|
|
||||||
})
|
|
||||||
histories.add(galleries[position])
|
|
||||||
}
|
|
||||||
onItemLongClickListener = { _, position, _ ->
|
|
||||||
GalleryDialog(context, galleries[position]).apply {
|
|
||||||
onChipClickedHandler.add { tag ->
|
|
||||||
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.let {
|
|
||||||
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
|
||||||
}
|
|
||||||
}.let {
|
|
||||||
gallery_contents.addView(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
gallery.related.forEach { galleryID ->
|
|
||||||
Cache.getInstance(context, galleryID).getGalleryBlock()?.let {
|
|
||||||
galleries.add(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,91 +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.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.adapters.MirrorAdapter
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
|
|
||||||
class MirrorDialog(context: Context) : AlertDialog(context) {
|
|
||||||
|
|
||||||
class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
|
|
||||||
|
|
||||||
var onMoveItem : ((Int, Int) -> (Unit))? = null
|
|
||||||
|
|
||||||
override fun getMovementFlags(
|
|
||||||
recyclerView: RecyclerView,
|
|
||||||
viewHolder: RecyclerView.ViewHolder
|
|
||||||
) = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
|
|
||||||
|
|
||||||
override fun onMove(
|
|
||||||
recyclerView: RecyclerView,
|
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
|
||||||
target: RecyclerView.ViewHolder
|
|
||||||
): Boolean {
|
|
||||||
onMoveItem?.invoke(viewHolder.adapterPosition, target.adapterPosition)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
setTitle(R.string.settings_mirror_title)
|
|
||||||
setView(build())
|
|
||||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun build() : View {
|
|
||||||
return RecyclerView(context).apply recyclerview@{
|
|
||||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
|
||||||
layoutManager = LinearLayoutManager(context)
|
|
||||||
adapter = MirrorAdapter(context).apply adapter@{
|
|
||||||
val itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback().apply {
|
|
||||||
onMoveItem = this@adapter.onItemMove
|
|
||||||
}).apply {
|
|
||||||
attachToRecyclerView(this@recyclerview)
|
|
||||||
}
|
|
||||||
|
|
||||||
onStartDrag = {
|
|
||||||
itemTouchHelper.startDrag(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemMoved = {
|
|
||||||
Preferences["mirrors"] = it.joinToString(">")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import kotlinx.android.synthetic.main.dialog_proxy.view.*
|
|
||||||
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.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 ProxyDialog(context: Context) : AlertDialog(context) {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
setView(build())
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
private fun build() : View {
|
|
||||||
val proxyInfo = getProxyInfo()
|
|
||||||
|
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null)
|
|
||||||
|
|
||||||
val enabler = { enable: Boolean ->
|
|
||||||
view?.proxy_addr?.isEnabled = enable
|
|
||||||
view?.proxy_port?.isEnabled = enable
|
|
||||||
view?.proxy_username?.isEnabled = enable
|
|
||||||
view?.proxy_password?.isEnabled = enable
|
|
||||||
|
|
||||||
if (!enable) {
|
|
||||||
view?.proxy_addr?.text = null
|
|
||||||
view?.proxy_port?.text = null
|
|
||||||
view?.proxy_username?.text = null
|
|
||||||
view?.proxy_password?.text = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(view.proxy_type_selector) {
|
|
||||||
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<*>?) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.proxy_addr.setText(proxyInfo.host)
|
|
||||||
view.proxy_port.setText(proxyInfo.port?.toString())
|
|
||||||
view.proxy_username.setText(proxyInfo.username)
|
|
||||||
view.proxy_password.setText(proxyInfo.password)
|
|
||||||
|
|
||||||
enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT)
|
|
||||||
|
|
||||||
view.proxy_cancel.setOnClickListener {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
view.proxy_ok.setOnClickListener {
|
|
||||||
val type = Proxy.Type.values()[view.proxy_type_selector.selectedItemPosition]
|
|
||||||
val addr = view.proxy_addr.text?.toString()
|
|
||||||
val port = view.proxy_port.text?.toString()?.toIntOrNull()
|
|
||||||
val username = view.proxy_username.text?.toString()
|
|
||||||
val password = view.proxy_password.text?.toString()
|
|
||||||
|
|
||||||
if (type != Proxy.Type.DIRECT) {
|
|
||||||
if (addr == null || addr.isEmpty())
|
|
||||||
view.proxy_addr.error = context.getText(R.string.proxy_dialog_error)
|
|
||||||
if (port == null)
|
|
||||||
view.proxy_port.error = context.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()
|
|
||||||
}
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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,196 +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 { (it as? FileX)?.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,53 +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 kotlinx.android.synthetic.main.fragment_pin_lock.view.*
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
|
|
||||||
class PINLockFragment : Fragment(), PinLockListener {
|
|
||||||
|
|
||||||
var onPINEntered: ((String) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_pin_lock, container, false).apply {
|
|
||||||
pin_lock_view.attachIndicatorDots(indicator_dots)
|
|
||||||
pin_lock_view.setPinLockListener(this@PINLockFragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onComplete(pin: String?) {
|
|
||||||
onPINEntered?.invoke(pin!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEmpty() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPinChange(pinLength: Int, intermediatePin: String?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,63 +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 kotlinx.android.synthetic.main.fragment_pattern_lock.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.view.*
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
|
|
||||||
class PatternLockFragment : Fragment(), PatternLockViewListener {
|
|
||||||
|
|
||||||
var onPatternDrawn: ((String) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_pattern_lock, container, false).apply {
|
|
||||||
lock_pattern_view.addPatternLockListener(this@PatternLockFragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onComplete(pattern: MutableList<PatternLockView.Dot>?) {
|
|
||||||
val password = PatternLockUtils.patternToMD5(lock_pattern_view, pattern)
|
|
||||||
onPatternDrawn?.invoke(password)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProgress(progressPattern: MutableList<PatternLockView.Dot>?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStarted() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,276 +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.Preference
|
|
||||||
import androidx.preference.PreferenceCategory
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.getChild
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
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
|
|
||||||
|
|
||||||
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(requireContext()).apply {
|
|
||||||
onPositiveButtonClickListener = { newTags ->
|
|
||||||
Preferences["default_query"] = newTags.toString()
|
|
||||||
summary = newTags.toString()
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
"app_lock" -> {
|
|
||||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
|
||||||
putExtra("force", true)
|
|
||||||
}
|
|
||||||
lockLauncher.launch(intent)
|
|
||||||
}
|
|
||||||
"mirrors" -> {
|
|
||||||
MirrorDialog(requireContext())
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
"proxy" -> {
|
|
||||||
ProxyDialog(requireContext())
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
"user_id" -> {
|
|
||||||
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
|
||||||
ClipData.newPlainText("user_id", Preferences.get<String>("user_id"))
|
|
||||||
)
|
|
||||||
Toast.makeText(context, R.string.settings_user_id_toast, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean {
|
|
||||||
with (preference) {
|
|
||||||
this ?: return false
|
|
||||||
|
|
||||||
when (key) {
|
|
||||||
"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-"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
"mirrors" -> {
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"proxy" -> {
|
|
||||||
summary = getProxyInfo().type.name
|
|
||||||
|
|
||||||
onPreferenceClickListener = 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,220 +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.os.Parcelable
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.util.AttributeSet
|
|
||||||
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.MenuPopupHelper
|
|
||||||
import xyz.quaver.floatingsearchview.util.view.MenuView
|
|
||||||
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
|
|
||||||
|
|
||||||
searchInputView.addTextChangedListener(this)
|
|
||||||
onSearchListener = this
|
|
||||||
onBindSuggestionCallback = { a, b, c, d, e ->
|
|
||||||
onBindSuggestion(a, b, c, d, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.replace(Regex("\\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 == -1) {
|
|
||||||
textView?.text = item.s
|
|
||||||
} else {
|
|
||||||
(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,99 +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.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 -> tag.tag.wordCapitalize()
|
|
||||||
}
|
|
||||||
|
|
||||||
setEnsureMinTouchTargetSize(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,90 +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 com.google.android.material.chip.Chip
|
|
||||||
import com.google.android.material.chip.ChipGroup
|
|
||||||
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 {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh() {
|
|
||||||
this.removeAllViews()
|
|
||||||
|
|
||||||
tags.take(maxChipSize).forEach {
|
|
||||||
this.addView(TagChip(context, it).apply {
|
|
||||||
setOnClickListener {
|
|
||||||
onClickListener?.invoke(this.tag)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxChipSize > 0 && this.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,92 +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.serialization.*
|
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun load() {
|
|
||||||
synchronized(this) {
|
|
||||||
set.clear()
|
|
||||||
kotlin.runCatching {
|
|
||||||
Json.decodeFromString(serializer, file.readText())
|
|
||||||
}.onSuccess {
|
|
||||||
set.addAll(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
fun save() {
|
|
||||||
synchronized(this) {
|
|
||||||
file.writeText(Json.encodeToString(serializer, set.toList()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(element: T): Boolean {
|
|
||||||
load()
|
|
||||||
|
|
||||||
set.remove(element)
|
|
||||||
|
|
||||||
return set.add(element).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addAll(elements: Collection<T>): Boolean {
|
|
||||||
load()
|
|
||||||
|
|
||||||
set.removeAll(elements)
|
|
||||||
|
|
||||||
return set.addAll(elements).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun remove(element: T): Boolean {
|
|
||||||
load()
|
|
||||||
|
|
||||||
return set.remove(element).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clear() {
|
|
||||||
set.clear()
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.text.style.LineHeightSpan
|
|
||||||
|
|
||||||
class SetLineOverlap(private val overlap: Boolean) : LineHeightSpan {
|
|
||||||
companion object {
|
|
||||||
private var originalBottom = 15
|
|
||||||
private var originalDescent = 13
|
|
||||||
private var overlapSaved = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chooseHeight(
|
|
||||||
text: CharSequence?,
|
|
||||||
start: Int,
|
|
||||||
end: Int,
|
|
||||||
spanstartv: Int,
|
|
||||||
lineHeight: Int,
|
|
||||||
fm: Paint.FontMetricsInt?
|
|
||||||
) {
|
|
||||||
fm ?: return
|
|
||||||
|
|
||||||
if (overlap) {
|
|
||||||
if (overlapSaved) {
|
|
||||||
originalBottom = fm.bottom
|
|
||||||
originalDescent = fm.descent
|
|
||||||
overlapSaved = true
|
|
||||||
}
|
|
||||||
fm.bottom += fm.top
|
|
||||||
fm.descent += fm.top
|
|
||||||
} else {
|
|
||||||
fm.bottom = originalBottom
|
|
||||||
fm.descent = originalDescent
|
|
||||||
overlapSaved = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,253 +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.withContext
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Request
|
|
||||||
import xyz.quaver.Code
|
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.Reader
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.*
|
|
||||||
import xyz.quaver.pupil.client
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Metadata(
|
|
||||||
var galleryBlock: GalleryBlock? = null,
|
|
||||||
var reader: Reader? = null,
|
|
||||||
var imageList: MutableList<String?>? = null
|
|
||||||
) {
|
|
||||||
fun copy(): Metadata = Metadata(galleryBlock, reader, 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(galleryID: Int) {
|
|
||||||
instances[galleryID]?.cacheFolder?.deleteRecursively()
|
|
||||||
instances.remove(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
cacheFolder.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata = kotlin.runCatching {
|
|
||||||
findFile(".metadata")?.readText()?.let {
|
|
||||||
Json.decodeFromString<Metadata>(it)
|
|
||||||
}
|
|
||||||
}.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? {
|
|
||||||
val sources = listOf(
|
|
||||||
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
|
||||||
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
|
||||||
)
|
|
||||||
|
|
||||||
return metadata.galleryBlock
|
|
||||||
?: withContext(Dispatchers.IO) {
|
|
||||||
var galleryBlock: GalleryBlock? = null
|
|
||||||
|
|
||||||
for (source in sources) {
|
|
||||||
galleryBlock = try {
|
|
||||||
source.invoke()
|
|
||||||
} catch (e: Exception) { null }
|
|
||||||
|
|
||||||
if (galleryBlock != null)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
galleryBlock?.also {
|
|
||||||
setMetadata { metadata -> metadata.galleryBlock = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 { it.writeBytes(thumbnail) }
|
|
||||||
}.getOrNull()?.uri }
|
|
||||||
} } ?: Uri.EMPTY
|
|
||||||
|
|
||||||
suspend fun getReader(): Reader? {
|
|
||||||
val mirrors = Preferences.get<String>("mirrors").let { if (it.isEmpty()) emptyList() else it.split('>') }
|
|
||||||
|
|
||||||
val sources = mapOf(
|
|
||||||
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
|
|
||||||
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
|
|
||||||
).let {
|
|
||||||
if (mirrors.isNotEmpty())
|
|
||||||
it.toSortedMap{ o1, o2 -> mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name) }
|
|
||||||
else
|
|
||||||
it
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata.reader
|
|
||||||
?: withContext(Dispatchers.IO) {
|
|
||||||
var reader: Reader? = null
|
|
||||||
|
|
||||||
for (source in sources) {
|
|
||||||
reader = try {
|
|
||||||
source.value.invoke()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader != null)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
reader?.also {
|
|
||||||
setMetadata { metadata ->
|
|
||||||
metadata.reader = it
|
|
||||||
|
|
||||||
if (metadata.imageList == null)
|
|
||||||
metadata.imageList = MutableList(reader.galleryInfo.files.size) { 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 }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val downloadFolder = downloadFolder ?: return@launch
|
|
||||||
|
|
||||||
val cacheMetadata = cacheFolder.getChild(".metadata")
|
|
||||||
val downloadMetadata = downloadFolder.getChild(".metadata")
|
|
||||||
|
|
||||||
if (downloadMetadata.exists() || !cacheMetadata.exists())
|
|
||||||
return@launch
|
|
||||||
|
|
||||||
if (cacheMetadata.exists()) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
downloadMetadata.createNewFile()
|
|
||||||
downloadMetadata.writeText(Json.encodeToString(metadata))
|
|
||||||
|
|
||||||
cacheMetadata.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val cacheThumbnail = cacheFolder.getChild(".thumbnail")
|
|
||||||
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
|
|
||||||
|
|
||||||
if (cacheThumbnail.exists() && !downloadThumbnail.exists()) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
if (!downloadThumbnail.exists())
|
|
||||||
downloadThumbnail.createNewFile()
|
|
||||||
|
|
||||||
downloadThumbnail.outputStream()?.use { target -> 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() || target.exists())
|
|
||||||
return@forEach
|
|
||||||
|
|
||||||
kotlin.runCatching {
|
|
||||||
if (!target.exists())
|
|
||||||
target.createNewFile()
|
|
||||||
|
|
||||||
target.outputStream()?.use { target -> source.inputStream()?.use { source ->
|
|
||||||
source.copyTo(target)
|
|
||||||
} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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!!
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@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,81 +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)
|
|
||||||
|
|
||||||
cacheFolder.listFiles { file ->
|
|
||||||
val galleryID = file.name.toIntOrNull() ?: return@listFiles true
|
|
||||||
|
|
||||||
!(downloadManager.downloadFolderMap.containsKey(galleryID) || histories.contains(galleryID))
|
|
||||||
}?.forEach {
|
|
||||||
it.deleteRecursively()
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadManager.getInstance(context).downloadFolderMap.keys.forEach {
|
|
||||||
val folder = File(cacheFolder, it.toString())
|
|
||||||
|
|
||||||
if (!downloadManager.isDownloading(it) && folder.exists()) {
|
|
||||||
folder.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
(histories.firstOrNull {
|
|
||||||
caches.contains(it.toString()) && !downloadManager.isDownloading(it)
|
|
||||||
} ?: return@withLock).let {
|
|
||||||
Cache.delete(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,131 +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 android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import xyz.quaver.Code
|
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.Reader
|
|
||||||
import xyz.quaver.hitomi.getReferer
|
|
||||||
import xyz.quaver.hitomi.imageUrlFromImage
|
|
||||||
import xyz.quaver.hiyobi.createImgList
|
|
||||||
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.capitalize(Locale.US))
|
|
||||||
|
|
||||||
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("""[*\\|"?><:/]"""), "")
|
|
||||||
|
|
||||||
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
|
|
||||||
format.let {
|
|
||||||
formatMap.entries.fold(it) { str, (k, v) ->
|
|
||||||
str.replace(k, v.invoke(this), true)
|
|
||||||
}
|
|
||||||
}.replace(Regex("""[*\\|"?><:/]"""), "")
|
|
||||||
|
|
||||||
val Reader.requestBuilders: List<Request.Builder>
|
|
||||||
get() {
|
|
||||||
val galleryID = this.galleryInfo.id ?: 0
|
|
||||||
val lowQuality = Preferences["low_quality", true]
|
|
||||||
|
|
||||||
return when(code) {
|
|
||||||
Code.HITOMI -> {
|
|
||||||
this.galleryInfo.files.map {
|
|
||||||
Request.Builder()
|
|
||||||
.url(imageUrlFromImage(galleryID, it, !lowQuality))
|
|
||||||
.header("Referer", getReferer(galleryID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Code.HIYOBI -> {
|
|
||||||
createImgList(galleryID, this, lowQuality).map {
|
|
||||||
Request.Builder()
|
|
||||||
.url(it.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun String.ellipsize(n: Int): String =
|
|
||||||
if (this.length > n)
|
|
||||||
this.slice(0 until n) + "…"
|
|
||||||
else
|
|
||||||
this
|
|
||||||
@@ -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,335 +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 android.app.DownloadManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Base64
|
|
||||||
import android.webkit.URLUtil
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.*
|
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.Callback
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import ru.noties.markwon.Markwon
|
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.Reader
|
|
||||||
import xyz.quaver.hitomi.getGalleryBlock
|
|
||||||
import xyz.quaver.hitomi.getReader
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.getChild
|
|
||||||
import xyz.quaver.io.util.readText
|
|
||||||
import xyz.quaver.io.util.writeBytes
|
|
||||||
import xyz.quaver.io.util.writeText
|
|
||||||
import xyz.quaver.pupil.BuildConfig
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.client
|
|
||||||
import xyz.quaver.pupil.favorites
|
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.downloader.Metadata
|
|
||||||
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_update) { _, _ ->
|
|
||||||
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) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private var job: Job? = null
|
|
||||||
private val receiver = object: BroadcastReceiver() {
|
|
||||||
val ACTION_CANCEL = "ACTION_IMPORT_CANCEL"
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
context ?: return
|
|
||||||
|
|
||||||
when (intent?.action) {
|
|
||||||
ACTION_CANCEL -> {
|
|
||||||
job?.cancel()
|
|
||||||
NotificationManagerCompat.from(context).cancel(R.id.notification_id_import)
|
|
||||||
context.unregisterReceiver(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
|
|
||||||
registerReceiver(receiver, IntentFilter().apply { addAction(receiver.ACTION_CANCEL) })
|
|
||||||
|
|
||||||
val notificationManager = NotificationManagerCompat.from(this)
|
|
||||||
val action = NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
|
||||||
PendingIntent.getBroadcast(this, R.id.notification_import_cancel_action.normalizeID(), Intent(receiver.ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
).build()
|
|
||||||
val notification = NotificationCompat.Builder(this, "import")
|
|
||||||
.setContentTitle(getText(R.string.import_old_galleries_notification))
|
|
||||||
.setProgress(0, 0, true)
|
|
||||||
.addAction(action)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setOngoing(true)
|
|
||||||
|
|
||||||
DownloadService.cancel(this)
|
|
||||||
|
|
||||||
job?.cancel()
|
|
||||||
job = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val downloadFolders = downloadFolder.listFiles { folder ->
|
|
||||||
folder.isDirectory && !downloadFolderMap.values.contains(folder.name)
|
|
||||||
}?.map {
|
|
||||||
if (it !is FileX)
|
|
||||||
FileX(this@migrate, it)
|
|
||||||
else
|
|
||||||
it
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloadFolders.isNullOrEmpty()) return@launch
|
|
||||||
|
|
||||||
downloadFolders.forEachIndexed { index, folder ->
|
|
||||||
notification
|
|
||||||
.setContentText(getString(R.string.import_old_galleries_notification_text, index, downloadFolders.size))
|
|
||||||
.setProgress(index, downloadFolders.size, false)
|
|
||||||
notificationManager.notify(R.id.notification_id_import, notification.build())
|
|
||||||
|
|
||||||
kotlin.runCatching {
|
|
||||||
val metadata = kotlin.runCatching {
|
|
||||||
folder.getChild(".metadata").readText()?.let { Json.parseToJsonElement(it).jsonObject }
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
val galleryID = folder.name.toIntOrNull() ?: return@runCatching
|
|
||||||
|
|
||||||
val galleryBlock: GalleryBlock? = kotlin.runCatching {
|
|
||||||
metadata?.get("galleryBlock")?.let { Json.decodeFromJsonElement<GalleryBlock>(it) }
|
|
||||||
}.getOrNull() ?: getGalleryBlock(galleryID)
|
|
||||||
val reader: Reader? = kotlin.runCatching {
|
|
||||||
metadata?.get("reader")?.let { Json.decodeFromJsonElement<Reader>(it) }
|
|
||||||
}.getOrNull() ?: getReader(galleryID)
|
|
||||||
|
|
||||||
metadata?.get("thumbnail")?.jsonPrimitive?.contentOrNull?.also { thumbnail ->
|
|
||||||
val file = folder.getChild(".thumbnail").also {
|
|
||||||
if (it.exists())
|
|
||||||
it.delete()
|
|
||||||
it.createNewFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
file.writeBytes(Base64.decode(thumbnail, Base64.DEFAULT))
|
|
||||||
}
|
|
||||||
|
|
||||||
val list: MutableList<String?> =
|
|
||||||
MutableList(reader!!.galleryInfo.files.size) { null }
|
|
||||||
|
|
||||||
folder.listFiles { file ->
|
|
||||||
file?.nameWithoutExtension?.let {
|
|
||||||
Regex("""\d{5}""").matches(it) && it.toIntOrNull() != null
|
|
||||||
} == true
|
|
||||||
}?.forEach {
|
|
||||||
list[it.nameWithoutExtension.toInt()] = it.name
|
|
||||||
}
|
|
||||||
|
|
||||||
folder.getChild(".metadata").also { if (it.exists()) it.delete(); it.createNewFile() }.writeText(
|
|
||||||
Json.encodeToString(Metadata(galleryBlock, reader, list))
|
|
||||||
)
|
|
||||||
|
|
||||||
synchronized(Cache) {
|
|
||||||
Cache.delete(galleryID)
|
|
||||||
}
|
|
||||||
downloadFolderMap[galleryID] = folder.name
|
|
||||||
|
|
||||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile(); it.writeText(Json.encodeToString(downloadFolderMap)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notification
|
|
||||||
.setContentText(getText(R.string.import_old_galleries_notification_done))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
.setOngoing(false)
|
|
||||||
.mActions.clear()
|
|
||||||
notificationManager.notify(R.id.notification_id_import, notification.build())
|
|
||||||
|
|
||||||
kotlin.runCatching {
|
|
||||||
unregisterReceiver(receiver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 |
|
Before Width: | Height: | Size: 314 B |
|
Before Width: | Height: | Size: 311 B |
|
Before Width: | Height: | Size: 628 B |
|
Before Width: | Height: | Size: 550 B |