Compare commits
217 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1521bc1223 | ||
|
|
df3a478ef3 | ||
|
|
974ddf69d5 | ||
|
|
56a91268de | ||
|
|
3dda2f9a1c | ||
|
|
ed20456f9f | ||
|
|
281d4a0023 | ||
|
|
2170403662 | ||
|
|
b1c1e96135 | ||
|
|
a8de1429c1 | ||
|
|
3ba6cb81ae | ||
|
|
acc85da80f | ||
|
|
b53de8624d | ||
|
|
6e2eeb29cc | ||
|
|
62eb28ac01 | ||
|
|
fd298529bf | ||
|
|
297ce506b1 | ||
|
|
18c6954be3 | ||
|
|
cea3fb1e65 | ||
|
|
7f274fd238 | ||
|
|
439a8e93ec | ||
|
|
83801feee9 | ||
|
|
8a6860c96e | ||
|
|
5c959f2987 | ||
|
|
4e4397287a | ||
|
|
fe02abc9e8 | ||
|
|
59347ab317 | ||
|
|
f408a91176 | ||
|
|
6f6956ce27 | ||
|
|
4ecad8eccc | ||
|
|
486fbe46a0 | ||
|
|
1ddb636dd0 | ||
|
|
081c890b4e | ||
|
|
86d528ba13 | ||
|
|
6bda3cb75a | ||
|
|
12d8949c9e | ||
|
|
ffc7c2aa67 | ||
|
|
5ec67488eb | ||
|
|
be64703d3c | ||
|
|
705925a050 | ||
|
|
29665be34d | ||
|
|
1edf986acf | ||
|
|
37be8ccf7f | ||
|
|
ead68b5201 | ||
|
|
4409664698 | ||
|
|
fc6bc7965c | ||
|
|
f70eccb1da | ||
|
|
861994e804 | ||
|
|
2b8facfb97 | ||
|
|
9583897ada | ||
|
|
7704c96955 | ||
|
|
c96d609803 | ||
|
|
aa0e5000ab | ||
|
|
7ca4418a50 | ||
|
|
fdd9b02388 | ||
|
|
ece127e982 | ||
|
|
5488e14f32 | ||
|
|
3558d826fb | ||
|
|
68c94d1d8b | ||
|
|
1a4ae5dfc6 | ||
|
|
1a95afe266 | ||
|
|
6579db3cc8 | ||
|
|
ceac01533a | ||
|
|
216914882c | ||
|
|
735dbab695 | ||
|
|
dbaab152ef | ||
|
|
9da1b30984 | ||
|
|
9415ab4ef9 | ||
|
|
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 |
3
.gitignore
vendored
@@ -16,4 +16,5 @@
|
||||
/gh-pages
|
||||
|
||||
#Private files
|
||||
**/google-services.json
|
||||
**/google-services.json
|
||||
**/credentials.json
|
||||
19
.idea/codeStyles/Project.xml
generated
@@ -1,10 +1,23 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="RIGHT_MARGIN" value="120" />
|
||||
<AndroidXmlCodeStyleSettings>
|
||||
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="java.util" alias="false" withSubpackages="false" />
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
<package name="java" alias="false" withSubpackages="true" />
|
||||
<package name="javax" alias="false" withSubpackages="true" />
|
||||
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||
<package name="" alias="true" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
|
||||
6
.idea/copyright/Apache.xml
generated
@@ -1,6 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value=" Copyright &#36;today.year tom5079 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License." />
|
||||
<option name="myName" value="Apache" />
|
||||
</copyright>
|
||||
</component>
|
||||
1
.idea/copyright/profiles_settings.xml
generated
@@ -2,7 +2,6 @@
|
||||
<settings>
|
||||
<module2copyright>
|
||||
<element module="Pupil" copyright="GPL" />
|
||||
<element module="libpupil" copyright="Apache" />
|
||||
</module2copyright>
|
||||
</settings>
|
||||
</component>
|
||||
7
.idea/dictionaries/tom50.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="tom50">
|
||||
<words>
|
||||
<w>hitomi</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
3
.idea/gradle.xml
generated
@@ -1,15 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/libpupil" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
|
||||
65
.idea/jarRepositories.xml
generated
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven2" />
|
||||
<option name="name" value="maven2" />
|
||||
<option name="url" value="http://guardian.github.com/maven/repo-releases" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven" />
|
||||
<option name="name" value="maven" />
|
||||
<option name="url" value="https://jitpack.io" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenRepo" />
|
||||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven2" />
|
||||
<option name="name" value="maven2" />
|
||||
<option name="url" value="https://guardian.github.com/maven/repo-releases" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven3" />
|
||||
<option name="name" value="maven3" />
|
||||
<option name="url" value="https://s3.amazonaws.com/fabric-artifacts-private/internal-snapshots" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven4" />
|
||||
<option name="name" value="maven4" />
|
||||
<option name="url" value="https://maven.fabric.io/public" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenLocal" />
|
||||
<option name="name" value="MavenLocal" />
|
||||
<option name="url" value="file:/$USER_HOME$/.m2/repository/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenLocal" />
|
||||
<option name="name" value="MavenLocal" />
|
||||
<option name="url" value="file:/$USER_HOME$/.m2/repository" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
3
.idea/scopes/libpupil.xml
generated
@@ -1,3 +0,0 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<scope name="libpupil" pattern="file[libpupil]:*/" />
|
||||
</component>
|
||||
@@ -1,7 +1,9 @@
|
||||
# Pupil
|
||||
|
||||

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

