Compare commits
265 Commits
3.3-beta
...
4.20-hotfi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
647294daf2 | ||
|
|
6ebc386474 | ||
|
|
3e657bdc09 | ||
|
|
0b0adb76a1 | ||
|
|
17b3e010aa | ||
|
|
20003acd73 | ||
|
|
2ab7672092 | ||
|
|
c317abe64b | ||
|
|
bc33ce1ebc | ||
|
|
684c5cf38b | ||
|
|
c34e15f0a1 | ||
|
|
bad004f892 | ||
|
|
828d3de020 | ||
|
|
132b3b9be1 | ||
|
|
388bc6fda5 | ||
|
|
a93edc184d | ||
|
|
08672d10ac | ||
|
|
b563dae3a8 | ||
|
|
917f9672dd | ||
|
|
9ddb19530b | ||
|
|
431e56a9f1 | ||
|
|
71093aac4c | ||
|
|
47c9e8127e | ||
|
|
24b801b346 | ||
|
|
70608c3ed9 | ||
|
|
f185196e85 | ||
|
|
a8766a8bbe | ||
|
|
27a8c93cfe | ||
|
|
a3cd29fda9 | ||
|
|
adda8ab640 | ||
|
|
1538ea5fc8 | ||
|
|
2367a97a54 | ||
|
|
090ec0e4af | ||
|
|
de7f552e5c | ||
|
|
d763f5dca0 | ||
|
|
9f41116241 | ||
|
|
57faada201 | ||
|
|
1edb95f0c5 | ||
|
|
9f363d8900 | ||
|
|
0bf2f1b6e1 | ||
|
|
68c7a38390 | ||
|
|
841c8a7a15 | ||
|
|
6c9688183b | ||
|
|
ccd84c91f6 | ||
|
|
318d6f9b52 | ||
|
|
8f5d612ee0 | ||
|
|
56b2a05596 | ||
|
|
4db0022d6a | ||
|
|
67f37d3188 | ||
|
|
ed81cc7207 | ||
|
|
065845f1be | ||
|
|
902f705e89 | ||
|
|
ec2e0ef773 | ||
|
|
d28c5741d0 | ||
|
|
e6e3f9e8f8 | ||
|
|
90e1dc59bd | ||
|
|
0b1c9b097c | ||
|
|
2b553d1116 | ||
|
|
567eec8bc5 | ||
|
|
293ca5b31d | ||
|
|
0d0f2bd827 | ||
|
|
5bc4610061 | ||
|
|
e6b7c107f2 | ||
|
|
51a9bf2570 | ||
|
|
8385f6f390 | ||
|
|
772e9daf57 | ||
|
|
8adc4405c5 | ||
|
|
349da7aa81 | ||
|
|
01a01d481d | ||
|
|
2f8445fb83 | ||
|
|
b04a5fc150 | ||
|
|
bbe29941df | ||
|
|
2720e445ea | ||
|
|
49ba579a59 | ||
|
|
3198c6cbfd | ||
|
|
b3feee6d9d | ||
|
|
f0f53e6bce | ||
|
|
24486d13f2 | ||
|
|
20bc9461de | ||
|
|
c8e94cc295 | ||
|
|
b2bfb0c237 | ||
|
|
0a003da724 | ||
|
|
b4f2a33016 | ||
|
|
ee7ede2885 | ||
|
|
6abc404eb7 | ||
|
|
61afe01e36 | ||
|
|
c3e60f9988 | ||
|
|
593197cd7e | ||
|
|
ee1592b478 | ||
|
|
dfe435c4f3 | ||
|
|
69e85f8b90 | ||
|
|
c9bde3c487 | ||
|
|
65e9557d9f | ||
|
|
4f249c07e7 | ||
|
|
5fd35b492c | ||
|
|
9bddf95013 | ||
|
|
03444f070f | ||
|
|
2f1a63eb64 | ||
|
|
9d0898b26c | ||
|
|
994aa99797 | ||
|
|
8204a15276 | ||
|
|
4a8bff0b98 | ||
|
|
a4336cd954 | ||
|
|
4f0dbead79 | ||
|
|
c0e7c87ca4 | ||
|
|
b967bf9a26 | ||
|
|
764a265053 | ||
|
|
68c2b2dbfa | ||
|
|
061f1263f4 | ||
|
|
2a27355479 | ||
|
|
ae2a8e8ada | ||
|
|
68dcc2333b | ||
|
|
66fb2e9a62 | ||
|
|
1dbfc64f37 | ||
|
|
98d1f88579 | ||
|
|
bb6fadc182 | ||
|
|
ac1ca71299 | ||
|
|
0d93785581 | ||
|
|
69a9d63e1d | ||
|
|
5dea35343b | ||
|
|
5c768d2121 | ||
|
|
4d5834821a | ||
|
|
ca077c4fee | ||
|
|
85d01f60f1 | ||
|
|
066d73b217 | ||
|
|
ba069d8f8e | ||
|
|
275684c9ce | ||
|
|
49d87a08d2 | ||
|
|
04c500f3d8 | ||
|
|
d05c1e4d08 | ||
|
|
bb63959678 | ||
|
|
842148647f | ||
|
|
19308d840a | ||
|
|
46bd1318cd | ||
|
|
9d1998fe52 | ||
|
|
a714a8230b | ||
|
|
b5432cd0b4 | ||
|
|
5634e94f3e | ||
|
|
c1a71b0db3 | ||
|
|
d93e7f8834 | ||
|
|
3175b2c45c | ||
|
|
547b6e8e3b | ||
|
|
d88ac27e72 | ||
|
|
e551a40d08 | ||
|
|
e810abe33a | ||
|
|
6172a73719 | ||
|
|
7455e68a45 | ||
|
|
748495ca64 | ||
|
|
f6d9c7f550 | ||
|
|
384e6c61b0 | ||
|
|
d49c9cec20 | ||
|
|
4b27f1aba1 | ||
|
|
a0a989c785 | ||
|
|
ecaecc1b91 | ||
|
|
938156aa71 | ||
|
|
d30c51bb3a | ||
|
|
874606bff9 | ||
|
|
07643e4b4c | ||
|
|
20bc5423cf | ||
|
|
b84cddffdc | ||
|
|
e46d1123df | ||
|
|
48f90faf4e | ||
|
|
615b52c4fa | ||
|
|
2c9c8e223c | ||
|
|
01a653835e | ||
|
|
9d80857a38 | ||
|
|
8a9ab6b36c | ||
|
|
4edc87c197 | ||
|
|
10712e6e62 | ||
|
|
d73dc19d3d | ||
|
|
c204353220 | ||
|
|
37123a2cd5 | ||
|
|
a39484b6ea | ||
|
|
e81b5a4e3a | ||
|
|
0b87c57fbf | ||
|
|
5fd985ba39 | ||
|
|
8c64548513 | ||
|
|
a6de64ceb9 | ||
|
|
16ebb437a3 | ||
|
|
683118a3f4 | ||
|
|
08e38ed45c | ||
|
|
7abf08f1fb | ||
|
|
f3019e9b84 | ||
|
|
9ea55664b6 | ||
|
|
c468764234 | ||
|
|
31c3178430 | ||
|
|
e81c189afc | ||
|
|
e0ccac13c1 | ||
|
|
93228459d7 | ||
|
|
63e07f56e0 | ||
|
|
ee87122bb2 | ||
|
|
290dda9018 | ||
|
|
1d3d78b936 | ||
|
|
a947bc6415 | ||
|
|
9ca891b2f5 | ||
|
|
48e0ebc8ae | ||
|
|
b323353006 | ||
|
|
c85d3ebe81 | ||
|
|
ce843abec8 | ||
|
|
6b43faa70e | ||
|
|
2d0c997b2e | ||
|
|
1db5118377 | ||
|
|
26b53ed7ac | ||
|
|
2c85ea6443 | ||
|
|
cbc2b30f47 | ||
|
|
0b58deb92c | ||
|
|
ed1cf23c91 | ||
|
|
6fbb644e4b | ||
|
|
774867502d | ||
|
|
c8b1439aeb | ||
|
|
38c16adffe | ||
|
|
18aede2701 | ||
|
|
c59d08a0a1 | ||
|
|
66ae29eb5b | ||
|
|
7d9cb3e150 | ||
|
|
9922a9f82a | ||
|
|
445b9b4673 | ||
|
|
0ef7b358e0 | ||
|
|
2d3fb75576 | ||
|
|
d55ff6d68e | ||
|
|
079654a9c7 | ||
|
|
30263c6260 | ||
|
|
3159c343c1 | ||
|
|
ceaa930623 | ||
|
|
6a8539106b | ||
|
|
7a24c3c08e | ||
|
|
251abeb090 | ||
|
|
a61fe9f98c | ||
|
|
d29c7bf91a | ||
|
|
ed4911c441 | ||
|
|
d40b4f3748 | ||
|
|
f3c4fe1914 | ||
|
|
55ee841bd0 | ||
|
|
657fb488ee | ||
|
|
4eef0b93fb | ||
|
|
f2be56435c | ||
|
|
fa6b3ad7ba | ||
|
|
52c05e6888 | ||
|
|
865bf0ba83 | ||
|
|
3f827d1bad | ||
|
|
0561d5f55c | ||
|
|
1bf2e1dacc | ||
|
|
db5a221b56 | ||
|
|
295285f132 | ||
|
|
5052b6c074 | ||
|
|
f98f45dc54 | ||
|
|
8d16950f46 | ||
|
|
74033b9f4a | ||
|
|
e497d47374 | ||
|
|
a97af59260 | ||
|
|
2197de98ea | ||
|
|
c004c7f71a | ||
|
|
69fc3ad4e8 | ||
|
|
678a8f0914 | ||
|
|
08c4c0bf1f | ||
|
|
f2a2656837 | ||
|
|
2011572270 | ||
|
|
3b682667e1 | ||
|
|
6da8de6463 | ||
|
|
039d415871 | ||
|
|
776f53bde0 | ||
|
|
58e535595e | ||
|
|
96ad5f6a6c | ||
|
|
043f7bedd8 | ||
|
|
8a58564812 |
2
.gitignore
vendored
@@ -16,4 +16,4 @@
|
|||||||
/gh-pages
|
/gh-pages
|
||||||
|
|
||||||
#Private files
|
#Private files
|
||||||
/app/google-services.json
|
**/google-services.json
|
||||||
10
.idea/codeStyles/Project.xml
generated
@@ -1,5 +1,6 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
|
<option name="RIGHT_MARGIN" value="120" />
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
<match>
|
<match>
|
||||||
<AND>
|
<AND>
|
||||||
<NAME>xmlns:android</NAME>
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
</AND>
|
</AND>
|
||||||
</match>
|
</match>
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
<match>
|
<match>
|
||||||
<AND>
|
<AND>
|
||||||
<NAME>xmlns:.*</NAME>
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
</AND>
|
</AND>
|
||||||
</match>
|
</match>
|
||||||
@@ -35,6 +38,7 @@
|
|||||||
<match>
|
<match>
|
||||||
<AND>
|
<AND>
|
||||||
<NAME>.*:id</NAME>
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
</AND>
|
</AND>
|
||||||
</match>
|
</match>
|
||||||
@@ -45,6 +49,7 @@
|
|||||||
<match>
|
<match>
|
||||||
<AND>
|
<AND>
|
||||||
<NAME>.*:name</NAME>
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
</AND>
|
</AND>
|
||||||
</match>
|
</match>
|
||||||
@@ -55,6 +60,7 @@
|
|||||||
<match>
|
<match>
|
||||||
<AND>
|
<AND>
|
||||||
<NAME>name</NAME>
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
</AND>
|
</AND>
|
||||||
</match>
|
</match>
|
||||||
@@ -65,6 +71,7 @@
|
|||||||
<match>
|
<match>
|
||||||
<AND>
|
<AND>
|
||||||
<NAME>style</NAME>
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
</AND>
|
</AND>
|
||||||
</match>
|
</match>
|
||||||
@@ -75,6 +82,7 @@
|
|||||||
<match>
|
<match>
|
||||||
<AND>
|
<AND>
|
||||||
<NAME>.*</NAME>
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
</AND>
|
</AND>
|
||||||
</match>
|
</match>
|
||||||
@@ -86,6 +94,7 @@
|
|||||||
<match>
|
<match>
|
||||||
<AND>
|
<AND>
|
||||||
<NAME>.*</NAME>
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
</AND>
|
</AND>
|
||||||
</match>
|
</match>
|
||||||
@@ -97,6 +106,7 @@
|
|||||||
<match>
|
<match>
|
||||||
<AND>
|
<AND>
|
||||||
<NAME>.*</NAME>
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
</AND>
|
</AND>
|
||||||
</match>
|
</match>
|
||||||
|
|||||||
2
.idea/gradle.xml
generated
@@ -1,8 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="PLATFORM" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
|
|||||||
55
.idea/jarRepositories.xml
generated
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?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>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/kotlinCodeInsightSettings.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?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
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Kotlin2JvmCompilerArguments">
|
||||||
|
<option name="jvmTarget" value="1.8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
29
README.md
@@ -1,2 +1,29 @@
|
|||||||
# Pupil
|
# Pupil
|
||||||
Hitomi.la viewer for Android
|
|
||||||
|