|
||||
|
||||
110
app/build.gradle
@@ -3,93 +3,113 @@ apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
|
||||
|
||||
if (file("google-services.json").exists()) {
|
||||
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: 'io.fabric'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
apply plugin: 'com.google.firebase.firebase-perf'
|
||||
} else {
|
||||
logger.lifecycle("Firebase Disabled")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
defaultConfig {
|
||||
applicationId "xyz.quaver.pupil"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
versionCode 33
|
||||
versionName "5.0"
|
||||
targetSdkVersion 30
|
||||
versionCode 59
|
||||
versionName "5.0.2"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled true
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
debug {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-DEBUG"
|
||||
|
||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
|
||||
ext.enableCrashlytics = false
|
||||
ext.alwaysUpdateBuildId = false
|
||||
}
|
||||
buildTypes.each {
|
||||
it.buildConfigField('boolean', 'CENSOR', 'false')
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
buildToolsVersion = '29.0.2'
|
||||
buildToolsVersion = '29.0.3'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def markwonVersion = "3.0.1"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
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.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
|
||||
//implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha04'
|
||||
implementation 'com.google.firebase:firebase-core:17.2.2'
|
||||
implementation 'com.google.firebase:firebase-perf:19.0.5'
|
||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
||||
implementation 'com.google.firebase:firebase-core:17.5.0'
|
||||
implementation 'com.google.firebase:firebase-analytics:17.5.0'
|
||||
implementation 'com.google.firebase:firebase-crashlytics:17.2.1'
|
||||
implementation 'com.google.firebase:firebase-perf:19.0.8'
|
||||
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
|
||||
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
||||
implementation 'com.github.clans:fab:1.6.4'
|
||||
implementation 'com.github.bumptech.glide:glide:4.10.0'
|
||||
implementation('com.github.bumptech.glide:recyclerview-integration:4.11.0') {
|
||||
//implementation 'com.quiph.ui:recyclerviewfastscroller:0.2.1'
|
||||
//noinspection GradleDependency
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
implementation ("com.github.bumptech.glide:okhttp3-integration:4.11.0") {
|
||||
transitive = false
|
||||
}
|
||||
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
|
||||
}
|
||||
implementation 'com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2'
|
||||
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
|
||||
implementation 'com.gu:option:1.3'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||
kapt 'com.github.bumptech.glide:compiler:4.10.0'
|
||||
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
|
||||
implementation "ru.noties.markwon:core:3.1.0"
|
||||
implementation ("xyz.quaver:libpupil:1.6") {
|
||||
exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-serialization-core-jvm'
|
||||
}
|
||||
implementation "xyz.quaver:documentfilex:0.2.15"
|
||||
testImplementation 'junit:junit:4.13'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
implementation project(path: ':libpupil')
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
}
|
||||
BIN
app/libs/kotlinx-serialization-core-1.0.0-RC.jar
Normal file
BIN
app/libs/pinlockview-release.aar
Normal file
BIN
app/libs/recyclerviewfastscroller-release.aar
Normal file
32
app/proguard-rules.pro
vendored
@@ -18,4 +18,34 @@
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# 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
|
||||
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.SerializationKt
|
||||
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; } # <-- change package name to your app's
|
||||
-keepclassmembers class xyz.quaver.pupil.** { # <-- change package name to your app's
|
||||
*** Companion;
|
||||
}
|
||||
-keepclasseswithmembers class xyz.quaver.pupil.** { # <-- change package name to your app's
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
|
||||
-keep class xyz.quaver.pupil.util.Preferences
|
||||
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": 59,
|
||||
"versionName": "5.0.1-hotfix2",
|
||||
"enabled": true,
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":33,"versionName":"5.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||
@@ -20,31 +20,10 @@
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hiyobi.createImgList
|
||||
import xyz.quaver.hiyobi.getReader
|
||||
import xyz.quaver.hiyobi.user_agent
|
||||
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 xyz.quaver.pupil.util.updateOldReaderGalleries
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
@@ -58,100 +37,5 @@ class ExampleInstrumentedTest {
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
Log.i("PUPILD", getDownloadDirectory(appContext).absolutePath ?: "")
|
||||
assertEquals("xyz.quaver.pupil", appContext.packageName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkCacheDir() {
|
||||
val activityTestRule = ActivityTestRule(LockActivity::class.java)
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
ContextCompat.getExternalFilesDirs(appContext, null).forEachIndexed { index, file ->
|
||||
Log.i("PUPILD", "$index: ${file?.absolutePath}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_doSearch() {
|
||||
val reader = getReader( 1426382)
|
||||
|
||||
val data: ByteArray
|
||||
|
||||
with(URL(createImgList(1426382, reader)[0].path).openConnection() as HttpsURLConnection) {
|
||||
setRequestProperty("User-Agent", user_agent)
|
||||
setRequestProperty("Cookie", cookie)
|
||||
|
||||
data = inputStream.readBytes()
|
||||
}
|
||||
|
||||
Log.d("Pupil", data.size.toString())
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
@Test
|
||||
fun test_deleteCodeFromReader() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
val json = Json(JsonConfiguration.Stable)
|
||||
|
||||
listOf(
|
||||
getDownloadDirectory(context),
|
||||
File(context.cacheDir, "imageCache")
|
||||
).forEach { root ->
|
||||
root.listFiles()?.forEach gallery@{ gallery ->
|
||||
val reader = json.parseJson(File(gallery, "reader.json").apply {
|
||||
if (!exists())
|
||||
return@gallery
|
||||
}.readText())
|
||||
.jsonObject.toMutableMap()
|
||||
|
||||
Log.d("PUPILD", gallery.name)
|
||||
|
||||
reader.remove("code")
|
||||
|
||||
File(gallery, "reader.json").writeText(JsonObject(reader).toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_updateOldReader() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
updateOldReaderGalleries(context)
|
||||
}
|
||||
|
||||
@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.isFinite() } == 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)?.title ?: "null")
|
||||
}
|
||||
|
||||
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.title ?: "null")
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
@@ -17,7 +17,6 @@
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="3"/>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name" translatable="false" tools:override="true">Pupil-Debug</string>
|
||||
</resources>
|
||||
@@ -6,9 +6,10 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="21" />
|
||||
<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
|
||||
android:name=".Pupil"
|
||||
@@ -19,10 +20,12 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:replace="android:theme">
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:replace="android:theme"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<provider
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
@@ -33,6 +36,17 @@
|
||||
|
||||
</provider>
|
||||
|
||||
<service android:name=".services.DownloadService"
|
||||
android:exported="false"/>
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.UpdateBroadcastReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity android:name=".ui.LockActivity" />
|
||||
<activity
|
||||
android:name=".ui.ReaderActivity"
|
||||
@@ -44,6 +58,61 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/galleries"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/manga"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/doujinshi"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/cg"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/galleries"
|
||||
@@ -55,6 +124,61 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/manga"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/doujinshi"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/cg"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hiyobi.me"
|
||||
android:scheme="http"
|
||||
android:pathPrefix="/reader" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hiyobi.me"
|
||||
android:pathPrefix="/reader"
|
||||
@@ -69,17 +193,6 @@
|
||||
<data
|
||||
android:host="e-hentai.org"
|
||||
android:pathPrefix="/g"
|
||||
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="/galleries"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
@@ -88,26 +201,17 @@
|
||||
<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="e-hentai.org"
|
||||
android:pathPrefix="/g"
|
||||
android:scheme="http" />
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.SettingsActivity"
|
||||
android:label="@string/settings_title" />
|
||||
android:label="@string/settings_title">
|
||||
<tools:validation testUrl="http://ix.io/eer" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
@@ -118,6 +222,17 @@
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:scheme="http"
|
||||
android:host="ix.io"
|
||||
android:pathPattern="/..*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
||||
</application>
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.os.Parcelable
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.arlib.floatingsearchview.suggestions.SearchSuggestionsAdapter
|
||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import com.arlib.floatingsearchview.util.view.SearchInputView
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.favoriteTags
|
||||
import xyz.quaver.pupil.types.*
|
||||
import java.util.*
|
||||
|
||||
class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
FloatingSearchView(context, attrs),
|
||||
FloatingSearchView.OnSearchListener,
|
||||
SearchSuggestionsAdapter.OnBindSuggestionCallback,
|
||||
TextWatcher
|
||||
{
|
||||
|
||||
private val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
||||
|
||||
var onHistoryDeleteClickedListener: ((String) -> Unit)? = null
|
||||
var onFavoriteHistorySwitchClickListener: (() -> Unit)? = null
|
||||
|
||||
init {
|
||||
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
|
||||
|
||||
searchInputView.addTextChangedListener(this)
|
||||
setOnSearchListener(this)
|
||||
setOnBindSuggestionCallback(this)
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
s ?: return
|
||||
|
||||
if (s.any { it.isUpperCase() })
|
||||
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
|
||||
}
|
||||
|
||||
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
|
||||
when (searchSuggestion) {
|
||||
is TagSuggestion -> {
|
||||
with(searchInputView.text) {
|
||||
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length)
|
||||
append("${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")} ")
|
||||
}
|
||||
}
|
||||
is Suggestion -> {
|
||||
with(searchInputView.text) {
|
||||
clear()
|
||||
append(searchSuggestion.str)
|
||||
}
|
||||
}
|
||||
is FavoriteHistorySwitch -> onFavoriteHistorySwitchClickListener?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSearchAction(currentQuery: String?) {}
|
||||
|
||||
override fun onBindSuggestion(
|
||||
suggestionView: View?,
|
||||
leftIcon: ImageView?,
|
||||
textView: TextView?,
|
||||
item: SearchSuggestion?,
|
||||
itemPosition: Int
|
||||
) {
|
||||
when(item) {
|
||||
is TagSuggestion -> {
|
||||
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
|
||||
|
||||
leftIcon?.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
resources,
|
||||
when(item.n) {
|
||||
"female" -> R.drawable.gender_female
|
||||
"male" -> R.drawable.gender_male
|
||||
"language" -> R.drawable.translate
|
||||
"group" -> R.drawable.account_group
|
||||
"character" -> R.drawable.account_star
|
||||
"series" -> R.drawable.book_open
|
||||
"artist" -> R.drawable.brush
|
||||
else -> R.drawable.tag
|
||||
},
|
||||
context.theme)
|
||||
)
|
||||
|
||||
with(suggestionView?.findViewById<ImageView>(R.id.right_icon)) {
|
||||
this ?: return@with
|
||||
|
||||
if (favoriteTags.contains(Tag.parse(tag)))
|
||||
setImageResource(R.drawable.ic_star_filled)
|
||||
else
|
||||
setImageResource(R.drawable.ic_star_empty)
|
||||
|
||||
visibility = View.VISIBLE
|
||||
rotation = 0f
|
||||
|
||||
isEnabled = true
|
||||
isClickable = true
|
||||
|
||||
setOnClickListener {
|
||||
val tag = Tag.parse(tag)
|
||||
|
||||
if (favoriteTags.contains(tag)) {
|
||||
setImageResource(R.drawable.ic_star_empty)
|
||||
favoriteTags.remove(tag)
|
||||
}
|
||||
else {
|
||||
setImageDrawable(
|
||||
AnimatedVectorDrawableCompat.create(context,
|
||||
R.drawable.avd_star
|
||||
))
|
||||
(drawable as Animatable).start()
|
||||
|
||||
favoriteTags.add(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.t == -1) {
|
||||
textView?.text = item.s
|
||||
} else {
|
||||
(suggestionView as? LinearLayout)?.let {
|
||||
val count = it.findViewById<TextView>(R.id.count)
|
||||
if (count == null)
|
||||
it.addView(
|
||||
LayoutInflater.from(context).inflate(R.layout.suggestion_count, suggestionView, false)
|
||||
.apply {
|
||||
this as TextView
|
||||
|
||||
text = item.t.toString()
|
||||
}, 2
|
||||
)
|
||||
else
|
||||
count.text = item.t.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
is FavoriteHistorySwitch -> {
|
||||
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.swap_horizontal, context.theme))
|
||||
}
|
||||
is Suggestion -> {
|
||||
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.history, context.theme))
|
||||
|
||||
with(suggestionView?.findViewById<ImageView>(R.id.right_icon)) {
|
||||
this ?: return@with
|
||||
|
||||
setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.delete, context.theme))
|
||||
|
||||
visibility = View.VISIBLE
|
||||
rotation = 0f
|
||||
|
||||
isEnabled = true
|
||||
isClickable = true
|
||||
|
||||
setOnClickListener {
|
||||
onHistoryDeleteClickedListener?.invoke(item.str)
|
||||
}
|
||||
}
|
||||
}
|
||||
is LoadingSuggestion -> {
|
||||
leftIcon?.setImageDrawable(CircularProgressDrawable(context).also {
|
||||
it.setStyle(CircularProgressDrawable.DEFAULT)
|
||||
it.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN)
|
||||
it.start()
|
||||
})
|
||||
}
|
||||
is NoResultSuggestion -> {
|
||||
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.close, context.theme))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hack to remove color attributes which should not be reused
|
||||
override fun onSaveInstanceState(): Parcelable? {
|
||||
super.onSaveInstanceState()
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -18,44 +18,123 @@
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.*
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.*
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.setClient
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class Pupil : MultiDexApplication() {
|
||||
typealias PupilInterceptor = (Interceptor.Chain) -> Response
|
||||
|
||||
lateinit var histories: Histories
|
||||
lateinit var favorites: Histories
|
||||
lateinit var histories: SavedSet<Int>
|
||||
private set
|
||||
lateinit var favorites: SavedSet<Int>
|
||||
private set
|
||||
lateinit var favoriteTags: SavedSet<Tag>
|
||||
private set
|
||||
lateinit var searchHistory: SavedSet<String>
|
||||
private set
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
val interceptors = mutableMapOf<KClass<out Any>, PupilInterceptor>()
|
||||
|
||||
lateinit var clientBuilder: OkHttpClient.Builder
|
||||
|
||||
var clientHolder: OkHttpClient? = null
|
||||
val client: OkHttpClient
|
||||
get() = clientHolder ?: clientBuilder.build().also {
|
||||
clientHolder = it
|
||||
setClient(it)
|
||||
}
|
||||
|
||||
class Pupil : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
|
||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
val download = preference.getString("dl_location", null)
|
||||
|
||||
if (download == null) {
|
||||
val default = ContextCompat.getExternalFilesDirs(this, null)[0]
|
||||
preference.edit().putString("dl_location", Uri.fromFile(default).toString()).apply()
|
||||
val userID = Preferences["user_id", ""].let { userID ->
|
||||
if (userID.isEmpty()) UUID.randomUUID().toString().also { Preferences["user_id"] = it }
|
||||
else userID
|
||||
}
|
||||
|
||||
FirebaseCrashlytics.getInstance().setUserId(userID)
|
||||
|
||||
val proxyInfo = getProxyInfo()
|
||||
|
||||
clientBuilder = OkHttpClient.Builder()
|
||||
.connectTimeout(0, TimeUnit.SECONDS)
|
||||
.readTimeout(0, TimeUnit.SECONDS)
|
||||
.proxyInfo(proxyInfo)
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
val tag = request.tag() ?: return@addInterceptor chain.proceed(request)
|
||||
|
||||
interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request)
|
||||
}
|
||||
|
||||
try {
|
||||
Preferences.get<String>("download_folder").also {
|
||||
if (it.startsWith("content") && Build.VERSION.SDK_INT > 19)
|
||||
contentResolver.takePersistableUriPermission(
|
||||
Uri.parse(it),
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
|
||||
if (!FileX(this, it).canWrite())
|
||||
throw Exception()
|
||||
|
||||
DownloadManager.getInstance(this).migrate()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Preferences.remove("download_folder")
|
||||
}
|
||||
|
||||
histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0)
|
||||
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0)
|
||||
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
|
||||
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
|
||||
|
||||
if (Preferences["new_history"]) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
histories.reversed().let {
|
||||
histories.clear()
|
||||
histories.addAll(it)
|
||||
}
|
||||
favorites.reversed().let {
|
||||
favorites.clear()
|
||||
favorites.addAll(it)
|
||||
}
|
||||
}
|
||||
Preferences["new_history"] = true
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG)
|
||||
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
||||
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(this)
|
||||
} catch (e: GooglePlayServicesRepairableException) {
|
||||
@@ -66,17 +145,37 @@ class Pupil : MultiDexApplication() {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_MIN).apply {
|
||||
|
||||
manager.createNotificationChannel(NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = getString(R.string.channel_download_description)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
})
|
||||
|
||||
manager.createNotificationChannel(NotificationChannel("downloader", getString(R.string.channel_downloader), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = getString(R.string.channel_downloader_description)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
})
|
||||
|
||||
manager.createNotificationChannel(NotificationChannel("update", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
|
||||
description = getString(R.string.channel_update_description)
|
||||
enableLights(true)
|
||||
enableVibration(true)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
})
|
||||
|
||||
manager.createNotificationChannel(NotificationChannel("import", getString(R.string.channel_update), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = getString(R.string.channel_update_description)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
})
|
||||
}
|
||||
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
|
||||
AppCompatDelegate.setDefaultNightMode(when (Preferences.get<Boolean>("dark_mode")) {
|
||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
})
|
||||
|
||||
@@ -16,9 +16,26 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
package xyz.quaver.pupil
|
||||
|
||||
const val REQUEST_LOCK = 38238
|
||||
const val REQUEST_RESTORE = 16546
|
||||
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
||||
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
||||
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 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(client)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,42 +20,47 @@ package xyz.quaver.pupil.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.util.SparseBooleanArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
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 kotlinx.android.synthetic.main.item_galleryblock.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import kotlinx.coroutines.withContext
|
||||
import xyz.quaver.hitomi.getReader
|
||||
import xyz.quaver.io.util.getChild
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import xyz.quaver.pupil.ui.view.TagChip
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
||||
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
||||
|
||||
enum class ViewType {
|
||||
NEXT,
|
||||
@@ -63,56 +68,70 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
||||
PREV
|
||||
}
|
||||
|
||||
private val glide = Glide.with(context)
|
||||
private lateinit var favorites: Histories
|
||||
|
||||
val timer = Timer()
|
||||
|
||||
var isThin = false
|
||||
|
||||
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||
var timerTask: TimerTask? = null
|
||||
|
||||
fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.Main).launch {
|
||||
val cache = Cache(context).getCachedGallery(galleryID)
|
||||
val reader = Cache(context).getReaderOrNull(galleryID)
|
||||
private fun updateProgress(context: Context, galleryID: Int) {
|
||||
val cache = Cache.getInstance(context, galleryID)
|
||||
|
||||
if (reader == null) {
|
||||
view.galleryblock_progressbar.visibility = View.GONE
|
||||
view.galleryblock_progress_complete.visibility = View.GONE
|
||||
return@launch
|
||||
}
|
||||
|
||||
with(view.galleryblock_progressbar) {
|
||||
|
||||
progress = cache?.listFiles()?.count { file ->
|
||||
Regex("^[0-9]+.+\$").matches(file.name!!)
|
||||
} ?: 0
|
||||
|
||||
if (visibility == View.GONE) {
|
||||
visibility = View.VISIBLE
|
||||
max = reader.galleryInfo.size
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (cache.metadata.reader == null || Preferences["cache_disable"]) {
|
||||
view.galleryblock_progressbar.visibility = View.GONE
|
||||
view.galleryblock_progress_complete.visibility = View.GONE
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (progress == max) {
|
||||
if (completeFlag.get(galleryID, false)) {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageResource(R.drawable.ic_progressbar)
|
||||
visibility = View.VISIBLE
|
||||
with(view.galleryblock_progressbar) {
|
||||
val imageList = cache.metadata.imageList!!
|
||||
|
||||
progress = imageList.filterNotNull().size
|
||||
max = imageList.size
|
||||
|
||||
if (visibility == View.GONE)
|
||||
visibility = View.VISIBLE
|
||||
|
||||
if (progress == max) {
|
||||
val downloadManager = DownloadManager.getInstance(context)
|
||||
|
||||
if (completeFlag.get(galleryID, false)) {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageResource(
|
||||
if (downloadManager.getDownloadFolder(galleryID) != null)
|
||||
R.drawable.ic_progressbar
|
||||
else R.drawable.ic_progressbar_cache
|
||||
)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context,
|
||||
if (downloadManager.getDownloadFolder(galleryID) != null)
|
||||
R.drawable.ic_progressbar_complete
|
||||
else R.drawable.ic_progressbar_complete_cache
|
||||
).apply {
|
||||
this?.start()
|
||||
})
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
completeFlag.put(galleryID, true)
|
||||
}
|
||||
} else {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
|
||||
this?.start()
|
||||
})
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
completeFlag.put(galleryID, true)
|
||||
}
|
||||
} else
|
||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||
} else
|
||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(galleryBlock: GalleryBlock) {
|
||||
fun bind(galleryID: Int) {
|
||||
val time = System.currentTimeMillis()
|
||||
|
||||
val cache = Cache.getInstance(view.context, galleryID)
|
||||
|
||||
val galleryBlock = cache.metadata.galleryBlock ?: return
|
||||
|
||||
with(view) {
|
||||
val resources = context.resources
|
||||
val languages = resources.getStringArray(R.array.languages).map {
|
||||
@@ -124,46 +143,54 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
||||
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 = Base64.decode(Cache(context).getThumbnail(galleryBlock.id), Base64.DEFAULT)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val thumbnail = cache.getThumbnail()
|
||||
|
||||
glide
|
||||
.load(thumbnail)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.listener(object: RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
Cache.getInstance(context, galleryID).let {
|
||||
it.cacheFolder.getChild(".thumbnail").let { if (it.exists()) it.delete() }
|
||||
it.downloadFolder?.getChild(".thumbnail")?.let { if (it.exists()) it.delete() }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean = false
|
||||
})
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
}
|
||||
.into(galleryblock_thumbnail)
|
||||
}.let { launch(Dispatchers.Main) { it.into(galleryblock_thumbnail) } }
|
||||
}
|
||||
|
||||
//Check cache
|
||||
val cache = Cache(context).getCachedGallery(galleryBlock.id)
|
||||
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
|
||||
|
||||
if (cache != null && reader != null) {
|
||||
val count = cache.listFiles().count {
|
||||
Regex("^[0-9]+.+\$").matches(it.name!!)
|
||||
}
|
||||
|
||||
with(galleryblock_progressbar) {
|
||||
max = reader.galleryInfo.size
|
||||
progress = count
|
||||
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else
|
||||
galleryblock_progressbar.visibility = View.GONE
|
||||
|
||||
if (timerTask == null)
|
||||
timerTask = timer.schedule(0, 1000) {
|
||||
updateProgress(context, galleryBlock.id)
|
||||
updateProgress(context, galleryID)
|
||||
}
|
||||
|
||||
galleryblock_title.text = galleryBlock.title
|
||||
@@ -195,41 +222,27 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
||||
}
|
||||
|
||||
galleryblock_tag_group.removeAllViews()
|
||||
galleryBlock.relatedTags.forEach {
|
||||
galleryblock_tag_group.addView(Chip(context).apply {
|
||||
val tag = Tag.parse(it).let { tag ->
|
||||
when {
|
||||
tag.area != null -> tag
|
||||
else -> Tag("tag", it)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
galleryBlock.relatedTags.forEach {
|
||||
TagChip(context, Tag.parse(it)).apply {
|
||||
setOnClickListener { view ->
|
||||
for (callback in onChipClickedHandler)
|
||||
callback.invoke((view as TagChip).tag)
|
||||
}
|
||||
}
|
||||
|
||||
chipIcon = when(tag.area) {
|
||||
"male" -> {
|
||||
setChipBackgroundColorResource(R.color.material_blue_700)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white)
|
||||
}
|
||||
"female" -> {
|
||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
text = tag.tag.wordCapitalize()
|
||||
setEnsureMinTouchTargetSize(false)
|
||||
setOnClickListener {
|
||||
for (callback in onChipClickedHandler)
|
||||
callback.invoke(tag)
|
||||
}
|
||||
})
|
||||
}.let { launch(Dispatchers.Main) { galleryblock_tag_group.addView(it) } }
|
||||
}
|
||||
}
|
||||
|
||||
galleryblock_id.text = galleryBlock.id.toString()
|
||||
|
||||
if (!::favorites.isInitialized)
|
||||
favorites = (context.applicationContext as Pupil).favorites
|
||||
galleryblock_pagecount.text = "-"
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val pageCount = kotlin.runCatching {
|
||||
getReader(galleryBlock.id).galleryInfo.files.size
|
||||
}.getOrNull() ?: return@launch
|
||||
withContext(Dispatchers.Main) {
|
||||
galleryblock_pagecount.text = context.getString(R.string.galleryblock_pagecount, pageCount)
|
||||
}
|
||||
}
|
||||
|
||||
with(galleryblock_favorite) {
|
||||
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
|
||||
@@ -257,7 +270,16 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Log.i("PUPILD", "${System.currentTimeMillis() - time}")
|
||||
}
|
||||
}
|
||||
class NextViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
|
||||
@@ -306,9 +328,9 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is GalleryViewHolder) {
|
||||
val gallery = galleries[position-(if (showPrev) 1 else 0)]
|
||||
val galleryID = galleries[position-(if (showPrev) 1 else 0)]
|
||||
|
||||
holder.bind(gallery)
|
||||
holder.bind(galleryID)
|
||||
|
||||
with(holder.view.galleryblock_primary) {
|
||||
setOnClickListener {
|
||||
@@ -334,10 +356,10 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
||||
mItemManger.closeAllExcept(layout)
|
||||
|
||||
holder.view.galleryblock_download.text =
|
||||
if (DownloadWorker.getInstance(holder.view.context).progress.indexOfKey(gallery.id) < 0)
|
||||
holder.view.context.getString(R.string.main_download)
|
||||
else
|
||||
if (DownloadManager.getInstance(holder.view.context).isDownloading(galleryID))
|
||||
holder.view.context.getString(android.R.string.cancel)
|
||||
else
|
||||
holder.view.context.getString(R.string.main_download)
|
||||
}
|
||||
|
||||
override fun onClose(layout: SwipeLayout?) {}
|
||||
@@ -359,8 +381,8 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
||||
}
|
||||
|
||||
override fun getItemCount() =
|
||||
(if (galleries.isEmpty()) 0 else galleries.size)+
|
||||
(if (showNext) 1 else 0)+
|
||||
galleries.size +
|
||||
(if (showNext) 1 else 0) +
|
||||
(if (showPrev) 1 else 0)
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
|
||||
@@ -18,15 +18,16 @@
|
||||
|
||||
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 xyz.quaver.pupil.util.Preferences
|
||||
import java.util.*
|
||||
|
||||
class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewHolder>() {
|
||||
@@ -40,8 +41,7 @@ class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewH
|
||||
}.toMap()
|
||||
|
||||
val list = mirrors.keys.toMutableList().apply {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString("mirrors", "")!!
|
||||
Preferences.get<String>("mirrors")
|
||||
.split(">")
|
||||
.reversed()
|
||||
.forEach {
|
||||
@@ -60,6 +60,7 @@ class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewH
|
||||
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)]
|
||||
|
||||
@@ -18,48 +18,51 @@
|
||||
|
||||
package xyz.quaver.pupil.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import kotlinx.android.synthetic.main.item_reader.view.*
|
||||
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.createImgList
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ReaderAdapter(private val context: Context,
|
||||
class ReaderAdapter(private val activity: ReaderActivity,
|
||||
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||
|
||||
var reader: Reader? = null
|
||||
val timer = Timer()
|
||||
|
||||
private val glide = Glide.with(activity)
|
||||
|
||||
var isFullScreen = false
|
||||
|
||||
var reader: Reader? = null
|
||||
private val glide = Glide.with(context)
|
||||
val timer = Timer()
|
||||
|
||||
var onItemClickListener : ((Int) -> (Unit))? = null
|
||||
|
||||
init {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
reader = Cache(context).getReader(galleryID)
|
||||
launch(Dispatchers.Main) {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
@@ -70,76 +73,133 @@ class ReaderAdapter(private val context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
private var cache: Cache? = null
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.view as ConstraintLayout
|
||||
|
||||
if (isFullScreen)
|
||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
||||
else
|
||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
|
||||
if (cache == null)
|
||||
cache = Cache.getInstance(holder.view.context, galleryID)
|
||||
|
||||
if (isFullScreen) {
|
||||
holder.view.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
||||
} else {
|
||||
holder.view.layoutParams.height = ConstraintLayout.LayoutParams.WRAP_CONTENT
|
||||
|
||||
(holder.view.progress_layout.layoutParams as ConstraintLayout.LayoutParams)
|
||||
.dimensionRatio = "${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
|
||||
}
|
||||
|
||||
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
||||
onItemClickListener?.invoke(position)
|
||||
}
|
||||
|
||||
holder.view.container.setOnClickListener {
|
||||
holder.view.setOnClickListener {
|
||||
onItemClickListener?.invoke(position)
|
||||
}
|
||||
|
||||
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
||||
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
|
||||
|
||||
holder.view.reader_index.text = (position+1).toString()
|
||||
|
||||
val images = Cache(context).getImages(galleryID)
|
||||
if (Preferences["cache_disable"]) {
|
||||
val lowQuality: Boolean = Preferences["low_quality"]
|
||||
|
||||
if (images?.get(position) != null) {
|
||||
glide
|
||||
.load(images[position]?.uri)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
}
|
||||
.into(holder.view.image)
|
||||
} else {
|
||||
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
||||
|
||||
if (progress?.isNaN() == true) {
|
||||
glide
|
||||
.load(R.drawable.image_broken_variant)
|
||||
.into(holder.view.image)
|
||||
Snackbar
|
||||
.make(
|
||||
holder.view,
|
||||
DownloadWorker.getInstance(context).exception[galleryID]!![position]?.message
|
||||
?: context.getText(R.string.default_error_msg),
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
)
|
||||
.show()
|
||||
|
||||
return
|
||||
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)
|
||||
else -> null
|
||||
}
|
||||
holder.view.image.post {
|
||||
glide
|
||||
.load(url!!)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
else
|
||||
override(
|
||||
holder.view.context.resources.displayMetrics.widthPixels,
|
||||
holder.view.context.resources.getDimensionPixelSize(R.dimen.reader_max_height)
|
||||
)
|
||||
}
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.into(holder.view.image)
|
||||
}
|
||||
} else {
|
||||
val image = cache!!.getImage(position)
|
||||
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
|
||||
|
||||
holder.view.reader_item_progressbar.progress =
|
||||
if (progress?.isInfinite() == true)
|
||||
100
|
||||
else
|
||||
progress?.roundToInt() ?: 0
|
||||
if (progress?.isInfinite() == true && image != null) {
|
||||
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
|
||||
|
||||
holder.view.image.setImageDrawable(null)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
glide
|
||||
.load(image.uri)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
else
|
||||
override(
|
||||
holder.view.context.resources.displayMetrics.widthPixels,
|
||||
holder.view.context.resources.getDimensionPixelSize(R.dimen.reader_max_height)
|
||||
)
|
||||
}
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.listener(object: RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
cache!!.metadata.imageList?.set(position, null)
|
||||
image.delete()
|
||||
DownloadService.cancel(holder.view.context, galleryID)
|
||||
DownloadService.download(holder.view.context, galleryID, true)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
) = false
|
||||
}).let { launch(Dispatchers.Main) { it.into(holder.view.image) } }
|
||||
}
|
||||
} else {
|
||||
holder.view.reader_item_progressbar.visibility = View.VISIBLE
|
||||
|
||||
timer.schedule(1000) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
notifyItemChanged(position)
|
||||
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?.size ?: 0
|
||||
override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0
|
||||
|
||||
}
|
||||
@@ -22,9 +22,10 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
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)
|
||||
|
||||
@@ -35,6 +36,7 @@ class ThumbnailAdapter(private val glide: RequestManager, private val thumbnails
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
glide
|
||||
.load(thumbnails[position])
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
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
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.receiver
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import java.io.File
|
||||
|
||||
class UpdateBroadcastReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
context ?: return
|
||||
|
||||
when (intent?.action) {
|
||||
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
||||
|
||||
// Validate download
|
||||
val downloadID: Long = Preferences["update_download_id"]
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -2) != downloadID)
|
||||
return
|
||||
|
||||
// Get target uri
|
||||
|
||||
val query = DownloadManager.Query()
|
||||
.setFilterById(downloadID)
|
||||
|
||||
val uri = downloadManager.query(query).use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).let {
|
||||
val uri = Uri.parse(it)
|
||||
|
||||
when (uri.scheme) {
|
||||
"file" ->
|
||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!)
|
||||
)
|
||||
"content" -> uri
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
} else
|
||||
null
|
||||
} ?: return
|
||||
|
||||
// Build Notification
|
||||
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||
}, 0)
|
||||
|
||||
val notification = NotificationCompat.Builder(context, "update")
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setContentTitle(context.getText(R.string.update_download_completed))
|
||||
.setContentText(context.getText(R.string.update_download_completed_description))
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(R.id.notification_id_update, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
426
app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
Normal file
@@ -0,0 +1,426 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.SparseArray
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okio.*
|
||||
import xyz.quaver.pupil.PupilInterceptor
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.interceptors
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.ellipsize
|
||||
import xyz.quaver.pupil.util.normalizeID
|
||||
import xyz.quaver.pupil.util.requestBuilders
|
||||
import java.io.IOException
|
||||
|
||||
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
|
||||
class DownloadService : Service() {
|
||||
data class Tag(val galleryID: Int, val index: Int, val startId: Int? = null)
|
||||
|
||||
//region Notification
|
||||
private val notificationManager by lazy {
|
||||
NotificationManagerCompat.from(this)
|
||||
}
|
||||
|
||||
private val serviceNotification by lazy {
|
||||
NotificationCompat.Builder(this, "downloader")
|
||||
.setContentTitle(getString(R.string.downloader_running))
|
||||
.setProgress(0, 0, false)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setOngoing(true)
|
||||
}
|
||||
|
||||
private val notification = SparseArray<NotificationCompat.Builder?>()
|
||||
|
||||
private fun initNotification(galleryID: Int) {
|
||||
val intent = Intent(this, ReaderActivity::class.java)
|
||||
.putExtra("galleryID", galleryID)
|
||||
|
||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||
addNextIntentWithParentStack(intent)
|
||||
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
val action =
|
||||
NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
||||
PendingIntent.getService(
|
||||
this,
|
||||
R.id.notification_download_cancel_action.normalizeID(),
|
||||
Intent(this, DownloadService::class.java)
|
||||
.putExtra(KEY_COMMAND, COMMAND_CANCEL)
|
||||
.putExtra(KEY_ID, galleryID),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT),
|
||||
).build()
|
||||
|
||||
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
|
||||
setContentTitle(getString(R.string.reader_loading))
|
||||
setContentText(getString(R.string.reader_notification_text))
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
setContentIntent(pendingIntent)
|
||||
addAction(action)
|
||||
setProgress(0, 0, true)
|
||||
setOngoing(true)
|
||||
})
|
||||
|
||||
notify(galleryID)
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun notify(galleryID: Int) {
|
||||
val max = progress[galleryID]?.size ?: 0
|
||||
val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0
|
||||
|
||||
val notification = notification[galleryID] ?: return
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
notification
|
||||
.setContentText(getString(R.string.reader_notification_complete))
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.mActions.clear()
|
||||
|
||||
notificationManager.cancel(galleryID)
|
||||
} else
|
||||
notification
|
||||
.setProgress(max, progress, false)
|
||||
.setContentText("$progress/$max")
|
||||
|
||||
if (DownloadManager.getInstance(this).getDownloadFolder(galleryID) != null)
|
||||
notification.let { notificationManager.notify(galleryID, it.build()) }
|
||||
else
|
||||
notificationManager.cancel(galleryID)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region ProgressListener
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private val progressListener: ProgressListener = { (galleryID, index), bytesRead, contentLength, done ->
|
||||
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
|
||||
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
|
||||
}
|
||||
|
||||
private class ProgressResponseBody(
|
||||
val tag: Any?,
|
||||
val responseBody: ResponseBody,
|
||||
val progressListener : ProgressListener
|
||||
) : ResponseBody() {
|
||||
private var bufferedSource : BufferedSource? = null
|
||||
|
||||
override fun contentLength() = responseBody.contentLength()
|
||||
override fun contentType() = responseBody.contentType()
|
||||
|
||||
override fun source(): BufferedSource {
|
||||
if (bufferedSource == null)
|
||||
bufferedSource = Okio.buffer(source(responseBody.source()))
|
||||
|
||||
return bufferedSource!!
|
||||
}
|
||||
|
||||
private fun source(source: Source) = object: ForwardingSource(source) {
|
||||
var totalBytesRead = 0L
|
||||
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val bytesRead = super.read(sink, byteCount)
|
||||
|
||||
totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
|
||||
progressListener.invoke(tag as Tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||
|
||||
return bytesRead
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val interceptor: PupilInterceptor = { chain ->
|
||||
val request = chain.request()
|
||||
var response = chain.proceed(request)
|
||||
|
||||
var retry = 5
|
||||
while (!response.isSuccessful && retry > 0) {
|
||||
response = chain.proceed(request)
|
||||
retry--
|
||||
}
|
||||
|
||||
response.newBuilder()
|
||||
.body(response.body()?.let {
|
||||
ProgressResponseBody(request.tag(), it, progressListener)
|
||||
}).build()
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Downloader
|
||||
/**
|
||||
* KEY
|
||||
* primary galleryID
|
||||
* secondary index
|
||||
* PRIMARY VALUE
|
||||
* MutableList -> Download in progress
|
||||
* null -> Loading / Gallery doesn't exist
|
||||
* SECONDARY VALUE
|
||||
* 0 <= value < 100 -> Download in progress
|
||||
* Float.POSITIVE_INFINITY -> Download completed
|
||||
*/
|
||||
val progress = SparseArray<MutableList<Float>?>()
|
||||
|
||||
fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
|
||||
|
||||
private val callback = object: Callback {
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
if (e.message?.contains("cancel", true) == false) {
|
||||
val galleryID = (call.request().tag() as Tag).galleryID
|
||||
|
||||
// Retry
|
||||
cancel(galleryID)
|
||||
download(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
val (galleryID, index, startId) = call.request().tag() as Tag
|
||||
val ext = call.request().url().encodedPath().split('.').last()
|
||||
|
||||
kotlin.runCatching {
|
||||
val image = response.also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } ?: throw Exception()
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
kotlin.runCatching {
|
||||
Cache.getInstance(this@DownloadService, galleryID).putImage(index, "$index.$ext", image)
|
||||
}.onSuccess {
|
||||
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
|
||||
notify(galleryID)
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
if (DownloadManager.getInstance(this@DownloadService)
|
||||
.getDownloadFolder(galleryID) != null)
|
||||
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
|
||||
|
||||
startId?.let { stopSelf(it) }
|
||||
}
|
||||
}.onFailure {
|
||||
cancel(galleryID)
|
||||
download(galleryID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel(startId: Int? = null) {
|
||||
client.dispatcher().queuedCalls().filter {
|
||||
it.request().tag() is Tag
|
||||
}.forEach {
|
||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||
it.cancel()
|
||||
}
|
||||
client.dispatcher().runningCalls().filter {
|
||||
it.request().tag() is Tag
|
||||
}.forEach {
|
||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||
it.cancel()
|
||||
}
|
||||
|
||||
progress.clear()
|
||||
notification.clear()
|
||||
notificationManager.cancelAll()
|
||||
|
||||
startId?.let { stopSelf(it) }
|
||||
}
|
||||
|
||||
fun cancel(galleryID: Int, startId: Int? = null) {
|
||||
client.dispatcher().queuedCalls().filter {
|
||||
(it.request().tag() as? Tag)?.galleryID == galleryID
|
||||
}.forEach {
|
||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||
it.cancel()
|
||||
}
|
||||
client.dispatcher().runningCalls().filter {
|
||||
(it.request().tag() as? Tag)?.galleryID == galleryID
|
||||
}.forEach {
|
||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||
it.cancel()
|
||||
}
|
||||
|
||||
progress.remove(galleryID)
|
||||
notification.remove(galleryID)
|
||||
notificationManager.cancel(galleryID)
|
||||
|
||||
startId?.let { stopSelf(it) }
|
||||
}
|
||||
|
||||
fun delete(galleryID: Int, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
|
||||
cancel(galleryID)
|
||||
DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
|
||||
Cache.delete(galleryID)
|
||||
|
||||
startId?.let { stopSelf(it) }
|
||||
}
|
||||
|
||||
fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
|
||||
if (progress.indexOfKey(galleryID) >= 0)
|
||||
cancel(galleryID)
|
||||
|
||||
val cache = Cache.getInstance(this@DownloadService, galleryID)
|
||||
|
||||
initNotification(galleryID)
|
||||
|
||||
val reader = cache.getReader()
|
||||
|
||||
// Gallery doesn't exist
|
||||
if (reader == null) {
|
||||
delete(galleryID)
|
||||
progress.put(galleryID, null)
|
||||
return@launch
|
||||
}
|
||||
|
||||
progress.put(galleryID, MutableList(reader.galleryInfo.files.size) { 0F })
|
||||
|
||||
cache.metadata.imageList?.let {
|
||||
if (progress[galleryID]?.size != it.size) {
|
||||
cache.metadata.imageList?.filterNotNull()?.forEach { file ->
|
||||
cache.findFile(file)?.delete()
|
||||
}
|
||||
cache.metadata.imageList = MutableList(reader.galleryInfo.files.size) { null }
|
||||
return@let
|
||||
}
|
||||
|
||||
it.forEachIndexed { index, image ->
|
||||
progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F)
|
||||
}
|
||||
}
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
if (DownloadManager.getInstance(this@DownloadService)
|
||||
.getDownloadFolder(galleryID) != null)
|
||||
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
|
||||
|
||||
notificationManager.cancel(galleryID)
|
||||
startId?.let { stopSelf(it) }
|
||||
return@launch
|
||||
}
|
||||
|
||||
notification[galleryID]?.setContentTitle(reader.galleryInfo.title?.ellipsize(30))
|
||||
notify(galleryID)
|
||||
|
||||
val queued = mutableSetOf<Int>()
|
||||
|
||||
if (priority) {
|
||||
client.dispatcher().queuedCalls().forEach {
|
||||
val queuedID = (it.request().tag() as? Tag)?.galleryID ?: return@forEach
|
||||
|
||||
if (queued.add(queuedID))
|
||||
cancel(queuedID)
|
||||
}
|
||||
}
|
||||
|
||||
reader.requestBuilders.forEachIndexed { index, it ->
|
||||
if (progress[galleryID]?.get(index)?.isInfinite() != true) {
|
||||
val request = it.tag(Tag(galleryID, index, startId)).build()
|
||||
client.newCall(request).enqueue(callback)
|
||||
}
|
||||
}
|
||||
|
||||
queued.forEach { download(it) }
|
||||
}
|
||||
//endregion
|
||||
|
||||
companion object {
|
||||
const val KEY_COMMAND = "COMMAND" // String
|
||||
const val KEY_ID = "ID" // Int
|
||||
const val KEY_PRIORITY = "PRIORITY" // Boolean
|
||||
|
||||
const val COMMAND_DOWNLOAD = "DOWNLOAD"
|
||||
const val COMMAND_CANCEL = "CANCEL"
|
||||
const val COMMAND_DELETE = "DELETE"
|
||||
|
||||
private fun command(context: Context, extras: Intent.() -> Unit) {
|
||||
ContextCompat.startForegroundService(context, Intent(context, DownloadService::class.java).apply(extras))
|
||||
}
|
||||
|
||||
fun download(context: Context, galleryID: Int, priority: Boolean = false) {
|
||||
command(context) {
|
||||
putExtra(KEY_COMMAND, COMMAND_DOWNLOAD)
|
||||
putExtra(KEY_PRIORITY, priority)
|
||||
putExtra(KEY_ID, galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel(context: Context, galleryID: Int? = null) {
|
||||
command(context) {
|
||||
putExtra(KEY_COMMAND, COMMAND_CANCEL)
|
||||
galleryID?.let { putExtra(KEY_ID, it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(context: Context, galleryID: Int) {
|
||||
command(context) {
|
||||
putExtra(KEY_COMMAND, COMMAND_DELETE)
|
||||
putExtra(KEY_ID, galleryID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
||||
|
||||
when (intent?.getStringExtra(KEY_COMMAND)) {
|
||||
COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0)
|
||||
download(it, intent.getBooleanExtra(KEY_PRIORITY, false), startId)
|
||||
}
|
||||
COMMAND_CANCEL -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) cancel(it, startId) else cancel(startId = startId) }
|
||||
COMMAND_DELETE -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) delete(it, startId) }
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
inner class Binder : android.os.Binder() {
|
||||
val service = this@DownloadService
|
||||
}
|
||||
|
||||
private val binder = Binder()
|
||||
override fun onBind(p0: Intent?) = binder
|
||||
|
||||
override fun onCreate() {
|
||||
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
||||
interceptors[Tag::class] = interceptor
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
interceptors.remove(Tag::class)
|
||||
}
|
||||
}
|
||||
@@ -29,4 +29,25 @@ data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String
|
||||
override fun getBody(): String {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class Suggestion(val str: String) : SearchSuggestion {
|
||||
override fun getBody() = str
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class NoResultSuggestion(val str: String) : SearchSuggestion {
|
||||
override fun getBody() = str
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class LoadingSuggestion(val str: String) : SearchSuggestion {
|
||||
override fun getBody() = str
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY")
|
||||
class FavoriteHistorySwitch(private val body: String) : SearchSuggestion {
|
||||
override fun getBody() = body
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import kotlinx.serialization.Serializable
|
||||
data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) {
|
||||
companion object {
|
||||
fun parse(tag: String) : Tag {
|
||||
if (tag.first() == '-') {
|
||||
if (tag.firstOrNull() == '-') {
|
||||
tag.substring(1).split(Regex(":"), 2).let {
|
||||
return when(it.size) {
|
||||
2 -> Tag(it[0], it[1], true)
|
||||
@@ -62,12 +62,10 @@ data class Tag(val area: String?, val tag: String, val isNegative: Boolean = fal
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return super.hashCode()
|
||||
}
|
||||
override fun hashCode() = toString().hashCode()
|
||||
}
|
||||
|
||||
class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
||||
class Tags(val tags: MutableSet<Tag> = mutableSetOf()) : MutableSet<Tag> by tags {
|
||||
|
||||
companion object {
|
||||
fun parse(tags: String) : Tags {
|
||||
@@ -77,20 +75,13 @@ class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
||||
Tag.parse(it)
|
||||
else
|
||||
null
|
||||
}
|
||||
}.filterNotNull().toMutableSet()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
tag?.forEach {
|
||||
if (it != null)
|
||||
add(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(element: String): Boolean {
|
||||
forEach {
|
||||
tags.forEach {
|
||||
if (it.toString() == element)
|
||||
return true
|
||||
}
|
||||
@@ -99,23 +90,22 @@ class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
||||
}
|
||||
|
||||
fun add(element: String): Boolean {
|
||||
return super.add(Tag.parse(element))
|
||||
return tags.add(Tag.parse(element))
|
||||
}
|
||||
|
||||
fun remove(element: String) {
|
||||
filter { it.toString() == element }.forEach {
|
||||
remove(it)
|
||||
tags.filter { it.toString() == element }.forEach {
|
||||
tags.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeByArea(area: String, isNegative: Boolean? = null) {
|
||||
filter { it.area == area && (if(isNegative == null) true else (it.isNegative == isNegative)) }.forEach {
|
||||
remove(it)
|
||||
tags.filter { it.area == area && (if(isNegative == null) true else (it.isNegative == isNegative)) }.forEach {
|
||||
tags.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return joinToString(" ") { it.toString() }
|
||||
return tags.joinToString(" ") { it.toString() }
|
||||
}
|
||||
|
||||
}
|
||||
72
app/src/main/java/xyz/quaver/pupil/ui/BaseActivity.kt
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.PersistableBundle
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.normalizeID
|
||||
|
||||
open class BaseActivity : AppCompatActivity() {
|
||||
|
||||
private var locked: Boolean = true
|
||||
|
||||
@CallSuper
|
||||
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
||||
super.onCreate(savedInstanceState, persistentState)
|
||||
|
||||
locked = !LockManager(this).locks.isNullOrEmpty()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (Preferences["security_mode"])
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
else
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
|
||||
if (locked)
|
||||
startActivityForResult(Intent(this, LockActivity::class.java), R.id.request_lock.normalizeID())
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when(requestCode) {
|
||||
R.id.request_lock.normalizeID() -> {
|
||||
if (resultCode == Activity.RESULT_OK)
|
||||
locked = false
|
||||
else
|
||||
finish()
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,23 +21,161 @@ package xyz.quaver.pupil.ui
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.andrognito.patternlockview.PatternLockView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_lock.*
|
||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
|
||||
import kotlinx.android.synthetic.main.fragment_pin_lock.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.fragment.PINLockFragment
|
||||
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
private var lastUnlocked = 0L
|
||||
class LockActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var lockManager: LockManager
|
||||
private var mode: String? = null
|
||||
|
||||
private val patternLockFragment = PatternLockFragment().apply {
|
||||
var lastPass = ""
|
||||
onPatternDrawn = {
|
||||
when(mode) {
|
||||
null -> {
|
||||
val result = lockManager.check(it)
|
||||
|
||||
if (result == true) {
|
||||
lastUnlocked = System.currentTimeMillis()
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} else
|
||||
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
}
|
||||
"add_lock" -> {
|
||||
if (lastPass.isEmpty()) {
|
||||
lastPass = it
|
||||
|
||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
if (lastPass == it) {
|
||||
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
|
||||
finish()
|
||||
} else {
|
||||
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
lastPass = ""
|
||||
|
||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val pinLockFragment = PINLockFragment().apply {
|
||||
var lastPass = ""
|
||||
onPINEntered = {
|
||||
when(mode) {
|
||||
null -> {
|
||||
val result = lockManager.check(it)
|
||||
|
||||
if (result == true) {
|
||||
lastUnlocked = System.currentTimeMillis()
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} else {
|
||||
indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
||||
setAnimationListener(object: Animation.AnimationListener {
|
||||
override fun onAnimationEnd(animation: Animation?) {
|
||||
pin_lock_view.resetPinLockView()
|
||||
pin_lock_view.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animation?) {
|
||||
pin_lock_view.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation?) {
|
||||
// Do Nothing
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
"add_lock" -> {
|
||||
if (lastPass.isEmpty()) {
|
||||
lastPass = it
|
||||
|
||||
pin_lock_view.resetPinLockView()
|
||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
if (lastPass == it) {
|
||||
LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it))
|
||||
finish()
|
||||
} else {
|
||||
indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
||||
setAnimationListener(object: Animation.AnimationListener {
|
||||
override fun onAnimationEnd(animation: Animation?) {
|
||||
pin_lock_view.resetPinLockView()
|
||||
pin_lock_view.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animation?) {
|
||||
pin_lock_view.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation?) {
|
||||
// Do Nothing
|
||||
}
|
||||
})
|
||||
})
|
||||
lastPass = ""
|
||||
|
||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBiometricPrompt() {
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getText(R.string.settings_lock_fingerprint_prompt))
|
||||
.setSubtitle(getText(R.string.settings_lock_fingerprint_prompt_subtitle))
|
||||
.setNegativeButtonText(getText(android.R.string.cancel))
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
|
||||
val biometricPrompt = BiometricPrompt(this, ContextCompat.getMainExecutor(this),
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
lastUnlocked = System.currentTimeMillis()
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// Displays the "log in" prompt.
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_lock)
|
||||
|
||||
val lockManager = try {
|
||||
lockManager = try {
|
||||
LockManager(this)
|
||||
} catch (e: Exception) {
|
||||
AlertDialog.Builder(this).apply {
|
||||
@@ -50,12 +188,8 @@ class LockActivity : AppCompatActivity() {
|
||||
return
|
||||
}
|
||||
|
||||
val mode = intent.getStringExtra("mode")
|
||||
|
||||
lock_pattern.isEnabled = false
|
||||
lock_pin.isEnabled = false
|
||||
lock_fingerprint.isEnabled = false
|
||||
lock_password.isEnabled = false
|
||||
mode = intent.getStringExtra("mode")
|
||||
val force = intent.getBooleanExtra("force", false)
|
||||
|
||||
when(mode) {
|
||||
null -> {
|
||||
@@ -64,52 +198,82 @@ class LockActivity : AppCompatActivity() {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() - lastUnlocked < 5*60*1000 && !force) {
|
||||
lastUnlocked = System.currentTimeMillis()
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
Preferences["lock_fingerprint"]
|
||||
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
||||
) {
|
||||
lock_fingerprint.apply {
|
||||
isEnabled = true
|
||||
setOnClickListener {
|
||||
showBiometricPrompt()
|
||||
}
|
||||
}
|
||||
showBiometricPrompt()
|
||||
}
|
||||
|
||||
lock_pattern.apply {
|
||||
isEnabled = lockManager.contains(Lock.Type.PATTERN)
|
||||
setOnClickListener {
|
||||
supportFragmentManager.beginTransaction().replace(
|
||||
R.id.lock_content, patternLockFragment
|
||||
).commit()
|
||||
}
|
||||
}
|
||||
lock_pin.apply {
|
||||
isEnabled = lockManager.contains(Lock.Type.PIN)
|
||||
setOnClickListener {
|
||||
supportFragmentManager.beginTransaction().replace(
|
||||
R.id.lock_content, pinLockFragment
|
||||
).commit()
|
||||
}
|
||||
}
|
||||
lock_password.isEnabled = false
|
||||
|
||||
when (lockManager.locks!!.first().type) {
|
||||
Lock.Type.PIN -> {
|
||||
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content, pinLockFragment
|
||||
).commit()
|
||||
}
|
||||
Lock.Type.PATTERN -> {
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content, patternLockFragment
|
||||
).commit()
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
"add_lock" -> {
|
||||
lock_pattern.isEnabled = false
|
||||
lock_pin.isEnabled = false
|
||||
lock_fingerprint.isEnabled = false
|
||||
lock_password.isEnabled = false
|
||||
|
||||
when(intent.getStringExtra("type")!!) {
|
||||
"pattern" -> {
|
||||
|
||||
lock_pattern.isEnabled = true
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content, patternLockFragment
|
||||
).commit()
|
||||
}
|
||||
"pin" -> {
|
||||
lock_pin.isEnabled = true
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content, pinLockFragment
|
||||
).commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content,
|
||||
PatternLockFragment().apply {
|
||||
var lastPass = ""
|
||||
onPatternDrawn = {
|
||||
when(mode) {
|
||||
null -> {
|
||||
val result = lockManager.check(it)
|
||||
|
||||
if (result == true) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} else
|
||||
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
}
|
||||
"add_lock" -> {
|
||||
if (lastPass.isEmpty()) {
|
||||
lastPass = it
|
||||
|
||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
if (lastPass == it) {
|
||||
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
|
||||
finish()
|
||||
} else {
|
||||
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
lastPass = ""
|
||||
|
||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
).commit()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,13 +18,16 @@
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -32,24 +35,30 @@ import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.fabric.sdk.android.Fabric
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
||||
import kotlinx.android.synthetic.main.activity_reader.*
|
||||
import kotlinx.android.synthetic.main.activity_reader.view.*
|
||||
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.histories
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.concurrent.timer
|
||||
|
||||
class ReaderActivity : AppCompatActivity() {
|
||||
class ReaderActivity : BaseActivity() {
|
||||
|
||||
private var galleryID = 0
|
||||
private var currentPage = 0
|
||||
@@ -67,40 +76,74 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var cache: Cache
|
||||
var downloader: DownloadService? = null
|
||||
private val conn = object: ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
downloader = (service as DownloadService.Binder).service
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
downloader = null
|
||||
}
|
||||
}
|
||||
|
||||
private val timer = Timer()
|
||||
private var autoTimer: Timer? = null
|
||||
|
||||
private val snapHelper = PagerSnapHelper()
|
||||
|
||||
private var menu: Menu? = null
|
||||
|
||||
private lateinit var favorites: Histories
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_reader)
|
||||
|
||||
title = getString(R.string.reader_loading)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||
|
||||
favorites = (application as Pupil).favorites
|
||||
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
|
||||
setContentView(R.layout.activity_reader)
|
||||
|
||||
handleIntent(intent)
|
||||
|
||||
if (Fabric.isInitialized())
|
||||
Crashlytics.setInt("GalleryID", galleryID)
|
||||
cache = Cache.getInstance(this, galleryID)
|
||||
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
|
||||
|
||||
if (galleryID == 0) {
|
||||
onBackPressed()
|
||||
return
|
||||
}
|
||||
|
||||
if (Preferences["cache_disable"]) {
|
||||
reader_download_progressbar.visibility = View.GONE
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val reader = cache.getReader()
|
||||
|
||||
launch(Dispatchers.Main) initDownloader@{
|
||||
if (reader == null) {
|
||||
Snackbar
|
||||
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
||||
.show()
|
||||
return@initDownloader
|
||||
}
|
||||
|
||||
histories.add(galleryID)
|
||||
(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()
|
||||
|
||||
initView()
|
||||
initDownloader()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
@@ -113,14 +156,12 @@ class ReaderActivity : AppCompatActivity() {
|
||||
val uri = intent.data
|
||||
val lastPathSegment = uri?.lastPathSegment
|
||||
if (uri != null && lastPathSegment != null) {
|
||||
val nonNumber = Regex("[^-?0-9]+")
|
||||
|
||||
galleryID = when (uri.host) {
|
||||
"hitomi.la" -> lastPathSegment.replace(nonNumber, "").toInt()
|
||||
"히요비.asia" -> lastPathSegment.toInt()
|
||||
"xn--9w3b15m8vo.asia" -> lastPathSegment.toInt()
|
||||
"hitomi.la" ->
|
||||
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
||||
"hiyobi.me" -> lastPathSegment.toInt()
|
||||
"e-hentai.org" -> uri.pathSegments[1].toInt()
|
||||
else -> return
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -128,20 +169,6 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
if (preferences.getBoolean("security_mode", false))
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
else
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.reader, menu)
|
||||
|
||||
@@ -156,14 +183,14 @@ class ReaderActivity : AppCompatActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
when(item?.itemId) {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when(item.itemId) {
|
||||
R.id.reader_menu_page_indicator -> {
|
||||
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
|
||||
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
|
||||
with(view.dialog_number_picker) {
|
||||
minValue=1
|
||||
maxValue=reader_recyclerview.adapter?.itemCount ?: 0
|
||||
value=currentPage
|
||||
minValue = 1
|
||||
maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0
|
||||
value = currentPage
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this).apply {
|
||||
setView(view)
|
||||
@@ -196,10 +223,13 @@ class ReaderActivity : AppCompatActivity() {
|
||||
super.onDestroy()
|
||||
|
||||
timer.cancel()
|
||||
(reader_recyclerview.adapter as ReaderAdapter).timer.cancel()
|
||||
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
|
||||
|
||||
if (!Cache(this).isDownloading(galleryID))
|
||||
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)
|
||||
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
|
||||
DownloadService.cancel(this, galleryID)
|
||||
|
||||
if (downloader != null)
|
||||
unbindService(conn)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
@@ -235,32 +265,40 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun initDownloader() {
|
||||
val worker = DownloadWorker.getInstance(this).apply {
|
||||
queue.add(galleryID)
|
||||
}
|
||||
DownloadService.download(this, galleryID, true)
|
||||
bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
|
||||
|
||||
timer.schedule(0, 1000) {
|
||||
if (worker.progress.indexOfKey(galleryID) < 0) //loading
|
||||
timer.schedule(1000, 1000) {
|
||||
val downloader = downloader ?: return@schedule
|
||||
|
||||
if (downloader.progress.indexOfKey(galleryID) < 0) //loading
|
||||
return@schedule
|
||||
|
||||
if (worker.progress[galleryID] == null) { //Gallery not found
|
||||
if (downloader.progress[galleryID] == null) { //Gallery not found
|
||||
timer.cancel()
|
||||
Snackbar
|
||||
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
||||
.show()
|
||||
}
|
||||
|
||||
histories.add(galleryID)
|
||||
|
||||
runOnUiThread {
|
||||
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||
reader_download_progressbar.progress = worker.progress[galleryID]?.count { !it.isFinite() } ?: 0
|
||||
reader_download_progressbar.progress = downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
|
||||
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||
|
||||
if (title == getString(R.string.reader_loading)) {
|
||||
val reader = (reader_recyclerview.adapter as ReaderAdapter).reader
|
||||
val reader = cache.metadata.reader
|
||||
|
||||
if (reader != null) {
|
||||
title = reader.title
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.size}"
|
||||
with (reader_recyclerview.adapter as ReaderAdapter) {
|
||||
this.reader = reader
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
title = reader.galleryInfo.title
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.files.size}"
|
||||
|
||||
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
|
||||
when (reader.code) {
|
||||
@@ -271,7 +309,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) { //Download finished
|
||||
if (downloader.isCompleted(galleryID)) { //Download finished
|
||||
reader_download_progressbar.visibility = View.GONE
|
||||
|
||||
animateDownloadFAB(false)
|
||||
@@ -317,16 +355,53 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
with(reader_fab_download) {
|
||||
animateDownloadFAB(Cache(context).isDownloading(galleryID)) //If download in progress, animate button
|
||||
animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button
|
||||
|
||||
setOnClickListener {
|
||||
if (Cache(context).isDownloading(galleryID)) {
|
||||
Cache(context).setDownloading(galleryID, false)
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
|
||||
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
|
||||
else {
|
||||
val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
|
||||
|
||||
animateDownloadFAB(false)
|
||||
if (downloadManager.isDownloading(galleryID)) {
|
||||
downloadManager.deleteDownloadFolder(galleryID)
|
||||
animateDownloadFAB(false)
|
||||
} else {
|
||||
downloadManager.addDownloadFolder(galleryID)
|
||||
animateDownloadFAB(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with(reader_fab_retry) {
|
||||
setImageResource(R.drawable.refresh)
|
||||
setOnClickListener {
|
||||
downloader?.cancel(galleryID)
|
||||
downloader?.download(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
with(reader_fab_auto) {
|
||||
setImageResource(R.drawable.clock_start)
|
||||
setOnClickListener {
|
||||
if (autoTimer == null) {
|
||||
autoTimer = timer(initialDelay = 10000L, period = 10000L) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
with(this@ReaderActivity.reader_recyclerview) {
|
||||
val lastItem =
|
||||
(layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition()
|
||||
|
||||
if (lastItem < adapter!!.itemCount - 1)
|
||||
(layoutManager as LinearLayoutManager).scrollToPosition(lastItem + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
setImageResource(R.drawable.clock_end)
|
||||
} else {
|
||||
Cache(context).setDownloading(galleryID, true)
|
||||
animateDownloadFAB(true)
|
||||
autoTimer?.cancel()
|
||||
autoTimer = null
|
||||
setImageResource(R.drawable.clock_start)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,14 +423,28 @@ class ReaderActivity : AppCompatActivity() {
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
supportActionBar?.hide()
|
||||
this@ReaderActivity.reader_fab.visibility = View.INVISIBLE
|
||||
this@ReaderActivity.scroller.let {
|
||||
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
||||
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
||||
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb_horizontal)
|
||||
it.fastScrollDirection = RecyclerViewFastScroller.FastScrollDirection.HORIZONTAL
|
||||
}
|
||||
} else {
|
||||
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
||||
supportActionBar?.show()
|
||||
this@ReaderActivity.reader_fab.visibility = View.VISIBLE
|
||||
this@ReaderActivity.scroller.let {
|
||||
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
||||
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
||||
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb)
|
||||
it.fastScrollDirection = RecyclerViewFastScroller.FastScrollDirection.VERTICAL
|
||||
}
|
||||
}
|
||||
|
||||
window.attributes = this
|
||||
}
|
||||
|
||||
reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw
|
||||
}
|
||||
|
||||
private fun scrollMode(isScroll: Boolean) {
|
||||
@@ -364,7 +453,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this)
|
||||
} else {
|
||||
snapHelper.attachToRecyclerView(reader_recyclerview)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, Preferences["rtl", false])
|
||||
}
|
||||
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
||||
@@ -377,11 +466,10 @@ class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
|
||||
override fun onAnimationEnd(drawable: Drawable?) {
|
||||
val worker = DownloadWorker.getInstance(context)
|
||||
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
|
||||
if (downloader?.isCompleted(galleryID) == true) // If download is finished, stop animating
|
||||
post {
|
||||
setImageResource(R.drawable.ic_download)
|
||||
labelText = getString(R.string.reader_fab_download)
|
||||
labelText = getString(R.string.reader_fab_download_cancel)
|
||||
}
|
||||
else // Or continue animate
|
||||
post {
|
||||
@@ -399,4 +487,14 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
Glide.get(this).onLowMemory()
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
Glide.get(this).onTrimMemory(level)
|
||||
}
|
||||
}
|
||||
@@ -18,42 +18,29 @@
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.settings_activity.*
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.parseList
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.fragment.LockFragment
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.ui.fragment.LockSettingsFragment
|
||||
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
|
||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
|
||||
import xyz.quaver.pupil.util.REQUEST_LOCK
|
||||
import xyz.quaver.pupil.util.REQUEST_RESTORE
|
||||
import java.io.File
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.normalizeID
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
class SettingsActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
|
||||
setContentView(R.layout.settings_activity)
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
@@ -62,95 +49,26 @@ class SettingsActivity : AppCompatActivity() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
if (preferences.getBoolean("security_mode", false))
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
else
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
when (item?.itemId) {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> onBackPressed()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when(requestCode) {
|
||||
REQUEST_LOCK -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, LockFragment())
|
||||
.addToBackStack("Lock")
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
REQUEST_RESTORE -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val uri = data?.data ?: return
|
||||
|
||||
try {
|
||||
val json = contentResolver.openInputStream(uri).use { inputStream ->
|
||||
inputStream!!
|
||||
|
||||
inputStream.readBytes().toString(Charset.defaultCharset())
|
||||
}
|
||||
|
||||
(application as Pupil).favorites.addAll(Json.parseList<Int>(json).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()
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
when (requestCode) {
|
||||
R.id.request_write_permission_and_saf.normalizeID() -> {
|
||||
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||
}
|
||||
|
||||
startActivityForResult(intent, R.id.request_download_folder.normalizeID())
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
if (DocumentFile.fromTreeUri(this, uri)?.canWrite() == false)
|
||||
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
|
||||
else
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString("dl_location", uri.toString())
|
||||
.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", Uri.fromFile(File(directory)).toString())
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,11 @@ 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
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
|
||||
@@ -45,17 +45,14 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
|
||||
private val excludeBL = "-male:yaoi"
|
||||
private val excludeGuro = listOf("-female:guro", "-male:guro")
|
||||
|
||||
private lateinit var dialogView : View
|
||||
private val excludeLoli = listOf("-female:loli", "-male:shota")
|
||||
|
||||
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
initDialog()
|
||||
|
||||
setTitle(R.string.default_query_dialog_title)
|
||||
setView(dialogView)
|
||||
setView(build())
|
||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
|
||||
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
|
||||
|
||||
@@ -72,6 +69,11 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
newTags.add(tag)
|
||||
}
|
||||
|
||||
if (default_query_dialog_loli_checkbox.isChecked)
|
||||
excludeLoli.forEach { tag ->
|
||||
newTags.add(tag)
|
||||
}
|
||||
|
||||
onPositiveButtonClickListener?.invoke(newTags)
|
||||
}
|
||||
|
||||
@@ -79,15 +81,14 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun initDialog() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private fun build() : View {
|
||||
val tags = Tags.parse(
|
||||
preferences.getString("default_query", "") ?: ""
|
||||
Preferences["default_query"]
|
||||
)
|
||||
|
||||
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
||||
|
||||
with(dialogView.default_query_dialog_language_selector) {
|
||||
with(view.default_query_dialog_language_selector) {
|
||||
adapter =
|
||||
ArrayAdapter(
|
||||
context,
|
||||
@@ -110,13 +111,13 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
}
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_BL_checkbox) {
|
||||
with(view.default_query_dialog_BL_checkbox) {
|
||||
isChecked = tags.contains(excludeBL)
|
||||
if (tags.contains(excludeBL))
|
||||
tags.remove(excludeBL)
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_guro_checkbox) {
|
||||
with(view.default_query_dialog_guro_checkbox) {
|
||||
isChecked = excludeGuro.all { tags.contains(it) }
|
||||
if (excludeGuro.all { tags.contains(it) })
|
||||
excludeGuro.forEach {
|
||||
@@ -124,7 +125,15 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
}
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_edittext) {
|
||||
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(
|
||||
@@ -149,6 +158,8 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.formatDownloadFolder
|
||||
import xyz.quaver.pupil.util.formatDownloadFolderTest
|
||||
import xyz.quaver.pupil.util.formatMap
|
||||
|
||||
class DownloadFolderNameDialogFragment : DialogFragment() {
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun build(): View {
|
||||
val galleryID = Cache.instances.let { if (it.size() == 0) 1199708 else it.keyAt((0 until it.size()).random()) }
|
||||
val galleryBlock = runBlocking {
|
||||
Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
|
||||
}
|
||||
|
||||
return layoutInflater.inflate(R.layout.dialog_download_folder_name, null).apply {
|
||||
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
|
||||
edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
|
||||
edittext.addTextChangedListener {
|
||||
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
|
||||
}
|
||||
ok_button.setOnClickListener {
|
||||
val newValue = edittext.text.toString()
|
||||
|
||||
if ((newValue as? String)?.contains("/") != false) {
|
||||
Snackbar.make(this, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
Preferences["download_folder_name"] = edittext.text.toString()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||
Dialog(requireContext()).apply {
|
||||
setContentView(build())
|
||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RadioButton
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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.REQUEST_DOWNLOAD_FOLDER
|
||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
|
||||
import xyz.quaver.pupil.util.byteToString
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
||||
|
||||
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val buttons = mutableListOf<Pair<RadioButton, Uri?>>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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", Uri.fromFile(dir).toString()).apply()
|
||||
}
|
||||
buttons.add(button to Uri.fromFile(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) {
|
||||
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)
|
||||
})
|
||||
|
||||
val pref = Uri.parse(preference.getString("dl_location", null))
|
||||
val index = externalFilesDirs.indexOfFirst {
|
||||
Uri.fromFile(it).toString() == pref.toString()
|
||||
}
|
||||
|
||||
if (index < 0)
|
||||
buttons.last().first.isChecked = true
|
||||
else
|
||||
buttons[index].first.isChecked = true
|
||||
|
||||
setTitle(R.string.settings_dl_location)
|
||||
|
||||
setView(view)
|
||||
|
||||
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.item_download_folder.view.*
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.byteToString
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.migrate
|
||||
import xyz.quaver.pupil.util.normalizeID
|
||||
import java.io.File
|
||||
|
||||
class DownloadLocationDialogFragment : DialogFragment() {
|
||||
private val entries = mutableMapOf<File?, View>()
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun build() : View? {
|
||||
val context = context ?: return null
|
||||
|
||||
val view = layoutInflater.inflate(R.layout.dialog_download_folder, null) as LinearLayout
|
||||
|
||||
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
||||
|
||||
externalFilesDirs.forEachIndexed { index, dir ->
|
||||
dir ?: return@forEachIndexed
|
||||
|
||||
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
|
||||
location_type.text = context.getString(when (index) {
|
||||
0 -> R.string.settings_download_folder_internal
|
||||
else -> R.string.settings_download_folder_removable
|
||||
})
|
||||
location_available.text = context.getString(
|
||||
R.string.settings_download_folder_available,
|
||||
byteToString(dir.freeSpace)
|
||||
)
|
||||
setOnClickListener {
|
||||
entries.values.forEach {
|
||||
it.button.isChecked = false
|
||||
}
|
||||
button.performClick()
|
||||
Preferences["download_folder"] = dir.toUri().toString()
|
||||
}
|
||||
entries[dir] = this
|
||||
})
|
||||
}
|
||||
|
||||
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
|
||||
location_type.text = context.getString(R.string.settings_download_folder_custom)
|
||||
setOnClickListener {
|
||||
entries.values.forEach {
|
||||
it.button.isChecked = false
|
||||
}
|
||||
button.performClick()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||
}
|
||||
|
||||
startActivityForResult(intent, R.id.request_download_folder.normalizeID())
|
||||
} 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)
|
||||
}
|
||||
|
||||
startActivityForResult(intent, R.id.request_download_folder_old.normalizeID())
|
||||
}
|
||||
}
|
||||
entries[null] = this
|
||||
})
|
||||
|
||||
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||
entries[key]!!.button.isChecked = true
|
||||
if (key == null) entries[key]!!.location_available.text = downloadFolder
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
|
||||
builder
|
||||
.setTitle(R.string.settings_download_folder)
|
||||
.setView(build())
|
||||
.setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
|
||||
if (Preferences["download_folder", ""].isEmpty())
|
||||
Preferences["download_folder"] = context?.getExternalFilesDir(null)?.toUri()?.toString() ?: ""
|
||||
|
||||
DownloadManager.getInstance(requireContext()).migrate()
|
||||
}
|
||||
|
||||
isCancelable = false
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
R.id.request_download_folder.normalizeID() -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val activity = activity ?: return
|
||||
val context = context ?: return
|
||||
val dialog = dialog ?: return
|
||||
|
||||
data?.data?.also { uri ->
|
||||
val takeFlags: Int =
|
||||
activity.intent.flags and
|
||||
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
|
||||
if (kotlin.runCatching { FileX(context, uri).canWrite() }.getOrDefault(false))
|
||||
Preferences["download_folder"] = uri.toString()
|
||||
else {
|
||||
Snackbar.make(
|
||||
dialog.window!!.decorView.rootView,
|
||||
R.string.settings_download_folder_not_writable,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||
entries[key]!!.button.isChecked = true
|
||||
if (key == null) entries[key]!!.location_available.text = downloadFolder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
R.id.request_download_folder_old.normalizeID() -> {
|
||||
val context = context ?: return
|
||||
val dialog = dialog ?: return
|
||||
|
||||
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
||||
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
||||
|
||||
if (!File(directory).canWrite()) {
|
||||
Snackbar.make(
|
||||
dialog.window!!.decorView.rootView,
|
||||
R.string.settings_download_folder_not_writable,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||
entries[key]!!.button.isChecked = true
|
||||
if (key == null) entries[key]!!.location_available.text = downloadFolder
|
||||
}
|
||||
else
|
||||
Preferences["download_folder"] = File(directory).canonicalPath
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,42 +26,34 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout.LayoutParams
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.chip.Chip
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
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.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import xyz.quaver.hitomi.Gallery
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.getGallery
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||
import xyz.quaver.pupil.adapters.ThumbnailAdapter
|
||||
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
||||
import xyz.quaver.pupil.histories
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.ui.view.TagChip
|
||||
import xyz.quaver.pupil.util.ItemClickSupport
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
|
||||
class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) {
|
||||
|
||||
private val languages = context.resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
private val glide = Glide.with(context)
|
||||
class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : Dialog(context) {
|
||||
|
||||
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
||||
|
||||
@@ -82,7 +74,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryID", galleryID)
|
||||
})
|
||||
(context.applicationContext as Pupil).histories.add(galleryID)
|
||||
histories.add(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +82,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
try {
|
||||
val gallery = getGallery(galleryID)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
gallery_cover.post {
|
||||
gallery_progressbar.visibility = View.GONE
|
||||
gallery_title.text = gallery.title
|
||||
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
|
||||
@@ -112,7 +104,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
}
|
||||
}
|
||||
|
||||
Glide.with(context)
|
||||
glide
|
||||
.load(gallery.cover)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
@@ -131,8 +123,8 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
|
||||
private fun addDetails(gallery: Gallery) {
|
||||
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)
|
||||
|
||||
listOf(
|
||||
@@ -166,28 +158,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
|
||||
content.forEach { tag ->
|
||||
gallery_details_tags.addView(
|
||||
Chip(context).apply {
|
||||
chipIcon = when(tag.area) {
|
||||
"male" -> {
|
||||
setChipBackgroundColorResource(R.color.material_blue_700)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white)
|
||||
}
|
||||
"female" -> {
|
||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
text = when (tag.area) {
|
||||
"language" -> languages[tag.tag]
|
||||
else -> tag.tag.wordCapitalize()
|
||||
}
|
||||
|
||||
setEnsureMinTouchTargetSize(false)
|
||||
|
||||
TagChip(context, tag).apply {
|
||||
setOnClickListener {
|
||||
onChipClickedHandler.forEach { handler ->
|
||||
handler.invoke(tag)
|
||||
@@ -208,15 +179,21 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
private fun addThumbnails(gallery: Gallery) {
|
||||
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)
|
||||
|
||||
RecyclerView(context).apply {
|
||||
layoutManager = GridLayoutManager(context, 3)
|
||||
adapter = ThumbnailAdapter(glide, gallery.thumbnails)
|
||||
}.let {
|
||||
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
||||
val pager = ViewPager2(context).apply {
|
||||
adapter = ThumbnailPageAdapter(glide, gallery.thumbnails)
|
||||
}
|
||||
|
||||
gallery_details_contents.addView(
|
||||
pager,
|
||||
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
)
|
||||
|
||||
LayoutInflater.from(context).inflate(R.layout.dialog_gallery_dotindicator, gallery_details_contents)
|
||||
|
||||
gallery_dotindicator.setViewPager2(pager)
|
||||
}.let {
|
||||
gallery_contents.addView(it)
|
||||
}
|
||||
@@ -224,9 +201,9 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
|
||||
private fun addRelated(gallery: Gallery) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val galleries = ArrayList<GalleryBlock>()
|
||||
val galleries = ArrayList<Int>()
|
||||
|
||||
val adapter = GalleryBlockAdapter(context, galleries).apply {
|
||||
val adapter = GalleryBlockAdapter(glide, galleries).apply {
|
||||
onChipClickedHandler.add { tag ->
|
||||
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
|
||||
handler.invoke(tag)
|
||||
@@ -234,37 +211,25 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
}
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
gallery.related.forEachIndexed { i, galleryID ->
|
||||
async(Dispatchers.IO) {
|
||||
Cache(context).getGalleryBlock(galleryID)
|
||||
}.let {
|
||||
val galleryBlock = it.await() ?: return@let
|
||||
|
||||
galleries.add(galleryBlock)
|
||||
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)
|
||||
|
||||
RecyclerView(context).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
this.adapter = adapter
|
||||
|
||||
ItemClickSupport.addTo(this)
|
||||
.setOnItemClickListener { _, position, _ ->
|
||||
ItemClickSupport.addTo(this).apply {
|
||||
onItemClickListener = { _, position, _ ->
|
||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryID", galleries[position].id)
|
||||
putExtra("galleryID", galleries[position])
|
||||
})
|
||||
(context.applicationContext as Pupil).histories.add(galleries[position].id)
|
||||
histories.add(galleries[position])
|
||||
}
|
||||
.setOnItemLongClickListener { _, position, _ ->
|
||||
onItemLongClickListener = { _, position, _ ->
|
||||
GalleryDialog(
|
||||
context,
|
||||
galleries[position].id
|
||||
glide,
|
||||
galleries[position]
|
||||
).apply {
|
||||
onChipClickedHandler.add { tag ->
|
||||
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
||||
@@ -273,12 +238,25 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}.let {
|
||||
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
||||
}
|
||||
}.let {
|
||||
gallery_contents.addView(it)
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
gallery.related.forEach { galleryID ->
|
||||
Cache.getInstance(context, galleryID).getGalleryBlock()?.let {
|
||||
galleries.add(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,14 +22,15 @@ 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
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
class MirrorDialog(context: Context) : AlertDialog(context) {
|
||||
|
||||
@@ -56,21 +57,17 @@ class MirrorDialog(context: Context) : AlertDialog(context) {
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
initDialog()
|
||||
|
||||
setTitle(R.string.settings_mirror_title)
|
||||
setView(recyclerView)
|
||||
setView(build())
|
||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun initDialog() {
|
||||
recyclerView = RecyclerView(context).apply recyclerview@{
|
||||
private fun build() : View {
|
||||
return RecyclerView(context).apply recyclerview@{
|
||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = MirrorAdapter(context).apply adapter@{
|
||||
@@ -85,10 +82,7 @@ class MirrorDialog(context: Context) : AlertDialog(context) {
|
||||
}
|
||||
|
||||
onItemMoved = {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putString("mirrors", it.joinToString(">"))
|
||||
.apply()
|
||||
Preferences["mirrors"] = it.joinToString(">")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 kotlinx.android.synthetic.main.dialog_proxy.view.*
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.clientBuilder
|
||||
import xyz.quaver.pupil.clientHolder
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.ProxyInfo
|
||||
import xyz.quaver.pupil.util.getProxyInfo
|
||||
import xyz.quaver.pupil.util.proxyInfo
|
||||
import java.net.Proxy
|
||||
|
||||
class ProxyDialog(context: Context) : Dialog(context) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setContentView(build())
|
||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun build() : View {
|
||||
val proxyInfo = getProxyInfo()
|
||||
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null)
|
||||
|
||||
val enabler = { enable: Boolean ->
|
||||
view?.proxy_addr?.isEnabled = enable
|
||||
view?.proxy_port?.isEnabled = enable
|
||||
view?.proxy_username?.isEnabled = enable
|
||||
view?.proxy_password?.isEnabled = enable
|
||||
|
||||
if (!enable) {
|
||||
view?.proxy_addr?.text = null
|
||||
view?.proxy_port?.text = null
|
||||
view?.proxy_username?.text = null
|
||||
view?.proxy_password?.text = null
|
||||
}
|
||||
}
|
||||
|
||||
with(view.proxy_type_selector) {
|
||||
adapter = ArrayAdapter(
|
||||
context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
context.resources.getStringArray(R.array.proxy_type)
|
||||
)
|
||||
|
||||
setSelection(proxyInfo.type.ordinal)
|
||||
|
||||
onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
enabler.invoke(position != 0)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
}
|
||||
|
||||
view.proxy_addr.setText(proxyInfo.host)
|
||||
view.proxy_port.setText(proxyInfo.port?.toString())
|
||||
view.proxy_username.setText(proxyInfo.username)
|
||||
view.proxy_password.setText(proxyInfo.password)
|
||||
|
||||
enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT)
|
||||
|
||||
view.proxy_cancel.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
view.proxy_ok.setOnClickListener {
|
||||
val type = Proxy.Type.values()[view.proxy_type_selector.selectedItemPosition]
|
||||
val addr = view.proxy_addr.text?.toString()
|
||||
val port = view.proxy_port.text?.toString()?.toIntOrNull()
|
||||
val username = view.proxy_username.text?.toString()
|
||||
val password = view.proxy_password.text?.toString()
|
||||
|
||||
if (type != Proxy.Type.DIRECT) {
|
||||
if (addr == null || addr.isEmpty())
|
||||
view.proxy_addr.error = context.getText(R.string.proxy_dialog_error)
|
||||
if (port == null)
|
||||
view.proxy_port.error = context.getText(R.string.proxy_dialog_error)
|
||||
|
||||
if (addr == null || addr.isEmpty() || port == null)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
ProxyInfo(type, addr, port, username, password).let {
|
||||
Preferences["proxy"] = Json.encodeToString(it)
|
||||
|
||||
clientBuilder
|
||||
.proxyInfo(it)
|
||||
clientHolder = null
|
||||
client
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.LockActivity
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.LockActivity
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
class LockSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val lockManager = LockManager(requireContext())
|
||||
|
||||
findPreference<Preference>("lock_pattern")?.summary =
|
||||
if (lockManager.contains(Lock.Type.PATTERN))
|
||||
getString(R.string.settings_lock_enabled)
|
||||
else
|
||||
""
|
||||
|
||||
findPreference<Preference>("lock_pin")?.summary =
|
||||
if (lockManager.contains(Lock.Type.PIN))
|
||||
getString(R.string.settings_lock_enabled)
|
||||
else
|
||||
""
|
||||
|
||||
if (lockManager.isEmpty()) {
|
||||
(findPreference<Preference>("lock_fingerprint") as SwitchPreferenceCompat).isChecked = false
|
||||
|
||||
Preferences["lock_fingerprint"] = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
|
||||
|
||||
with(findPreference<Preference>("lock_pattern")) {
|
||||
this!!
|
||||
|
||||
if (LockManager(requireContext()).contains(Lock.Type.PATTERN))
|
||||
summary = getString(R.string.settings_lock_enabled)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val lockManager = LockManager(requireContext())
|
||||
|
||||
if (lockManager.contains(Lock.Type.PATTERN)) {
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_lock_remove_message)
|
||||
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
lockManager.remove(Lock.Type.PATTERN)
|
||||
onResume()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
} else {
|
||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||
putExtra("mode", "add_lock")
|
||||
putExtra("type", "pattern")
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("lock_pin")) {
|
||||
this!!
|
||||
|
||||
if (LockManager(requireContext()).contains(Lock.Type.PIN))
|
||||
summary = getString(R.string.settings_lock_enabled)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val lockManager = LockManager(requireContext())
|
||||
|
||||
if (lockManager.contains(Lock.Type.PIN)) {
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_lock_remove_message)
|
||||
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
lockManager.remove(Lock.Type.PIN)
|
||||
onResume()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
} else {
|
||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||
putExtra("mode", "add_lock")
|
||||
putExtra("type", "pin")
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("lock_fingerprint")) {
|
||||
this!!
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
this as SwitchPreferenceCompat
|
||||
|
||||
if (newValue == true && LockManager(requireContext()).isEmpty()) {
|
||||
isChecked = false
|
||||
|
||||
Toast.makeText(requireContext(), R.string.settings_lock_fingerprint_without_lock, Toast.LENGTH_SHORT).show()
|
||||
} else
|
||||
isChecked = newValue as Boolean
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import okhttp3.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.util.restore
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class ManageFavoritesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey)
|
||||
|
||||
initPreferences()
|
||||
}
|
||||
|
||||
private fun initPreferences() {
|
||||
val context = context ?: return
|
||||
|
||||
findPreference<Preference>("backup")?.setOnPreferenceClickListener {
|
||||
val request = Request.Builder()
|
||||
.url(context.getString(R.string.backup_url))
|
||||
.post(
|
||||
FormBody.Builder()
|
||||
.add("f:1", File(ContextCompat.getDataDir(context), "favorites.json").readText())
|
||||
.build()
|
||||
).build()
|
||||
|
||||
client.newCall(request).enqueue(object: Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
val view = view ?: return
|
||||
Snackbar.make(view, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (response.code() != 200) {
|
||||
response.close()
|
||||
return
|
||||
}
|
||||
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, response.body()?.use { it.string() }?.replace("\n", ""))
|
||||
}.let {
|
||||
getContext()?.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
true
|
||||
}
|
||||
findPreference<Preference>("restore")?.setOnPreferenceClickListener {
|
||||
val editText = EditText(context).apply {
|
||||
setText(context.getString(R.string.backup_url), TextView.BufferType.EDITABLE)
|
||||
}
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.settings_restore_title)
|
||||
.setView(editText)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
restore(editText.text.toString(),
|
||||
onFailure = onFailure@{
|
||||
val view = view ?: return@onFailure
|
||||
Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show()
|
||||
}, onSuccess = onSuccess@{
|
||||
val view = view ?: return@onSuccess
|
||||
Snackbar.make(view, context.getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
|
||||
})
|
||||
}.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// Do Nothing
|
||||
}.show()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.deleteRecursively
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.histories
|
||||
import xyz.quaver.pupil.util.byteToString
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import java.io.File
|
||||
|
||||
class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey)
|
||||
|
||||
initPreferences()
|
||||
}
|
||||
|
||||
override fun onPreferenceClick(preference: Preference?): Boolean {
|
||||
val context = context ?: return false
|
||||
|
||||
with(preference) {
|
||||
this ?: return false
|
||||
|
||||
when (key) {
|
||||
"delete_cache" -> {
|
||||
val dir = File(context.cacheDir, "imageCache")
|
||||
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_cache_alert_message)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (dir.exists())
|
||||
dir.deleteRecursively()
|
||||
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
var size = 0L
|
||||
|
||||
dir.walk().forEach {
|
||||
size += it.length()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"delete_downloads" -> {
|
||||
val dir = DownloadManager.getInstance(context).downloadFolder
|
||||
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_downloads_alert_message)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
job?.cancel()
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage_loading)
|
||||
}
|
||||
|
||||
if (dir.exists())
|
||||
dir.listFiles()?.forEach { (it as? FileX)?.deleteRecursively() }
|
||||
|
||||
job = launch {
|
||||
var size = 0L
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
||||
}
|
||||
dir.walk().forEach {
|
||||
size += it.length()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"clear_history" -> {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_history_alert_message)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
histories.clear()
|
||||
summary = context.getString(R.string.settings_clear_history_summary, histories.size)
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun initPreferences() {
|
||||
val context = context ?: return
|
||||
|
||||
with(findPreference<Preference>("delete_cache")) {
|
||||
this ?: return@with
|
||||
|
||||
val dir = File(context.cacheDir, "imageCache")
|
||||
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
var size = 0L
|
||||
|
||||
dir.walk().forEach {
|
||||
size += it.length()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPreferenceClickListener = this@ManageStorageFragment
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("delete_downloads")) {
|
||||
this ?: return@with
|
||||
|
||||
val dir = DownloadManager.getInstance(context).downloadFolder
|
||||
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
||||
job?.cancel()
|
||||
job = CoroutineScope(Dispatchers.IO).launch {
|
||||
var size = 0L
|
||||
|
||||
dir.walk().forEach {
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
||||
}
|
||||
|
||||
size += it.length()
|
||||
}
|
||||
}
|
||||
|
||||
onPreferenceClickListener = this@ManageStorageFragment
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("clear_history")) {
|
||||
this ?: return@with
|
||||
|
||||
summary = context.getString(R.string.settings_clear_history_summary, histories.size)
|
||||
|
||||
onPreferenceClickListener = this@ManageStorageFragment
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
job?.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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?) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,28 +18,29 @@
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.getChild
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.favorites
|
||||
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.*
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.io.File
|
||||
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class SettingsFragment :
|
||||
PreferenceFragmentCompat(),
|
||||
@@ -47,16 +48,10 @@ class SettingsFragment :
|
||||
Preference.OnPreferenceChangeListener,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val lockManager = LockManager(context!!)
|
||||
val lockManager = LockManager(requireContext())
|
||||
|
||||
findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) {
|
||||
getString(R.string.settings_lock_none)
|
||||
@@ -71,12 +66,6 @@ class SettingsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDirSize(dir: DocumentFile) : String {
|
||||
val size = dir.walk().map { it.length() }.sum()
|
||||
|
||||
return getString(R.string.settings_clear_summary, byteToString(size))
|
||||
}
|
||||
|
||||
override fun onPreferenceClick(preference: Preference?): Boolean {
|
||||
with (preference) {
|
||||
this ?: return false
|
||||
@@ -85,84 +74,36 @@ class SettingsFragment :
|
||||
"app_version" -> {
|
||||
checkUpdate(activity as SettingsActivity, true)
|
||||
}
|
||||
"delete_cache" -> {
|
||||
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
|
||||
|
||||
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()
|
||||
}
|
||||
"delete_downloads" -> {
|
||||
val dir = getDownloadDirectory(context)!!
|
||||
|
||||
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()
|
||||
|
||||
summary = getDirSize(dir)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"clear_history" -> {
|
||||
val histories = (context.applicationContext as Pupil).histories
|
||||
|
||||
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()
|
||||
}
|
||||
"dl_location" -> {
|
||||
DownloadLocationDialog(activity!!).show()
|
||||
"download_folder" -> {
|
||||
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
|
||||
}
|
||||
"default_query" -> {
|
||||
DefaultQueryDialog(context).apply {
|
||||
DefaultQueryDialog(requireContext()).apply {
|
||||
onPositiveButtonClickListener = { newTags ->
|
||||
sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
|
||||
Preferences["default_query"] = newTags.toString()
|
||||
summary = newTags.toString()
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
"app_lock" -> {
|
||||
val intent = Intent(context, LockActivity::class.java)
|
||||
activity?.startActivityForResult(intent, REQUEST_LOCK)
|
||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||
putExtra("force", true)
|
||||
}
|
||||
startActivityForResult(intent, R.id.request_lock.normalizeID())
|
||||
}
|
||||
"mirrors" -> {
|
||||
MirrorDialog(context)
|
||||
MirrorDialog(requireContext())
|
||||
.show()
|
||||
}
|
||||
"backup" -> {
|
||||
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
|
||||
context,
|
||||
getDownloadDirectory(context)?.createFile("null", "favorites.json")!!
|
||||
"proxy" -> {
|
||||
ProxyDialog(requireContext())
|
||||
.show()
|
||||
}
|
||||
"user_id" -> {
|
||||
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("user_id", Preferences.get<String>("user_id"))
|
||||
)
|
||||
|
||||
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)
|
||||
Toast.makeText(context, R.string.settings_user_id_toast, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
@@ -176,6 +117,18 @@ class SettingsFragment :
|
||||
this ?: return false
|
||||
|
||||
when (key) {
|
||||
"nomedia" -> {
|
||||
val create = (newValue as? Boolean) ?: return false
|
||||
|
||||
return kotlin.runCatching {
|
||||
val nomedia = DownloadManager.getInstance(context).downloadFolder.getChild(".nomedia")
|
||||
|
||||
if (create)
|
||||
nomedia.createNewFile()
|
||||
else
|
||||
nomedia.delete()
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
"dark_mode" -> {
|
||||
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
|
||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
@@ -190,10 +143,21 @@ class SettingsFragment :
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
"dl_location" -> {
|
||||
findPreference<Preference>(key)?.summary =
|
||||
FileUtils.getPath(context, getDownloadDirectory(context!!)?.uri)
|
||||
key ?: return
|
||||
|
||||
with(findPreference<Preference>(key)) {
|
||||
this ?: return
|
||||
|
||||
when (key) {
|
||||
"proxy" -> {
|
||||
summary = context?.let { getProxyInfo().type.name }
|
||||
}
|
||||
"download_folder" -> {
|
||||
summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath
|
||||
}
|
||||
"download_folder_name" -> {
|
||||
summary = Preferences["download_folder_name", "[-id-] -title-"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,9 +165,16 @@ class SettingsFragment :
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
||||
|
||||
Preferences.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
initPreferences()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Preferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun initPreferences() {
|
||||
for (i in 0 until preferenceScreen.preferenceCount) {
|
||||
|
||||
@@ -213,47 +184,44 @@ class SettingsFragment :
|
||||
else
|
||||
listOf(this)
|
||||
}.forEach { preference ->
|
||||
with (preference) {
|
||||
with (preference) with@{
|
||||
|
||||
when (key) {
|
||||
"app_version" -> {
|
||||
val manager = context.packageManager
|
||||
val info = manager.getPackageInfo(context.packageName, 0)
|
||||
summary = context.getString(R.string.settings_app_version_description, info.versionName)
|
||||
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 = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
|
||||
summary = getDirSize(dir)
|
||||
"download_folder_name" -> {
|
||||
summary = Preferences["download_folder_name", "[-id-] -title-"]
|
||||
|
||||
setOnPreferenceClickListener {
|
||||
DownloadFolderNameDialogFragment().show(requireActivity().supportFragmentManager, "Download Location Dialog")
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
"download_folder" -> {
|
||||
summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"delete_downloads" -> {
|
||||
val dir = getDownloadDirectory(context)!!
|
||||
summary = getDirSize(dir)
|
||||
"nomedia" -> {
|
||||
(this as SwitchPreferenceCompat).isChecked = kotlin.runCatching {
|
||||
DownloadManager.getInstance(context).downloadFolder.getChild(".nomedia").exists()
|
||||
}.getOrDefault(false)
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"clear_history" -> {
|
||||
val histories = (activity!!.application as Pupil).histories
|
||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"dl_location" -> {
|
||||
summary = FileUtils.getPath(context, getDownloadDirectory(context)?.uri)
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
onPreferenceChangeListener = this@SettingsFragment
|
||||
}
|
||||
"default_query" -> {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
summary = preferences.getString("default_query", "") ?: ""
|
||||
summary = Preferences.get<String>("default_query")
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"app_lock" -> {
|
||||
val lockManager = LockManager(context)
|
||||
val lockManager = LockManager(requireContext())
|
||||
summary =
|
||||
if (lockManager.locks.isNullOrEmpty()) {
|
||||
getString(R.string.settings_lock_none)
|
||||
@@ -272,19 +240,46 @@ class SettingsFragment :
|
||||
"mirrors" -> {
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"proxy" -> {
|
||||
summary = getProxyInfo().type.name
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"dark_mode" -> {
|
||||
onPreferenceChangeListener = this@SettingsFragment
|
||||
}
|
||||
"backup" -> {
|
||||
"old_import_galleries" -> {
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"restore" -> {
|
||||
"user_id" -> {
|
||||
summary = Preferences.get<String>("user_id")
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"oss" -> {
|
||||
setOnPreferenceClickListener {
|
||||
context?.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when(requestCode) {
|
||||
R.id.request_lock.normalizeID() -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
parentFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, LockSettingsFragment())
|
||||
.addToBackStack("Lock")
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
69
app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.view
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.chip.Chip
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class TagChip(context: Context, tag: Tag) : Chip(context) {
|
||||
|
||||
val tag: Tag =
|
||||
tag.let {
|
||||
when {
|
||||
it.area != null -> it
|
||||
else -> Tag("tag", tag.tag)
|
||||
}
|
||||
}
|
||||
|
||||
private val languages = context.resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
init {
|
||||
chipIcon = when(tag.area) {
|
||||
"male" -> {
|
||||
setChipBackgroundColorResource(R.color.material_blue_700)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.gender_male_white)
|
||||
}
|
||||
"female" -> {
|
||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.gender_female_white)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
text = when (tag.area) {
|
||||
"language" -> languages[tag.tag]
|
||||
else -> tag.tag.wordCapitalize()
|
||||
}
|
||||
|
||||
setEnsureMinTouchTargetSize(false)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2007-2008 OpenIntents.org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
/**
|
||||
* @version 2009-07-03
|
||||
* @author Peli
|
||||
* @version 2013-12-11
|
||||
* @author paulburke (ipaulpro)
|
||||
*/
|
||||
public class FileUtils {
|
||||
/**
|
||||
* Get a file path from a Uri. This will get the the path for Storage Access
|
||||
* Framework Documents, as well as the _data field for the MediaStore and
|
||||
* other file-based ContentProviders.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param uri The Uri to query.
|
||||
* @author paulburke
|
||||
*/
|
||||
public static String getPath(final Context context, final Uri uri) {
|
||||
|
||||
// DocumentProvider
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
return context.getExternalFilesDir(null).getParentFile().getParentFile().getParentFile().getParent() + "/" + split[1];
|
||||
}
|
||||
|
||||
// TODO handle non-primary volumes
|
||||
}
|
||||
// DownloadsProvider
|
||||
else if (isDownloadsDocument(uri)) {
|
||||
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
final Uri contentUri = ContentUris.withAppendedId(
|
||||
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
||||
|
||||
return getDataColumn(context, contentUri, null, null);
|
||||
}
|
||||
// MediaProvider
|
||||
else if (isMediaDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
Uri contentUri = null;
|
||||
if ("image".equals(type)) {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("video".equals(type)) {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("audio".equals(type)) {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
|
||||
final String selection = "_id=?";
|
||||
final String[] selectionArgs = new String[] {
|
||||
split[1]
|
||||
};
|
||||
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
}
|
||||
// MediaStore (and general)
|
||||
else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
return getDataColumn(context, uri, null, null);
|
||||
}
|
||||
// File
|
||||
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the data column for this Uri. This is useful for
|
||||
* MediaStore Uris, and other file-based ContentProviders.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param uri The Uri to query.
|
||||
* @param selection (Optional) Filter used in the query.
|
||||
* @param selectionArgs (Optional) Selection arguments used in the query.
|
||||
* @return The value of the _data column, which is typically a file path.
|
||||
*/
|
||||
public static String getDataColumn(Context context, Uri uri, String selection,
|
||||
String[] selectionArgs) {
|
||||
|
||||
Cursor cursor = null;
|
||||
final String column = "_data";
|
||||
final String[] projection = {
|
||||
column
|
||||
};
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final int column_index = cursor.getColumnIndexOrThrow(column);
|
||||
return cursor.getString(column_index);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is ExternalStorageProvider.
|
||||
*/
|
||||
public static boolean isExternalStorageDocument(Uri uri) {
|
||||
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is DownloadsProvider.
|
||||
*/
|
||||
public static boolean isDownloadsDocument(Uri uri) {
|
||||
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is MediaProvider.
|
||||
*/
|
||||
public static boolean isMediaDocument(Uri uri) {
|
||||
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package xyz.quaver.pupil.util;
|
||||
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import xyz.quaver.pupil.R;
|
||||
|
||||
/*
|
||||
Source: http://www.littlerobots.nl/blog/Handle-Android-RecyclerView-Clicks/
|
||||
USAGE:
|
||||
|
||||
ItemClickSupport.addTo(mRecyclerView).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
|
||||
// do it
|
||||
}
|
||||
});
|
||||
|
||||
*/
|
||||
public class ItemClickSupport {
|
||||
private final RecyclerView mRecyclerView;
|
||||
private OnItemClickListener mOnItemClickListener;
|
||||
private OnItemLongClickListener mOnItemLongClickListener;
|
||||
private View.OnClickListener mOnClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mOnItemClickListener != null) {
|
||||
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
|
||||
mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
|
||||
}
|
||||
}
|
||||
};
|
||||
private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (mOnItemLongClickListener != null) {
|
||||
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
|
||||
return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
private RecyclerView.OnChildAttachStateChangeListener mAttachListener
|
||||
= new RecyclerView.OnChildAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onChildViewAttachedToWindow(@NonNull View view) {
|
||||
if (mOnItemClickListener != null) {
|
||||
view.setOnClickListener(mOnClickListener);
|
||||
}
|
||||
if (mOnItemLongClickListener != null) {
|
||||
view.setOnLongClickListener(mOnLongClickListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewDetachedFromWindow(@NonNull View view) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private ItemClickSupport(RecyclerView recyclerView) {
|
||||
mRecyclerView = recyclerView;
|
||||
mRecyclerView.setTag(R.id.item_click_support, this);
|
||||
mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
|
||||
}
|
||||
|
||||
public static ItemClickSupport addTo(RecyclerView view) {
|
||||
ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
|
||||
if (support == null) {
|
||||
support = new ItemClickSupport(view);
|
||||
}
|
||||
return support;
|
||||
}
|
||||
|
||||
public static ItemClickSupport removeFrom(RecyclerView view) {
|
||||
ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
|
||||
if (support != null) {
|
||||
support.detach(view);
|
||||
}
|
||||
return support;
|
||||
}
|
||||
|
||||
public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
|
||||
mOnItemClickListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
|
||||
mOnItemLongClickListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void detach(RecyclerView view) {
|
||||
view.removeOnChildAttachStateChangeListener(mAttachListener);
|
||||
view.setTag(R.id.item_click_support, null);
|
||||
}
|
||||
|
||||
public interface OnItemClickListener {
|
||||
|
||||
void onItemClicked(RecyclerView recyclerView, int position, View v);
|
||||
}
|
||||
|
||||
public interface OnItemLongClickListener {
|
||||
|
||||
boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
|
||||
}
|
||||
}
|
||||
69
app/src/main/java/xyz/quaver/pupil/util/ItemClickSupport.kt
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import xyz.quaver.pupil.R
|
||||
|
||||
class ItemClickSupport(private val recyclerView: RecyclerView) {
|
||||
|
||||
var onItemClickListener: ((RecyclerView, Int, View) -> Unit)? = null
|
||||
var onItemLongClickListener: ((RecyclerView, Int, View) -> Boolean)? = null
|
||||
|
||||
init {
|
||||
recyclerView.apply {
|
||||
setTag(R.id.item_click_support, this)
|
||||
addOnChildAttachStateChangeListener(object: RecyclerView.OnChildAttachStateChangeListener {
|
||||
override fun onChildViewAttachedToWindow(view: View) {
|
||||
onItemClickListener?.let { listener ->
|
||||
view.setOnClickListener {
|
||||
recyclerView.getChildViewHolder(view).let { holder ->
|
||||
listener.invoke(recyclerView, holder.adapterPosition, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
onItemLongClickListener?.let { listener ->
|
||||
view.setOnLongClickListener {
|
||||
recyclerView.getChildViewHolder(view).let { holder ->
|
||||
listener.invoke(recyclerView, holder.adapterPosition, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChildViewDetachedFromWindow(view: View) {
|
||||
// Do Nothing
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun detach() {
|
||||
recyclerView.apply {
|
||||
clearOnChildAttachStateChangeListeners()
|
||||
setTag(R.id.item_click_support, null)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun addTo(view: RecyclerView) = view.let { removeFrom(it); ItemClickSupport(it) }
|
||||
fun removeFrom(view: RecyclerView) = (view.tag as? ItemClickSupport)?.detach()
|
||||
}
|
||||
}
|
||||
48
app/src/main/java/xyz/quaver/pupil/util/Preferences.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
object Preferences: SharedPreferences by preferences {
|
||||
|
||||
val defMap = mapOf(
|
||||
String::class to "",
|
||||
Int::class to -1,
|
||||
Long::class to -1L,
|
||||
Boolean::class to false,
|
||||
Set::class to emptySet<Any>()
|
||||
)
|
||||
|
||||
operator fun set(key: String, value: String) = edit().putString(key, value).apply()
|
||||
operator fun set(key: String, value: Int) = edit().putInt(key, value).apply()
|
||||
operator fun set(key: String, value: Long) = edit().putLong(key, value).apply()
|
||||
operator fun set(key: String, value: Boolean) = edit().putBoolean(key, value).apply()
|
||||
operator fun set(key: String, value: Set<String>) = edit().putStringSet(key, value).apply()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline operator fun <reified T: Any> get(key: String, defaultVal: T = defMap[T::class] as T): T = (all[key] as? T) ?: defaultVal
|
||||
|
||||
fun remove(key: String) {
|
||||
edit().remove(key).apply()
|
||||
}
|
||||
}
|
||||
88
app/src/main/java/xyz/quaver/pupil/util/SavedSet.kt
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
val serializer: KSerializer<List<T>>
|
||||
get() = ListSerializer(serializer(any::class.java) as KSerializer<T>)
|
||||
|
||||
init {
|
||||
if (!file.exists()) {
|
||||
file.parentFile?.mkdirs()
|
||||
save()
|
||||
}
|
||||
load()
|
||||
}
|
||||
|
||||
fun load() {
|
||||
synchronized(this) {
|
||||
set.clear()
|
||||
kotlin.runCatching {
|
||||
Json.decodeFromString(serializer, file.readText())
|
||||
}.onSuccess {
|
||||
set.addAll(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun save() {
|
||||
synchronized(this) {
|
||||
file.writeText(Json.encodeToString(serializer, set.toList()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun add(element: T): Boolean {
|
||||
load()
|
||||
|
||||
return set.add(element).also {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<T>): Boolean {
|
||||
load()
|
||||
|
||||
return set.addAll(elements).also {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
override fun remove(element: T): Boolean {
|
||||
load()
|
||||
|
||||
return set.remove(element).also {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
set.clear()
|
||||
save()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,48 +21,52 @@ package xyz.quaver.pupil.util.download
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.util.Base64
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import android.util.SparseArray
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.parse
|
||||
import kotlinx.serialization.stringify
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.pupil.util.*
|
||||
import xyz.quaver.pupil.util.getCachedGallery
|
||||
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||
import xyz.quaver.pupil.util.isParentOf
|
||||
import xyz.quaver.readBytes
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use downloader.Cache instead")
|
||||
class Cache(context: Context) : ContextWrapper(context) {
|
||||
|
||||
companion object {
|
||||
private val moving = mutableListOf<Int>()
|
||||
private val readers = SparseArray<Reader?>()
|
||||
}
|
||||
|
||||
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
// Search in this order
|
||||
// Download -> Cache
|
||||
fun getCachedGallery(galleryID: Int) : DocumentFile? {
|
||||
var file = getDownloadDirectory(this)?.findFile(galleryID.toString())
|
||||
|
||||
if (file?.exists() == true)
|
||||
return file
|
||||
|
||||
file = DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID"))
|
||||
|
||||
return if (file.exists())
|
||||
file
|
||||
else
|
||||
null
|
||||
fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also {
|
||||
if (!it.exists())
|
||||
it.mkdirs()
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun getCachedMetadata(galleryID: Int) : Metadata? {
|
||||
val file = (getCachedGallery(galleryID) ?: return null).findFile(".metadata")
|
||||
val file = File(getCachedGallery(galleryID), ".metadata")
|
||||
|
||||
if (file?.exists() != true)
|
||||
if (!file.exists())
|
||||
return null
|
||||
|
||||
return try {
|
||||
Json.parse(file.readText(this))
|
||||
Json.decodeFromString(file.readText())
|
||||
} catch (e: Exception) {
|
||||
//File corrupted
|
||||
file.delete()
|
||||
@@ -70,25 +74,30 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
}
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
|
||||
val file = getCachedGallery(galleryID)?.findFile(".metadata") ?:
|
||||
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
|
||||
if (!it.exists())
|
||||
it.mkdirs()
|
||||
}).createFile("null", ".metadata") ?: return
|
||||
if (preference.getBoolean("cache_disable", false))
|
||||
return
|
||||
|
||||
file.writeText(this, Json.stringify(metadata))
|
||||
val file = File(getCachedGallery(galleryID), ".metadata").also {
|
||||
if (!it.exists())
|
||||
it.createNewFile()
|
||||
}
|
||||
|
||||
file.writeText(Json.encodeToString(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
|
||||
val thumbnail = getGalleryBlock(galleryID)?.thumbnails?.firstOrNull() ?: return@withContext null
|
||||
try {
|
||||
Base64.encodeToString(URL(thumbnails?.firstOrNull()).readBytes(), Base64.DEFAULT)
|
||||
val data = URL(thumbnail).readBytes().apply {
|
||||
if (isEmpty()) return@withContext null
|
||||
}
|
||||
Base64.encodeToString(data, Base64.DEFAULT)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
@@ -107,17 +116,29 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
|
||||
val metadata = Cache(this).getCachedMetadata(galleryID)
|
||||
|
||||
val galleryBlock = if (metadata?.galleryBlock == null)
|
||||
listOf(
|
||||
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||
).map {
|
||||
CoroutineScope(Dispatchers.IO).async {
|
||||
kotlin.runCatching {
|
||||
it.invoke()
|
||||
}.getOrNull()
|
||||
}
|
||||
}.awaitAll().filterNotNull()
|
||||
val sources = listOf(
|
||||
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||
)
|
||||
|
||||
val galleryBlock = if (metadata?.galleryBlock == null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
var galleryBlock: GalleryBlock? = null
|
||||
|
||||
for (source in sources) {
|
||||
galleryBlock = try {
|
||||
source.invoke()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
if (galleryBlock != null)
|
||||
break
|
||||
}
|
||||
|
||||
galleryBlock
|
||||
} ?: return null
|
||||
}
|
||||
else
|
||||
metadata.galleryBlock
|
||||
|
||||
@@ -126,89 +147,145 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock)
|
||||
)
|
||||
|
||||
val mirrors = preference.getString("mirrors", "")!!.split('>')
|
||||
|
||||
return galleryBlock.firstOrNull {
|
||||
mirrors.contains(it.code.name)
|
||||
} ?: galleryBlock.firstOrNull()
|
||||
return galleryBlock
|
||||
}
|
||||
|
||||
fun getReaderOrNull(galleryID: Int): Reader? {
|
||||
val metadata = getCachedMetadata(galleryID)
|
||||
|
||||
val mirrors = preference.getString("mirrors", "")!!.split('>')
|
||||
|
||||
return metadata?.readers?.firstOrNull {
|
||||
mirrors.contains(it.code.name)
|
||||
} ?: metadata?.readers?.firstOrNull()
|
||||
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 readers = if (metadata?.readers == null) {
|
||||
listOf(
|
||||
{ xyz.quaver.hitomi.getReader(galleryID) },
|
||||
{ xyz.quaver.hiyobi.getReader(galleryID) }
|
||||
).map {
|
||||
CoroutineScope(Dispatchers.IO).async {
|
||||
kotlin.runCatching {
|
||||
it.invoke()
|
||||
}.getOrNull()
|
||||
val sources = mapOf(
|
||||
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
|
||||
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
|
||||
).let {
|
||||
if (mirrors.isNotEmpty())
|
||||
it.toSortedMap{ o1, o2 ->
|
||||
mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
|
||||
}
|
||||
}.awaitAll().filterNotNull()
|
||||
} else {
|
||||
metadata.readers
|
||||
else
|
||||
it
|
||||
}
|
||||
|
||||
if (readers.isNotEmpty())
|
||||
setCachedMetadata(
|
||||
galleryID,
|
||||
Metadata(Cache(this).getCachedMetadata(galleryID), readers = readers)
|
||||
)
|
||||
val reader =
|
||||
if (readers[galleryID] != null)
|
||||
return readers[galleryID]
|
||||
else if (metadata?.reader == null) {
|
||||
var retval: Reader? = null
|
||||
|
||||
val mirrors = preference.getString("mirrors", "")!!.split('>')
|
||||
for (source in sources) {
|
||||
retval = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
withTimeoutOrNull(1000) {
|
||||
source.value.invoke()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
null
|
||||
}
|
||||
|
||||
return readers.firstOrNull {
|
||||
mirrors.contains(it.code.name)
|
||||
} ?: readers.firstOrNull()
|
||||
if (retval != null)
|
||||
break
|
||||
}
|
||||
|
||||
retval
|
||||
} else
|
||||
metadata.reader
|
||||
|
||||
readers.put(galleryID, reader)
|
||||
|
||||
setCachedMetadata(
|
||||
galleryID,
|
||||
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
|
||||
)
|
||||
|
||||
return reader
|
||||
}
|
||||
|
||||
fun getImages(galleryID: Int): List<DocumentFile?>? {
|
||||
val gallery = getCachedGallery(galleryID) ?: return null
|
||||
val reader = getReaderOrNull(galleryID) ?: return null
|
||||
val images = gallery.listFiles()
|
||||
val imageNameRegex = Regex("""^\d+\..+$""")
|
||||
fun getImages(galleryID: Int): List<File?>? {
|
||||
val gallery = getCachedGallery(galleryID)
|
||||
|
||||
return reader.galleryInfo.indices.map { index ->
|
||||
images.firstOrNull { file -> file.name?.startsWith(index.toString()) == true }
|
||||
return gallery.list { _, name ->
|
||||
imageNameRegex.matches(name)
|
||||
}?.map {
|
||||
File(gallery, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun putImage(galleryID: Int, name: String, data: ByteArray) {
|
||||
val cache = getCachedGallery(galleryID) ?:
|
||||
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
|
||||
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.mkdirs()
|
||||
}) ?: return
|
||||
it.createNewFile()
|
||||
}
|
||||
|
||||
if (!Regex("""^[0-9]+.+$""").matches(name))
|
||||
throw IllegalArgumentException("File name is not a number")
|
||||
|
||||
cache.createFile("null", name)?.writeBytes(this, data)
|
||||
try {
|
||||
BufferedInputStream(data).use { inputStream ->
|
||||
FileOutputStream(cache).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
cache.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun moveToDownload(galleryID: Int) {
|
||||
val cache = getCachedGallery(galleryID)
|
||||
if (preference.getBoolean("cache_disable", false))
|
||||
return
|
||||
|
||||
if (cache != null) {
|
||||
val download = getDownloadDirectory(this)!!
|
||||
if (moving.contains(galleryID))
|
||||
return
|
||||
|
||||
if (!download.isParentOf(cache)) {
|
||||
cache.copyRecursively(this, download)
|
||||
cache.deleteRecursively()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val cache = getCachedGallery(galleryID).also {
|
||||
if (!it.exists())
|
||||
return@launch
|
||||
}
|
||||
} else
|
||||
getDownloadDirectory(this)?.createDirectory(galleryID.toString())
|
||||
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
|
||||
|
||||
@@ -23,30 +23,32 @@ 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.crashlytics.android.Crashlytics
|
||||
import io.fabric.sdk.android.Fabric
|
||||
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.urlFromUrlFromHash
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hitomi.imageUrlFromImage
|
||||
import xyz.quaver.hiyobi.createImgList
|
||||
import xyz.quaver.hiyobi.user_agent
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.interceptors
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use DownloadService instead")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
|
||||
|
||||
private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
@@ -74,7 +76,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
private var bufferedSource : BufferedSource? = null
|
||||
|
||||
override fun contentLength() = responseBody.contentLength()
|
||||
override fun contentType() = responseBody.contentType() ?: null
|
||||
override fun contentType() = responseBody.contentType()
|
||||
|
||||
override fun source(): BufferedSource {
|
||||
if (bufferedSource == null)
|
||||
@@ -84,7 +86,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
}
|
||||
|
||||
private fun source(source: Source) = object: ForwardingSource(source) {
|
||||
|
||||
var totalBytesRead = 0L
|
||||
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
@@ -98,6 +99,24 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
interceptors[Pair::class] = { chain ->
|
||||
val request = chain.request()
|
||||
var response = chain.proceed(request)
|
||||
|
||||
var retry = 5
|
||||
while (!response.isSuccessful && retry > 0) {
|
||||
response = chain.proceed(request)
|
||||
retry--
|
||||
}
|
||||
|
||||
response.newBuilder()
|
||||
.body(response.body()?.let {
|
||||
ProgressResponseBody(request.tag(), it, progressListener)
|
||||
}).build()
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Singleton
|
||||
@@ -126,132 +145,79 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
* SECONDARY VALUE
|
||||
* 0 <= value < 100 -> Download in progress
|
||||
* Float.POSITIVE_INFINITY -> Download completed
|
||||
* Float.NaN -> Exception
|
||||
*/
|
||||
val progress = SparseArray<MutableList<Float>?>()
|
||||
/*
|
||||
* KEY
|
||||
* primary galleryID
|
||||
* secondary index
|
||||
* PRIMARY VALUE
|
||||
* MutableList -> Download in progress / Loading
|
||||
* null -> Gallery doesn't exist
|
||||
* SECONDARY VALUE
|
||||
* Throwable -> Exception
|
||||
* null -> Download in progress / Loading
|
||||
*/
|
||||
val exception = SparseArray<MutableList<Throwable?>?>()
|
||||
val notification = SparseArray<NotificationCompat.Builder>()
|
||||
val notification = SparseArray<NotificationCompat.Builder?>()
|
||||
|
||||
private val loop = loop()
|
||||
private val worker = SparseArray<Job?>()
|
||||
@Volatile var nRunners = 0
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
var response = chain.proceed(request)
|
||||
|
||||
var retry = preferences.getInt("retry", 3)
|
||||
while (!response.isSuccessful && retry > 0) {
|
||||
response = chain.proceed(request)
|
||||
retry--
|
||||
}
|
||||
|
||||
response.newBuilder()
|
||||
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
||||
.build()
|
||||
}
|
||||
.dispatcher(Dispatcher(Executors.newSingleThreadExecutor()))
|
||||
.build()
|
||||
|
||||
fun stop() {
|
||||
queue.clear()
|
||||
|
||||
loop.cancel()
|
||||
for (i in 0..worker.size()) {
|
||||
for (i in 0 until worker.size()) {
|
||||
val galleryID = worker.keyAt(i)
|
||||
|
||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||
worker[galleryID]?.cancel()
|
||||
}
|
||||
|
||||
client.dispatcher().cancelAll()
|
||||
client.dispatcher().queuedCalls().filter {
|
||||
it.request().tag() is Pair<*, *>
|
||||
}.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
client.dispatcher().runningCalls().filter {
|
||||
it.request().tag() is Pair<*, *>
|
||||
}.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
|
||||
progress.clear()
|
||||
exception.clear()
|
||||
notification.clear()
|
||||
notificationManager.cancelAll()
|
||||
|
||||
nRunners = 0
|
||||
|
||||
}
|
||||
|
||||
fun cancel(galleryID: Int) {
|
||||
queue.remove(galleryID)
|
||||
worker[galleryID]?.cancel()
|
||||
|
||||
client.dispatcher().queuedCalls()
|
||||
.filter {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID
|
||||
}
|
||||
.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
client.dispatcher().queuedCalls().filter {
|
||||
((it.request().tag() as Pair<*, *>).first as Int) == galleryID
|
||||
}.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
client.dispatcher().runningCalls().filter {
|
||||
((it.request().tag() as Pair<*, *>).first as Int) == galleryID
|
||||
}.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
|
||||
progress.remove(galleryID)
|
||||
exception.remove(galleryID)
|
||||
notification.remove(galleryID)
|
||||
notificationManager.cancel(galleryID)
|
||||
|
||||
if (progress.indexOfKey(galleryID) >= 0) {
|
||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||
nRunners--
|
||||
}
|
||||
}
|
||||
|
||||
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
|
||||
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { it.isInfinite() } == true
|
||||
|
||||
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
|
||||
val cache = Cache(this@DownloadWorker).getImages(galleryID)
|
||||
val lowQuality = preferences.getBoolean("low_quality", false)
|
||||
|
||||
//Cache exists :P
|
||||
cache?.get(index)?.let {
|
||||
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
|
||||
|
||||
notify(galleryID)
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
with(Cache(this@DownloadWorker)) {
|
||||
if (isDownloading(galleryID)) {
|
||||
moveToDownload(galleryID)
|
||||
setDownloading(galleryID, false)
|
||||
}
|
||||
}
|
||||
nRunners--
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val request = Request.Builder().apply {
|
||||
when (reader.code) {
|
||||
Code.HITOMI -> {
|
||||
url(
|
||||
urlFromUrlFromHash(
|
||||
imageUrlFromImage(
|
||||
galleryID,
|
||||
reader.galleryInfo[index],
|
||||
if (lowQuality) "webp" else null
|
||||
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
|
||||
@@ -269,82 +235,113 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
//gallery doesn't exist
|
||||
if (reader == null) {
|
||||
progress.put(galleryID, null)
|
||||
exception.put(galleryID, null)
|
||||
|
||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||
nRunners--
|
||||
return@launch
|
||||
}
|
||||
|
||||
progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList())
|
||||
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
|
||||
val cache = Cache(this@DownloadWorker).getImages(galleryID)
|
||||
|
||||
notification[galleryID].setContentTitle(reader.title)
|
||||
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)
|
||||
|
||||
for (i in reader.galleryInfo.indices) {
|
||||
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 (Fabric.isInitialized())
|
||||
Crashlytics.logException(e)
|
||||
if (e.message?.contains("cancel", true) != false)
|
||||
return
|
||||
|
||||
progress[galleryID]?.set(i, Float.NaN)
|
||||
exception[galleryID]?.set(i, e)
|
||||
|
||||
notify(galleryID)
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
val cache = Cache(this@DownloadWorker)
|
||||
if (cache.isDownloading(galleryID)) {
|
||||
cache.moveToDownload(galleryID)
|
||||
cache.setDownloading(galleryID, false)
|
||||
}
|
||||
nRunners--
|
||||
}
|
||||
cancel(galleryID)
|
||||
queue.add(galleryID)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.body().use {
|
||||
val res = it.bytes()
|
||||
val ext =
|
||||
call.request().url().encodedPath().split('.').last()
|
||||
|
||||
Cache(this@DownloadWorker).putImage(galleryID, "$i.$ext", res)
|
||||
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
|
||||
if (response.code() != 200) {
|
||||
response.close()
|
||||
onFailure(call, IOException())
|
||||
return
|
||||
}
|
||||
|
||||
notify(galleryID)
|
||||
val ext = call.request().url().encodedPath().split('.').last()
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
val cache = Cache(this@DownloadWorker)
|
||||
if (cache.isDownloading(galleryID)) {
|
||||
cache.moveToDownload(galleryID)
|
||||
cache.setDownloading(galleryID, false)
|
||||
try {
|
||||
response.body()!!.use {
|
||||
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream())
|
||||
}
|
||||
nRunners--
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queueDownload(galleryID, reader, i, callback)
|
||||
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.isFinite() } ?: 0
|
||||
val progress = progress[galleryID]?.count { it.isInfinite() } ?: 0
|
||||
|
||||
if (isCompleted(galleryID))
|
||||
if (isCompleted(galleryID)) {
|
||||
notification[galleryID]
|
||||
?.setContentText(getString(R.string.reader_notification_complete))
|
||||
?.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
?.setProgress(0, 0, false)
|
||||
else
|
||||
?.setOngoing(false)
|
||||
|
||||
notificationManager.cancel(galleryID)
|
||||
} else
|
||||
notification[galleryID]
|
||||
?.setProgress(max, progress, false)
|
||||
?.setContentText("$progress/$max")
|
||||
|
||||
if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null)
|
||||
notificationManager.notify(galleryID, notification[galleryID].build())
|
||||
notification[galleryID]?.let { notificationManager.notify(galleryID, it.build()) }
|
||||
else
|
||||
notificationManager.cancel(galleryID)
|
||||
}
|
||||
@@ -355,7 +352,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
}
|
||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||
addNextIntentWithParentStack(intent)
|
||||
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
|
||||
@@ -364,24 +361,28 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
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() || nRunners > preferences.getInt("max_download", 4))
|
||||
if (queue.isEmpty())
|
||||
continue
|
||||
|
||||
val galleryID = queue.poll() ?: continue
|
||||
val galleryID = queue.peek() ?: continue
|
||||
|
||||
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
|
||||
continue
|
||||
cancel(galleryID)
|
||||
|
||||
if (notification[galleryID] == null)
|
||||
initNotification(galleryID)
|
||||
|
||||
initNotification(galleryID)
|
||||
if (Cache(this@DownloadWorker).isDownloading(galleryID))
|
||||
notificationManager.notify(galleryID, notification[galleryID].build())
|
||||
notification[galleryID]?.let { notificationManager.notify(galleryID, it.build()) }
|
||||
|
||||
worker.put(galleryID, download(galleryID))
|
||||
nRunners++
|
||||
queue.poll()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,23 +22,25 @@ import kotlinx.serialization.Serializable
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use downloader.Cache.Metadata instead")
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
val thumbnail: String? = null,
|
||||
val galleryBlock: List<GalleryBlock>? = null,
|
||||
val readers: List<Reader>? = null,
|
||||
val isDownloading: Boolean? = null
|
||||
var thumbnail: String? = null,
|
||||
var galleryBlock: GalleryBlock? = null,
|
||||
var reader: Reader? = null,
|
||||
var isDownloading: Boolean? = null
|
||||
) {
|
||||
constructor(
|
||||
metadata: Metadata?,
|
||||
thumbnail: String? = null,
|
||||
galleryBlock: List<GalleryBlock>? = null,
|
||||
readers: List<Reader>? = null,
|
||||
galleryBlock: GalleryBlock? = null,
|
||||
readers: Reader? = null,
|
||||
isDownloading: Boolean? = null
|
||||
) : this(
|
||||
thumbnail ?: metadata?.thumbnail,
|
||||
galleryBlock ?: metadata?.galleryBlock,
|
||||
readers ?: metadata?.readers,
|
||||
readers ?: metadata?.reader,
|
||||
isDownloading ?: metadata?.isDownloading
|
||||
)
|
||||
}
|
||||
248
app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt
Normal file
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util.downloader
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.util.SparseArray
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Request
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.*
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import java.io.IOException
|
||||
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
var galleryBlock: GalleryBlock? = null,
|
||||
var reader: Reader? = null,
|
||||
var imageList: MutableList<String?>? = null
|
||||
) {
|
||||
fun copy(): Metadata = Metadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
||||
}
|
||||
|
||||
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
|
||||
|
||||
companion object {
|
||||
val instances = SparseArray<Cache>()
|
||||
|
||||
fun getInstance(context: Context, galleryID: Int) =
|
||||
instances[galleryID] ?: synchronized(this) {
|
||||
instances[galleryID] ?: Cache(context, galleryID).also { instances.put(galleryID, it) }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun delete(galleryID: Int) {
|
||||
instances[galleryID]?.cacheFolder?.deleteRecursively()
|
||||
instances.delete(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
cacheFolder.mkdirs()
|
||||
}
|
||||
|
||||
var metadata = kotlin.runCatching {
|
||||
findFile(".metadata")?.readText()?.let {
|
||||
Json.decodeFromString<Metadata>(it)
|
||||
}
|
||||
}.getOrNull() ?: Metadata()
|
||||
|
||||
val downloadFolder: FileX?
|
||||
get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID)
|
||||
|
||||
val cacheFolder: FileX
|
||||
get() = FileX(this, cacheDir, "imageCache/$galleryID").also {
|
||||
if (!it.exists())
|
||||
it.mkdirs()
|
||||
}
|
||||
|
||||
fun findFile(fileName: String): FileX? =
|
||||
downloadFolder?.let { downloadFolder -> downloadFolder.getChild(fileName).let {
|
||||
if (it.exists()) it else null
|
||||
} } ?: cacheFolder.getChild(fileName).let {
|
||||
if (it.exists()) it else null
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
fun setMetadata(change: (Metadata) -> Unit) {
|
||||
change.invoke(metadata)
|
||||
|
||||
val file = cacheFolder.getChild(".metadata")
|
||||
|
||||
kotlin.runCatching {
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
}
|
||||
file.writeText(Json.encodeToString(metadata))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGalleryBlock(): GalleryBlock? {
|
||||
val sources = listOf(
|
||||
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||
)
|
||||
|
||||
return metadata.galleryBlock
|
||||
?: withContext(Dispatchers.IO) {
|
||||
var galleryBlock: GalleryBlock? = null
|
||||
|
||||
for (source in sources) {
|
||||
galleryBlock = try {
|
||||
source.invoke()
|
||||
} catch (e: Exception) { null }
|
||||
|
||||
if (galleryBlock != null)
|
||||
break
|
||||
}
|
||||
|
||||
galleryBlock?.also {
|
||||
setMetadata { metadata -> metadata.galleryBlock = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun getThumbnail(): ByteArray? =
|
||||
findFile(".thumbnail")?.readBytes()
|
||||
?: getGalleryBlock()?.thumbnails?.firstOrNull()?.let { withContext(Dispatchers.IO) {
|
||||
kotlin.runCatching {
|
||||
val request = Request.Builder()
|
||||
.url(it)
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() }
|
||||
}.getOrNull()?.also { kotlin.run {
|
||||
cacheFolder.getChild(".thumbnail").writeBytes(it)
|
||||
} }
|
||||
} }
|
||||
|
||||
suspend fun getReader(): Reader? {
|
||||
val mirrors = Preferences.get<String>("mirrors").let { if (it.isEmpty()) emptyList() else it.split('>') }
|
||||
|
||||
val sources = mapOf(
|
||||
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
|
||||
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
|
||||
).let {
|
||||
if (mirrors.isNotEmpty())
|
||||
it.toSortedMap{ o1, o2 -> mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name) }
|
||||
else
|
||||
it
|
||||
}
|
||||
|
||||
return metadata.reader
|
||||
?: withContext(Dispatchers.IO) {
|
||||
var reader: Reader? = null
|
||||
|
||||
for (source in sources) {
|
||||
reader = try {
|
||||
source.value.invoke()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
if (reader != null)
|
||||
break
|
||||
}
|
||||
|
||||
reader?.also {
|
||||
setMetadata { metadata ->
|
||||
metadata.reader = it
|
||||
|
||||
if (metadata.imageList == null)
|
||||
metadata.imageList = MutableList(reader.galleryInfo.files.size) { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getImage(index: Int): FileX? =
|
||||
metadata.imageList?.get(index)?.let { findFile(it) }
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
fun putImage(index: Int, fileName: String, data: ByteArray) {
|
||||
val file = cacheFolder.getChild(fileName)
|
||||
|
||||
file.createNewFile()
|
||||
file.writeBytes(data)
|
||||
setMetadata { metadata -> metadata.imageList!![index] = fileName }
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
|
||||
val downloadFolder = downloadFolder ?: return@launch
|
||||
|
||||
if (downloadFolder.getChild(".metadata").exists())
|
||||
return@launch
|
||||
|
||||
metadata.imageList?.forEach { imageName ->
|
||||
imageName ?: return@forEach
|
||||
val target = downloadFolder.getChild(imageName)
|
||||
val source = cacheFolder.getChild(imageName)
|
||||
|
||||
if (!source.exists() || target.exists())
|
||||
return@forEach
|
||||
|
||||
kotlin.runCatching {
|
||||
target.createNewFile()
|
||||
target.outputStream()?.use { target -> source.inputStream()?.use { source ->
|
||||
source.copyTo(target)
|
||||
} }
|
||||
}
|
||||
}
|
||||
|
||||
val cacheThumbnail = cacheFolder.getChild(".thumbnail")
|
||||
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
|
||||
|
||||
if (cacheThumbnail.exists() && !downloadThumbnail.exists()) {
|
||||
kotlin.runCatching {
|
||||
downloadThumbnail.createNewFile()
|
||||
downloadThumbnail.outputStream()?.use { target -> cacheThumbnail.inputStream()?.use { source ->
|
||||
source.copyTo(target)
|
||||
} }
|
||||
cacheThumbnail.delete()
|
||||
}
|
||||
}
|
||||
|
||||
val cacheMetadata = cacheFolder.getChild(".metadata")
|
||||
val downloadMetadata = downloadFolder.getChild(".metadata")
|
||||
|
||||
if (cacheMetadata.exists() && !downloadMetadata.exists()) {
|
||||
kotlin.runCatching {
|
||||
downloadMetadata.createNewFile()
|
||||
downloadMetadata.writeText(Json.encodeToString(metadata))
|
||||
cacheMetadata.delete()
|
||||
}
|
||||
}
|
||||
|
||||
cacheFolder.delete()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util.downloader
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Call
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.*
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.formatDownloadFolder
|
||||
|
||||
class DownloadManager private constructor(context: Context) : ContextWrapper(context) {
|
||||
|
||||
companion object {
|
||||
@Volatile private var instance: DownloadManager? = null
|
||||
|
||||
fun getInstance(context: Context) =
|
||||
instance ?: synchronized(this) {
|
||||
instance ?: DownloadManager(context).also { instance = it }
|
||||
}
|
||||
}
|
||||
|
||||
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
|
||||
|
||||
val downloadFolder: FileX
|
||||
get() = {
|
||||
kotlin.runCatching {
|
||||
FileX(this, Preferences.get<String>("download_folder"))
|
||||
}.getOrElse {
|
||||
Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
|
||||
defaultDownloadFolder
|
||||
}
|
||||
}.invoke()
|
||||
|
||||
private var prevDownloadFolder: FileX? = null
|
||||
private var downloadFolderMapInstance: MutableMap<Int, String>? = null
|
||||
val downloadFolderMap: MutableMap<Int, String>
|
||||
@Synchronized
|
||||
get() {
|
||||
if (prevDownloadFolder != downloadFolder) {
|
||||
prevDownloadFolder = downloadFolder
|
||||
downloadFolderMapInstance = {
|
||||
val file = downloadFolder.getChild(".download")
|
||||
|
||||
val data = if (file.exists())
|
||||
kotlin.runCatching {
|
||||
file.readText()?.let { Json.decodeFromString<MutableMap<Int, String>>(it) }
|
||||
}.onFailure { file.delete() }.getOrNull()
|
||||
else
|
||||
null
|
||||
|
||||
data ?: {
|
||||
file.createNewFile()
|
||||
mutableMapOf<Int, String>()
|
||||
}.invoke()
|
||||
}.invoke()
|
||||
}
|
||||
|
||||
return downloadFolderMapInstance!!
|
||||
}
|
||||
|
||||
|
||||
@Synchronized
|
||||
fun isDownloading(galleryID: Int): Boolean {
|
||||
val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
|
||||
|
||||
return downloadFolderMap.containsKey(galleryID)
|
||||
&& client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getDownloadFolder(galleryID: Int): FileX? =
|
||||
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
|
||||
|
||||
@Synchronized
|
||||
fun addDownloadFolder(galleryID: Int) {
|
||||
val name = runBlocking {
|
||||
Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
|
||||
}?.formatDownloadFolder() ?: return
|
||||
|
||||
val folder = downloadFolder.getChild(name)
|
||||
|
||||
if (!folder.exists())
|
||||
folder.mkdir()
|
||||
|
||||
downloadFolderMap[galleryID] = folder.name
|
||||
|
||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
|
||||
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteDownloadFolder(galleryID: Int) {
|
||||
downloadFolderMap[galleryID]?.let {
|
||||
kotlin.runCatching {
|
||||
downloadFolder.getChild(it).deleteRecursively()
|
||||
downloadFolderMap.remove(galleryID)
|
||||
|
||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
|
||||
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,118 +19,35 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import android.os.storage.StorageManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.lang.reflect.Array
|
||||
import java.net.URL
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use downloader.Cache instead")
|
||||
fun getCachedGallery(context: Context, galleryID: Int) =
|
||||
getDownloadDirectory(context)?.findFile(galleryID.toString()) ?:
|
||||
DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID"))
|
||||
|
||||
fun getDownloadDirectory(context: Context) : DocumentFile? {
|
||||
val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
|
||||
Uri.parse(it)
|
||||
File(getDownloadDirectory(context), galleryID.toString()).let {
|
||||
if (it.exists())
|
||||
it
|
||||
else
|
||||
File(context.cacheDir, "imageCache/$galleryID")
|
||||
}
|
||||
|
||||
return if (uri.toString().startsWith("file"))
|
||||
DocumentFile.fromFile(File(uri.path!!))
|
||||
else
|
||||
DocumentFile.fromTreeUri(context, uri)
|
||||
}
|
||||
|
||||
fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
||||
context.contentResolver.openOutputStream(to.uri).use { out ->
|
||||
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 DocumentFile.isParentOf(file: DocumentFile?) : Boolean {
|
||||
var parent = file?.parentFile
|
||||
while (parent != null) {
|
||||
if (this.uri.path == parent.uri.path)
|
||||
return true
|
||||
|
||||
parent = parent.parentFile
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use downloader.Cache instead")
|
||||
fun getDownloadDirectory(context: Context) =
|
||||
Preferences.get<String>("dl_location").let {
|
||||
if (it.isNotEmpty() && !it.startsWith("content"))
|
||||
File(it)
|
||||
else
|
||||
context.getExternalFilesDir(null)!!
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun DocumentFile.reader(context: Context, charset: Charset = Charsets.UTF_8) = context.contentResolver.openInputStream(uri)!!.reader(charset)
|
||||
fun DocumentFile.readBytes(context: Context) = context.contentResolver.openInputStream(uri)!!.readBytes()
|
||||
fun DocumentFile.readText(context: Context, charset: Charset = Charsets.UTF_8) = reader(context, charset).use { it.readText() }
|
||||
|
||||
fun DocumentFile.writeBytes(context: Context, array: ByteArray) = context.contentResolver.openOutputStream(uri)!!.write(array)
|
||||
fun DocumentFile.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8) = writeBytes(context, text.toByteArray(charset))
|
||||
|
||||
fun DocumentFile.copyRecursively(
|
||||
context: Context,
|
||||
target: DocumentFile
|
||||
) {
|
||||
if (!exists())
|
||||
throw Exception("The source file doesn't exist.")
|
||||
|
||||
if (this.isFile)
|
||||
target.createFile("null", name!!)!!.writeBytes(
|
||||
context,
|
||||
readBytes(context)
|
||||
)
|
||||
else if (this.isDirectory) {
|
||||
target.createDirectory(name!!).also { newTarget ->
|
||||
listFiles().forEach { child ->
|
||||
child.copyRecursively(context, newTarget!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun DocumentFile.deleteRecursively() {
|
||||
|
||||
if (this.isDirectory)
|
||||
listFiles().forEach {
|
||||
it.deleteRecursively()
|
||||
}
|
||||
|
||||
this.delete()
|
||||
}
|
||||
|
||||
fun DocumentFile.walk(state: LinkedList<DocumentFile> = LinkedList()) : Queue<DocumentFile> {
|
||||
if (state.isEmpty())
|
||||
state.push(this)
|
||||
|
||||
listFiles().forEach {
|
||||
state.push(it)
|
||||
|
||||
if (it.isDirectory) {
|
||||
it.walk(state)
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
fun File.copyTo(context: Context, target: DocumentFile) = target.writeBytes(context, this.readBytes())
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use FileX instead")
|
||||
fun File.isParentOf(another: File) =
|
||||
another.absolutePath.startsWith(this.absolutePath)
|
||||
@@ -1,83 +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 kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.parseList
|
||||
import kotlinx.serialization.stringify
|
||||
import java.io.File
|
||||
|
||||
class Histories(private val file: File) : ArrayList<Int>() {
|
||||
|
||||
init {
|
||||
if (!file.exists())
|
||||
file.parentFile?.mkdirs()
|
||||
|
||||
try {
|
||||
load()
|
||||
} catch (e: Exception) {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun load() : Histories {
|
||||
return apply {
|
||||
super.clear()
|
||||
addAll(
|
||||
Json(JsonConfiguration.Stable).parseList(
|
||||
file.bufferedReader().use { it.readText() }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun save() {
|
||||
file.writeText(Json(JsonConfiguration.Stable).stringify(this))
|
||||
}
|
||||
|
||||
override fun add(element: Int): Boolean {
|
||||
load()
|
||||
|
||||
if (contains(element))
|
||||
super.remove(element)
|
||||
|
||||
super.add(0, element)
|
||||
|
||||
save()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun remove(element: Int): Boolean {
|
||||
load()
|
||||
val retval = super.remove(element)
|
||||
save()
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
super.clear()
|
||||
save()
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,10 @@ package xyz.quaver.pupil.util
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
@@ -42,7 +43,7 @@ fun hashWithSalt(password: String): Pair<String, String> {
|
||||
return Pair(hash(password+salt), salt)
|
||||
}
|
||||
|
||||
val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
const val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
@Serializable
|
||||
data class Lock(val type: Type, val hash: String, val salt: String) {
|
||||
@@ -73,7 +74,6 @@ class LockManager(base: Context): ContextWrapper(base) {
|
||||
load()
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
private fun load() {
|
||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||
|
||||
@@ -82,17 +82,16 @@ class LockManager(base: Context): ContextWrapper(base) {
|
||||
lock.writeText("[]")
|
||||
}
|
||||
|
||||
locks = ArrayList(Json(JsonConfiguration.Stable).parseList(lock.readText()))
|
||||
locks = Json.decodeFromString(lock.readText())
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
private fun save() {
|
||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||
|
||||
if (!lock.exists())
|
||||
lock.createNewFile()
|
||||
|
||||
lock.writeText(Json(JsonConfiguration.Stable).stringify(locks?.toList() ?: listOf()))
|
||||
lock.writeText(Json.encodeToString(locks?.toList() ?: listOf()))
|
||||
}
|
||||
|
||||
fun add(lock: Lock) {
|
||||
|
||||
@@ -19,10 +19,22 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.getReferer
|
||||
import xyz.quaver.hitomi.imageUrlFromImage
|
||||
import xyz.quaver.hiyobi.createImgList
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@UseExperimental(ExperimentalStdlibApi::class)
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun String.wordCapitalize() : String {
|
||||
val result = ArrayList<String>()
|
||||
|
||||
@@ -33,15 +45,15 @@ fun String.wordCapitalize() : String {
|
||||
return result.joinToString(" ")
|
||||
}
|
||||
|
||||
fun byteToString(byte: Long, precision : Int = 1) : String {
|
||||
private val suffix = listOf(
|
||||
"B",
|
||||
"kB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB" //really?
|
||||
)
|
||||
|
||||
val suffix = listOf(
|
||||
"B",
|
||||
"kB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB" //really?
|
||||
)
|
||||
fun byteToString(byte: Long, precision : Int = 1) : String {
|
||||
var size = byte.toDouble(); var suffixIndex = 0
|
||||
|
||||
while (size >= 1024) {
|
||||
@@ -50,5 +62,70 @@ fun byteToString(byte: Long, precision : Int = 1) : String {
|
||||
}
|
||||
|
||||
return "%.${precision}f ${suffix[suffixIndex]}".format(size)
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Convert android generated ID to requestCode
|
||||
* to prevent java.lang.IllegalArgumentException: Can only use lower 16 bits for requestCode
|
||||
*
|
||||
* https://stackoverflow.com/questions/38072322/generate-16-bit-unique-ids-in-android-for-startactivityforresult
|
||||
*/
|
||||
fun Int.normalizeID() = this.and(0xFFFF)
|
||||
|
||||
fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
|
||||
proxy(proxyInfo.proxy())
|
||||
proxyInfo.authenticator()?.let {
|
||||
proxyAuthenticator(it)
|
||||
}
|
||||
}
|
||||
|
||||
val formatMap = mapOf<String, GalleryBlock.() -> (String)>(
|
||||
"-id-" to { id.toString() },
|
||||
"-title-" to { title },
|
||||
"-artist-" to { artists.joinToString() }
|
||||
// TODO
|
||||
)
|
||||
/**
|
||||
* Formats download folder name with given Metadata
|
||||
*/
|
||||
fun GalleryBlock.formatDownloadFolder(): String =
|
||||
Preferences["download_folder_name", "[-id-] -title-"].let {
|
||||
formatMap.entries.fold(it) { str, (k, v) ->
|
||||
str.replace(k, v.invoke(this), true)
|
||||
}
|
||||
}.replace("/", "")
|
||||
|
||||
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
|
||||
format.let {
|
||||
formatMap.entries.fold(it) { str, (k, v) ->
|
||||
str.replace(k, v.invoke(this), true)
|
||||
}
|
||||
}.replace("/", "")
|
||||
|
||||
val Reader.requestBuilders: List<Request.Builder>
|
||||
get() {
|
||||
val galleryID = this.galleryInfo.id ?: 0
|
||||
val lowQuality = Preferences["low_quality", true]
|
||||
|
||||
return when(code) {
|
||||
Code.HITOMI -> {
|
||||
this.galleryInfo.files.map {
|
||||
Request.Builder()
|
||||
.url(imageUrlFromImage(galleryID, it, !lowQuality))
|
||||
.header("Referer", getReferer(galleryID))
|
||||
}
|
||||
}
|
||||
Code.HIYOBI -> {
|
||||
createImgList(galleryID, this, lowQuality).map {
|
||||
Request.Builder()
|
||||
.url(it.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.ellipsize(n: Int): String =
|
||||
if (this.length > n)
|
||||
this.slice(0 until n) + "…"
|
||||
else
|
||||
this
|
||||
58
app/src/main/java/xyz/quaver/pupil/util/proxy.kt
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Credentials
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
|
||||
@Serializable
|
||||
data class ProxyInfo(
|
||||
val type: Proxy.Type,
|
||||
val host: String? = null,
|
||||
val port: Int? = null,
|
||||
val username: String? = null,
|
||||
val password: String? = null
|
||||
) {
|
||||
fun proxy() : Proxy {
|
||||
return if (host.isNullOrBlank() || port == null)
|
||||
return Proxy.NO_PROXY
|
||||
else
|
||||
Proxy(type, InetSocketAddress.createUnresolved(host, port))
|
||||
}
|
||||
|
||||
fun authenticator(): Authenticator? = if (username.isNullOrBlank() || password.isNullOrBlank()) null else
|
||||
Authenticator { _, response ->
|
||||
val credential = Credentials.basic(username, password)
|
||||
|
||||
response.request().newBuilder()
|
||||
.header("Proxy-Authorization", credential)
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun getProxyInfo(): ProxyInfo =
|
||||
Json.decodeFromString(Preferences["proxy", Json.encodeToString(ProxyInfo(Proxy.Type.DIRECT))])
|
||||
@@ -18,49 +18,73 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.DownloadManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.webkit.URLUtil
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import ru.noties.markwon.Markwon
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.getGalleryBlock
|
||||
import xyz.quaver.hitomi.getReader
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.getChild
|
||||
import xyz.quaver.io.util.*
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.Metadata
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
|
||||
fun getReleases(url: String) : JsonArray {
|
||||
return try {
|
||||
URL(url).readText().let {
|
||||
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it)
|
||||
Json.parseToJsonElement(it).jsonArray
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
JsonArray(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
fun checkUpdate(context: Context, url: String) : JsonObject? {
|
||||
fun checkUpdate(url: String) : JsonObject? {
|
||||
val releases = getReleases(url)
|
||||
|
||||
if (releases.isEmpty())
|
||||
return null
|
||||
|
||||
return releases.firstOrNull {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("beta", false))
|
||||
true
|
||||
else
|
||||
it.jsonObject["prerelease"]?.boolean == false
|
||||
Preferences["beta"] || it.jsonObject["prerelease"]?.jsonPrimitive?.booleanOrNull == false
|
||||
}?.let {
|
||||
if (it.jsonObject["tag_name"]?.content == BuildConfig.VERSION_NAME)
|
||||
if (it.jsonObject["tag_name"]?.jsonPrimitive?.contentOrNull == BuildConfig.VERSION_NAME)
|
||||
null
|
||||
else
|
||||
it.jsonObject
|
||||
@@ -69,14 +93,13 @@ fun checkUpdate(context: Context, url: String) : JsonObject? {
|
||||
|
||||
fun getApkUrl(releases: JsonObject) : String? {
|
||||
return releases["assets"]?.jsonArray?.firstOrNull {
|
||||
Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.content ?: "")
|
||||
Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.jsonPrimitive?.contentOrNull ?: "")
|
||||
}.let {
|
||||
it?.jsonObject?.get("browser_download_url")?.content
|
||||
it?.jsonObject?.get("browser_download_url")?.jsonPrimitive?.contentOrNull
|
||||
}
|
||||
}
|
||||
|
||||
const val UPDATE_NOTIFICATION_ID = 384823
|
||||
fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
||||
fun checkUpdate(context: Context, force: Boolean = false) {
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
|
||||
@@ -85,7 +108,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
||||
return
|
||||
|
||||
fun extractReleaseNote(update: JsonObject, locale: Locale) : String {
|
||||
val markdown = update["body"]!!.content
|
||||
val markdown = update["body"]!!.jsonPrimitive.content
|
||||
|
||||
val target = when(locale.language) {
|
||||
"ko" -> "한국어"
|
||||
@@ -123,12 +146,12 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
return context.getString(R.string.update_release_note, update["tag_name"]?.content, result.toString())
|
||||
return context.getString(R.string.update_release_note, update["tag_name"]?.jsonPrimitive?.contentOrNull, result.toString())
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val update =
|
||||
checkUpdate(context, context.getString(R.string.release_url)) ?: return@launch
|
||||
checkUpdate(context.getString(R.string.release_url)) ?: return@launch
|
||||
|
||||
val url = getApkUrl(update) ?: return@launch
|
||||
|
||||
@@ -136,58 +159,30 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
||||
setTitle(R.string.update_title)
|
||||
val msg = extractReleaseNote(update, Locale.getDefault())
|
||||
setMessage(Markwon.create(context).toMarkdown(msg))
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
val builder = NotificationCompat.Builder(context, "download").apply {
|
||||
setContentTitle(context.getString(R.string.update_notification_description))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
//Cancel any download queued before
|
||||
|
||||
val id: Long = Preferences["update_download_id"]
|
||||
|
||||
if (id != -1L)
|
||||
downloadManager.remove(id)
|
||||
|
||||
val target = File(context.getExternalFilesDir(null), "Pupil.apk").also {
|
||||
it.delete()
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch io@{
|
||||
val target = getDownloadDirectory(context)?.createFile("null", "Pupil.apk")!!
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
.setTitle(context.getText(R.string.update_notification_description))
|
||||
.setDestinationUri(Uri.fromFile(target))
|
||||
|
||||
try {
|
||||
URL(url).download(context, target) { progress, fileSize ->
|
||||
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
|
||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
builder.apply {
|
||||
setContentText(context.getString(R.string.update_failed))
|
||||
setMessage(context.getString(R.string.update_failed_message))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
}
|
||||
|
||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
||||
|
||||
return@io
|
||||
}
|
||||
|
||||
val install = Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
setDataAndType(target.uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||
}
|
||||
|
||||
builder.apply {
|
||||
setContentIntent(PendingIntent.getActivity(context, 0, install, 0))
|
||||
setProgress(0, 0, false)
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setContentTitle(context.getString(R.string.update_download_completed))
|
||||
setContentText(context.getString(R.string.update_download_completed_description))
|
||||
}
|
||||
|
||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||
|
||||
if (context.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED))
|
||||
context.startActivity(install)
|
||||
else
|
||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
||||
downloadManager.enqueue(request).also {
|
||||
Preferences["update_download_id"] = it
|
||||
}
|
||||
}
|
||||
setNegativeButton(if (force) android.R.string.no else R.string.ignore_update) { _, _ ->
|
||||
setNegativeButton(if (force) android.R.string.cancel else R.string.ignore_update) { _, _ ->
|
||||
if (!force)
|
||||
preferences.edit()
|
||||
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
||||
@@ -199,4 +194,143 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<Int>) -> Unit)? = null) {
|
||||
if (!URLUtil.isValidUrl(url)) {
|
||||
onFailure?.invoke(IllegalArgumentException())
|
||||
return
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
client.newCall(request).enqueue(object: Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
onFailure?.invoke(e)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
kotlin.runCatching {
|
||||
Json.decodeFromString<List<Int>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let {
|
||||
favorites.addAll(it)
|
||||
onSuccess?.invoke(it)
|
||||
}
|
||||
}.onFailure { onFailure?.invoke(it) }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private var job: Job? = null
|
||||
private val receiver = object: BroadcastReceiver() {
|
||||
val ACTION_CANCEL = "ACTION_IMPORT_CANCEL"
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
context ?: return
|
||||
|
||||
when (intent?.action) {
|
||||
ACTION_CANCEL -> {
|
||||
job?.cancel()
|
||||
NotificationManagerCompat.from(context).cancel(R.id.notification_id_import)
|
||||
context.unregisterReceiver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
|
||||
registerReceiver(receiver, IntentFilter().apply { addAction(receiver.ACTION_CANCEL) })
|
||||
|
||||
val notificationManager = NotificationManagerCompat.from(this)
|
||||
val action = NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
||||
PendingIntent.getBroadcast(this, R.id.notification_import_cancel_action.normalizeID(), Intent(receiver.ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
).build()
|
||||
val notification = NotificationCompat.Builder(this, "import")
|
||||
.setContentTitle(getText(R.string.import_old_galleries_notification))
|
||||
.setProgress(0, 0, true)
|
||||
.addAction(action)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setOngoing(true)
|
||||
|
||||
DownloadService.cancel(this)
|
||||
|
||||
job?.cancel()
|
||||
job = CoroutineScope(Dispatchers.IO).launch {
|
||||
val downloadFolders = downloadFolder.listFiles { folder ->
|
||||
folder.isDirectory && !downloadFolderMap.values.contains(folder.name)
|
||||
}?.map {
|
||||
if (it !is FileX)
|
||||
FileX(this@migrate, it)
|
||||
else
|
||||
it
|
||||
}
|
||||
|
||||
if (downloadFolders.isNullOrEmpty()) return@launch
|
||||
|
||||
downloadFolders.forEachIndexed { index, folder ->
|
||||
notification
|
||||
.setContentText(getString(R.string.import_old_galleries_notification_text, index, downloadFolders.size))
|
||||
.setProgress(index, downloadFolders.size, false)
|
||||
notificationManager.notify(R.id.notification_id_import, notification.build())
|
||||
|
||||
kotlin.runCatching {
|
||||
val metadata = kotlin.runCatching {
|
||||
folder.getChild(".metadata").readText()?.let { Json.parseToJsonElement(it).jsonObject }
|
||||
}.getOrNull()
|
||||
|
||||
val galleryID = folder.name.toIntOrNull() ?: return@runCatching
|
||||
|
||||
val galleryBlock: GalleryBlock? = kotlin.runCatching {
|
||||
metadata?.get("galleryBlock")?.let { Json.decodeFromJsonElement<GalleryBlock>(it) }
|
||||
}.getOrNull() ?: getGalleryBlock(galleryID)
|
||||
val reader: Reader? = kotlin.runCatching {
|
||||
metadata?.get("reader")?.let { Json.decodeFromJsonElement<Reader>(it) }
|
||||
}.getOrNull() ?: getReader(galleryID)
|
||||
|
||||
metadata?.get("thumbnail")?.jsonPrimitive?.contentOrNull?.also { thumbnail ->
|
||||
val file = folder.getChild(".thumbnail").also {
|
||||
if (it.exists())
|
||||
it.delete()
|
||||
it.createNewFile()
|
||||
}
|
||||
|
||||
file.writeBytes(Base64.decode(thumbnail, Base64.DEFAULT))
|
||||
}
|
||||
|
||||
val list: MutableList<String?> =
|
||||
MutableList(reader!!.galleryInfo.files.size) { null }
|
||||
|
||||
folder.listFiles { file ->
|
||||
file?.nameWithoutExtension?.let {
|
||||
Regex("""\d{5}""").matches(it) && it.toIntOrNull() != null
|
||||
} == true
|
||||
}?.forEach {
|
||||
list[it.nameWithoutExtension.toInt()] = it.name
|
||||
}
|
||||
|
||||
folder.getChild(".metadata").also { if (it.exists()) it.delete(); it.createNewFile() }.writeText(
|
||||
Json.encodeToString(Metadata(galleryBlock, reader, list))
|
||||
)
|
||||
|
||||
synchronized(Cache) {
|
||||
Cache.delete(galleryID)
|
||||
}
|
||||
downloadFolderMap[galleryID] = folder.name
|
||||
|
||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile(); it.writeText(Json.encodeToString(downloadFolderMap)) }
|
||||
}
|
||||
}
|
||||
|
||||
notification
|
||||
.setContentText(getText(R.string.import_old_galleries_notification_done))
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.mActions.clear()
|
||||
notificationManager.notify(R.id.notification_id_import, notification.build())
|
||||
|
||||
kotlin.runCatching {
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
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: 197 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 585 B |
BIN
app/src/main/res/drawable-hdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 255 B |
|
Before Width: | Height: | Size: 361 B |
|
Before Width: | Height: | Size: 470 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: 145 B |
|
Before Width: | Height: | Size: 947 B |
|
Before Width: | Height: | Size: 1001 B |
|
Before Width: | Height: | Size: 366 B |
BIN
app/src/main/res/drawable-mdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 183 B |
|
Before Width: | Height: | Size: 224 B |