|
||||||
|
*Pupil, Hitomi.la viewer for Android*
|
||||||
|
|
||||||
|
[](https://discord.gg/Stj4b5v)
|
||||||
|
|
||||||
|
# Screenshot
|
||||||
|

|
||||||
|
*Main Screen*
|
||||||
|
|
||||||
|

|
||||||
|
*Reader Screen*
|
||||||
|
|
||||||
|
Images are censored to be SFW
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
Go [Releases page](https://github.com/tom5079/Pupil/releases) and get latest version or
|
||||||
|
Visit [github page](https://tom5079.github.io/Pupil/) (only available in Korean)
|
||||||
|
or Build app yourself
|
||||||
|
|
||||||
|
# Manual
|
||||||
|
|
||||||
|
[Manual](https://tom5079.github.io/Pupil/2019/06/06/manual-kr.html) is only available in Korean. Consider using translator.
|
||||||
|
|
||||||
|
# Contribution
|
||||||
|
|
||||||
|
Any kind of contribution is appriciated. Feel free to leave PR!
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ apply plugin: 'kotlin-android'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
|
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.gms.google-services'
|
||||||
apply plugin: 'io.fabric'
|
apply plugin: 'com.google.firebase.crashlytics'
|
||||||
apply plugin: 'com.google.firebase.firebase-perf'
|
apply plugin: 'com.google.firebase.firebase-perf'
|
||||||
|
} else {
|
||||||
|
logger.lifecycle("Firebase Disabled")
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 29
|
||||||
@@ -13,20 +19,24 @@ android {
|
|||||||
applicationId "xyz.quaver.pupil"
|
applicationId "xyz.quaver.pupil"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 27
|
versionCode 57
|
||||||
versionName "3.2-beta2"
|
versionName "4.20-hotfix2"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
debug {
|
||||||
minifyEnabled false
|
debuggable true
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
versionNameSuffix "-DEBUG"
|
||||||
|
|
||||||
|
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
buildTypes.each {
|
release {
|
||||||
it.buildConfigField('boolean', 'PRERELEASE', 'true')
|
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||||
it.buildConfigField('boolean', 'CENSOR', 'false')
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@@ -36,41 +46,46 @@ android {
|
|||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
buildToolsVersion = '29.0.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
def markwonVersion = "3.0.1"
|
def markwonVersion = '3.1.0'
|
||||||
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0"
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
|
||||||
implementation 'androidx.preference:preference:1.1.0-rc01'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
implementation "androidx.biometric:biometric:1.0.1"
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
||||||
implementation 'com.google.android.material:material:1.1.0-alpha09'
|
implementation 'com.google.firebase:firebase-core:17.4.4'
|
||||||
implementation 'com.google.firebase:firebase-core:17.1.0'
|
implementation 'com.google.firebase:firebase-analytics:17.4.4'
|
||||||
implementation 'com.google.firebase:firebase-perf:19.0.0'
|
implementation 'com.google.firebase:firebase-crashlytics:17.1.1'
|
||||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
implementation 'com.google.firebase:firebase-perf:19.0.8'
|
||||||
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
||||||
implementation 'com.github.clans:fab:1.6.4'
|
implementation 'com.github.clans:fab:1.6.4'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||||
implementation ("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
|
implementation "com.github.bumptech.glide:okhttp3-integration:4.11.0"
|
||||||
|
implementation 'com.github.bumptech.glide:annotations:4.11.0'
|
||||||
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
||||||
|
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||||
|
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
|
||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
|
implementation 'com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2'
|
||||||
|
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
|
||||||
|
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
||||||
implementation 'com.jsibbold:zoomage:1.3.0'
|
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
|
||||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||||
kapt 'com.github.bumptech.glide:compiler:4.9.0'
|
testImplementation 'junit:junit:4.13'
|
||||||
testImplementation 'junit:junit:4.12'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||||
|
|||||||
BIN
app/libs/pinlockview-release.aar
Normal file
17
app/proguard-rules.pro
vendored
@@ -19,3 +19,20 @@
|
|||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
-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
|
||||||
20
app/release/output-metadata.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"artifactType": {
|
||||||
|
"type": "APK",
|
||||||
|
"kind": "Directory"
|
||||||
|
},
|
||||||
|
"applicationId": "xyz.quaver.pupil",
|
||||||
|
"variantName": "release",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "SINGLE",
|
||||||
|
"filters": [],
|
||||||
|
"properties": [],
|
||||||
|
"versionCode": 57,
|
||||||
|
"versionName": "4.20-hotfix2",
|
||||||
|
"enabled": true,
|
||||||
|
"outputFile": "app-release.apk"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":25,"versionName":"3.1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
|
||||||
@@ -20,20 +20,23 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.util.Log
|
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 androidx.test.rule.ActivityTestRule
|
import androidx.test.rule.ActivityTestRule
|
||||||
import org.junit.Assert.assertEquals
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import xyz.quaver.hitomi.fetchNozomi
|
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
||||||
import xyz.quaver.hiyobi.cookie
|
import xyz.quaver.hiyobi.cookie
|
||||||
|
import xyz.quaver.hiyobi.createImgList
|
||||||
import xyz.quaver.hiyobi.getReader
|
import xyz.quaver.hiyobi.getReader
|
||||||
import xyz.quaver.hiyobi.user_agent
|
import xyz.quaver.hiyobi.user_agent
|
||||||
import xyz.quaver.pupil.ui.LockActivity
|
import xyz.quaver.pupil.ui.LockActivity
|
||||||
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
|
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||||
|
import java.io.InputStreamReader
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
|
||||||
@@ -49,9 +52,6 @@ class ExampleInstrumentedTest {
|
|||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
assertEquals("xyz.quaver.pupil", appContext.packageName)
|
|
||||||
|
|
||||||
Log.d("Pupil", fetchNozomi().first.size.toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -59,9 +59,18 @@ class ExampleInstrumentedTest {
|
|||||||
val activityTestRule = ActivityTestRule(LockActivity::class.java)
|
val activityTestRule = ActivityTestRule(LockActivity::class.java)
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
activityTestRule.launchActivity(Intent())
|
Runtime.getRuntime().exec("du -hs " + getDownloadDirectory(appContext)).let {
|
||||||
|
InputStreamReader(it.inputStream).readLines().forEach { res ->
|
||||||
|
Log.i("PUPILD", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while(true);
|
@Test
|
||||||
|
fun test_nozomi() {
|
||||||
|
val nozomi = getGalleryIDsFromNozomi(null, "index", "all")
|
||||||
|
|
||||||
|
Log.i("PUPILD", nozomi.size.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -70,7 +79,7 @@ class ExampleInstrumentedTest {
|
|||||||
|
|
||||||
val data: ByteArray
|
val data: ByteArray
|
||||||
|
|
||||||
with(URL(reader.readerItems[0].url).openConnection() as HttpsURLConnection) {
|
with(URL(createImgList(1426382, reader)[0].path).openConnection() as HttpsURLConnection) {
|
||||||
setRequestProperty("User-Agent", user_agent)
|
setRequestProperty("User-Agent", user_agent)
|
||||||
setRequestProperty("Cookie", cookie)
|
setRequestProperty("Cookie", cookie)
|
||||||
|
|
||||||
@@ -79,4 +88,37 @@ class ExampleInstrumentedTest {
|
|||||||
|
|
||||||
Log.d("Pupil", data.size.toString())
|
Log.d("Pupil", data.size.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_downloadWorker() {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
|
val galleryID = 515515
|
||||||
|
|
||||||
|
val worker = DownloadWorker.getInstance(context)
|
||||||
|
|
||||||
|
worker.queue.add(galleryID)
|
||||||
|
|
||||||
|
while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) {
|
||||||
|
Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null")
|
||||||
|
|
||||||
|
if (worker.progress[galleryID]?.all { it.isInfinite() } == true)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i("PUPILD", "DONE!!")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getReaderOrNull() {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
|
val galleryID = 1561552
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
Log.i("PUPILD", Cache(context).getReader(galleryID)?.galleryInfo?.title ?: "null")
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
22
app/src/debug/res/values/strings.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?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/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<string name="app_name" translatable="false" tools:override="true">Pupil-Debug</string>
|
||||||
|
</resources>
|
||||||
@@ -1,11 +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"
|
||||||
package="xyz.quaver.pupil">
|
package="xyz.quaver.pupil">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
||||||
android:maxSdkVersion="28"/>
|
|
||||||
<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.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Pupil"
|
android:name=".Pupil"
|
||||||
@@ -15,7 +19,28 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
|
tools:replace="android:theme"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<receiver android:name=".BroadcastReciever" 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.LockActivity" />
|
||||||
<activity
|
<activity
|
||||||
@@ -31,7 +56,7 @@
|
|||||||
<data
|
<data
|
||||||
android:host="hitomi.la"
|
android:host="hitomi.la"
|
||||||
android:pathPrefix="/galleries"
|
android:pathPrefix="/galleries"
|
||||||
android:scheme="https" />
|
android:scheme="http" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -40,31 +65,42 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="히요비.asia"
|
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:pathPrefix="/reader"
|
||||||
android:scheme="https" />
|
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="xn--9w3b15m8vo.asia"
|
|
||||||
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="https" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -75,7 +111,7 @@
|
|||||||
<data
|
<data
|
||||||
android:host="hitomi.la"
|
android:host="hitomi.la"
|
||||||
android:pathPrefix="/galleries"
|
android:pathPrefix="/galleries"
|
||||||
android:scheme="http" />
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -84,9 +120,9 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="히요비.asia"
|
android:host="hitomi.la"
|
||||||
android:pathPrefix="/reader"
|
android:pathPrefix="/manga"
|
||||||
android:scheme="http" />
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -95,9 +131,53 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="xn--9w3b15m8vo.asia"
|
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:pathPrefix="/reader"
|
||||||
android:scheme="http" />
|
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>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -110,6 +190,17 @@
|
|||||||
android:pathPrefix="/g"
|
android:pathPrefix="/g"
|
||||||
android:scheme="http" />
|
android:scheme="http" />
|
||||||
</intent-filter>
|
</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>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.SettingsActivity"
|
android:name=".ui.SettingsActivity"
|
||||||
@@ -125,6 +216,7 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.arlib.floatingsearchview
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
|
||||||
|
class FloatingSearchViewDayNight @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null)
|
||||||
|
: FloatingSearchView(context, attrs) {
|
||||||
|
|
||||||
|
// hack to remove color attributes which should not be reused
|
||||||
|
override fun onSaveInstanceState(): Parcelable? {
|
||||||
|
super.onSaveInstanceState()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
103
app/src/main/java/xyz/quaver/pupil/BroadcastReciever.kt
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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 androidx.preference.PreferenceManager
|
||||||
|
import xyz.quaver.pupil.util.NOTIFICATION_ID_UPDATE
|
||||||
|
import xyz.quaver.pupil.util.cancelImport
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class BroadcastReciever : BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_CANCEL_IMPORT = "ACTION_CANCEL_IMPORT"
|
||||||
|
|
||||||
|
const val EXTRA_IMPORT_NOTIFICATION_ID = "EXTRA_IMPORT_NOTIFICATION_ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
context ?: return
|
||||||
|
|
||||||
|
when (intent?.action) {
|
||||||
|
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
||||||
|
|
||||||
|
// Validate download
|
||||||
|
|
||||||
|
val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val downloadID = preference.getLong("update_download_id", -1)
|
||||||
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
|
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) != downloadID)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Get target uri
|
||||||
|
|
||||||
|
val query = DownloadManager.Query()
|
||||||
|
.setFilterById(downloadID)
|
||||||
|
|
||||||
|
val uri = downloadManager.query(query).use { cursor ->
|
||||||
|
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 -> 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(NOTIFICATION_ID_UPDATE, notification)
|
||||||
|
}
|
||||||
|
ACTION_CANCEL_IMPORT -> {
|
||||||
|
cancelImport = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -30,13 +30,17 @@ import androidx.preference.PreferenceManager
|
|||||||
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 com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import xyz.quaver.proxy
|
||||||
import xyz.quaver.pupil.util.Histories
|
import xyz.quaver.pupil.util.Histories
|
||||||
|
import xyz.quaver.pupil.util.getProxy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class Pupil : MultiDexApplication() {
|
class Pupil : MultiDexApplication() {
|
||||||
|
|
||||||
lateinit var histories: Histories
|
lateinit var histories: Histories
|
||||||
lateinit var downloads: Histories
|
|
||||||
lateinit var favorites: Histories
|
lateinit var favorites: Histories
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -46,10 +50,33 @@ class Pupil : MultiDexApplication() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
|
val userID =
|
||||||
|
if (preference.getString("user_id", "").isNullOrEmpty()) {
|
||||||
|
UUID.randomUUID().toString().also {
|
||||||
|
preference.edit().putString("user_id", it).apply()
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
preference.getString("user_id", "") ?: ""
|
||||||
|
|
||||||
|
FirebaseCrashlytics.getInstance().setUserId(userID)
|
||||||
|
|
||||||
|
proxy = getProxy(this)
|
||||||
|
|
||||||
|
try {
|
||||||
|
preference.getString("dl_location", null).also {
|
||||||
|
if (!File(it!!).canWrite())
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
preference.edit().remove("dl_location").apply()
|
||||||
|
}
|
||||||
|
|
||||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||||
downloads = Histories(File(ContextCompat.getDataDir(this), "downloads.json"))
|
|
||||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG)
|
||||||
|
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProviderInstaller.installIfNeeded(this)
|
ProviderInstaller.installIfNeeded(this)
|
||||||
} catch (e: GooglePlayServicesRepairableException) {
|
} catch (e: GooglePlayServicesRepairableException) {
|
||||||
@@ -58,21 +85,32 @@ class Pupil : MultiDexApplication() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preference.getBoolean("channel_created", false)) {
|
|
||||||
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
|
||||||
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
|
|
||||||
|
manager.createNotificationChannel(NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
|
||||||
description = getString(R.string.channel_download_description)
|
description = getString(R.string.channel_download_description)
|
||||||
enableLights(false)
|
enableLights(false)
|
||||||
enableVibration(false)
|
enableVibration(false)
|
||||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||||
}
|
})
|
||||||
manager.createNotificationChannel(channel)
|
|
||||||
}
|
manager.createNotificationChannel(NotificationChannel("update", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
|
||||||
|
description = getString(R.string.channel_update_description)
|
||||||
preference.edit().putBoolean("channel_created", true).apply()
|
enableLights(true)
|
||||||
|
enableVibration(true)
|
||||||
|
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||||
|
})
|
||||||
|
|
||||||
|
manager.createNotificationChannel(NotificationChannel("import", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
|
||||||
|
description = getString(R.string.channel_update_description)
|
||||||
|
enableLights(false)
|
||||||
|
enableVibration(false)
|
||||||
|
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||||
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
|
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
|
||||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
false -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
|
|||||||
42
app/src/main/java/xyz/quaver/pupil/PupilGlideModule.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.Registry
|
||||||
|
import com.bumptech.glide.annotation.GlideModule
|
||||||
|
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||||
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
import com.bumptech.glide.module.AppGlideModule
|
||||||
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@GlideModule
|
||||||
|
class PupilGlideModule : AppGlideModule() {
|
||||||
|
|
||||||
|
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||||
|
registry.append(
|
||||||
|
GlideUrl::class.java,
|
||||||
|
InputStream::class.java,
|
||||||
|
OkHttpUrlLoader.Factory(DownloadWorker.getInstance(context).client)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -18,7 +18,9 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
package xyz.quaver.pupil.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.Base64
|
||||||
import android.util.SparseBooleanArray
|
import android.util.SparseBooleanArray
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -26,35 +28,36 @@ import android.view.ViewGroup
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.daimajia.swipe.SwipeLayout
|
||||||
|
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
||||||
|
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import kotlinx.android.synthetic.main.item_galleryblock.view.*
|
import kotlinx.android.synthetic.main.item_galleryblock.view.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.getReader
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
import xyz.quaver.pupil.util.Histories
|
import xyz.quaver.pupil.util.Histories
|
||||||
import xyz.quaver.pupil.util.getCachedGallery
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
import java.io.File
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.HashMap
|
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
||||||
|
|
||||||
enum class ViewType {
|
enum class ViewType {
|
||||||
NEXT,
|
NEXT,
|
||||||
@@ -64,83 +67,34 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
|
|
||||||
private lateinit var favorites: Histories
|
private lateinit var favorites: Histories
|
||||||
|
|
||||||
inner class GalleryViewHolder(val view: CardView) : RecyclerView.ViewHolder(view) {
|
val timer = Timer()
|
||||||
fun bind(item: Pair<GalleryBlock, Deferred<String>>) {
|
|
||||||
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 (galleryBlock: GalleryBlock, thumbnail: Deferred<String>) = item
|
var isThin = false
|
||||||
|
|
||||||
val artists = galleryBlock.artists
|
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||||
val series = galleryBlock.series
|
var timerTask: TimerTask? = null
|
||||||
|
|
||||||
|
private fun updateProgress(context: Context, galleryID: Int) {
|
||||||
|
val reader = Cache(context).getReaderOrNull(galleryID)
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
val cache = thumbnail.await()
|
if (reader == null || PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false)) {
|
||||||
|
view.galleryblock_progressbar.visibility = View.GONE
|
||||||
glide
|
view.galleryblock_progress_complete.visibility = View.GONE
|
||||||
.load(cache)
|
return@launch
|
||||||
.skipMemoryCache(true)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.error(R.drawable.image_broken_variant)
|
|
||||||
.apply {
|
|
||||||
if (BuildConfig.CENSOR)
|
|
||||||
override(5, 8)
|
|
||||||
}
|
|
||||||
.into(galleryblock_thumbnail)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check cache
|
|
||||||
val readerCache = { File(getCachedGallery(context, galleryBlock.id), "reader.json") }
|
|
||||||
val imageCache = { File(getCachedGallery(context, galleryBlock.id), "images") }
|
|
||||||
|
|
||||||
try {
|
|
||||||
Json(JsonConfiguration.Stable)
|
|
||||||
.parse(Reader.serializer(), readerCache.invoke().readText())
|
|
||||||
} catch(e: Exception) {
|
|
||||||
readerCache.invoke().delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (readerCache.invoke().exists()) {
|
|
||||||
val reader = Json(JsonConfiguration.Stable)
|
|
||||||
.parse(Reader.serializer(), readerCache.invoke().readText())
|
|
||||||
|
|
||||||
with(galleryblock_progressbar) {
|
|
||||||
max = reader.readerItems.size
|
|
||||||
progress = imageCache.invoke().list()?.size ?: 0
|
|
||||||
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
galleryblock_progressbar.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refreshTasks[this@GalleryViewHolder] == null) {
|
|
||||||
val refresh = Timer(false).schedule(0, 1000) {
|
|
||||||
post {
|
|
||||||
with(view.galleryblock_progressbar) {
|
with(view.galleryblock_progressbar) {
|
||||||
progress = imageCache.invoke().list()?.size ?: 0
|
|
||||||
|
|
||||||
if (!readerCache.invoke().exists()) {
|
progress = Cache(context).getImages(galleryID)?.size ?: 0
|
||||||
visibility = View.GONE
|
|
||||||
max = 0
|
|
||||||
progress = 0
|
|
||||||
|
|
||||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
|
||||||
} else {
|
|
||||||
if (visibility == View.GONE) {
|
if (visibility == View.GONE) {
|
||||||
val reader = Json(JsonConfiguration.Stable)
|
|
||||||
.parse(Reader.serializer(), readerCache.invoke().readText())
|
|
||||||
max = reader.readerItems.size
|
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
|
max = reader.galleryInfo.files.size
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progress == max) {
|
if (progress == max) {
|
||||||
if (completeFlag.get(galleryBlock.id, false)) {
|
if (completeFlag.get(galleryID, false)) {
|
||||||
with(view.galleryblock_progress_complete) {
|
with(view.galleryblock_progress_complete) {
|
||||||
setImageResource(R.drawable.ic_progressbar)
|
setImageResource(R.drawable.ic_progressbar)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@@ -152,16 +106,77 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
})
|
})
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
completeFlag.put(galleryBlock.id, true)
|
completeFlag.put(galleryID, true)
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun bind(galleryBlock: GalleryBlock) {
|
||||||
|
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
|
||||||
|
|
||||||
|
if (isThin)
|
||||||
|
galleryblock_thumbnail.layoutParams.width = context.resources.getDimensionPixelSize(
|
||||||
|
R.dimen.galleryblock_thumbnail_thin
|
||||||
|
)
|
||||||
|
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
|
||||||
|
it.start()
|
||||||
|
})
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
val thumbnail = Cache(context).getThumbnail(galleryBlock.id).let {
|
||||||
|
if (it != null)
|
||||||
|
Base64.decode(it, Base64.DEFAULT)
|
||||||
|
else
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshTasks[this@GalleryViewHolder] = refresh
|
galleryblock_thumbnail.post {
|
||||||
|
glide
|
||||||
|
.load(thumbnail)
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.error(R.drawable.image_broken_variant)
|
||||||
|
.apply {
|
||||||
|
if (BuildConfig.CENSOR)
|
||||||
|
override(5, 8)
|
||||||
|
}
|
||||||
|
.into(galleryblock_thumbnail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check cache
|
||||||
|
val cache = Cache(context).getCachedGallery(galleryBlock.id)
|
||||||
|
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
|
||||||
|
|
||||||
|
if (reader != null) {
|
||||||
|
val count = cache.listFiles()?.count {
|
||||||
|
Regex("^[0-9]+.+\$").matches(it.name)
|
||||||
|
} ?: 0
|
||||||
|
|
||||||
|
with(galleryblock_progressbar) {
|
||||||
|
max = reader.galleryInfo.files.size
|
||||||
|
progress = count
|
||||||
|
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
galleryblock_progressbar.visibility = View.GONE
|
||||||
|
|
||||||
|
if (timerTask == null)
|
||||||
|
timerTask = timer.schedule(0, 1000) {
|
||||||
|
updateProgress(context, galleryBlock.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
galleryblock_title.text = galleryBlock.title
|
galleryblock_title.text = galleryBlock.title
|
||||||
@@ -206,16 +221,17 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
"male" -> {
|
"male" -> {
|
||||||
setChipBackgroundColorResource(R.color.material_blue_700)
|
setChipBackgroundColorResource(R.color.material_blue_700)
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white)
|
ContextCompat.getDrawable(context, R.drawable.gender_male)
|
||||||
}
|
}
|
||||||
"female" -> {
|
"female" -> {
|
||||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
setChipBackgroundColorResource(R.color.material_pink_600)
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white)
|
ContextCompat.getDrawable(context, R.drawable.gender_female)
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
text = tag.tag.wordCapitalize()
|
text = tag.tag.wordCapitalize()
|
||||||
|
setEnsureMinTouchTargetSize(false)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
for (callback in onChipClickedHandler)
|
for (callback in onChipClickedHandler)
|
||||||
callback.invoke(tag)
|
callback.invoke(tag)
|
||||||
@@ -224,6 +240,15 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
}
|
}
|
||||||
|
|
||||||
galleryblock_id.text = galleryBlock.id.toString()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!::favorites.isInitialized)
|
if (!::favorites.isInitialized)
|
||||||
favorites = (context.applicationContext as Pupil).favorites
|
favorites = (context.applicationContext as Pupil).favorites
|
||||||
@@ -254,6 +279,14 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Make some views invisible to make it thinner
|
||||||
|
if (isThin) {
|
||||||
|
galleryblock_language.visibility = View.GONE
|
||||||
|
galleryblock_type.visibility = View.GONE
|
||||||
|
galleryblock_tag_group.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,10 +305,11 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val refreshTasks = HashMap<GalleryViewHolder, TimerTask>()
|
|
||||||
val completeFlag = SparseBooleanArray()
|
val completeFlag = SparseBooleanArray()
|
||||||
|
|
||||||
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
||||||
|
var onDownloadClickedHandler: ((Int) -> Unit)? = null
|
||||||
|
var onDeleteClickedHandler: ((Int) -> Unit)? = null
|
||||||
|
|
||||||
var showNext = false
|
var showNext = false
|
||||||
var showPrev = false
|
var showPrev = false
|
||||||
@@ -301,18 +335,56 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
if (holder is GalleryViewHolder)
|
if (holder is GalleryViewHolder) {
|
||||||
holder.bind(galleries[position-(if (showPrev) 1 else 0)])
|
val gallery = galleries[position-(if (showPrev) 1 else 0)]
|
||||||
|
|
||||||
|
holder.bind(gallery)
|
||||||
|
|
||||||
|
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 (Cache(holder.view.context).isDownloading(gallery.id))
|
||||||
|
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 onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
||||||
super.onViewDetachedFromWindow(holder)
|
super.onViewDetachedFromWindow(holder)
|
||||||
|
|
||||||
if (holder is GalleryViewHolder) {
|
if (holder is GalleryViewHolder) {
|
||||||
val task = refreshTasks[holder] ?: return
|
holder.timerTask?.cancel()
|
||||||
|
holder.timerTask = null
|
||||||
task.cancel()
|
|
||||||
refreshTasks.remove(holder)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,4 +400,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
else -> ViewType.GALLERY
|
else -> ViewType.GALLERY
|
||||||
}.ordinal
|
}.ordinal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getSwipeLayoutResourceId(position: Int) = R.id.galleryblock_swipe_layout
|
||||||
}
|
}
|
||||||
87
app/src/main/java/xyz/quaver/pupil/adapters/MirrorAdapter.kt
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* 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.preference.PreferenceManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.android.synthetic.main.item_mirrors.view.*
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
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 {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getString("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
|
||||||
|
|
||||||
|
}
|
||||||
@@ -18,32 +18,50 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
package xyz.quaver.pupil.adapters
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.app.Activity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
import com.bumptech.glide.load.DataSource
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.bumptech.glide.load.model.LazyHeaders
|
||||||
import com.bumptech.glide.request.target.Target
|
import kotlinx.android.synthetic.main.item_reader.view.*
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import xyz.quaver.Code
|
||||||
|
import xyz.quaver.hitomi.Reader
|
||||||
|
import xyz.quaver.hitomi.getReferer
|
||||||
|
import xyz.quaver.hitomi.imageUrlFromImage
|
||||||
|
import xyz.quaver.hiyobi.cookie
|
||||||
|
import xyz.quaver.hiyobi.createImgList
|
||||||
|
import xyz.quaver.hiyobi.user_agent
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.util.getCachedGallery
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
import java.io.File
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class ReaderAdapter(private val glide: RequestManager,
|
class ReaderAdapter(private val glide: RequestManager,
|
||||||
private val galleryID: Int,
|
private val galleryID: Int,
|
||||||
private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
private val activity: Activity) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
var reader: Reader? = null
|
||||||
|
val timer = Timer()
|
||||||
|
|
||||||
var isFullScreen = false
|
var isFullScreen = false
|
||||||
private var prev : Drawable? = null
|
|
||||||
|
var onItemClickListener : ((Int) -> (Unit))? = null
|
||||||
|
|
||||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
|
var downloadWorker: DownloadWorker? = null
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
return LayoutInflater.from(parent.context).inflate(
|
return LayoutInflater.from(parent.context).inflate(
|
||||||
R.layout.item_reader, parent, false
|
R.layout.item_reader, parent, false
|
||||||
@@ -53,43 +71,100 @@ class ReaderAdapter(private val glide: RequestManager,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
holder.view as ImageView
|
holder.view as ConstraintLayout
|
||||||
|
|
||||||
|
if (downloadWorker == null)
|
||||||
|
downloadWorker = DownloadWorker.getInstance(holder.view.context)
|
||||||
|
|
||||||
|
if (isFullScreen) {
|
||||||
|
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
||||||
|
holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
||||||
|
} else {
|
||||||
|
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
|
||||||
|
holder.view.container.layoutParams.height = 0
|
||||||
|
|
||||||
|
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
||||||
|
.dimensionRatio = "W,${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
||||||
|
onItemClickListener?.invoke(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.view.container.setOnClickListener {
|
||||||
|
onItemClickListener?.invoke(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.view.reader_index.text = (position+1).toString()
|
||||||
|
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(holder.view.context)
|
||||||
|
if (preferences.getBoolean("cache_disable", false)) {
|
||||||
|
val lowQuality = preferences.getBoolean("low_quality", false)
|
||||||
|
|
||||||
|
val url = when (reader!!.code) {
|
||||||
|
Code.HITOMI ->
|
||||||
|
GlideUrl(
|
||||||
|
imageUrlFromImage(
|
||||||
|
galleryID,
|
||||||
|
reader!!.galleryInfo.files[position],
|
||||||
|
!lowQuality
|
||||||
|
)
|
||||||
|
, LazyHeaders.Builder().addHeader("Referer", getReferer(galleryID)).build())
|
||||||
|
Code.HIYOBI ->
|
||||||
|
GlideUrl(createImgList(galleryID, reader!!, lowQuality)[position].path, LazyHeaders.Builder()
|
||||||
|
.addHeader("User-Agent", user_agent)
|
||||||
|
.addHeader("Cookie", cookie)
|
||||||
|
.build())
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
holder.view.image.post {
|
||||||
glide
|
glide
|
||||||
.load(File(getCachedGallery(holder.view.context, galleryID), images[position]))
|
.load(url!!)
|
||||||
.dontTransform()
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(false)
|
||||||
|
.fitCenter()
|
||||||
|
.error(R.drawable.image_broken_variant)
|
||||||
|
.into(holder.view.image)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val image = Cache(holder.view.context).getImage(galleryID, position)
|
||||||
|
val progress = downloadWorker!!.progress[galleryID]?.get(position)
|
||||||
|
|
||||||
|
if (progress?.isInfinite() == true && image != null) {
|
||||||
|
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
|
||||||
|
|
||||||
|
holder.view.image.post {
|
||||||
|
glide
|
||||||
|
.load(image)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
|
.fitCenter()
|
||||||
.error(R.drawable.image_broken_variant)
|
.error(R.drawable.image_broken_variant)
|
||||||
.apply {
|
.into(holder.view.image)
|
||||||
if (BuildConfig.CENSOR)
|
|
||||||
override(5, 8)
|
|
||||||
if (isFullScreen)
|
|
||||||
placeholder(prev)
|
|
||||||
}
|
|
||||||
.listener(object: RequestListener<Drawable> {
|
|
||||||
override fun onLoadFailed(
|
|
||||||
e: GlideException?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<Drawable>?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
) = false
|
|
||||||
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: Drawable?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<Drawable>?,
|
|
||||||
dataSource: DataSource?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
prev = resource?.constantState?.newDrawable()?.mutate()
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into(holder.view)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = images.size
|
} else {
|
||||||
|
holder.view.reader_item_progressbar.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
glide.clear(holder.view.image)
|
||||||
|
|
||||||
|
holder.view.reader_item_progressbar.progress =
|
||||||
|
if (progress?.isInfinite() == true)
|
||||||
|
100
|
||||||
|
else
|
||||||
|
progress?.roundToInt() ?: 0
|
||||||
|
|
||||||
|
holder.view.image.setImageDrawable(null)
|
||||||
|
|
||||||
|
timer.schedule(1000) {
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -22,9 +22,10 @@ import android.view.ViewGroup
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
|
|
||||||
class ThumbnailAdapter(private val glide: RequestManager, private val thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() {
|
class ThumbnailAdapter(private val glide: RequestManager, var thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() {
|
||||||
|
|
||||||
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
|
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ class ThumbnailAdapter(private val glide: RequestManager, private val thumbnails
|
|||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
glide
|
glide
|
||||||
.load(thumbnails[position])
|
.load(thumbnails[position])
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.apply {
|
.apply {
|
||||||
if (BuildConfig.CENSOR)
|
if (BuildConfig.CENSOR)
|
||||||
override(5, 8)
|
override(5, 8)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.bumptech.glide.RequestManager
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class ThumbnailPageAdapter(private val glide: RequestManager, 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 {
|
||||||
|
layoutManager = GridLayoutManager(parent.context, 3)
|
||||||
|
adapter = ThumbnailAdapter(glide, listOf())
|
||||||
|
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?.scrollToPosition(itemCount-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = if (thumbnails.isEmpty()) 0 else thumbnails.size/9 + if (thumbnails.size%9 != 0) 1 else 0
|
||||||
|
|
||||||
|
}
|
||||||
@@ -21,61 +21,30 @@ package xyz.quaver.pupil.ui
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import com.andrognito.patternlockview.PatternLockView
|
import com.andrognito.patternlockview.PatternLockView
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.activity_lock.*
|
import kotlinx.android.synthetic.main.activity_lock.*
|
||||||
import kotlinx.android.synthetic.main.fragment_pattern_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.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.Lock
|
||||||
import xyz.quaver.pupil.util.LockManager
|
import xyz.quaver.pupil.util.LockManager
|
||||||
|
|
||||||
class LockActivity : AppCompatActivity() {
|
class LockActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
private lateinit var lockManager: LockManager
|
||||||
super.onCreate(savedInstanceState)
|
private var mode: String? = null
|
||||||
setContentView(R.layout.activity_lock)
|
|
||||||
|
|
||||||
val lockManager = try {
|
private val patternLockFragment = PatternLockFragment().apply {
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
val mode = intent.getStringExtra("mode")
|
|
||||||
|
|
||||||
lock_pattern.isEnabled = false
|
|
||||||
lock_pin.isEnabled = false
|
|
||||||
lock_fingerprint.isEnabled = false
|
|
||||||
lock_password.isEnabled = false
|
|
||||||
|
|
||||||
when(mode) {
|
|
||||||
null -> {
|
|
||||||
if (lockManager.isEmpty()) {
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"add_lock" -> {
|
|
||||||
when(intent.getStringExtra("type")!!) {
|
|
||||||
"pattern" -> {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
supportFragmentManager.beginTransaction().add(
|
|
||||||
R.id.lock_content,
|
|
||||||
PatternLockFragment().apply {
|
|
||||||
var lastPass = ""
|
var lastPass = ""
|
||||||
onPatternDrawn = {
|
onPatternDrawn = {
|
||||||
when(mode) {
|
when(mode) {
|
||||||
@@ -108,7 +77,191 @@ class LockActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val pinLockFragment = PINLockFragment().apply {
|
||||||
|
var lastPass = ""
|
||||||
|
onPINEntered = {
|
||||||
|
when(mode) {
|
||||||
|
null -> {
|
||||||
|
val result = lockManager.check(it)
|
||||||
|
|
||||||
|
if (result == true) {
|
||||||
|
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)
|
||||||
|
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")
|
||||||
|
|
||||||
|
when(mode) {
|
||||||
|
null -> {
|
||||||
|
if (lockManager.isEmpty()) {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lock_fingerprint", false)
|
||||||
|
&& 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()
|
).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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,66 +18,56 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
|
||||||
import android.text.*
|
import android.text.*
|
||||||
import android.text.style.AlignmentSpan
|
import android.text.style.AlignmentSpan
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.EditText
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.ImageView
|
import android.widget.*
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||||
import com.arlib.floatingsearchview.FloatingSearchView
|
import com.arlib.floatingsearchview.FloatingSearchView
|
||||||
|
import com.arlib.floatingsearchview.FloatingSearchViewDayNight
|
||||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import com.arlib.floatingsearchview.util.view.SearchInputView
|
import com.arlib.floatingsearchview.util.view.SearchInputView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import kotlinx.android.synthetic.main.activity_main_content.*
|
import kotlinx.android.synthetic.main.activity_main_content.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
import kotlinx.serialization.builtins.list
|
||||||
import kotlinx.serialization.json.Json
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
import xyz.quaver.hitomi.doSearch
|
||||||
import kotlinx.serialization.json.JsonObject
|
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
||||||
import kotlinx.serialization.json.content
|
import xyz.quaver.hitomi.getSuggestionsForQuery
|
||||||
import kotlinx.serialization.list
|
|
||||||
import kotlinx.serialization.stringify
|
|
||||||
import ru.noties.markwon.Markwon
|
|
||||||
import xyz.quaver.hitomi.*
|
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
import xyz.quaver.pupil.types.TagSuggestion
|
import xyz.quaver.pupil.types.TagSuggestion
|
||||||
import xyz.quaver.pupil.types.Tags
|
import xyz.quaver.pupil.types.Tags
|
||||||
|
import xyz.quaver.pupil.ui.dialog.GalleryDialog
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
@@ -98,7 +88,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
POPULAR
|
POPULAR
|
||||||
}
|
}
|
||||||
|
|
||||||
private val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>()
|
private val galleries = ArrayList<GalleryBlock>()
|
||||||
|
|
||||||
private var query = ""
|
private var query = ""
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -108,6 +98,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setText(query, TextView.BufferType.EDITABLE)
|
setText(query, TextView.BufferType.EDITABLE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private var queryStack = mutableListOf<String>()
|
||||||
|
|
||||||
private var mode = Mode.SEARCH
|
private var mode = Mode.SEARCH
|
||||||
private var sortMode = SortMode.NEWEST
|
private var sortMode = SortMode.NEWEST
|
||||||
@@ -121,7 +112,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private var currentPage = 0
|
private var currentPage = 0
|
||||||
|
|
||||||
private lateinit var histories: Histories
|
private lateinit var histories: Histories
|
||||||
private lateinit var downloads: Histories
|
|
||||||
private lateinit var favorites: Histories
|
private lateinit var favorites: Histories
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -137,14 +127,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lockManager.isNotEmpty())
|
if (lockManager.isNotEmpty())
|
||||||
startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK)
|
startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK)
|
||||||
|
|
||||||
checkPermissions()
|
|
||||||
|
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
if (Locale.getDefault().language == "ko") {
|
if (Locale.getDefault().language == "ko") {
|
||||||
@@ -161,22 +150,22 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
with(application as Pupil) {
|
with(application as Pupil) {
|
||||||
this@MainActivity.histories = histories
|
this@MainActivity.histories = histories
|
||||||
this@MainActivity.downloads = downloads
|
|
||||||
this@MainActivity.favorites = favorites
|
this@MainActivity.favorites = favorites
|
||||||
}
|
}
|
||||||
|
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
checkUpdate()
|
checkUpdate(this)
|
||||||
|
|
||||||
initView()
|
initView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
when {
|
when {
|
||||||
main_drawer_layout.isDrawerOpen(GravityCompat.START) -> main_drawer_layout.closeDrawer(GravityCompat.START)
|
main_drawer_layout.isDrawerOpen(GravityCompat.START) -> main_drawer_layout.closeDrawer(GravityCompat.START)
|
||||||
query.isNotEmpty() -> runOnUiThread {
|
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
|
||||||
query = ""
|
query = queryStack.last()
|
||||||
|
|
||||||
cancelFetch()
|
cancelFetch()
|
||||||
clearGalleries()
|
clearGalleries()
|
||||||
@@ -187,6 +176,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
|
||||||
|
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
@@ -202,7 +197,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
val perPage = preference.getString("per_page", "25")!!.toInt()
|
val perPage = preference.getString("per_page", "25")!!.toIntOrNull() ?: 25
|
||||||
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
|
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
|
||||||
|
|
||||||
return when(keyCode) {
|
return when(keyCode) {
|
||||||
@@ -256,125 +251,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkUpdate() {
|
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
|
|
||||||
|
|
||||||
if (ignoreUpdateUntil > System.currentTimeMillis())
|
|
||||||
return
|
|
||||||
|
|
||||||
fun extractReleaseNote(update: JsonObject, locale: String) : String {
|
|
||||||
val markdown = update["body"]!!.content
|
|
||||||
|
|
||||||
val target = when(locale) {
|
|
||||||
"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 getString(R.string.update_release_note, update["tag_name"]?.content, result.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
val update =
|
|
||||||
checkUpdate(getString(R.string.release_url)) ?: return@launch
|
|
||||||
|
|
||||||
val (url, fileName) = getApkUrl(update) ?: return@launch
|
|
||||||
fileName ?: return@launch
|
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(this@MainActivity).apply {
|
|
||||||
setTitle(R.string.update_title)
|
|
||||||
val msg = extractReleaseNote(update, Locale.getDefault().language)
|
|
||||||
setMessage(Markwon.create(context).toMarkdown(msg))
|
|
||||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
|
||||||
if (!this@MainActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
|
||||||
AlertDialog.Builder(this@MainActivity).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.update_no_permission)
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
return@setPositiveButton
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = DownloadManager.Request(Uri.parse(url)).apply {
|
|
||||||
setDescription(getString(R.string.update_notification_description))
|
|
||||||
setTitle(getString(R.string.app_name))
|
|
||||||
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
val manager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
||||||
val id = manager.enqueue(request)
|
|
||||||
|
|
||||||
registerReceiver(object: BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
try {
|
|
||||||
val install = Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
setDataAndType(manager.getUriForDownloadedFile(id), manager.getMimeTypeForDownloadedFile(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
startActivity(install)
|
|
||||||
unregisterReceiver(this)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
AlertDialog.Builder(this@MainActivity).apply {
|
|
||||||
setTitle(R.string.update_failed)
|
|
||||||
setMessage(R.string.update_failed_message)
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
|
|
||||||
}
|
|
||||||
setNegativeButton(R.string.ignore_update) { _, _ ->
|
|
||||||
preferences.edit()
|
|
||||||
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
dialog.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkPermissions() {
|
|
||||||
if (this.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 13489)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
var prevP1 = 0
|
var prevP1 = 0
|
||||||
main_appbar_layout.addOnOffsetChangedListener(
|
main_appbar_layout.addOnOffsetChangedListener(
|
||||||
@@ -404,6 +280,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
clearGalleries()
|
clearGalleries()
|
||||||
currentPage = 0
|
currentPage = 0
|
||||||
query = ""
|
query = ""
|
||||||
|
queryStack.clear()
|
||||||
mode = Mode.SEARCH
|
mode = Mode.SEARCH
|
||||||
fetchGalleries(query, sortMode)
|
fetchGalleries(query, sortMode)
|
||||||
loadBlocks()
|
loadBlocks()
|
||||||
@@ -413,6 +290,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
clearGalleries()
|
clearGalleries()
|
||||||
currentPage = 0
|
currentPage = 0
|
||||||
query = ""
|
query = ""
|
||||||
|
queryStack.clear()
|
||||||
mode = Mode.HISTORY
|
mode = Mode.HISTORY
|
||||||
fetchGalleries(query, sortMode)
|
fetchGalleries(query, sortMode)
|
||||||
loadBlocks()
|
loadBlocks()
|
||||||
@@ -422,6 +300,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
clearGalleries()
|
clearGalleries()
|
||||||
currentPage = 0
|
currentPage = 0
|
||||||
query = ""
|
query = ""
|
||||||
|
queryStack.clear()
|
||||||
mode = Mode.DOWNLOAD
|
mode = Mode.DOWNLOAD
|
||||||
fetchGalleries(query, sortMode)
|
fetchGalleries(query, sortMode)
|
||||||
loadBlocks()
|
loadBlocks()
|
||||||
@@ -431,6 +310,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
clearGalleries()
|
clearGalleries()
|
||||||
currentPage = 0
|
currentPage = 0
|
||||||
query = ""
|
query = ""
|
||||||
|
queryStack.clear()
|
||||||
mode = Mode.FAVORITE
|
mode = Mode.FAVORITE
|
||||||
fetchGalleries(query, sortMode)
|
fetchGalleries(query, sortMode)
|
||||||
loadBlocks()
|
loadBlocks()
|
||||||
@@ -448,7 +328,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
|
||||||
}
|
}
|
||||||
R.id.main_drawer_kakaotalk -> {
|
R.id.main_drawer_kakaotalk -> {
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.kakaotalk))))
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -456,11 +336,18 @@ class MainActivity : AppCompatActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
with(main_fab_cancel) {
|
||||||
|
setImageResource(R.drawable.cancel)
|
||||||
|
setOnClickListener {
|
||||||
|
DownloadWorker.getInstance(context).stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
with(main_fab_jump) {
|
with(main_fab_jump) {
|
||||||
setImageResource(R.drawable.ic_jump)
|
setImageResource(R.drawable.ic_jump)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
val perPage = preference.getString("per_page", "25")!!.toInt()
|
val perPage = preference.getString("per_page", "25")!!.toIntOrNull() ?: 25
|
||||||
val editText = EditText(context)
|
val editText = EditText(context)
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
@@ -485,31 +372,60 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
with(main_fab_random) {
|
||||||
|
setImageResource(R.drawable.shuffle_variant)
|
||||||
|
setOnClickListener {
|
||||||
|
runBlocking {
|
||||||
|
withTimeoutOrNull(100) {
|
||||||
|
galleryIDs?.await()
|
||||||
|
}
|
||||||
|
}.let {
|
||||||
|
if (it?.isEmpty() == false) {
|
||||||
|
val galleryID = it.random()
|
||||||
|
|
||||||
|
GalleryDialog(
|
||||||
|
this@MainActivity,
|
||||||
|
Glide.with(this@MainActivity),
|
||||||
|
galleryID
|
||||||
|
).apply {
|
||||||
|
onChipClickedHandler.add {
|
||||||
|
runOnUiThread {
|
||||||
|
query = it.toQuery()
|
||||||
|
currentPage = 0
|
||||||
|
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
with(main_fab_id) {
|
with(main_fab_id) {
|
||||||
setImageResource(R.drawable.numeric)
|
setImageResource(R.drawable.numeric)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
val editText = EditText(context)
|
val editText = EditText(context).apply {
|
||||||
|
inputType = InputType.TYPE_CLASS_NUMBER
|
||||||
|
}
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
setView(editText)
|
setView(editText)
|
||||||
setTitle(R.string.main_open_gallery_by_id)
|
setTitle(R.string.main_open_gallery_by_id)
|
||||||
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton
|
||||||
try {
|
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
|
||||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
putExtra("galleryID", galleryID)
|
||||||
val gallery =
|
}
|
||||||
getGalleryBlock(editText.text.toString().toInt()) ?: throw Exception()
|
|
||||||
intent.putExtra("galleryID", gallery.id)
|
|
||||||
|
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|
||||||
histories.add(gallery.id)
|
histories.add(galleryID)
|
||||||
} catch (e: Exception) {
|
|
||||||
Snackbar.make(main_layout,
|
|
||||||
R.string.main_open_gallery_by_id_error, Snackbar.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
@@ -521,6 +437,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
loadBlocks()
|
loadBlocks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
with(main_recyclerview) {
|
with(main_recyclerview) {
|
||||||
adapter = GalleryBlockAdapter(Glide.with(this@MainActivity), galleries).apply {
|
adapter = GalleryBlockAdapter(Glide.with(this@MainActivity), galleries).apply {
|
||||||
@@ -535,6 +452,49 @@ class MainActivity : AppCompatActivity() {
|
|||||||
loadBlocks()
|
loadBlocks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onDownloadClickedHandler = { position ->
|
||||||
|
val galleryID = galleries[position].id
|
||||||
|
val worker = DownloadWorker.getInstance(context)
|
||||||
|
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
|
||||||
|
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
|
||||||
|
else {
|
||||||
|
if (worker.progress.indexOfKey(galleryID) >= 0 && Cache(context).isDownloading(galleryID)) { //download in progress
|
||||||
|
Cache(context).setDownloading(galleryID, false)
|
||||||
|
worker.cancel(galleryID)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Cache(context).setDownloading(galleryID, true)
|
||||||
|
|
||||||
|
worker.queue.add(galleryID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAllItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteClickedHandler = { position ->
|
||||||
|
val galleryID = galleries[position].id
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
DownloadWorker.getInstance(context).cancel(galleryID)
|
||||||
|
|
||||||
|
Cache(context).getCachedGallery(galleryID).deleteRecursively()
|
||||||
|
|
||||||
|
histories.remove(galleryID)
|
||||||
|
|
||||||
|
if (this@MainActivity.mode != Mode.SEARCH)
|
||||||
|
runOnUiThread {
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
|
||||||
|
completeFlag.put(galleryID, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAllItems()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ItemClickSupport.addTo(this)
|
ItemClickSupport.addTo(this)
|
||||||
.setOnItemClickListener { _, position, v ->
|
.setOnItemClickListener { _, position, v ->
|
||||||
@@ -542,7 +502,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return@setOnItemClickListener
|
return@setOnItemClickListener
|
||||||
|
|
||||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
||||||
val gallery = galleries[position].first
|
val gallery = galleries[position]
|
||||||
intent.putExtra("galleryID", gallery.id)
|
intent.putExtra("galleryID", gallery.id)
|
||||||
|
|
||||||
//TODO: Maybe sprinkling some transitions will be nice :D
|
//TODO: Maybe sprinkling some transitions will be nice :D
|
||||||
@@ -554,9 +514,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
if (v !is CardView)
|
if (v !is CardView)
|
||||||
return@setOnItemLongClickListener true
|
return@setOnItemLongClickListener true
|
||||||
|
|
||||||
val galleryID = galleries[position].first.id
|
val galleryID = galleries[position].id
|
||||||
|
|
||||||
GalleryDialog(this@MainActivity, galleryID).apply {
|
GalleryDialog(
|
||||||
|
this@MainActivity,
|
||||||
|
Glide.with(this@MainActivity),
|
||||||
|
galleryID
|
||||||
|
).apply {
|
||||||
onChipClickedHandler.add {
|
onChipClickedHandler.add {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
query = it.toQuery()
|
query = it.toQuery()
|
||||||
@@ -779,7 +743,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var suggestionJob : Job? = null
|
private var suggestionJob : Job? = null
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
private fun setupSearchBar() {
|
private fun setupSearchBar() {
|
||||||
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
||||||
//Change upper case letters to lower case
|
//Change upper case letters to lower case
|
||||||
@@ -799,20 +762,39 @@ class MainActivity : AppCompatActivity() {
|
|||||||
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
|
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
|
||||||
|
|
||||||
with(main_searchview as FloatingSearchView) {
|
with(main_searchview as FloatingSearchViewDayNight) {
|
||||||
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
|
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
|
||||||
val json = Json(JsonConfiguration.Stable)
|
|
||||||
val serializer = Tag.serializer().list
|
val serializer = Tag.serializer().list
|
||||||
|
|
||||||
if (!favoritesFile.exists()) {
|
if (!favoritesFile.exists()) {
|
||||||
favoritesFile.createNewFile()
|
favoritesFile.createNewFile()
|
||||||
favoritesFile.writeText(json.stringify(Tags(listOf())))
|
favoritesFile.writeText(json.stringify(serializer, Tags(listOf())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
|
||||||
|
override fun onMenuOpened() {
|
||||||
|
(this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuClosed() {
|
||||||
|
//Do Nothing
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
when(it.itemId) {
|
when(it.itemId) {
|
||||||
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
|
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
|
||||||
|
R.id.main_menu_thin -> {
|
||||||
|
main_recyclerview.apply {
|
||||||
|
(adapter as GalleryBlockAdapter).apply {
|
||||||
|
isThin = !isThin
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = adapter // Force to redraw
|
||||||
|
}
|
||||||
|
}
|
||||||
R.id.main_menu_sort_newest -> {
|
R.id.main_menu_sort_newest -> {
|
||||||
sortMode = SortMode.NEWEST
|
sortMode = SortMode.NEWEST
|
||||||
it.isChecked = true
|
it.isChecked = true
|
||||||
@@ -881,20 +863,23 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
|
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
|
||||||
|
|
||||||
|
val color = TypedValue()
|
||||||
|
theme.resolveAttribute(R.attr.colorControlNormal, color, true)
|
||||||
|
|
||||||
leftIcon.setImageDrawable(
|
leftIcon.setImageDrawable(
|
||||||
ResourcesCompat.getDrawable(
|
ResourcesCompat.getDrawable(
|
||||||
resources,
|
resources,
|
||||||
when(item.n) {
|
when(item.n) {
|
||||||
"female" -> R.drawable.ic_gender_female
|
"female" -> R.drawable.gender_female
|
||||||
"male" -> R.drawable.ic_gender_male
|
"male" -> R.drawable.gender_male
|
||||||
"language" -> R.drawable.ic_translate
|
"language" -> R.drawable.translate
|
||||||
"group" -> R.drawable.ic_account_group
|
"group" -> R.drawable.account_group
|
||||||
"character" -> R.drawable.ic_account_star
|
"character" -> R.drawable.account_star
|
||||||
"series" -> R.drawable.ic_book_open
|
"series" -> R.drawable.book_open
|
||||||
"artist" -> R.drawable.ic_brush
|
"artist" -> R.drawable.brush
|
||||||
else -> R.drawable.ic_tag
|
else -> R.drawable.tag
|
||||||
},
|
},
|
||||||
null)
|
context.theme)
|
||||||
)
|
)
|
||||||
|
|
||||||
with(suggestionView.findViewById<ImageView>(R.id.right_icon)) {
|
with(suggestionView.findViewById<ImageView>(R.id.right_icon)) {
|
||||||
@@ -925,7 +910,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
favorites.add(tag)
|
favorites.add(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
favoritesFile.writeText(json.stringify(favorites))
|
favoritesFile.writeText(json.stringify(serializer, favorites))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,6 +996,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
val defaultQuery = preference.getString("default_query", "")!!
|
val defaultQuery = preference.getString("default_query", "")!!
|
||||||
|
|
||||||
|
if (query != queryStack.lastOrNull()) {
|
||||||
|
queryStack.remove(query)
|
||||||
|
queryStack.add(query)
|
||||||
|
}
|
||||||
|
|
||||||
galleryIDs = null
|
galleryIDs = null
|
||||||
|
|
||||||
if (galleryIDs?.isActive == true)
|
if (galleryIDs?.isActive == true)
|
||||||
@@ -1024,52 +1014,60 @@ class MainActivity : AppCompatActivity() {
|
|||||||
when(sortMode) {
|
when(sortMode) {
|
||||||
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
|
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
|
||||||
else -> getGalleryIDsFromNozomi(null, "index", "all")
|
else -> getGalleryIDsFromNozomi(null, "index", "all")
|
||||||
}.apply {
|
}.also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).apply {
|
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode.HISTORY -> {
|
Mode.HISTORY -> {
|
||||||
when {
|
when {
|
||||||
query.isEmpty() -> {
|
query.isEmpty() -> {
|
||||||
histories.toList().apply {
|
histories.toList().also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val result = doSearch(query).sorted()
|
val result = doSearch(query).sorted()
|
||||||
histories.filter { result.binarySearch(it) >= 0 }.apply {
|
histories.filter { result.binarySearch(it) >= 0 }.also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode.DOWNLOAD -> {
|
Mode.DOWNLOAD -> {
|
||||||
|
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
|
||||||
|
file.isDirectory && file.name.toIntOrNull() != null
|
||||||
|
}?.sortedByDescending {
|
||||||
|
it.lastModified()
|
||||||
|
}?.map {
|
||||||
|
it.name.toInt()
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
when {
|
when {
|
||||||
query.isEmpty() -> downloads.toList().apply {
|
query.isEmpty() -> downloads.also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val result = doSearch(query).sorted()
|
val result = doSearch(query).sorted()
|
||||||
downloads.filter { result.binarySearch(it) >= 0 }.apply {
|
downloads.filter { result.binarySearch(it) >= 0 }.also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode.FAVORITE -> {
|
Mode.FAVORITE -> {
|
||||||
when {
|
when {
|
||||||
query.isEmpty() -> favorites.toList().apply {
|
query.isEmpty() -> favorites.toList().also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val result = doSearch(query).sorted()
|
val result = doSearch(query).sorted()
|
||||||
favorites.filter { result.binarySearch(it) >= 0 }.apply {
|
favorites.filter { result.binarySearch(it) >= 0 }.also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1083,9 +1081,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
|
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
|
||||||
|
|
||||||
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val galleryIDs = galleryIDs?.await()
|
val galleryIDs = try {
|
||||||
|
galleryIDs!!.await().also {
|
||||||
|
if (it.isEmpty())
|
||||||
|
throw Exception("No result")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
if (e.message != "No result")
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
|
||||||
if (galleryIDs.isNullOrEmpty()) { //No result
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
main_noresult.visibility = View.VISIBLE
|
main_noresult.visibility = View.VISIBLE
|
||||||
main_progressbar.hide()
|
main_progressbar.hide()
|
||||||
@@ -1098,48 +1103,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
for (chunk in chunks)
|
for (chunk in chunks)
|
||||||
chunk.map { galleryID ->
|
chunk.map { galleryID ->
|
||||||
async {
|
async {
|
||||||
try {
|
Cache(this@MainActivity).getGalleryBlock(galleryID)
|
||||||
val json = Json(JsonConfiguration.Stable)
|
|
||||||
val serializer = GalleryBlock.serializer()
|
|
||||||
|
|
||||||
val galleryBlock =
|
|
||||||
File(getCachedGallery(this@MainActivity, galleryID), "galleryBlock.json").let { cache ->
|
|
||||||
when {
|
|
||||||
cache.exists() -> json.parse(serializer, cache.readText())
|
|
||||||
else -> {
|
|
||||||
getGalleryBlock(galleryID).apply {
|
|
||||||
this ?: return@apply
|
|
||||||
|
|
||||||
if (cache.parentFile?.exists() == false)
|
|
||||||
cache.parentFile!!.mkdirs()
|
|
||||||
|
|
||||||
cache.writeText(json.stringify(serializer, this))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return@async null
|
|
||||||
|
|
||||||
val thumbnail = async {
|
|
||||||
val ext = galleryBlock.thumbnails[0].split('.').last()
|
|
||||||
File(getCachedGallery(this@MainActivity, galleryBlock.id), "thumbnail.$ext").apply {
|
|
||||||
if (!exists())
|
|
||||||
try {
|
|
||||||
with(URL(galleryBlock.thumbnails[0]).openConnection() as HttpsURLConnection) {
|
|
||||||
if (this@apply.parentFile?.exists() == false)
|
|
||||||
this@apply.parentFile!!.mkdirs()
|
|
||||||
|
|
||||||
inputStream.copyTo(FileOutputStream(this@apply))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
delete()
|
|
||||||
}
|
|
||||||
}.absolutePath
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair(galleryBlock, thumbnail)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.forEach {
|
}.forEach {
|
||||||
val galleryBlock = it.await()
|
val galleryBlock = it.await()
|
||||||
|
|||||||
@@ -18,15 +18,15 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.PagerSnapHelper
|
import androidx.recyclerview.widget.PagerSnapHelper
|
||||||
@@ -34,28 +34,27 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.crashlytics.android.Crashlytics
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import kotlinx.android.synthetic.main.activity_reader.*
|
import kotlinx.android.synthetic.main.activity_reader.*
|
||||||
import kotlinx.android.synthetic.main.activity_reader.view.*
|
import kotlinx.android.synthetic.main.activity_reader.view.*
|
||||||
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
import xyz.quaver.pupil.adapters.ReaderAdapter
|
||||||
import xyz.quaver.pupil.util.GalleryDownloader
|
|
||||||
import xyz.quaver.pupil.util.Histories
|
import xyz.quaver.pupil.util.Histories
|
||||||
import xyz.quaver.pupil.util.ItemClickSupport
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
import xyz.quaver.pupil.util.hasPermission
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
class ReaderActivity : AppCompatActivity() {
|
class ReaderActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private var galleryID = 0
|
private var galleryID = 0
|
||||||
private val images = ArrayList<String>()
|
|
||||||
private var gallerySize = 0
|
|
||||||
private var currentPage = 0
|
private var currentPage = 0
|
||||||
|
|
||||||
private var isScroll = true
|
private var isScroll = true
|
||||||
@@ -71,7 +70,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var downloader: GalleryDownloader
|
private val timer = Timer()
|
||||||
|
|
||||||
private val snapHelper = PagerSnapHelper()
|
private val snapHelper = PagerSnapHelper()
|
||||||
|
|
||||||
@@ -95,19 +94,44 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
handleIntent(intent)
|
handleIntent(intent)
|
||||||
|
|
||||||
Crashlytics.setInt("GalleryID", galleryID)
|
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
|
||||||
|
|
||||||
if (galleryID == 0) {
|
if (galleryID == 0) {
|
||||||
onBackPressed()
|
onBackPressed()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
initDownloader()
|
|
||||||
|
|
||||||
initView()
|
initView()
|
||||||
|
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("cache_disable", false)) {
|
||||||
|
reader_download_progressbar.visibility = View.GONE
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val reader = Cache(this@ReaderActivity).getReader(galleryID)
|
||||||
|
|
||||||
if (!downloader.download)
|
launch(Dispatchers.Main) initDownloader@{
|
||||||
downloader.start()
|
if (reader == null) {
|
||||||
|
Snackbar
|
||||||
|
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
||||||
|
.show()
|
||||||
|
return@initDownloader
|
||||||
|
}
|
||||||
|
|
||||||
|
(reader_recyclerview.adapter as ReaderAdapter).apply {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
initDownloader()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
@@ -120,14 +144,12 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
val uri = intent.data
|
val uri = intent.data
|
||||||
val lastPathSegment = uri?.lastPathSegment
|
val lastPathSegment = uri?.lastPathSegment
|
||||||
if (uri != null && lastPathSegment != null) {
|
if (uri != null && lastPathSegment != null) {
|
||||||
val nonNumber = Regex("[^-?0-9]+")
|
|
||||||
|
|
||||||
galleryID = when (uri.host) {
|
galleryID = when (uri.host) {
|
||||||
"hitomi.la" -> lastPathSegment.replace(nonNumber, "").toInt()
|
"hitomi.la" ->
|
||||||
"히요비.asia" -> lastPathSegment.toInt()
|
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
||||||
"xn--9w3b15m8vo.asia" -> lastPathSegment.toInt()
|
"hiyobi.me" -> lastPathSegment.toInt()
|
||||||
"e-hentai.org" -> uri.pathSegments[1].toInt()
|
"e-hentai.org" -> uri.pathSegments[1].toInt()
|
||||||
else -> return
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -148,7 +170,6 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
menuInflater.inflate(R.menu.reader, menu)
|
menuInflater.inflate(R.menu.reader, menu)
|
||||||
|
|
||||||
@@ -166,10 +187,10 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||||
when(item?.itemId) {
|
when(item?.itemId) {
|
||||||
R.id.reader_menu_page_indicator -> {
|
R.id.reader_menu_page_indicator -> {
|
||||||
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
|
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
|
||||||
with(view.dialog_number_picker) {
|
with(view.dialog_number_picker) {
|
||||||
minValue=1
|
minValue=1
|
||||||
maxValue=gallerySize
|
maxValue=Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.size ?: 0
|
||||||
value=currentPage
|
value=currentPage
|
||||||
}
|
}
|
||||||
val dialog = AlertDialog.Builder(this).apply {
|
val dialog = AlertDialog.Builder(this).apply {
|
||||||
@@ -202,8 +223,11 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
if (::downloader.isInitialized && !downloader.download)
|
timer.cancel()
|
||||||
downloader.cancel()
|
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
|
||||||
|
|
||||||
|
if (!Cache(this).isDownloading(galleryID))
|
||||||
|
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
@@ -239,101 +263,73 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initDownloader() {
|
private fun initDownloader() {
|
||||||
var d: GalleryDownloader? = GalleryDownloader.get(galleryID)
|
val worker = DownloadWorker.getInstance(this).apply {
|
||||||
|
cancel(galleryID)
|
||||||
if (d == null)
|
queue.add(galleryID)
|
||||||
d = GalleryDownloader(this, galleryID)
|
|
||||||
|
|
||||||
downloader = d.apply {
|
|
||||||
onReaderLoadedHandler = {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
title = it.title
|
|
||||||
with(reader_download_progressbar) {
|
|
||||||
max = it.readerItems.size
|
|
||||||
progress = 0
|
|
||||||
}
|
|
||||||
with(reader_progressbar) {
|
|
||||||
max = it.readerItems.size
|
|
||||||
progress = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gallerySize = it.readerItems.size
|
timer.schedule(1000, 1000) {
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.readerItems.size}"
|
if (worker.progress.indexOfKey(galleryID) < 0) //loading
|
||||||
}
|
return@schedule
|
||||||
}
|
|
||||||
onProgressHandler = {
|
if (worker.progress[galleryID] == null) { //Gallery not found
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
timer.cancel()
|
||||||
reader_download_progressbar.progress = it
|
|
||||||
menu?.findItem(R.id.reader_menu_use_hiyobi)?.isVisible = downloader.useHiyobi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onDownloadedHandler = {
|
|
||||||
val item = it.toList()
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
if (images.isEmpty()) {
|
|
||||||
images.addAll(item)
|
|
||||||
reader_recyclerview.adapter?.notifyDataSetChanged()
|
|
||||||
} else {
|
|
||||||
images.add(item.last())
|
|
||||||
reader_recyclerview.adapter?.notifyItemInserted(images.size-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onErrorHandler = {
|
|
||||||
Snackbar
|
Snackbar
|
||||||
.make(reader_layout, it.message ?: it.javaClass.name, Snackbar.LENGTH_INDEFINITE)
|
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
||||||
.setAction(R.string.reader_help) { _ ->
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.error_help))))
|
|
||||||
}
|
|
||||||
.show()
|
.show()
|
||||||
downloader.download = false
|
|
||||||
}
|
}
|
||||||
onCompleteHandler = {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
reader_download_progressbar.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onNotifyChangedHandler = { notify ->
|
|
||||||
val fab = reader_fab_download
|
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (notify) {
|
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||||
val icon = AnimatedVectorDrawableCompat.create(this, R.drawable.ic_downloading)
|
reader_download_progressbar.progress = worker.progress[galleryID]?.count { it.isInfinite() } ?: 0
|
||||||
icon?.registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
|
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||||
override fun onAnimationEnd(drawable: Drawable?) {
|
|
||||||
if (downloader.download)
|
if (title == getString(R.string.reader_loading)) {
|
||||||
fab.post {
|
val reader = Cache(this@ReaderActivity).getReaderOrNull(galleryID)
|
||||||
icon.start()
|
|
||||||
fab.labelText = getString(R.string.reader_fab_download_cancel)
|
if (reader != null) {
|
||||||
}
|
|
||||||
else
|
with (reader_recyclerview.adapter as ReaderAdapter) {
|
||||||
fab.post {
|
this.reader = reader
|
||||||
fab.setImageResource(R.drawable.ic_download)
|
notifyDataSetChanged()
|
||||||
fab.labelText = getString(R.string.reader_fab_download)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
})
|
})
|
||||||
|
|
||||||
fab.setImageDrawable(icon)
|
|
||||||
icon?.start()
|
|
||||||
} else {
|
|
||||||
runOnUiThread {
|
|
||||||
fab.setImageResource(R.drawable.ic_download)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloader.download) {
|
if (worker.progress[galleryID]?.all { it.isInfinite() } == true) { //Download finished
|
||||||
downloader.invokeOnReaderLoaded()
|
reader_download_progressbar.visibility = View.GONE
|
||||||
downloader.invokeOnNotifyChanged()
|
|
||||||
|
animateDownloadFAB(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
with(reader_recyclerview) {
|
with(reader_recyclerview) {
|
||||||
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, images)
|
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, this@ReaderActivity).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() {
|
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
@@ -349,43 +345,38 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
if (layoutManager.findFirstVisibleItemPosition() == -1)
|
if (layoutManager.findFirstVisibleItemPosition() == -1)
|
||||||
return
|
return
|
||||||
currentPage = layoutManager.findFirstVisibleItemPosition()+1
|
currentPage = layoutManager.findFirstVisibleItemPosition()+1
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/$gallerySize"
|
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
|
||||||
this@ReaderActivity.reader_progressbar.progress = currentPage
|
this@ReaderActivity.reader_progressbar.progress = currentPage
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ItemClickSupport.addTo(this)
|
|
||||||
.setOnItemClickListener { _, _, _ ->
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
with(reader_fab_download) {
|
with(reader_fab_download) {
|
||||||
setImageResource(R.drawable.ic_download)
|
animateDownloadFAB(Cache(context).isDownloading(galleryID)) //If download in progress, animate button
|
||||||
|
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
|
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
|
||||||
|
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
|
||||||
|
else {
|
||||||
|
if (Cache(context).isDownloading(galleryID)) {
|
||||||
|
Cache(context).setDownloading(galleryID, false)
|
||||||
|
|
||||||
if (!this@ReaderActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
animateDownloadFAB(false)
|
||||||
AlertDialog.Builder(this@ReaderActivity).apply {
|
} else {
|
||||||
setTitle(R.string.warning)
|
Cache(context).setDownloading(galleryID, true)
|
||||||
setMessage(R.string.update_no_permission)
|
animateDownloadFAB(true)
|
||||||
setPositiveButton(android.R.string.ok) { _, _ -> }
|
}
|
||||||
}.show()
|
}
|
||||||
|
}
|
||||||
return@setOnClickListener
|
|
||||||
}
|
}
|
||||||
|
|
||||||
downloader.download = !downloader.download
|
with(reader_fab_retry) {
|
||||||
|
setImageResource(R.drawable.refresh)
|
||||||
if (!downloader.download)
|
setOnClickListener {
|
||||||
downloader.clearNotification()
|
DownloadWorker.getInstance(context).let {
|
||||||
|
it.cancel(galleryID)
|
||||||
|
it.queue.add(galleryID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +405,8 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
window.attributes = this
|
window.attributes = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scrollMode(isScroll: Boolean) {
|
private fun scrollMode(isScroll: Boolean) {
|
||||||
@@ -427,4 +420,34 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
(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?) {
|
||||||
|
val worker = DownloadWorker.getInstance(context)
|
||||||
|
if (worker.progress[galleryID]?.all { it.isInfinite() } == 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,36 +18,31 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import kotlinx.android.synthetic.main.settings_activity.*
|
||||||
|
import kotlinx.serialization.builtins.list
|
||||||
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.types.Tags
|
import xyz.quaver.pupil.ui.fragment.LockSettingsFragment
|
||||||
import xyz.quaver.pupil.util.Lock
|
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
||||||
import xyz.quaver.pupil.util.LockManager
|
import xyz.quaver.pupil.util.*
|
||||||
import xyz.quaver.pupil.util.getDownloadDirectory
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
val REQUEST_LOCK = 38238
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -75,338 +70,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsFragment : PreferenceFragmentCompat() {
|
|
||||||
|
|
||||||
private val suffix = listOf(
|
|
||||||
"B",
|
|
||||||
"kB",
|
|
||||||
"MB",
|
|
||||||
"GB",
|
|
||||||
"TB" //really?
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
val lockManager = LockManager(context!!)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDirSize(dir: File) : String {
|
|
||||||
var size = dir.walk().map { it.length() }.sum()
|
|
||||||
var suffixIndex = 0
|
|
||||||
|
|
||||||
while (size >= 1024) {
|
|
||||||
size /= 1024
|
|
||||||
suffixIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
return getString(R.string.settings_clear_summary, size, suffix[suffixIndex])
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
|
||||||
|
|
||||||
with(findPreference<Preference>("app_version")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
val manager = context.packageManager
|
|
||||||
val info = manager.getPackageInfo(context.packageName, 0)
|
|
||||||
|
|
||||||
summary = info.versionName
|
|
||||||
}
|
|
||||||
|
|
||||||
with(findPreference<Preference>("delete_cache")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
val dir = File(context.cacheDir, "imageCache")
|
|
||||||
|
|
||||||
summary = getDirSize(dir)
|
|
||||||
|
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
AlertDialog.Builder(context).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_clear_cache_alert_message)
|
|
||||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
|
||||||
if (dir.exists())
|
|
||||||
dir.deleteRecursively()
|
|
||||||
|
|
||||||
summary = getDirSize(dir)
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(findPreference<Preference>("delete_downloads")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
val dir = getDownloadDirectory(context)!!
|
|
||||||
|
|
||||||
summary = getDirSize(dir)
|
|
||||||
|
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
AlertDialog.Builder(context).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_clear_downloads_alert_message)
|
|
||||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
|
||||||
if (dir.exists())
|
|
||||||
dir.deleteRecursively()
|
|
||||||
|
|
||||||
val downloads = (activity!!.application as Pupil).downloads
|
|
||||||
|
|
||||||
downloads.clear()
|
|
||||||
|
|
||||||
summary = getDirSize(dir)
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
with(findPreference<Preference>("clear_history")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
val histories = (activity!!.application as Pupil).histories
|
|
||||||
|
|
||||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
|
||||||
|
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
AlertDialog.Builder(context).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_clear_history_alert_message)
|
|
||||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
|
||||||
histories.clear()
|
|
||||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(findPreference<Preference>("default_query")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
summary = preferences.getString("default_query", "") ?: ""
|
|
||||||
|
|
||||||
val languages = resources.getStringArray(R.array.languages).map {
|
|
||||||
it.split("|").let { split ->
|
|
||||||
Pair(split[0], split[1])
|
|
||||||
}
|
|
||||||
}.toMap()
|
|
||||||
val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
|
|
||||||
|
|
||||||
val excludeBL = "-male:yaoi"
|
|
||||||
val excludeGuro = listOf("-female:guro", "-male:guro")
|
|
||||||
|
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
val dialogView = LayoutInflater.from(context).inflate(
|
|
||||||
R.layout.dialog_default_query,
|
|
||||||
LinearLayout(context),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
val tags = Tags.parse(
|
|
||||||
preferences.getString("default_query", "") ?: ""
|
|
||||||
)
|
|
||||||
|
|
||||||
summary = tags.toString()
|
|
||||||
|
|
||||||
with(dialogView.default_query_dialog_language_selector) {
|
|
||||||
adapter =
|
|
||||||
ArrayAdapter(
|
|
||||||
context,
|
|
||||||
android.R.layout.simple_spinner_dropdown_item,
|
|
||||||
arrayListOf(
|
|
||||||
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(dialogView.default_query_dialog_BL_checkbox) {
|
|
||||||
isChecked = tags.contains(excludeBL)
|
|
||||||
if (tags.contains(excludeBL))
|
|
||||||
tags.remove(excludeBL)
|
|
||||||
}
|
|
||||||
|
|
||||||
with(dialogView.default_query_dialog_guro_checkbox) {
|
|
||||||
isChecked = excludeGuro.all { tags.contains(it) }
|
|
||||||
if (excludeGuro.all { tags.contains(it) })
|
|
||||||
excludeGuro.forEach {
|
|
||||||
tags.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(dialogView.default_query_dialog_edittext) {
|
|
||||||
setText(tags.toString(), 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())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(context!!).apply {
|
|
||||||
setView(dialogView)
|
|
||||||
}.create()
|
|
||||||
|
|
||||||
dialogView.default_query_dialog_ok.setOnClickListener {
|
|
||||||
val newTags = Tags.parse(dialogView.default_query_dialog_edittext.text.toString())
|
|
||||||
|
|
||||||
with(dialogView.default_query_dialog_language_selector) {
|
|
||||||
if (selectedItemPosition != 0)
|
|
||||||
newTags.add("language:${reverseLanguages[selectedItem]}")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dialogView.default_query_dialog_BL_checkbox.isChecked)
|
|
||||||
newTags.add(excludeBL)
|
|
||||||
|
|
||||||
if (dialogView.default_query_dialog_guro_checkbox.isChecked)
|
|
||||||
excludeGuro.forEach { tag ->
|
|
||||||
newTags.add(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
preferenceManager.sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
|
|
||||||
summary = preferences.getString("default_query", "") ?: ""
|
|
||||||
tags.clear()
|
|
||||||
tags.addAll(newTags)
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.show()
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
with(findPreference<Preference>("app_lock")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
val lockManager = LockManager(context)
|
|
||||||
|
|
||||||
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 = Preference.OnPreferenceClickListener {
|
|
||||||
val intent = Intent(context, LockActivity::class.java)
|
|
||||||
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_LOCK)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(findPreference<Preference>("dark_mode")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
|
||||||
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
|
|
||||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
|
||||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
|
||||||
})
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LockFragment : PreferenceFragmentCompat() {
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
val lockManager = LockManager(context!!)
|
|
||||||
|
|
||||||
findPreference<Preference>("lock_pattern")?.summary =
|
|
||||||
if (lockManager.contains(Lock.Type.PATTERN))
|
|
||||||
getString(R.string.settings_lock_enabled)
|
|
||||||
else
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
|
|
||||||
|
|
||||||
with(findPreference<Preference>("lock_pattern")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
if (LockManager(context!!).contains(Lock.Type.PATTERN))
|
|
||||||
summary = getString(R.string.settings_lock_enabled)
|
|
||||||
|
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
val lockManager = LockManager(context!!)
|
|
||||||
|
|
||||||
if (lockManager.contains(Lock.Type.PATTERN)) {
|
|
||||||
AlertDialog.Builder(context).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_lock_remove_message)
|
|
||||||
|
|
||||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
|
||||||
lockManager.remove(Lock.Type.PATTERN)
|
|
||||||
onResume()
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
} else {
|
|
||||||
val intent = Intent(context, LockActivity::class.java).apply {
|
|
||||||
putExtra("mode", "add_lock")
|
|
||||||
putExtra("type", "pattern")
|
|
||||||
}
|
|
||||||
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||||
when (item?.itemId) {
|
when (item?.itemId) {
|
||||||
android.R.id.home -> onBackPressed()
|
android.R.id.home -> onBackPressed()
|
||||||
@@ -421,12 +84,131 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
supportFragmentManager
|
supportFragmentManager
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.replace(R.id.settings, LockFragment())
|
.replace(R.id.settings, LockSettingsFragment())
|
||||||
.addToBackStack("Lock")
|
.addToBackStack("Lock")
|
||||||
.commitAllowingStateLoss()
|
.commitAllowingStateLoss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
REQUEST_RESTORE -> {
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = data?.data ?: return
|
||||||
|
|
||||||
|
try {
|
||||||
|
val str = contentResolver.openInputStream(uri).use { inputStream ->
|
||||||
|
inputStream!!
|
||||||
|
|
||||||
|
inputStream.readBytes().toString(Charset.defaultCharset())
|
||||||
|
}
|
||||||
|
|
||||||
|
(application as Pupil).favorites.addAll(json.parse(Int.serializer().list, str).also {
|
||||||
|
Snackbar.make(
|
||||||
|
window.decorView,
|
||||||
|
getString(R.string.settings_restore_successful, it.size),
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
})
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Snackbar.make(
|
||||||
|
window.decorView,
|
||||||
|
R.string.settings_restore_failed,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUEST_DOWNLOAD_FOLDER -> {
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
data?.data?.also { uri ->
|
||||||
|
val takeFlags: Int =
|
||||||
|
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)
|
||||||
|
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||||
|
|
||||||
|
val file = uri.toFile(this)
|
||||||
|
|
||||||
|
if (file?.canWrite() != true)
|
||||||
|
Snackbar.make(
|
||||||
|
settings,
|
||||||
|
R.string.settings_dl_location_not_writable,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
else
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||||
|
.putString("dl_location", file.canonicalPath)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUEST_DOWNLOAD_FOLDER_OLD -> {
|
||||||
|
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
||||||
|
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
||||||
|
|
||||||
|
if (!File(directory).canWrite())
|
||||||
|
Snackbar.make(
|
||||||
|
settings,
|
||||||
|
R.string.settings_dl_location_not_writable,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
else
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||||
|
.putString("dl_location", File(directory).canonicalPath)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUEST_IMPORT_OLD_GALLERIES -> {
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
data?.data?.also { uri ->
|
||||||
|
val takeFlags: Int =
|
||||||
|
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)
|
||||||
|
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||||
|
|
||||||
|
val file = uri.toFile(this)
|
||||||
|
|
||||||
|
if (file?.canRead() != true)
|
||||||
|
Snackbar.make(
|
||||||
|
settings,
|
||||||
|
resources.getText(R.string.import_old_galleries_folder_not_readable),
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
else
|
||||||
|
importOldGalleries(this, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUEST_IMPORT_OLD_GALLERIES_OLD -> {
|
||||||
|
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
||||||
|
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
||||||
|
|
||||||
|
if (!File(directory).canRead())
|
||||||
|
Snackbar.make(
|
||||||
|
settings,
|
||||||
|
resources.getText(R.string.import_old_galleries_folder_not_readable),
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
else {
|
||||||
|
importOldGalleries(this, File(directory))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
when (requestCode) {
|
||||||
|
REQUEST_WRITE_PERMISSION_AND_SAF -> {
|
||||||
|
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||||
|
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
* 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 androidx.preference.PreferenceManager
|
||||||
|
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
|
||||||
|
|
||||||
|
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 preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val tags = Tags.parse(
|
||||||
|
preferences.getString("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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* 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.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.RadioButton
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import kotlinx.android.synthetic.main.item_dl_location.view.*
|
||||||
|
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||||
|
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.util.*
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
||||||
|
|
||||||
|
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
private val buttons = mutableListOf<Pair<RadioButton, File?>>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
setTitle(R.string.settings_dl_location)
|
||||||
|
|
||||||
|
setView(build())
|
||||||
|
|
||||||
|
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ -> }
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun build() : View {
|
||||||
|
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
|
||||||
|
|
||||||
|
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
||||||
|
|
||||||
|
externalFilesDirs.forEachIndexed { index, dir ->
|
||||||
|
|
||||||
|
dir ?: return@forEachIndexed
|
||||||
|
|
||||||
|
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
|
||||||
|
location_type.text = context.getString(when (index) {
|
||||||
|
0 -> R.string.settings_dl_location_internal
|
||||||
|
else -> R.string.settings_dl_location_removable
|
||||||
|
})
|
||||||
|
location_available.text = context.getString(
|
||||||
|
R.string.settings_dl_location_available,
|
||||||
|
byteToString(dir.freeSpace)
|
||||||
|
)
|
||||||
|
setOnClickListener {
|
||||||
|
buttons.forEach { pair ->
|
||||||
|
pair.first.isChecked = false
|
||||||
|
}
|
||||||
|
button.performClick()
|
||||||
|
preference.edit().putString("dl_location", dir.canonicalPath).apply()
|
||||||
|
}
|
||||||
|
buttons.add(button to dir)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
|
||||||
|
location_type.text = context.getString(R.string.settings_dl_location_custom)
|
||||||
|
setOnClickListener {
|
||||||
|
buttons.forEach { pair ->
|
||||||
|
pair.first.isChecked = false
|
||||||
|
}
|
||||||
|
button.performClick()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
|
||||||
|
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_PERMISSION_AND_SAF)
|
||||||
|
else {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||||
|
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER_OLD)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buttons.add(button to null)
|
||||||
|
})
|
||||||
|
|
||||||
|
externalFilesDirs.indexOfFirst {
|
||||||
|
it.canonicalPath == getDownloadDirectory(context).canonicalPath
|
||||||
|
}.let { index ->
|
||||||
|
if (index < 0)
|
||||||
|
buttons.first().first.isChecked = true
|
||||||
|
else
|
||||||
|
buttons[index].first.isChecked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Pupil, Hitomi.la viewer for Android
|
* Pupil, Hitomi.la viewer for Android
|
||||||
* Copyright (C) 2019 tom5079
|
* Copyright (C) 2020 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,44 +16,45 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui.dialog
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.LinearLayout.LayoutParams
|
import android.widget.LinearLayout.LayoutParams
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.gridlayout.widget.GridLayout
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.dialog_galleryblock.*
|
import kotlinx.android.synthetic.main.dialog_gallery.*
|
||||||
import kotlinx.android.synthetic.main.gallery_details.view.*
|
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.android.synthetic.main.item_gallery_details.view.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import xyz.quaver.hitomi.Gallery
|
import xyz.quaver.hitomi.Gallery
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.getGallery
|
import xyz.quaver.hitomi.getGallery
|
||||||
import xyz.quaver.hitomi.getGalleryBlock
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||||
import xyz.quaver.pupil.adapters.ThumbnailAdapter
|
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
|
import xyz.quaver.pupil.ui.ReaderActivity
|
||||||
import xyz.quaver.pupil.util.ItemClickSupport
|
import xyz.quaver.pupil.util.ItemClickSupport
|
||||||
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
|
|
||||||
class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) {
|
class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : Dialog(context) {
|
||||||
|
|
||||||
private val languages = context.resources.getStringArray(R.array.languages).map {
|
private val languages = context.resources.getStringArray(R.array.languages).map {
|
||||||
it.split("|").let { split ->
|
it.split("|").let { split ->
|
||||||
@@ -61,13 +62,11 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
}
|
}
|
||||||
}.toMap()
|
}.toMap()
|
||||||
|
|
||||||
private val glide = Glide.with(context)
|
|
||||||
|
|
||||||
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.dialog_galleryblock)
|
setContentView(R.layout.dialog_gallery)
|
||||||
|
|
||||||
window?.attributes.apply {
|
window?.attributes.apply {
|
||||||
this ?: return@apply
|
this ?: return@apply
|
||||||
@@ -90,7 +89,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
try {
|
try {
|
||||||
val gallery = getGallery(galleryID)
|
val gallery = getGallery(galleryID)
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
gallery_cover.post {
|
||||||
gallery_progressbar.visibility = View.GONE
|
gallery_progressbar.visibility = View.GONE
|
||||||
gallery_title.text = gallery.title
|
gallery_title.text = gallery.title
|
||||||
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
|
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
|
||||||
@@ -112,9 +111,12 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Glide.with(context)
|
glide
|
||||||
.load(gallery.thumbnails.firstOrNull())
|
.load(gallery.cover)
|
||||||
.into(gallery_thumbnail)
|
.apply {
|
||||||
|
if (BuildConfig.CENSOR)
|
||||||
|
override(5, 8)
|
||||||
|
}.into(gallery_cover)
|
||||||
|
|
||||||
addDetails(gallery)
|
addDetails(gallery)
|
||||||
addThumbnails(gallery)
|
addThumbnails(gallery)
|
||||||
@@ -129,7 +131,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
private fun addDetails(gallery: Gallery) {
|
private fun addDetails(gallery: Gallery) {
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply {
|
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
||||||
gallery_details.setText(R.string.gallery_details)
|
gallery_details.setText(R.string.gallery_details)
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
@@ -168,12 +170,12 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
"male" -> {
|
"male" -> {
|
||||||
setChipBackgroundColorResource(R.color.material_blue_700)
|
setChipBackgroundColorResource(R.color.material_blue_700)
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white)
|
ContextCompat.getDrawable(context, R.drawable.gender_male)
|
||||||
}
|
}
|
||||||
"female" -> {
|
"female" -> {
|
||||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
setChipBackgroundColorResource(R.color.material_pink_600)
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white)
|
ContextCompat.getDrawable(context, R.drawable.gender_female)
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -183,6 +185,8 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
else -> tag.tag.wordCapitalize()
|
else -> tag.tag.wordCapitalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEnsureMinTouchTargetSize(false)
|
||||||
|
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
onChipClickedHandler.forEach { handler ->
|
onChipClickedHandler.forEach { handler ->
|
||||||
handler.invoke(tag)
|
handler.invoke(tag)
|
||||||
@@ -203,15 +207,21 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
private fun addThumbnails(gallery: Gallery) {
|
private fun addThumbnails(gallery: Gallery) {
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply {
|
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
||||||
gallery_details.setText(R.string.gallery_thumbnails)
|
gallery_details.setText(R.string.gallery_thumbnails)
|
||||||
|
|
||||||
RecyclerView(context).apply {
|
val pager = ViewPager2(context).apply {
|
||||||
layoutManager = GridLayoutManager(context, 3)
|
adapter = ThumbnailPageAdapter(glide, gallery.thumbnails)
|
||||||
adapter = ThumbnailAdapter(glide, gallery.thumbnails)
|
|
||||||
}.let {
|
|
||||||
gallery_details_contents.addView(it, LayoutParams(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 {
|
}.let {
|
||||||
gallery_contents.addView(it)
|
gallery_contents.addView(it)
|
||||||
}
|
}
|
||||||
@@ -219,7 +229,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
|
|
||||||
private fun addRelated(gallery: Gallery) {
|
private fun addRelated(gallery: Gallery) {
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>()
|
val galleries = ArrayList<GalleryBlock>()
|
||||||
|
|
||||||
val adapter = GalleryBlockAdapter(glide, galleries).apply {
|
val adapter = GalleryBlockAdapter(glide, galleries).apply {
|
||||||
onChipClickedHandler.add { tag ->
|
onChipClickedHandler.add { tag ->
|
||||||
@@ -232,17 +242,17 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
gallery.related.forEachIndexed { i, galleryID ->
|
gallery.related.forEachIndexed { i, galleryID ->
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
getGalleryBlock(galleryID)
|
Cache(context).getGalleryBlock(galleryID)
|
||||||
}.let {
|
}.let {
|
||||||
val galleryBlock = it.await() ?: return@let
|
val galleryBlock = it.await() ?: return@let
|
||||||
|
|
||||||
galleries.add(Pair(galleryBlock, GlobalScope.async { galleryBlock.thumbnails.first() }))
|
galleries.add(galleryBlock)
|
||||||
adapter.notifyItemInserted(i)
|
adapter.notifyItemInserted(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply {
|
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
||||||
gallery_details.setText(R.string.gallery_related)
|
gallery_details.setText(R.string.gallery_related)
|
||||||
|
|
||||||
RecyclerView(context).apply {
|
RecyclerView(context).apply {
|
||||||
@@ -252,12 +262,16 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
ItemClickSupport.addTo(this)
|
ItemClickSupport.addTo(this)
|
||||||
.setOnItemClickListener { _, position, _ ->
|
.setOnItemClickListener { _, position, _ ->
|
||||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
||||||
putExtra("galleryID", galleries[position].first.id)
|
putExtra("galleryID", galleries[position].id)
|
||||||
})
|
})
|
||||||
(context.applicationContext as Pupil).histories.add(galleries[position].first.id)
|
(context.applicationContext as Pupil).histories.add(galleries[position].id)
|
||||||
}
|
}
|
||||||
.setOnItemLongClickListener { _, position, _ ->
|
.setOnItemLongClickListener { _, position, _ ->
|
||||||
GalleryDialog(context, galleries[position].first.id).apply {
|
GalleryDialog(
|
||||||
|
context,
|
||||||
|
glide,
|
||||||
|
galleries[position].id
|
||||||
|
).apply {
|
||||||
onChipClickedHandler.add { tag ->
|
onChipClickedHandler.add { tag ->
|
||||||
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
||||||
}
|
}
|
||||||
94
app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* 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.preference.PreferenceManager
|
||||||
|
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
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.edit()
|
||||||
|
.putString("mirrors", it.joinToString(">"))
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
133
app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* 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.preference.PreferenceManager
|
||||||
|
import kotlinx.android.synthetic.main.dialog_proxy.view.*
|
||||||
|
import xyz.quaver.proxy
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.util.ProxyInfo
|
||||||
|
import xyz.quaver.pupil.util.getProxyInfo
|
||||||
|
import xyz.quaver.pupil.util.json
|
||||||
|
import java.net.Proxy
|
||||||
|
|
||||||
|
class ProxyDialog(context: Context) : Dialog(context) {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
val view = build()
|
||||||
|
|
||||||
|
setTitle(R.string.settings_proxy_title)
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
|
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
private fun build() : View {
|
||||||
|
val proxyInfo = getProxyInfo(context)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().putString("proxy",
|
||||||
|
json.stringify(ProxyInfo.serializer(), it)
|
||||||
|
).apply()
|
||||||
|
|
||||||
|
proxy = it.proxy()
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
* 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.PreferenceManager
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("lock_fingerprint", false).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.yes) { _, _ ->
|
||||||
|
lockManager.remove(Lock.Type.PATTERN)
|
||||||
|
onResume()
|
||||||
|
}
|
||||||
|
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||||
|
}.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.yes) { _, _ ->
|
||||||
|
lockManager.remove(Lock.Type.PIN)
|
||||||
|
onResume()
|
||||||
|
}
|
||||||
|
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||||
|
}.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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?) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui.fragment
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -29,8 +29,6 @@ import com.andrognito.patternlockview.utils.PatternLockUtils
|
|||||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
|
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
|
||||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.view.*
|
import kotlinx.android.synthetic.main.fragment_pattern_lock.view.*
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.util.hash
|
|
||||||
import xyz.quaver.pupil.util.hashWithSalt
|
|
||||||
|
|
||||||
class PatternLockFragment : Fragment(), PatternLockViewListener {
|
class PatternLockFragment : Fragment(), PatternLockViewListener {
|
||||||
|
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
/*
|
||||||
|
* 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.Manifest
|
||||||
|
import android.content.*
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceCategory
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||||
|
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||||
|
import xyz.quaver.pupil.Pupil
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.ui.LockActivity
|
||||||
|
import xyz.quaver.pupil.ui.SettingsActivity
|
||||||
|
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
|
||||||
|
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
|
||||||
|
import xyz.quaver.pupil.ui.dialog.MirrorDialog
|
||||||
|
import xyz.quaver.pupil.ui.dialog.ProxyDialog
|
||||||
|
import xyz.quaver.pupil.util.*
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsFragment :
|
||||||
|
PreferenceFragmentCompat(),
|
||||||
|
Preference.OnPreferenceClickListener,
|
||||||
|
Preference.OnPreferenceChangeListener,
|
||||||
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
|
lateinit var sharedPreference: SharedPreferences
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDirSize(dir: File) : String {
|
||||||
|
return getString(R.string.settings_storage_usage,
|
||||||
|
Runtime.getRuntime().exec("du -hs " + dir.absolutePath).let {
|
||||||
|
BufferedReader(InputStreamReader(it.inputStream)).use { reader ->
|
||||||
|
reader.readLine()?.split('\t')?.firstOrNull() ?: "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPreferenceClick(preference: Preference?): Boolean {
|
||||||
|
with (preference) {
|
||||||
|
this ?: return false
|
||||||
|
|
||||||
|
when (key) {
|
||||||
|
"app_version" -> {
|
||||||
|
checkUpdate(activity as SettingsActivity, true)
|
||||||
|
}
|
||||||
|
"delete_cache" -> {
|
||||||
|
val dir = File(requireContext().cacheDir, "imageCache")
|
||||||
|
|
||||||
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
|
setTitle(R.string.warning)
|
||||||
|
setMessage(R.string.settings_clear_cache_alert_message)
|
||||||
|
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||||
|
if (dir.exists())
|
||||||
|
dir.deleteRecursively()
|
||||||
|
|
||||||
|
summary = getString(R.string.settings_storage_usage_loading)
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
getDirSize(dir).let {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
this@with.summary = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
"delete_downloads" -> {
|
||||||
|
val dir = getDownloadDirectory(requireContext())
|
||||||
|
|
||||||
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
|
setTitle(R.string.warning)
|
||||||
|
setMessage(R.string.settings_clear_downloads_alert_message)
|
||||||
|
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||||
|
if (dir.exists())
|
||||||
|
dir.deleteRecursively()
|
||||||
|
|
||||||
|
summary = getString(R.string.settings_storage_usage_loading)
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
getDirSize(dir).let {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
this@with.summary = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
"clear_history" -> {
|
||||||
|
val histories = (requireContext().applicationContext as Pupil).histories
|
||||||
|
|
||||||
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
|
setTitle(R.string.warning)
|
||||||
|
setMessage(R.string.settings_clear_history_alert_message)
|
||||||
|
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||||
|
histories.clear()
|
||||||
|
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
||||||
|
}
|
||||||
|
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
"dl_location" -> {
|
||||||
|
DownloadLocationDialog(requireActivity()).show()
|
||||||
|
}
|
||||||
|
"default_query" -> {
|
||||||
|
DefaultQueryDialog(requireContext()).apply {
|
||||||
|
onPositiveButtonClickListener = { newTags ->
|
||||||
|
sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
|
||||||
|
summary = newTags.toString()
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
"app_lock" -> {
|
||||||
|
val intent = Intent(requireContext(), LockActivity::class.java)
|
||||||
|
activity?.startActivityForResult(intent, REQUEST_LOCK)
|
||||||
|
}
|
||||||
|
"mirrors" -> {
|
||||||
|
MirrorDialog(requireContext())
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
"proxy" -> {
|
||||||
|
ProxyDialog(requireContext())
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
"nomedia" -> {
|
||||||
|
File(getDownloadDirectory(context), ".nomedia").createNewFile()
|
||||||
|
}
|
||||||
|
"backup" -> {
|
||||||
|
File(ContextCompat.getDataDir(requireContext()), "favorites.json").copyTo(
|
||||||
|
File(getDownloadDirectory(requireContext()), "favorites.json"),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
"restore" -> {
|
||||||
|
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "*/*"
|
||||||
|
}
|
||||||
|
|
||||||
|
activity?.startActivityForResult(intent, REQUEST_RESTORE)
|
||||||
|
}
|
||||||
|
"old_import_galleries" -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
|
||||||
|
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
|
||||||
|
ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_PERMISSION_AND_SAF)
|
||||||
|
else {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||||
|
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
activity?.startActivityForResult(intent, REQUEST_IMPORT_OLD_GALLERIES)
|
||||||
|
}
|
||||||
|
} else { // Can't use SAF on old Androids!
|
||||||
|
val config = DirectoryChooserConfig.builder()
|
||||||
|
.newDirectoryName("Pupil")
|
||||||
|
.allowNewDirectoryNameModification(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val intent = Intent(requireContext(), DirectoryChooserActivity::class.java).apply {
|
||||||
|
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
activity?.startActivityForResult(intent, REQUEST_IMPORT_OLD_GALLERIES_OLD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"user_id" -> {
|
||||||
|
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||||
|
ClipData.newPlainText("user_id", sharedPreference.getString("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) {
|
||||||
|
"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 = getProxyInfo(requireContext()).type.name
|
||||||
|
}
|
||||||
|
"dl_location" -> {
|
||||||
|
summary = getDownloadDirectory(requireContext()).canonicalPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
||||||
|
|
||||||
|
sharedPreference = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
sharedPreference.registerOnSharedPreferenceChangeListener(this)
|
||||||
|
|
||||||
|
initPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
"delete_cache" -> {
|
||||||
|
val dir = File(requireContext().cacheDir, "imageCache")
|
||||||
|
|
||||||
|
summary = getString(R.string.settings_storage_usage_loading)
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
getDirSize(dir).let {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
this@with.summary = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
|
"delete_downloads" -> {
|
||||||
|
val dir = getDownloadDirectory(requireContext())
|
||||||
|
|
||||||
|
summary = getString(R.string.settings_storage_usage_loading)
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
getDirSize(dir).let {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
this@with.summary = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
|
"clear_history" -> {
|
||||||
|
val histories = (requireActivity().application as Pupil).histories
|
||||||
|
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
||||||
|
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
|
"dl_location" -> {
|
||||||
|
summary = getDownloadDirectory(requireContext()).canonicalPath
|
||||||
|
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
|
"default_query" -> {
|
||||||
|
summary = sharedPreference.getString("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(requireContext()).type.name
|
||||||
|
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
|
"dark_mode" -> {
|
||||||
|
onPreferenceChangeListener = this@SettingsFragment
|
||||||
|
}
|
||||||
|
"nomedia" -> {
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
|
"backup" -> {
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
|
"restore" -> {
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
|
"old_import_galleries" -> {
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
|
"user_id" -> {
|
||||||
|
summary = sharedPreference.getString("user_id", "")
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonConfiguration
|
||||||
|
import okhttp3.Dispatcher
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import xyz.quaver.proxy
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
const val REQUEST_LOCK = 38238
|
||||||
|
const val REQUEST_RESTORE = 16546
|
||||||
|
const val REQUEST_IMPORT_OLD_GALLERIES = 6458
|
||||||
|
const val REQUEST_IMPORT_OLD_GALLERIES_OLD = 5946
|
||||||
|
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
||||||
|
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
||||||
|
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
|
||||||
|
|
||||||
|
const val NOTIFICATION_ID_UPDATE = 2345
|
||||||
|
|
||||||
|
val json = Json(JsonConfiguration.Stable)
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
|
||||||
import android.util.SparseArray
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.app.TaskStackBuilder
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.crashlytics.android.Crashlytics
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.io.IOException
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
|
||||||
import xyz.quaver.hitomi.Reader
|
|
||||||
import xyz.quaver.hitomi.getReader
|
|
||||||
import xyz.quaver.hitomi.getReferer
|
|
||||||
import xyz.quaver.hiyobi.cookie
|
|
||||||
import xyz.quaver.hiyobi.user_agent
|
|
||||||
import xyz.quaver.pupil.Pupil
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.*
|
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
|
|
||||||
class GalleryDownloader(
|
|
||||||
base: Context,
|
|
||||||
private val galleryID: Int,
|
|
||||||
_notify: Boolean = false
|
|
||||||
) : ContextWrapper(base) {
|
|
||||||
|
|
||||||
private val downloads = (applicationContext as Pupil).downloads
|
|
||||||
var useHiyobi = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("use_hiyobi", false)
|
|
||||||
|
|
||||||
var download: Boolean = false
|
|
||||||
set(value) {
|
|
||||||
if (value) {
|
|
||||||
field = true
|
|
||||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
|
||||||
|
|
||||||
if (reader?.isActive == false && downloadJob?.isActive != true) {
|
|
||||||
val data = File(getDownloadDirectory(this), galleryID.toString())
|
|
||||||
val cache = File(cacheDir, "imageCache/$galleryID")
|
|
||||||
|
|
||||||
if (File(cache, "images").exists() && !data.exists()) {
|
|
||||||
cache.copyRecursively(data, true)
|
|
||||||
cache.deleteRecursively()
|
|
||||||
}
|
|
||||||
|
|
||||||
field = false
|
|
||||||
}
|
|
||||||
|
|
||||||
downloads.add(galleryID)
|
|
||||||
} else {
|
|
||||||
field = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onNotifyChangedHandler?.invoke(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val reader: Deferred<Reader>?
|
|
||||||
private var downloadJob: Job? = null
|
|
||||||
|
|
||||||
private lateinit var notificationBuilder: NotificationCompat.Builder
|
|
||||||
private lateinit var notificationManager: NotificationManagerCompat
|
|
||||||
|
|
||||||
var onReaderLoadedHandler: ((Reader) -> Unit)? = null
|
|
||||||
var onProgressHandler: ((Int) -> Unit)? = null
|
|
||||||
var onDownloadedHandler: ((List<String>) -> Unit)? = null
|
|
||||||
var onErrorHandler: ((Exception) -> Unit)? = null
|
|
||||||
var onCompleteHandler: (() -> Unit)? = null
|
|
||||||
var onNotifyChangedHandler: ((Boolean) -> Unit)? = null
|
|
||||||
|
|
||||||
companion object : SparseArray<GalleryDownloader>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
put(galleryID, this)
|
|
||||||
|
|
||||||
initNotification()
|
|
||||||
|
|
||||||
reader = CoroutineScope(Dispatchers.IO).async {
|
|
||||||
try {
|
|
||||||
download = _notify
|
|
||||||
val json = Json(JsonConfiguration.Stable)
|
|
||||||
val serializer = Reader.serializer()
|
|
||||||
|
|
||||||
//Check cache
|
|
||||||
val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "reader.json")
|
|
||||||
|
|
||||||
try {
|
|
||||||
json.parse(serializer, cache.readText())
|
|
||||||
} catch(e: Exception) {
|
|
||||||
cache.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cache.exists()) {
|
|
||||||
val cached = json.parse(serializer, cache.readText())
|
|
||||||
|
|
||||||
if (cached.readerItems.isNotEmpty()) {
|
|
||||||
useHiyobi = when {
|
|
||||||
cached.readerItems[0].url.contains("hitomi.la") -> false
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
|
|
||||||
onReaderLoadedHandler?.invoke(cached)
|
|
||||||
|
|
||||||
return@async cached
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Cache doesn't exist. Load from internet
|
|
||||||
val reader = when {
|
|
||||||
useHiyobi -> {
|
|
||||||
try {
|
|
||||||
xyz.quaver.hiyobi.getReader(galleryID)
|
|
||||||
} catch(e: Exception) {
|
|
||||||
useHiyobi = false
|
|
||||||
getReader(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
getReader(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.readerItems.isNotEmpty()) {
|
|
||||||
//Save cache
|
|
||||||
if (cache.parentFile?.exists() == false)
|
|
||||||
cache.parentFile!!.mkdirs()
|
|
||||||
|
|
||||||
cache.writeText(json.stringify(serializer, reader))
|
|
||||||
}
|
|
||||||
|
|
||||||
reader
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Crashlytics.logException(e)
|
|
||||||
Reader("", listOf())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
|
|
||||||
|
|
||||||
fun start() {
|
|
||||||
downloadJob = CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
val reader = reader!!.await()
|
|
||||||
|
|
||||||
notificationBuilder.setContentTitle(reader.title)
|
|
||||||
|
|
||||||
if (reader.readerItems.isEmpty()) {
|
|
||||||
onErrorHandler?.invoke(IOException(getString(R.string.unable_to_connect)))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val list = ArrayList<String>()
|
|
||||||
|
|
||||||
onReaderLoadedHandler?.invoke(reader)
|
|
||||||
|
|
||||||
notificationBuilder
|
|
||||||
.setProgress(reader.readerItems.size, 0, false)
|
|
||||||
.setContentText("0/${reader.readerItems.size}")
|
|
||||||
|
|
||||||
reader.readerItems.chunked(4).forEachIndexed { chunkIndex, chunked ->
|
|
||||||
chunked.mapIndexed { i, it ->
|
|
||||||
val index = chunkIndex*4+i
|
|
||||||
|
|
||||||
async(Dispatchers.IO) {
|
|
||||||
val url = if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url
|
|
||||||
|
|
||||||
val name = "$index".padStart(4, '0')
|
|
||||||
val ext = url.split('.').last()
|
|
||||||
|
|
||||||
val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "images/$name.$ext")
|
|
||||||
|
|
||||||
if (!cache.exists())
|
|
||||||
try {
|
|
||||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
|
||||||
if (useHiyobi) {
|
|
||||||
setRequestProperty("User-Agent", user_agent)
|
|
||||||
setRequestProperty("Cookie", cookie)
|
|
||||||
} else
|
|
||||||
setRequestProperty("Referer", getReferer(galleryID))
|
|
||||||
|
|
||||||
if (cache.parentFile?.exists() == false)
|
|
||||||
cache.parentFile!!.mkdirs()
|
|
||||||
|
|
||||||
inputStream.copyTo(FileOutputStream(cache))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
cache.delete()
|
|
||||||
|
|
||||||
onErrorHandler?.invoke(e)
|
|
||||||
|
|
||||||
notificationBuilder
|
|
||||||
.setContentTitle(reader.title)
|
|
||||||
.setContentText(getString(R.string.reader_notification_error))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
|
|
||||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
"images/$name.$ext"
|
|
||||||
}
|
|
||||||
}.forEach {
|
|
||||||
list.add(it.await())
|
|
||||||
|
|
||||||
val index = list.size
|
|
||||||
|
|
||||||
onProgressHandler?.invoke(index)
|
|
||||||
|
|
||||||
notificationBuilder
|
|
||||||
.setProgress(reader.readerItems.size, index, false)
|
|
||||||
.setContentText("$index/${reader.readerItems.size}")
|
|
||||||
|
|
||||||
if (download)
|
|
||||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
|
||||||
|
|
||||||
onDownloadedHandler?.invoke(list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer(false).schedule(1000) {
|
|
||||||
notificationBuilder
|
|
||||||
.setContentTitle(reader.title)
|
|
||||||
.setContentText(getString(R.string.reader_notification_complete))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
|
|
||||||
if (download) {
|
|
||||||
File(cacheDir, "imageCache/${galleryID}").let {
|
|
||||||
if (it.exists()) {
|
|
||||||
val target = File(getDownloadDirectory(this@GalleryDownloader), galleryID.toString())
|
|
||||||
|
|
||||||
if (!target.exists())
|
|
||||||
target.mkdirs()
|
|
||||||
|
|
||||||
it.copyRecursively(target, true)
|
|
||||||
it.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
|
||||||
|
|
||||||
download = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onCompleteHandler?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancel() {
|
|
||||||
downloadJob?.cancel()
|
|
||||||
|
|
||||||
remove(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun cancelAndJoin() {
|
|
||||||
downloadJob?.cancelAndJoin()
|
|
||||||
|
|
||||||
remove(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invokeOnReaderLoaded() {
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
onReaderLoadedHandler?.invoke(reader?.await() ?: return@launch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearNotification() {
|
|
||||||
notificationManager.cancel(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invokeOnNotifyChanged() {
|
|
||||||
onNotifyChangedHandler?.invoke(download)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNotification() {
|
|
||||||
val intent = Intent(this, ReaderActivity::class.java).apply {
|
|
||||||
putExtra("galleryID", galleryID)
|
|
||||||
}
|
|
||||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
|
||||||
addNextIntentWithParentStack(intent)
|
|
||||||
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager = NotificationManagerCompat.from(this)
|
|
||||||
|
|
||||||
notificationBuilder = NotificationCompat.Builder(this, "download").apply {
|
|
||||||
setContentTitle(getString(R.string.reader_loading))
|
|
||||||
setContentText(getString(R.string.reader_notification_text))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
|
||||||
setContentIntent(pendingIntent)
|
|
||||||
setProgress(0, 0, true)
|
|
||||||
priority = NotificationCompat.PRIORITY_LOW
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
311
app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
* 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.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.SparseArray
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import xyz.quaver.Code
|
||||||
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
|
import xyz.quaver.hitomi.Reader
|
||||||
|
import xyz.quaver.proxy
|
||||||
|
import xyz.quaver.pupil.util.getCachedGallery
|
||||||
|
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||||
|
import xyz.quaver.pupil.util.isParentOf
|
||||||
|
import xyz.quaver.pupil.util.json
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.locks.Lock
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
|
||||||
|
class Cache(context: Context) : ContextWrapper(context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val moving = mutableListOf<Int>()
|
||||||
|
private val readers = SparseArray<Reader?>()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val locks = SparseArray<Lock>()
|
||||||
|
private fun lock(galleryID: Int) {
|
||||||
|
synchronized(locks) {
|
||||||
|
if (locks.indexOfKey(galleryID) < 0)
|
||||||
|
locks.put(galleryID, ReentrantLock())
|
||||||
|
}
|
||||||
|
|
||||||
|
locks[galleryID].lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unlock(galleryID: Int) {
|
||||||
|
locks[galleryID]?.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
|
// Search in this order
|
||||||
|
// Download -> Cache
|
||||||
|
fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also {
|
||||||
|
if (!it.exists())
|
||||||
|
it.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCachedMetadata(galleryID: Int) : Metadata? {
|
||||||
|
val file = File(getCachedGallery(galleryID), ".metadata")
|
||||||
|
|
||||||
|
if (!file.exists())
|
||||||
|
return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
json.parse(Metadata.serializer(), file.readText())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
//File corrupted
|
||||||
|
file.delete()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
|
||||||
|
if (preference.getBoolean("cache_disable", false))
|
||||||
|
return
|
||||||
|
|
||||||
|
val file = File(getCachedGallery(galleryID), ".metadata").also {
|
||||||
|
if (!it.exists())
|
||||||
|
it.createNewFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
file.writeText(json.stringify(Metadata.serializer(), metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getThumbnail(galleryID: Int): String? {
|
||||||
|
val metadata = Cache(this).getCachedMetadata(galleryID)
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
val thumbnail = if (metadata?.thumbnail == null)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
|
||||||
|
try {
|
||||||
|
Base64.encodeToString(URL(thumbnails?.firstOrNull()).openConnection(proxy).getInputStream().use {
|
||||||
|
it.readBytes()
|
||||||
|
}, Base64.DEFAULT)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
metadata.thumbnail
|
||||||
|
|
||||||
|
setCachedMetadata(
|
||||||
|
galleryID,
|
||||||
|
Metadata(Cache(this).getCachedMetadata(galleryID), thumbnail = thumbnail)
|
||||||
|
)
|
||||||
|
|
||||||
|
return thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
|
||||||
|
val metadata = Cache(this).getCachedMetadata(galleryID)
|
||||||
|
|
||||||
|
val sources = listOf(
|
||||||
|
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||||
|
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||||
|
)
|
||||||
|
|
||||||
|
val galleryBlock = if (metadata?.galleryBlock == null) {
|
||||||
|
CoroutineScope(Dispatchers.IO).async {
|
||||||
|
var galleryBlock: GalleryBlock? = null
|
||||||
|
|
||||||
|
for (source in sources) {
|
||||||
|
galleryBlock = try {
|
||||||
|
source.invoke()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (galleryBlock != null)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryBlock
|
||||||
|
}.await() ?: return null
|
||||||
|
}
|
||||||
|
else
|
||||||
|
metadata.galleryBlock
|
||||||
|
|
||||||
|
setCachedMetadata(
|
||||||
|
galleryID,
|
||||||
|
Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock)
|
||||||
|
)
|
||||||
|
|
||||||
|
return galleryBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getReaderOrNull(galleryID: Int): Reader? {
|
||||||
|
return readers[galleryID] ?: getCachedMetadata(galleryID)?.reader
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getReader(galleryID: Int): Reader? {
|
||||||
|
val metadata = getCachedMetadata(galleryID)
|
||||||
|
val mirrors = preference.getString("mirrors", null)?.split('>') ?: listOf()
|
||||||
|
|
||||||
|
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(
|
||||||
|
Comparator { o1, o2 ->
|
||||||
|
mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else
|
||||||
|
it
|
||||||
|
}
|
||||||
|
|
||||||
|
val reader =
|
||||||
|
if (readers[galleryID] != null)
|
||||||
|
return readers[galleryID]
|
||||||
|
else if (metadata?.reader == null) {
|
||||||
|
var retval: Reader? = null
|
||||||
|
|
||||||
|
for (source in sources) {
|
||||||
|
retval = try {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
withTimeoutOrNull(1000) {
|
||||||
|
source.value.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retval != null)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
retval
|
||||||
|
} else
|
||||||
|
metadata.reader
|
||||||
|
|
||||||
|
readers.put(galleryID, reader)
|
||||||
|
|
||||||
|
setCachedMetadata(
|
||||||
|
galleryID,
|
||||||
|
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
|
||||||
|
)
|
||||||
|
|
||||||
|
return reader
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageNameRegex = Regex("""^\d+\..+$""")
|
||||||
|
fun getImages(galleryID: Int): List<File?>? {
|
||||||
|
val gallery = getCachedGallery(galleryID)
|
||||||
|
|
||||||
|
return gallery.list { _, name ->
|
||||||
|
imageNameRegex.matches(name)
|
||||||
|
}?.map {
|
||||||
|
File(gallery, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageExtensions = listOf(
|
||||||
|
"png",
|
||||||
|
"jpg",
|
||||||
|
"webp",
|
||||||
|
"gif"
|
||||||
|
)
|
||||||
|
fun getImage(galleryID: Int, index: Int): File? {
|
||||||
|
val gallery = getCachedGallery(galleryID)
|
||||||
|
|
||||||
|
for (ext in imageExtensions) {
|
||||||
|
File(gallery, "%05d.$ext".format(index)).let {
|
||||||
|
if (it.exists())
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun putImage(galleryID: Int, index: Int, ext: String, data: InputStream) {
|
||||||
|
if (preference.getBoolean("cache_disable", false))
|
||||||
|
return
|
||||||
|
|
||||||
|
val cache = File(getCachedGallery(galleryID), "%05d.$ext".format(index)).also {
|
||||||
|
if (!it.exists())
|
||||||
|
it.createNewFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
BufferedInputStream(data).use { inputStream ->
|
||||||
|
FileOutputStream(cache).use { outputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
cache.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveToDownload(galleryID: Int) {
|
||||||
|
if (preference.getBoolean("cache_disable", false))
|
||||||
|
return
|
||||||
|
|
||||||
|
if (moving.contains(galleryID))
|
||||||
|
return
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val cache = getCachedGallery(galleryID).also {
|
||||||
|
if (!it.exists())
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val download = File(getDownloadDirectory(this@Cache), galleryID.toString())
|
||||||
|
|
||||||
|
if (download.isParentOf(cache))
|
||||||
|
return@launch
|
||||||
|
|
||||||
|
FirebaseCrashlytics.getInstance().log("MOVING ${cache.canonicalPath} --> ${download.canonicalPath}")
|
||||||
|
|
||||||
|
cache.copyRecursively(download, true) { file, err ->
|
||||||
|
FirebaseCrashlytics.getInstance().log("MOVING ERROR ${file.canonicalPath} ${err.message}")
|
||||||
|
OnErrorAction.SKIP
|
||||||
|
}
|
||||||
|
FirebaseCrashlytics.getInstance().log("MOVED ${cache.canonicalPath}")
|
||||||
|
|
||||||
|
FirebaseCrashlytics.getInstance().log("DELETING ${cache.canonicalPath}")
|
||||||
|
cache.deleteRecursively()
|
||||||
|
FirebaseCrashlytics.getInstance().log("DELETED ${cache.canonicalPath}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
|
||||||
|
|
||||||
|
fun setDownloading(galleryID: Int, isDownloading: Boolean) {
|
||||||
|
setCachedMetadata(galleryID, Metadata(getCachedMetadata(galleryID), isDownloading = isDownloading))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
/*
|
||||||
|
* 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.download
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.SparseArray
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.TaskStackBuilder
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import okhttp3.*
|
||||||
|
import okio.*
|
||||||
|
import xyz.quaver.Code
|
||||||
|
import xyz.quaver.hitomi.Reader
|
||||||
|
import xyz.quaver.hitomi.getReferer
|
||||||
|
import xyz.quaver.hitomi.imageUrlFromImage
|
||||||
|
import xyz.quaver.hiyobi.cookie
|
||||||
|
import xyz.quaver.hiyobi.createImgList
|
||||||
|
import xyz.quaver.hiyobi.user_agent
|
||||||
|
import xyz.quaver.proxy
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.ui.ReaderActivity
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
|
||||||
|
|
||||||
|
private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
|
//region ProgressListener
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private val progressListener = object: ProgressListener {
|
||||||
|
override fun update(tag: Any?, bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||||
|
val (galleryID, index) = (tag as? Pair<Int, Int>) ?: return
|
||||||
|
|
||||||
|
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
|
||||||
|
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProgressListener {
|
||||||
|
fun update(tag: Any?, bytesRead : Long, contentLength: Long, done: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.update(tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||||
|
|
||||||
|
return bytesRead
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Singleton
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@Volatile private var instance: DownloadWorker? = null
|
||||||
|
|
||||||
|
fun getInstance(context: Context) =
|
||||||
|
instance ?: synchronized(this) {
|
||||||
|
instance ?: DownloadWorker(context).also { instance = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
val notificationManager = NotificationManagerCompat.from(context)
|
||||||
|
|
||||||
|
val queue = LinkedBlockingQueue<Int>()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 = SparseArray<MutableList<Float>?>()
|
||||||
|
val notification = SparseArray<NotificationCompat.Builder?>()
|
||||||
|
|
||||||
|
private val loop = loop()
|
||||||
|
private val worker = SparseArray<Job?>()
|
||||||
|
|
||||||
|
val interceptor = Interceptor { 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
val client =
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.connectTimeout(0, TimeUnit.SECONDS)
|
||||||
|
.addInterceptor(interceptor)
|
||||||
|
.readTimeout(0, TimeUnit.SECONDS)
|
||||||
|
.dispatcher(Dispatcher().apply {
|
||||||
|
maxRequests = 4
|
||||||
|
maxRequestsPerHost = 4
|
||||||
|
})
|
||||||
|
.proxy(proxy)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
queue.clear()
|
||||||
|
|
||||||
|
loop.cancel()
|
||||||
|
for (i in 0 until worker.size()) {
|
||||||
|
val galleryID = worker.keyAt(i)
|
||||||
|
|
||||||
|
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||||
|
worker[galleryID]?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
client.dispatcher().queuedCalls().filter {
|
||||||
|
it.request().tag() is Pair<*, *>
|
||||||
|
}.forEach {
|
||||||
|
it.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.clear()
|
||||||
|
notification.clear()
|
||||||
|
notificationManager.cancelAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel(galleryID: Int) {
|
||||||
|
queue.remove(galleryID)
|
||||||
|
worker[galleryID]?.cancel()
|
||||||
|
|
||||||
|
client.dispatcher().queuedCalls().filter {
|
||||||
|
((it.request().tag() as Pair<*, *>).first as Int) == galleryID
|
||||||
|
}.forEach {
|
||||||
|
it.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.remove(galleryID)
|
||||||
|
notification.remove(galleryID)
|
||||||
|
notificationManager.cancel(galleryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { it.isInfinite() } == true
|
||||||
|
|
||||||
|
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
|
||||||
|
val lowQuality = preferences.getBoolean("low_quality", false)
|
||||||
|
|
||||||
|
val request = Request.Builder().apply {
|
||||||
|
when (reader.code) {
|
||||||
|
Code.HITOMI -> {
|
||||||
|
url(
|
||||||
|
imageUrlFromImage(
|
||||||
|
galleryID,
|
||||||
|
reader.galleryInfo.files[index],
|
||||||
|
!lowQuality
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addHeader("Referer", getReferer(galleryID))
|
||||||
|
}
|
||||||
|
Code.HIYOBI -> {
|
||||||
|
url(createImgList(galleryID, reader, lowQuality)[index].path)
|
||||||
|
addHeader("User-Agent", user_agent)
|
||||||
|
addHeader("Cookie", cookie)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
//shouldn't be called anyway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tag(galleryID to index)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
client.newCall(request).enqueue(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val reader = Cache(this@DownloadWorker).getReader(galleryID)
|
||||||
|
|
||||||
|
//gallery doesn't exist
|
||||||
|
if (reader == null) {
|
||||||
|
progress.put(galleryID, null)
|
||||||
|
|
||||||
|
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val cache = Cache(this@DownloadWorker).getImages(galleryID)
|
||||||
|
|
||||||
|
progress.put(galleryID, reader.galleryInfo.files.indices.map { index ->
|
||||||
|
if (cache?.firstOrNull { it?.nameWithoutExtension?.toIntOrNull() == index } != null)
|
||||||
|
Float.POSITIVE_INFINITY
|
||||||
|
else
|
||||||
|
0F
|
||||||
|
}.toMutableList())
|
||||||
|
|
||||||
|
if (notification[galleryID] == null)
|
||||||
|
initNotification(galleryID)
|
||||||
|
|
||||||
|
notification[galleryID]?.setContentTitle(reader.galleryInfo.title)
|
||||||
|
notify(galleryID)
|
||||||
|
|
||||||
|
if (isCompleted(galleryID)) {
|
||||||
|
with(Cache(this@DownloadWorker)) {
|
||||||
|
if (isDownloading(galleryID)) {
|
||||||
|
moveToDownload(galleryID)
|
||||||
|
setDownloading(galleryID, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in reader.galleryInfo.files.indices) {
|
||||||
|
val callback = object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
if (e.message?.contains("cancel", true) != false)
|
||||||
|
return
|
||||||
|
|
||||||
|
Log.i("PUPILD", "FAIL ${call.request().tag()} (${e.message})")
|
||||||
|
FirebaseCrashlytics.getInstance().apply {
|
||||||
|
log("FAIL ${call.request().tag()} (${e.message})")
|
||||||
|
setCustomKey("POS", "FAIL")
|
||||||
|
recordException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(galleryID)
|
||||||
|
queue.add(galleryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
val ext = call.request().url().encodedPath().split('.').last()
|
||||||
|
|
||||||
|
try {
|
||||||
|
response.body().use {
|
||||||
|
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it!!.byteStream())
|
||||||
|
}
|
||||||
|
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
|
||||||
|
|
||||||
|
notify(galleryID)
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
if (isCompleted(galleryID)) {
|
||||||
|
with(Cache(this@DownloadWorker)) {
|
||||||
|
if (isDownloading(galleryID)) {
|
||||||
|
moveToDownload(galleryID)
|
||||||
|
setDownloading(galleryID, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
FirebaseCrashlytics.getInstance().apply {
|
||||||
|
log("FAIL ON OK ${call.request().tag()} (${e.message})")
|
||||||
|
setCustomKey("POS", "FAIL ON OK")
|
||||||
|
recordException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
File(Cache(this@DownloadWorker).getCachedGallery(galleryID), "%05d.$ext".format(i)).delete()
|
||||||
|
|
||||||
|
cancel(galleryID)
|
||||||
|
queue.add(galleryID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress[galleryID]?.get(i)?.isFinite() == true)
|
||||||
|
queueDownload(galleryID, reader, i, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notify(galleryID: Int) {
|
||||||
|
val max = progress[galleryID]?.size ?: 0
|
||||||
|
val progress = progress[galleryID]?.count { it.isInfinite() } ?: 0
|
||||||
|
|
||||||
|
if (isCompleted(galleryID)) {
|
||||||
|
notification[galleryID]
|
||||||
|
?.setContentText(getString(R.string.reader_notification_complete))
|
||||||
|
?.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
?.setProgress(0, 0, false)
|
||||||
|
?.setOngoing(false)
|
||||||
|
|
||||||
|
notificationManager.cancel(galleryID)
|
||||||
|
} else
|
||||||
|
notification[galleryID]
|
||||||
|
?.setProgress(max, progress, false)
|
||||||
|
?.setContentText("$progress/$max")
|
||||||
|
|
||||||
|
if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null)
|
||||||
|
notification[galleryID]?.let { notificationManager.notify(galleryID, it.build()) }
|
||||||
|
else
|
||||||
|
notificationManager.cancel(galleryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNotification(galleryID: Int) {
|
||||||
|
val intent = Intent(this, ReaderActivity::class.java).apply {
|
||||||
|
putExtra("galleryID", galleryID)
|
||||||
|
}
|
||||||
|
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||||
|
addNextIntentWithParentStack(intent)
|
||||||
|
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
|
||||||
|
setContentTitle(getString(R.string.reader_loading))
|
||||||
|
setContentText(getString(R.string.reader_notification_text))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P
|
||||||
|
setContentIntent(pendingIntent)
|
||||||
|
setProgress(0, 0, true)
|
||||||
|
setOngoing(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
while (true) {
|
||||||
|
if (queue.isEmpty())
|
||||||
|
continue
|
||||||
|
|
||||||
|
val galleryID = queue.peek() ?: continue
|
||||||
|
|
||||||
|
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
|
||||||
|
cancel(galleryID)
|
||||||
|
|
||||||
|
if (notification[galleryID] == null)
|
||||||
|
initNotification(galleryID)
|
||||||
|
|
||||||
|
if (Cache(this@DownloadWorker).isDownloading(galleryID))
|
||||||
|
notification[galleryID]?.let { notificationManager.notify(galleryID, it.build()) }
|
||||||
|
|
||||||
|
worker.put(galleryID, download(galleryID))
|
||||||
|
queue.poll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
app/src/main/java/xyz/quaver/pupil/util/download/Metadata.kt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.download
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
|
import xyz.quaver.hitomi.Reader
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Metadata(
|
||||||
|
val thumbnail: String? = null,
|
||||||
|
val galleryBlock: GalleryBlock? = null,
|
||||||
|
val reader: Reader? = null,
|
||||||
|
val isDownloading: Boolean? = null
|
||||||
|
) {
|
||||||
|
constructor(
|
||||||
|
metadata: Metadata?,
|
||||||
|
thumbnail: String? = null,
|
||||||
|
galleryBlock: GalleryBlock? = null,
|
||||||
|
readers: Reader? = null,
|
||||||
|
isDownloading: Boolean? = null
|
||||||
|
) : this(
|
||||||
|
thumbnail ?: metadata?.thumbnail,
|
||||||
|
galleryBlock ?: metadata?.galleryBlock,
|
||||||
|
readers ?: metadata?.reader,
|
||||||
|
isDownloading ?: metadata?.isDownloading
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -18,26 +18,200 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.storage.StorageManager
|
||||||
import android.provider.MediaStore
|
import android.provider.DocumentsContract
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.lang.reflect.Array
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
fun getCachedGallery(context: Context, galleryID: Int): File {
|
|
||||||
return File(getDownloadDirectory(context), galleryID.toString()).let {
|
|
||||||
when {
|
|
||||||
it.exists() -> it
|
|
||||||
else -> File(context.cacheDir, "imageCache/$galleryID")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
fun getCachedGallery(context: Context, galleryID: Int) =
|
||||||
fun getDownloadDirectory(context: Context): File? {
|
File(getDownloadDirectory(context), galleryID.toString()).let {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
if (it.exists())
|
||||||
context.getExternalFilesDir("Pupil")
|
it
|
||||||
else
|
else
|
||||||
File(Environment.getExternalStorageDirectory(), "Pupil")
|
File(context.cacheDir, "imageCache/$galleryID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDownloadDirectory(context: Context) =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
|
||||||
|
if (it != null && !it.startsWith("content"))
|
||||||
|
File(it)
|
||||||
|
else
|
||||||
|
context.getExternalFilesDir(null)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
||||||
|
|
||||||
|
if (to.parentFile?.exists() == false)
|
||||||
|
to.parentFile!!.mkdirs()
|
||||||
|
|
||||||
|
if (!to.exists())
|
||||||
|
to.createNewFile()
|
||||||
|
|
||||||
|
FileOutputStream(to).use { out ->
|
||||||
|
|
||||||
|
with(openConnection()) {
|
||||||
|
val fileSize = contentLength.toLong()
|
||||||
|
|
||||||
|
getInputStream().use {
|
||||||
|
|
||||||
|
var bytesCopied: Long = 0
|
||||||
|
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
|
||||||
|
var bytes = it.read(buffer)
|
||||||
|
while (bytes >= 0) {
|
||||||
|
out.write(buffer, 0, bytes)
|
||||||
|
bytesCopied += bytes
|
||||||
|
onDownloadProgress?.invoke(bytesCopied, fileSize)
|
||||||
|
bytes = it.read(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExtSdCardPaths(context: Context) =
|
||||||
|
ContextCompat.getExternalFilesDirs(context, null).drop(1).map {
|
||||||
|
it.absolutePath.substringBeforeLast("/Android/data").let { path ->
|
||||||
|
runCatching {
|
||||||
|
File(path).canonicalPath
|
||||||
|
}.getOrElse {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val PRIMARY_VOLUME_NAME = "primary"
|
||||||
|
fun getVolumePath(context: Context, volumeID: String?): String? {
|
||||||
|
return runCatching {
|
||||||
|
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||||
|
val storageVolumeClass = Class.forName("android.os.storage.StorageVolume")
|
||||||
|
|
||||||
|
val getVolumeList = storageVolumeClass.javaClass.getMethod("getVolumeList")
|
||||||
|
val getUUID = storageVolumeClass.getMethod("getUuid")
|
||||||
|
val getPath = storageVolumeClass.getMethod("getPath")
|
||||||
|
val isPrimary = storageVolumeClass.getMethod("isPrimary")
|
||||||
|
|
||||||
|
val result = getVolumeList.invoke(storageManager)!!
|
||||||
|
|
||||||
|
val length = Array.getLength(result)
|
||||||
|
|
||||||
|
for (i in 0 until length) {
|
||||||
|
val storageVolumeElement = Array.get(result, i)
|
||||||
|
val uuid = getUUID.invoke(storageVolumeElement) as? String
|
||||||
|
val primary = isPrimary.invoke(storageVolumeElement) as? Boolean
|
||||||
|
|
||||||
|
// primary volume?
|
||||||
|
if (primary == true && volumeID == PRIMARY_VOLUME_NAME)
|
||||||
|
return@runCatching getPath.invoke(storageVolumeElement) as? String
|
||||||
|
|
||||||
|
// other volumes?
|
||||||
|
if (volumeID == uuid) {
|
||||||
|
return@runCatching getPath.invoke(storageVolumeElement) as? String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@runCatching null
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credits go to https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri/36162691#36162691
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
fun getVolumeIdFromTreeUri(uri: Uri) =
|
||||||
|
DocumentsContract.getTreeDocumentId(uri).split(':').let {
|
||||||
|
if (it.isNotEmpty())
|
||||||
|
it[0]
|
||||||
|
else
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
fun getDocumentPathFromTreeUri(uri: Uri) =
|
||||||
|
DocumentsContract.getTreeDocumentId(uri).split(':').let {
|
||||||
|
if (it.size >= 2)
|
||||||
|
it[1]
|
||||||
|
else
|
||||||
|
File.separator
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFullPathFromTreeUri(context: Context, uri: Uri) : String? {
|
||||||
|
val volumePath = getVolumePath(context, getVolumeIdFromTreeUri(uri) ?: return null).let {
|
||||||
|
it ?: return File.separator
|
||||||
|
|
||||||
|
if (it.endsWith(File.separator))
|
||||||
|
it.dropLast(1)
|
||||||
|
else
|
||||||
|
it
|
||||||
|
}
|
||||||
|
|
||||||
|
val documentPath = getDocumentPathFromTreeUri(uri).let {
|
||||||
|
if (it.endsWith(File.separator))
|
||||||
|
it.dropLast(1)
|
||||||
|
else
|
||||||
|
it
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (documentPath.isNotEmpty()) {
|
||||||
|
if (documentPath.startsWith(File.separator))
|
||||||
|
volumePath + documentPath
|
||||||
|
else
|
||||||
|
volumePath + File.separator + documentPath
|
||||||
|
} else
|
||||||
|
volumePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Huge thanks to avluis(https://github.com/avluis)
|
||||||
|
// This code is originated from Hentoid(https://github.com/avluis/Hentoid) under Apache-2.0 license.
|
||||||
|
fun Uri.toFile(context: Context): File? {
|
||||||
|
val path = this.path ?: return null
|
||||||
|
|
||||||
|
val pathSeparator = path.indexOf(':')
|
||||||
|
val folderName = path.substring(pathSeparator+1)
|
||||||
|
|
||||||
|
// Determine whether the designated file is
|
||||||
|
// - on a removable media (e.g. SD card, OTG)
|
||||||
|
// or
|
||||||
|
// - on the internal phone memory
|
||||||
|
val removableMediaFolderRoots = getExtSdCardPaths(context)
|
||||||
|
|
||||||
|
/* First test is to compare root names with known roots of removable media
|
||||||
|
* In many cases, the SD card root name is shared between pre-SAF (File) and SAF (DocumentFile) frameworks
|
||||||
|
* (e.g. /storage/3437-3934 vs. /tree/3437-3934)
|
||||||
|
* This is what the following block is trying to do
|
||||||
|
*/
|
||||||
|
for (s in removableMediaFolderRoots) {
|
||||||
|
val sRoot = s.substring(s.lastIndexOf(File.separatorChar))
|
||||||
|
val root = path.substring(0, pathSeparator).let {
|
||||||
|
it.substring(it.lastIndexOf(File.separatorChar))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sRoot.equals(root, true)) {
|
||||||
|
return File(s + File.separatorChar + folderName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* In some other cases, there is no common name (e.g. /storage/sdcard1 vs. /tree/3437-3934)
|
||||||
|
* We can use a slower method to translate the Uri obtained with SAF into a pre-SAF path
|
||||||
|
* and compare it to the known removable media volume names
|
||||||
|
*/
|
||||||
|
val root = getFullPathFromTreeUri(context, this)
|
||||||
|
|
||||||
|
for (s in removableMediaFolderRoots) {
|
||||||
|
if (root?.startsWith(s) == true) {
|
||||||
|
return File(root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun File.isParentOf(another: File) =
|
||||||
|
another.absolutePath.startsWith(this.absolutePath)
|
||||||
@@ -18,15 +18,15 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.builtins.list
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
import kotlinx.serialization.builtins.serializer
|
||||||
import kotlinx.serialization.parseList
|
|
||||||
import kotlinx.serialization.stringify
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class Histories(private val file: File) : ArrayList<Int>() {
|
class Histories(private val file: File) : ArrayList<Int>() {
|
||||||
|
|
||||||
|
val serializer: KSerializer<List<Int>> = Int.serializer().list
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (!file.exists())
|
if (!file.exists())
|
||||||
file.parentFile?.mkdirs()
|
file.parentFile?.mkdirs()
|
||||||
@@ -38,21 +38,20 @@ class Histories(private val file: File) : ArrayList<Int>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
fun load() : Histories {
|
fun load() : Histories {
|
||||||
return apply {
|
return apply {
|
||||||
super.clear()
|
super.clear()
|
||||||
addAll(
|
super.addAll(
|
||||||
Json(JsonConfiguration.Stable).parseList(
|
json.parse(
|
||||||
|
serializer,
|
||||||
file.bufferedReader().use { it.readText() }
|
file.bufferedReader().use { it.readText() }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
fun save() {
|
fun save() {
|
||||||
file.writeText(Json(JsonConfiguration.Stable).stringify(this))
|
file.writeText(json.stringify(serializer, this))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(element: Int): Boolean {
|
override fun add(element: Int): Boolean {
|
||||||
@@ -68,6 +67,20 @@ class Histories(private val file: File) : ArrayList<Int>() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addAll(elements: Collection<Int>): Boolean {
|
||||||
|
load()
|
||||||
|
|
||||||
|
for (e in elements) {
|
||||||
|
if (contains(e))
|
||||||
|
super.remove(e)
|
||||||
|
super.add(0, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
save()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun remove(element: Int): Boolean {
|
override fun remove(element: Int): Boolean {
|
||||||
load()
|
load()
|
||||||
val retval = super.remove(element)
|
val retval = super.remove(element)
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ package xyz.quaver.pupil.util
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.builtins.list
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
@@ -73,7 +72,6 @@ class LockManager(base: Context): ContextWrapper(base) {
|
|||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
private fun load() {
|
private fun load() {
|
||||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||||
|
|
||||||
@@ -82,17 +80,16 @@ class LockManager(base: Context): ContextWrapper(base) {
|
|||||||
lock.writeText("[]")
|
lock.writeText("[]")
|
||||||
}
|
}
|
||||||
|
|
||||||
locks = ArrayList(Json(JsonConfiguration.Stable).parseList(lock.readText()))
|
locks = ArrayList(json.parse(Lock.serializer().list, lock.readText()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
private fun save() {
|
private fun save() {
|
||||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||||
|
|
||||||
if (!lock.exists())
|
if (!lock.exists())
|
||||||
lock.createNewFile()
|
lock.createNewFile()
|
||||||
|
|
||||||
lock.writeText(Json(JsonConfiguration.Stable).stringify(locks?.toList() ?: listOf()))
|
lock.writeText(json.stringify(Lock.serializer().list, locks?.toList() ?: listOf()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(lock: Lock) {
|
fun add(lock: Lock) {
|
||||||
|
|||||||
@@ -18,18 +18,37 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.PackageManager
|
import java.util.*
|
||||||
import androidx.core.content.ContextCompat
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
fun Context.hasPermission(permission: String) =
|
|
||||||
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
fun String.wordCapitalize() : String {
|
fun String.wordCapitalize() : String {
|
||||||
val result = ArrayList<String>()
|
val result = ArrayList<String>()
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
for (word in this.split(" "))
|
for (word in this.split(" "))
|
||||||
result.add(word.capitalize())
|
result.add(word.capitalize(Locale.US))
|
||||||
|
|
||||||
return result.joinToString(" ")
|
return result.joinToString(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun byteToString(byte: Long, precision : Int = 1) : String {
|
||||||
|
|
||||||
|
val suffix = listOf(
|
||||||
|
"B",
|
||||||
|
"kB",
|
||||||
|
"MB",
|
||||||
|
"GB",
|
||||||
|
"TB" //really?
|
||||||
|
)
|
||||||
|
var size = byte.toDouble(); var suffixIndex = 0
|
||||||
|
|
||||||
|
while (size >= 1024) {
|
||||||
|
size /= 1024
|
||||||
|
suffixIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
return "%.${precision}f ${suffix[suffixIndex]}".format(size)
|
||||||
|
|
||||||
|
}
|
||||||
63
app/src/main/java/xyz/quaver/pupil/util/proxy.kt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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 androidx.preference.PreferenceManager
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
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 == null || port == null)
|
||||||
|
return Proxy.NO_PROXY
|
||||||
|
else
|
||||||
|
Proxy(type, InetSocketAddress.createUnresolved(host, port))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authenticator() = Authenticator { _, response ->
|
||||||
|
val credential = Credentials.basic(username ?: "", password ?: "")
|
||||||
|
|
||||||
|
response.request().newBuilder()
|
||||||
|
.header("Proxy-Authorization", credential)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProxy(context: Context) =
|
||||||
|
getProxyInfo(context).proxy()
|
||||||
|
|
||||||
|
fun getProxyInfo(context: Context) =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).getString("proxy", null).let {
|
||||||
|
if (it == null)
|
||||||
|
ProxyInfo(Proxy.Type.DIRECT)
|
||||||
|
else
|
||||||
|
json.parse(ProxyInfo.serializer(), it)
|
||||||
|
}
|
||||||
@@ -18,32 +18,61 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import kotlinx.serialization.json.*
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.boolean
|
||||||
|
import kotlinx.serialization.json.content
|
||||||
|
import okhttp3.*
|
||||||
|
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.proxy
|
||||||
|
import xyz.quaver.pupil.BroadcastReciever
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
|
import xyz.quaver.pupil.util.download.Metadata
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
fun getReleases(url: String) : JsonArray {
|
fun getReleases(url: String) : JsonArray {
|
||||||
return try {
|
return try {
|
||||||
URL(url).readText().let {
|
URL(url).readText().let {
|
||||||
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it)
|
json.parse(JsonArray.serializer(), it)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
JsonArray(emptyList())
|
JsonArray(emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkUpdate(url: String) : JsonObject? {
|
fun checkUpdate(context: Context, url: String) : JsonObject? {
|
||||||
val releases = getReleases(url)
|
val releases = getReleases(url)
|
||||||
|
|
||||||
if (releases.isEmpty())
|
if (releases.isEmpty())
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return releases.firstOrNull {
|
return releases.firstOrNull {
|
||||||
if (BuildConfig.PRERELEASE) {
|
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("beta", false))
|
||||||
true
|
true
|
||||||
} else {
|
else
|
||||||
it.jsonObject["prerelease"]?.boolean == false
|
it.jsonObject["prerelease"]?.boolean == false
|
||||||
}
|
|
||||||
}?.let {
|
}?.let {
|
||||||
if (it.jsonObject["tag_name"]?.content == BuildConfig.VERSION_NAME)
|
if (it.jsonObject["tag_name"]?.content == BuildConfig.VERSION_NAME)
|
||||||
null
|
null
|
||||||
@@ -52,13 +81,255 @@ fun checkUpdate(url: String) : JsonObject? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApkUrl(releases: JsonObject) : Pair<String?, String?>? {
|
fun getApkUrl(releases: JsonObject) : String? {
|
||||||
return releases["assets"]?.jsonArray?.firstOrNull {
|
return releases["assets"]?.jsonArray?.firstOrNull {
|
||||||
Regex("Pupil-v(\\d+\\.)+\\d+\\.apk").matches(it.jsonObject["name"]?.content ?: "")
|
Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.content ?: "")
|
||||||
}.let {
|
}.let {
|
||||||
if (it == null)
|
it?.jsonObject?.get("browser_download_url")?.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val UPDATE_NOTIFICATION_ID = 384823
|
||||||
|
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"]!!.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"]?.content, result.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
val update =
|
||||||
|
checkUpdate(context, 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.yes) { _, _ ->
|
||||||
|
|
||||||
|
val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
|
//Cancel any download queued before
|
||||||
|
|
||||||
|
val id = preference.getLong("update_download_id", -1)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
preference.edit().putLong("update_download_id", it).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setNegativeButton(if (force) android.R.string.no else R.string.ignore_update) { _, _ ->
|
||||||
|
if (!force)
|
||||||
|
preferences.edit()
|
||||||
|
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cancelImport = false
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
fun importOldGalleries(context: Context, folder: File) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(0, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(0, TimeUnit.SECONDS)
|
||||||
|
.proxy(proxy)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val cancelIntent = Intent(context, BroadcastReciever::class.java).apply {
|
||||||
|
action = BroadcastReciever.ACTION_CANCEL_IMPORT
|
||||||
|
putExtra(BroadcastReciever.EXTRA_IMPORT_NOTIFICATION_ID, 0)
|
||||||
|
}
|
||||||
|
val pendingIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, 0)
|
||||||
|
|
||||||
|
val notificationManager = NotificationManagerCompat.from(context)
|
||||||
|
val notificationBuilder = NotificationCompat.Builder(context, "import").apply {
|
||||||
|
setContentTitle(context.getText(R.string.import_old_galleries_notification))
|
||||||
|
setProgress(0, 0, true)
|
||||||
|
setSmallIcon(R.drawable.ic_notification)
|
||||||
|
addAction(0, context.getText(android.R.string.cancel), pendingIntent)
|
||||||
|
setOngoing(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.notify(0, notificationBuilder.build())
|
||||||
|
|
||||||
|
if (!folder.isDirectory)
|
||||||
|
return@launch
|
||||||
|
|
||||||
|
val galleryRegex = Regex("""[0-9]+$""")
|
||||||
|
val imageRegex = Regex("""^[0-9]+\..+$""")
|
||||||
|
var size = 0
|
||||||
|
fun setProgress(progress: Int) {
|
||||||
|
notificationBuilder.apply {
|
||||||
|
setContentText(
|
||||||
|
context.getString(
|
||||||
|
R.string.import_old_galleries_notification_text,
|
||||||
|
progress,
|
||||||
|
size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setProgress(size, progress, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.notify(0, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.listFiles { _, name ->
|
||||||
|
galleryRegex.matches(name)
|
||||||
|
}?.also {
|
||||||
|
size = it.size
|
||||||
|
setProgress(0)
|
||||||
|
}?.forEachIndexed { index, gallery ->
|
||||||
|
if (cancelImport)
|
||||||
|
return@forEachIndexed
|
||||||
|
|
||||||
|
setProgress(index)
|
||||||
|
|
||||||
|
val galleryID = gallery.name.toIntOrNull() ?: return@forEachIndexed
|
||||||
|
|
||||||
|
File(getDownloadDirectory(context), galleryID.toString()).mkdirs()
|
||||||
|
|
||||||
|
val reader = async {
|
||||||
|
kotlin.runCatching {
|
||||||
|
json.parse(Reader.serializer(), File(gallery, "reader.json").readText())
|
||||||
|
}.getOrElse {
|
||||||
|
getReader(galleryID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val galleryBlock = async {
|
||||||
|
kotlin.runCatching {
|
||||||
|
json.parse(GalleryBlock.serializer(), File(gallery, "galleryBlock.json").readText())
|
||||||
|
}.getOrElse {
|
||||||
|
getGalleryBlock(galleryID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val thumbnail = async thumbnail@{
|
||||||
|
val galleryBlock = galleryBlock.await()
|
||||||
|
|
||||||
|
Base64.encodeToString(try {
|
||||||
|
File(gallery, "thumbnail.jpg").readBytes()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val url = galleryBlock?.thumbnails?.firstOrNull()
|
||||||
|
|
||||||
|
if (url == null)
|
||||||
null
|
null
|
||||||
else
|
else {
|
||||||
Pair(it.jsonObject["browser_download_url"]?.content, it.jsonObject["name"]?.content)
|
val request = Request.Builder().url(url).build()
|
||||||
|
|
||||||
|
var done = false
|
||||||
|
var result: ByteArray? = null
|
||||||
|
client.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call?, e: IOException?) {
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call?, response: Response?) {
|
||||||
|
result = response?.body()?.use {
|
||||||
|
it.bytes()
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!done)
|
||||||
|
yield()
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
} ?: return@thumbnail null, Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache(context).setCachedMetadata(galleryID,
|
||||||
|
Metadata(
|
||||||
|
thumbnail.await(),
|
||||||
|
galleryBlock.await(),
|
||||||
|
reader.await()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
File(gallery, "images").listFiles { _, name ->
|
||||||
|
imageRegex.matches(name)
|
||||||
|
}?.forEach {
|
||||||
|
if (cancelImport)
|
||||||
|
return@forEach
|
||||||
|
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val index = it.nameWithoutExtension.toIntOrNull() ?: return@forEach
|
||||||
|
|
||||||
|
Cache(context).putImage(galleryID, index, it.extension, it.inputStream())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notificationBuilder.apply {
|
||||||
|
setContentText(context.getText(R.string.import_old_galleries_notification_done))
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
setOngoing(false)
|
||||||
|
mActions.clear()
|
||||||
|
}
|
||||||
|
notificationManager.notify(0, notificationBuilder.build())
|
||||||
|
|
||||||
|
cancelImport = false
|
||||||
|
}
|
||||||
24
app/src/main/res/anim/shake.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?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" />
|
||||||
21
app/src/main/res/anim/shake_cycle.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?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" />
|
||||||
23
app/src/main/res/color/lock_fab.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?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: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 620 B |
|
Before Width: | Height: | Size: 975 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 255 B |
|
Before Width: | Height: | Size: 965 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 793 B |
|
Before Width: | Height: | Size: 802 B |
|
Before Width: | Height: | Size: 495 B |
|
Before Width: | Height: | Size: 639 B |
|
Before Width: | Height: | Size: 733 B |
|
Before Width: | Height: | Size: 817 B |
|
Before Width: | Height: | Size: 670 B |
|
Before Width: | Height: | Size: 934 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 979 B |
|
Before Width: | Height: | Size: 636 B |
|
Before Width: | Height: | Size: 760 B |
|
Before Width: | Height: | Size: 947 B |
|
Before Width: | Height: | Size: 1001 B |
BIN
app/src/main/res/drawable-mdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 183 B |
|
Before Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 892 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 311 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 948 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 432 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |