Compare commits
78 Commits
4.19-hotfi
...
5.0.1-hotf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
3
.gitignore
vendored
@@ -16,4 +16,5 @@
|
|||||||
/gh-pages
|
/gh-pages
|
||||||
|
|
||||||
#Private files
|
#Private files
|
||||||
**/google-services.json
|
**/google-services.json
|
||||||
|
**/credentials.json
|
||||||
16
.idea/codeStyles/Project.xml
generated
@@ -2,6 +2,22 @@
|
|||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<option name="RIGHT_MARGIN" value="120" />
|
<option name="RIGHT_MARGIN" value="120" />
|
||||||
<JetCodeStyleSettings>
|
<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" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
<codeStyleSettings language="XML">
|
<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>
|
<settings>
|
||||||
<module2copyright>
|
<module2copyright>
|
||||||
<element module="Pupil" copyright="GPL" />
|
<element module="Pupil" copyright="GPL" />
|
||||||
<element module="libpupil" copyright="Apache" />
|
|
||||||
</module2copyright>
|
</module2copyright>
|
||||||
</settings>
|
</settings>
|
||||||
</component>
|
</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>
|
||||||
1
.idea/gradle.xml
generated
@@ -11,7 +11,6 @@
|
|||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
<option value="$PROJECT_DIR$/libpupil" />
|
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveModulePerSourceSet" value="false" />
|
<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
.idea/vcs.xml
generated
@@ -2,5 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/gh-pages" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -3,6 +3,7 @@ apply plugin: 'kotlin-android'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
|
||||||
|
|
||||||
if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
|
if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
|
||||||
logger.lifecycle("Firebase Enabled")
|
logger.lifecycle("Firebase Enabled")
|
||||||
@@ -14,32 +15,41 @@ if (file("google-services.json").exists() && file("src/debug/google-services.jso
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 30
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "xyz.quaver.pupil"
|
applicationId "xyz.quaver.pupil"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
versionCode 57
|
versionCode 59
|
||||||
versionName "4.19-hotfix1"
|
versionName "5.0.1-hotfix2"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
multiDexEnabled true
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
|
||||||
debuggable true
|
debuggable true
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
versionNameSuffix "-DEBUG"
|
versionNameSuffix "-DEBUG"
|
||||||
|
|
||||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
|
||||||
|
ext.enableCrashlytics = false
|
||||||
|
ext.alwaysUpdateBuildId = false
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
|
||||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
|
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -50,28 +60,34 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
def markwonVersion = '3.1.0'
|
|
||||||
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0"
|
//implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC"
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation "androidx.biometric:biometric:1.0.1"
|
implementation "androidx.biometric:biometric:1.0.1"
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
||||||
implementation 'com.google.firebase:firebase-core:17.4.4'
|
implementation 'com.google.firebase:firebase-core:17.5.0'
|
||||||
implementation 'com.google.firebase:firebase-analytics:17.4.4'
|
implementation 'com.google.firebase:firebase-analytics:17.5.0'
|
||||||
implementation 'com.google.firebase:firebase-crashlytics:17.1.1'
|
implementation 'com.google.firebase:firebase-crashlytics:17.2.1'
|
||||||
implementation 'com.google.firebase:firebase-perf:19.0.8'
|
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.arimorty:floatingsearchview:2.1.1'
|
||||||
implementation 'com.github.clans:fab:1.6.4'
|
implementation 'com.github.clans:fab:1.6.4'
|
||||||
|
//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: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'
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
||||||
kapt '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") {
|
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
|
||||||
@@ -82,15 +98,18 @@ dependencies {
|
|||||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
||||||
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
|
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
|
||||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
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'
|
testImplementation 'junit:junit:4.13'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||||
implementation project(path: ':libpupil')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
androidExtensions {
|
androidExtensions {
|
||||||
experimental = true
|
experimental = true
|
||||||
}
|
}
|
||||||
BIN
app/libs/kotlinx-serialization-core-1.0.0-RC.jar
Normal file
BIN
app/libs/recyclerviewfastscroller-release.aar
Normal file
15
app/proguard-rules.pro
vendored
@@ -35,4 +35,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||||
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
|
-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
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
"type": "SINGLE",
|
"type": "SINGLE",
|
||||||
"filters": [],
|
"filters": [],
|
||||||
"properties": [],
|
"properties": [],
|
||||||
"versionCode": 57,
|
"versionCode": 59,
|
||||||
"versionName": "4.19-hotfix1",
|
"versionName": "5.0.1-hotfix2",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"outputFile": "app-release.apk"
|
"outputFile": "app-release.apk"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,24 +20,10 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.rule.ActivityTestRule
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
|
||||||
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 java.net.URL
|
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
@@ -52,70 +38,4 @@ class ExampleInstrumentedTest {
|
|||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
}
|
}
|
||||||
|
|
||||||
@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_nozomi() {
|
|
||||||
val nozomi = getGalleryIDsFromNozomi(null, "index", "all")
|
|
||||||
|
|
||||||
Log.i("PUPILD", nozomi.size.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
@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())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_downloadWorker() {
|
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
|
|
||||||
val galleryID = 515515
|
|
||||||
|
|
||||||
val worker = DownloadWorker.getInstance(context)
|
|
||||||
|
|
||||||
worker.queue.add(galleryID)
|
|
||||||
|
|
||||||
while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) {
|
|
||||||
Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null")
|
|
||||||
|
|
||||||
if (worker.progress[galleryID]?.all { it.isInfinite() } == true)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i("PUPILD", "DONE!!")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_getReaderOrNull() {
|
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
|
|
||||||
val galleryID = 1561552
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
Log.i("PUPILD", Cache(context).getReader(galleryID)?.galleryInfo?.title ?: "null")
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
tools:replace="android:theme"
|
tools:replace="android:theme"
|
||||||
android:requestLegacyExternalStorage="true"
|
|
||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
@@ -36,9 +36,14 @@
|
|||||||
|
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<receiver android:name=".BroadcastReciever" android:exported="true">
|
<service android:name=".services.DownloadService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.UpdateBroadcastReceiver"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
|
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
@@ -204,7 +209,9 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.SettingsActivity"
|
android:name=".ui.SettingsActivity"
|
||||||
android:label="@string/settings_title" />
|
android:label="@string/settings_title">
|
||||||
|
<tools:validation testUrl="http://ix.io/eer" />
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
@@ -215,6 +222,17 @@
|
|||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:scheme="http"
|
||||||
|
android:host="ix.io"
|
||||||
|
android:pathPattern="/..*" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -19,13 +19,199 @@
|
|||||||
package com.arlib.floatingsearchview
|
package com.arlib.floatingsearchview
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffColorFilter
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
import android.util.AttributeSet
|
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(
|
class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||||
context: Context,
|
FloatingSearchView(context, attrs),
|
||||||
attrs: AttributeSet? = null)
|
FloatingSearchView.OnSearchListener,
|
||||||
: FloatingSearchView(context, attrs) {
|
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
|
// hack to remove color attributes which should not be reused
|
||||||
override fun onSaveInstanceState(): Parcelable? {
|
override fun onSaveInstanceState(): Parcelable? {
|
||||||
|
|||||||
@@ -18,61 +18,121 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.*
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.multidex.MultiDexApplication
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
import com.google.android.gms.security.ProviderInstaller
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import xyz.quaver.proxy
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import xyz.quaver.pupil.util.Histories
|
import kotlinx.coroutines.Dispatchers
|
||||||
import xyz.quaver.pupil.util.getProxy
|
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.io.File
|
||||||
import java.util.*
|
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 histories: SavedSet<Int>
|
||||||
lateinit var favorites: Histories
|
private set
|
||||||
|
lateinit var favorites: SavedSet<Int>
|
||||||
|
private set
|
||||||
|
lateinit var favoriteTags: SavedSet<Tag>
|
||||||
|
private set
|
||||||
|
lateinit var searchHistory: SavedSet<String>
|
||||||
|
private set
|
||||||
|
|
||||||
|
val interceptors = mutableMapOf<KClass<out Any>, PupilInterceptor>()
|
||||||
|
|
||||||
|
lateinit var clientBuilder: OkHttpClient.Builder
|
||||||
|
|
||||||
|
var clientHolder: OkHttpClient? = null
|
||||||
|
val client: OkHttpClient
|
||||||
|
get() = clientHolder ?: clientBuilder.build().also {
|
||||||
|
clientHolder = it
|
||||||
|
setClient(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Pupil : Application() {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
val userID =
|
val userID = Preferences["user_id", ""].let { userID ->
|
||||||
if (preference.getString("user_id", "").isNullOrEmpty()) {
|
if (userID.isEmpty()) UUID.randomUUID().toString().also { Preferences["user_id"] = it }
|
||||||
UUID.randomUUID().toString().also {
|
else userID
|
||||||
preference.edit().putString("user_id", it).apply()
|
}
|
||||||
}
|
|
||||||
} else
|
|
||||||
preference.getString("user_id", "") ?: ""
|
|
||||||
|
|
||||||
FirebaseCrashlytics.getInstance().setUserId(userID)
|
FirebaseCrashlytics.getInstance().setUserId(userID)
|
||||||
|
|
||||||
proxy = getProxy(this)
|
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 {
|
try {
|
||||||
preference.getString("dl_location", null).also {
|
Preferences.get<String>("download_folder").also {
|
||||||
if (!File(it!!).canWrite())
|
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()
|
throw Exception()
|
||||||
|
|
||||||
|
DownloadManager.getInstance(this).migrate()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
preference.edit().remove("dl_location").apply()
|
Preferences.remove("download_folder")
|
||||||
}
|
}
|
||||||
|
|
||||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0)
|
||||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
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)
|
if (BuildConfig.DEBUG)
|
||||||
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
||||||
@@ -95,6 +155,13 @@ class Pupil : MultiDexApplication() {
|
|||||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 {
|
manager.createNotificationChannel(NotificationChannel("update", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
|
||||||
description = getString(R.string.channel_update_description)
|
description = getString(R.string.channel_update_description)
|
||||||
enableLights(true)
|
enableLights(true)
|
||||||
@@ -102,7 +169,7 @@ class Pupil : MultiDexApplication() {
|
|||||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.createNotificationChannel(NotificationChannel("import", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
|
manager.createNotificationChannel(NotificationChannel("import", getString(R.string.channel_update), NotificationManager.IMPORTANCE_LOW).apply {
|
||||||
description = getString(R.string.channel_update_description)
|
description = getString(R.string.channel_update_description)
|
||||||
enableLights(false)
|
enableLights(false)
|
||||||
enableVibration(false)
|
enableVibration(false)
|
||||||
@@ -110,8 +177,7 @@ class Pupil : MultiDexApplication() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
AppCompatDelegate.setDefaultNightMode(when (Preferences.get<Boolean>("dark_mode")) {
|
||||||
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
|
|
||||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
false -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,24 +16,26 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
import android.content.Context
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
import com.bumptech.glide.Glide
|
||||||
import okhttp3.Dispatcher
|
import com.bumptech.glide.Registry
|
||||||
import okhttp3.OkHttpClient
|
import com.bumptech.glide.annotation.GlideModule
|
||||||
import xyz.quaver.proxy
|
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||||
import java.util.concurrent.Executors
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
import java.util.concurrent.TimeUnit
|
import com.bumptech.glide.module.AppGlideModule
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
const val REQUEST_LOCK = 38238
|
@GlideModule
|
||||||
const val REQUEST_RESTORE = 16546
|
class PupilGlideModule : AppGlideModule() {
|
||||||
const val REQUEST_IMPORT_OLD_GALLERIES = 6458
|
|
||||||
const val REQUEST_IMPORT_OLD_GALLERIES_OLD = 5946
|
|
||||||
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
|
||||||
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
|
||||||
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
|
|
||||||
|
|
||||||
const val NOTIFICATION_ID_UPDATE = 2345
|
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||||
|
registry.append(
|
||||||
|
GlideUrl::class.java,
|
||||||
|
InputStream::class.java,
|
||||||
|
OkHttpUrlLoader.Factory(client)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val json = Json(JsonConfiguration.Stable)
|
}
|
||||||
@@ -20,44 +20,46 @@ package xyz.quaver.pupil.adapters
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.util.Base64
|
|
||||||
import android.util.SparseBooleanArray
|
import android.util.SparseBooleanArray
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
|
import com.bumptech.glide.request.RequestListener
|
||||||
|
import com.bumptech.glide.request.target.Target
|
||||||
import com.daimajia.swipe.SwipeLayout
|
import com.daimajia.swipe.SwipeLayout
|
||||||
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
||||||
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
|
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
|
||||||
import com.google.android.material.chip.Chip
|
|
||||||
import kotlinx.android.synthetic.main.item_galleryblock.view.*
|
import kotlinx.android.synthetic.main.item_galleryblock.view.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.getReader
|
import xyz.quaver.hitomi.getReader
|
||||||
|
import xyz.quaver.io.util.getChild
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.Pupil
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.favorites
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
import xyz.quaver.pupil.util.Histories
|
import xyz.quaver.pupil.ui.view.TagChip
|
||||||
import xyz.quaver.pupil.util.download.Cache
|
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 xyz.quaver.pupil.util.wordCapitalize
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
class GalleryBlockAdapter(private val glide: RequestManager, 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 {
|
enum class ViewType {
|
||||||
NEXT,
|
NEXT,
|
||||||
@@ -65,8 +67,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
PREV
|
PREV
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var favorites: Histories
|
|
||||||
|
|
||||||
val timer = Timer()
|
val timer = Timer()
|
||||||
|
|
||||||
var isThin = false
|
var isThin = false
|
||||||
@@ -75,33 +75,43 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
var timerTask: TimerTask? = null
|
var timerTask: TimerTask? = null
|
||||||
|
|
||||||
private fun updateProgress(context: Context, galleryID: Int) {
|
private fun updateProgress(context: Context, galleryID: Int) {
|
||||||
val reader = Cache(context).getReaderOrNull(galleryID)
|
val cache = Cache.getInstance(context, galleryID)
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
if (reader == null || PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false)) {
|
if (cache.metadata.reader == null || Preferences["cache_disable"]) {
|
||||||
view.galleryblock_progressbar.visibility = View.GONE
|
view.galleryblock_progressbar.visibility = View.GONE
|
||||||
view.galleryblock_progress_complete.visibility = View.GONE
|
view.galleryblock_progress_complete.visibility = View.GONE
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
with(view.galleryblock_progressbar) {
|
with(view.galleryblock_progressbar) {
|
||||||
|
val imageList = cache.metadata.imageList!!
|
||||||
|
|
||||||
progress = Cache(context).getImages(galleryID)?.size ?: 0
|
progress = imageList.filterNotNull().size
|
||||||
|
max = imageList.size
|
||||||
|
|
||||||
if (visibility == View.GONE) {
|
if (visibility == View.GONE)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
max = reader.galleryInfo.files.size
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress == max) {
|
if (progress == max) {
|
||||||
|
val downloadManager = DownloadManager.getInstance(context)
|
||||||
|
|
||||||
if (completeFlag.get(galleryID, false)) {
|
if (completeFlag.get(galleryID, false)) {
|
||||||
with(view.galleryblock_progress_complete) {
|
with(view.galleryblock_progress_complete) {
|
||||||
setImageResource(R.drawable.ic_progressbar)
|
setImageResource(
|
||||||
|
if (downloadManager.getDownloadFolder(galleryID) != null)
|
||||||
|
R.drawable.ic_progressbar
|
||||||
|
else R.drawable.ic_progressbar_cache
|
||||||
|
)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
with(view.galleryblock_progress_complete) {
|
with(view.galleryblock_progress_complete) {
|
||||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
|
setImageDrawable(AnimatedVectorDrawableCompat.create(context,
|
||||||
|
if (downloadManager.getDownloadFolder(galleryID) != null)
|
||||||
|
R.drawable.ic_progressbar_complete
|
||||||
|
else R.drawable.ic_progressbar_complete_cache
|
||||||
|
).apply {
|
||||||
this?.start()
|
this?.start()
|
||||||
})
|
})
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@@ -114,7 +124,11 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(galleryBlock: GalleryBlock) {
|
fun bind(galleryID: Int) {
|
||||||
|
val cache = Cache.getInstance(view.context, galleryID)
|
||||||
|
|
||||||
|
val galleryBlock = cache.metadata.galleryBlock ?: return
|
||||||
|
|
||||||
with(view) {
|
with(view) {
|
||||||
val resources = context.resources
|
val resources = context.resources
|
||||||
val languages = resources.getStringArray(R.array.languages).map {
|
val languages = resources.getStringArray(R.array.languages).map {
|
||||||
@@ -130,53 +144,50 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
galleryblock_thumbnail.layoutParams.width = context.resources.getDimensionPixelSize(
|
galleryblock_thumbnail.layoutParams.width = context.resources.getDimensionPixelSize(
|
||||||
R.dimen.galleryblock_thumbnail_thin
|
R.dimen.galleryblock_thumbnail_thin
|
||||||
)
|
)
|
||||||
|
|
||||||
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
|
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
|
||||||
it.start()
|
it.start()
|
||||||
})
|
})
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val thumbnail = Cache(context).getThumbnail(galleryBlock.id).let {
|
val thumbnail = cache.getThumbnail()
|
||||||
if (it != null)
|
|
||||||
Base64.decode(it, Base64.DEFAULT)
|
|
||||||
else
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
galleryblock_thumbnail.post {
|
glide
|
||||||
glide
|
.load(thumbnail)
|
||||||
.load(thumbnail)
|
.skipMemoryCache(true)
|
||||||
.skipMemoryCache(true)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.error(R.drawable.image_broken_variant)
|
||||||
.error(R.drawable.image_broken_variant)
|
.listener(object: RequestListener<Drawable> {
|
||||||
.apply {
|
override fun onLoadFailed(
|
||||||
if (BuildConfig.CENSOR)
|
e: GlideException?,
|
||||||
override(5, 8)
|
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
|
||||||
}
|
}
|
||||||
.into(galleryblock_thumbnail)
|
|
||||||
}
|
override fun onResourceReady(
|
||||||
|
resource: Drawable?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<Drawable>?,
|
||||||
|
dataSource: DataSource?,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean = false
|
||||||
|
})
|
||||||
|
.apply {
|
||||||
|
if (BuildConfig.CENSOR)
|
||||||
|
override(5, 8)
|
||||||
|
}.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 (reader != null) {
|
|
||||||
val count = cache.listFiles()?.count {
|
|
||||||
Regex("^[0-9]+.+\$").matches(it.name)
|
|
||||||
} ?: 0
|
|
||||||
|
|
||||||
with(galleryblock_progressbar) {
|
|
||||||
max = reader.galleryInfo.files.size
|
|
||||||
progress = count
|
|
||||||
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
galleryblock_progressbar.visibility = View.GONE
|
|
||||||
|
|
||||||
if (timerTask == null)
|
if (timerTask == null)
|
||||||
timerTask = timer.schedule(0, 1000) {
|
timerTask = timer.schedule(0, 1000) {
|
||||||
updateProgress(context, galleryBlock.id)
|
updateProgress(context, galleryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
galleryblock_title.text = galleryBlock.title
|
galleryblock_title.text = galleryBlock.title
|
||||||
@@ -209,32 +220,10 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
|
|
||||||
galleryblock_tag_group.removeAllViews()
|
galleryblock_tag_group.removeAllViews()
|
||||||
galleryBlock.relatedTags.forEach {
|
galleryBlock.relatedTags.forEach {
|
||||||
galleryblock_tag_group.addView(Chip(context).apply {
|
galleryblock_tag_group.addView(TagChip(context, Tag.parse(it)).apply {
|
||||||
val tag = Tag.parse(it).let { tag ->
|
setOnClickListener { view ->
|
||||||
when {
|
|
||||||
tag.area != null -> tag
|
|
||||||
else -> Tag("tag", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
"female" -> {
|
|
||||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
|
||||||
ContextCompat.getDrawable(context, R.drawable.gender_female)
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
text = tag.tag.wordCapitalize()
|
|
||||||
setEnsureMinTouchTargetSize(false)
|
|
||||||
setOnClickListener {
|
|
||||||
for (callback in onChipClickedHandler)
|
for (callback in onChipClickedHandler)
|
||||||
callback.invoke(tag)
|
callback.invoke((view as TagChip).tag)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -250,9 +239,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!::favorites.isInitialized)
|
|
||||||
favorites = (context.applicationContext as Pupil).favorites
|
|
||||||
|
|
||||||
with(galleryblock_favorite) {
|
with(galleryblock_favorite) {
|
||||||
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
|
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
@@ -336,9 +322,9 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
if (holder is GalleryViewHolder) {
|
if (holder is GalleryViewHolder) {
|
||||||
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) {
|
with(holder.view.galleryblock_primary) {
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
@@ -364,7 +350,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
mItemManger.closeAllExcept(layout)
|
mItemManger.closeAllExcept(layout)
|
||||||
|
|
||||||
holder.view.galleryblock_download.text =
|
holder.view.galleryblock_download.text =
|
||||||
if (Cache(holder.view.context).isDownloading(gallery.id))
|
if (DownloadManager.getInstance(holder.view.context).isDownloading(galleryID))
|
||||||
holder.view.context.getString(android.R.string.cancel)
|
holder.view.context.getString(android.R.string.cancel)
|
||||||
else
|
else
|
||||||
holder.view.context.getString(R.string.main_download)
|
holder.view.context.getString(R.string.main_download)
|
||||||
@@ -389,8 +375,8 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() =
|
override fun getItemCount() =
|
||||||
(if (galleries.isEmpty()) 0 else galleries.size)+
|
galleries.size +
|
||||||
(if (showNext) 1 else 0)+
|
(if (showNext) 1 else 0) +
|
||||||
(if (showPrev) 1 else 0)
|
(if (showPrev) 1 else 0)
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ import android.view.LayoutInflater
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.synthetic.main.item_mirrors.view.*
|
import kotlinx.android.synthetic.main.item_mirrors.view.*
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewHolder>() {
|
class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewHolder>() {
|
||||||
@@ -41,8 +41,7 @@ class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewH
|
|||||||
}.toMap()
|
}.toMap()
|
||||||
|
|
||||||
val list = mirrors.keys.toMutableList().apply {
|
val list = mirrors.keys.toMutableList().apply {
|
||||||
PreferenceManager.getDefaultSharedPreferences(context)
|
Preferences.get<String>("mirrors")
|
||||||
.getString("mirrors", "")!!
|
|
||||||
.split(">")
|
.split(">")
|
||||||
.reversed()
|
.reversed()
|
||||||
.forEach {
|
.forEach {
|
||||||
|
|||||||
@@ -18,17 +18,20 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
package xyz.quaver.pupil.adapters
|
||||||
|
|
||||||
import android.app.Activity
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
import com.bumptech.glide.load.model.LazyHeaders
|
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.android.synthetic.main.item_reader.view.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -37,31 +40,32 @@ import xyz.quaver.Code
|
|||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.Reader
|
||||||
import xyz.quaver.hitomi.getReferer
|
import xyz.quaver.hitomi.getReferer
|
||||||
import xyz.quaver.hitomi.imageUrlFromImage
|
import xyz.quaver.hitomi.imageUrlFromImage
|
||||||
import xyz.quaver.hiyobi.cookie
|
|
||||||
import xyz.quaver.hiyobi.createImgList
|
import xyz.quaver.hiyobi.createImgList
|
||||||
import xyz.quaver.hiyobi.user_agent
|
import xyz.quaver.io.util.readBytes
|
||||||
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.util.download.Cache
|
import xyz.quaver.pupil.services.DownloadService
|
||||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
import xyz.quaver.pupil.ui.ReaderActivity
|
||||||
|
import xyz.quaver.pupil.util.Preferences
|
||||||
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class ReaderAdapter(private val glide: RequestManager,
|
class ReaderAdapter(private val activity: ReaderActivity,
|
||||||
private val galleryID: Int,
|
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||||
private val activity: Activity) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
var reader: Reader? = null
|
var reader: Reader? = null
|
||||||
val timer = Timer()
|
val timer = Timer()
|
||||||
|
|
||||||
|
private val glide = Glide.with(activity)
|
||||||
|
|
||||||
var isFullScreen = false
|
var isFullScreen = false
|
||||||
|
|
||||||
var onItemClickListener : ((Int) -> (Unit))? = null
|
var onItemClickListener : ((Int) -> (Unit))? = null
|
||||||
|
|
||||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
var downloadWorker: DownloadWorker? = null
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
return LayoutInflater.from(parent.context).inflate(
|
return LayoutInflater.from(parent.context).inflate(
|
||||||
R.layout.item_reader, parent, false
|
R.layout.item_reader, parent, false
|
||||||
@@ -70,36 +74,34 @@ class ReaderAdapter(private val glide: RequestManager,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var cache: Cache? = null
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
holder.view as ConstraintLayout
|
holder.view as ConstraintLayout
|
||||||
|
|
||||||
if (downloadWorker == null)
|
if (cache == null)
|
||||||
downloadWorker = DownloadWorker.getInstance(holder.view.context)
|
cache = Cache.getInstance(holder.view.context, galleryID)
|
||||||
|
|
||||||
if (isFullScreen) {
|
if (isFullScreen) {
|
||||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
holder.view.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
||||||
holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
|
||||||
} else {
|
} else {
|
||||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
|
holder.view.layoutParams.height = ConstraintLayout.LayoutParams.WRAP_CONTENT
|
||||||
holder.view.container.layoutParams.height = 0
|
|
||||||
|
|
||||||
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
(holder.view.progress_layout.layoutParams as ConstraintLayout.LayoutParams)
|
||||||
.dimensionRatio = "W,${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
|
.dimensionRatio = "${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
||||||
onItemClickListener?.invoke(position)
|
onItemClickListener?.invoke(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.view.container.setOnClickListener {
|
holder.view.setOnClickListener {
|
||||||
onItemClickListener?.invoke(position)
|
onItemClickListener?.invoke(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.view.reader_index.text = (position+1).toString()
|
holder.view.reader_index.text = (position+1).toString()
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(holder.view.context)
|
if (Preferences["cache_disable"]) {
|
||||||
if (preferences.getBoolean("cache_disable", false)) {
|
val lowQuality: Boolean = Preferences["low_quality"]
|
||||||
val lowQuality = preferences.getBoolean("low_quality", false)
|
|
||||||
|
|
||||||
val url = when (reader!!.code) {
|
val url = when (reader!!.code) {
|
||||||
Code.HITOMI ->
|
Code.HITOMI ->
|
||||||
@@ -111,10 +113,7 @@ class ReaderAdapter(private val glide: RequestManager,
|
|||||||
)
|
)
|
||||||
, LazyHeaders.Builder().addHeader("Referer", getReferer(galleryID)).build())
|
, LazyHeaders.Builder().addHeader("Referer", getReferer(galleryID)).build())
|
||||||
Code.HIYOBI ->
|
Code.HIYOBI ->
|
||||||
GlideUrl(createImgList(galleryID, reader!!, lowQuality)[position].path, LazyHeaders.Builder()
|
GlideUrl(createImgList(galleryID, reader!!, lowQuality)[position].path)
|
||||||
.addHeader("User-Agent", user_agent)
|
|
||||||
.addHeader("Cookie", cookie)
|
|
||||||
.build())
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
holder.view.image.post {
|
holder.view.image.post {
|
||||||
@@ -122,27 +121,64 @@ class ReaderAdapter(private val glide: RequestManager,
|
|||||||
.load(url!!)
|
.load(url!!)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.skipMemoryCache(false)
|
.skipMemoryCache(false)
|
||||||
.fitCenter()
|
.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)
|
.error(R.drawable.image_broken_variant)
|
||||||
.into(holder.view.image)
|
.into(holder.view.image)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val image = Cache(holder.view.context).getImage(galleryID, position)
|
val image = cache!!.getImage(position)
|
||||||
val progress = downloadWorker!!.progress[galleryID]?.get(position)
|
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
|
||||||
|
|
||||||
if (progress?.isInfinite() == true && image != null) {
|
if (progress?.isInfinite() == true && image != null) {
|
||||||
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
|
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
|
||||||
|
|
||||||
holder.view.image.post {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
glide
|
glide
|
||||||
.load(image)
|
.load(image.readBytes())
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.fitCenter()
|
.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)
|
.error(R.drawable.image_broken_variant)
|
||||||
.into(holder.view.image)
|
.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 {
|
} else {
|
||||||
holder.view.reader_item_progressbar.visibility = View.VISIBLE
|
holder.view.reader_item_progressbar.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package xyz.quaver.pupil
|
package xyz.quaver.pupil.receiver
|
||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
@@ -28,18 +28,11 @@ import android.webkit.MimeTypeMap
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.preference.PreferenceManager
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.util.NOTIFICATION_ID_UPDATE
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.cancelImport
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class BroadcastReciever : BroadcastReceiver() {
|
class UpdateBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ACTION_CANCEL_IMPORT = "ACTION_CANCEL_IMPORT"
|
|
||||||
|
|
||||||
const val EXTRA_IMPORT_NOTIFICATION_ID = "EXTRA_IMPORT_NOTIFICATION_ID"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
context ?: return
|
context ?: return
|
||||||
@@ -48,12 +41,10 @@ class BroadcastReciever : BroadcastReceiver() {
|
|||||||
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
||||||
|
|
||||||
// Validate download
|
// Validate download
|
||||||
|
val downloadID: Long = Preferences["update_download_id"]
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val downloadID = preference.getLong("update_download_id", -1)
|
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) != downloadID)
|
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -2) != downloadID)
|
||||||
return
|
return
|
||||||
|
|
||||||
// Get target uri
|
// Get target uri
|
||||||
@@ -62,19 +53,21 @@ class BroadcastReciever : BroadcastReceiver() {
|
|||||||
.setFilterById(downloadID)
|
.setFilterById(downloadID)
|
||||||
|
|
||||||
val uri = downloadManager.query(query).use { cursor ->
|
val uri = downloadManager.query(query).use { cursor ->
|
||||||
cursor.moveToFirst()
|
if (cursor.moveToFirst()) {
|
||||||
|
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).let {
|
||||||
|
val uri = Uri.parse(it)
|
||||||
|
|
||||||
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).let {
|
when (uri.scheme) {
|
||||||
val uri = Uri.parse(it)
|
"file" ->
|
||||||
|
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!)
|
||||||
when (uri.scheme) {
|
)
|
||||||
"file" ->
|
"content" -> uri
|
||||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!))
|
else -> null
|
||||||
"content" -> uri
|
}
|
||||||
else -> return
|
|
||||||
}
|
}
|
||||||
}
|
} else
|
||||||
}
|
null
|
||||||
|
} ?: return
|
||||||
|
|
||||||
// Build Notification
|
// Build Notification
|
||||||
|
|
||||||
@@ -92,10 +85,7 @@ class BroadcastReciever : BroadcastReceiver() {
|
|||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
notificationManager.notify(NOTIFICATION_ID_UPDATE, notification)
|
notificationManager.notify(R.id.notification_id_update, notification)
|
||||||
}
|
|
||||||
ACTION_CANCEL_IMPORT -> {
|
|
||||||
cancelImport = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
412
app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
/*
|
||||||
|
* 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?.forEachIndexed { index, image ->
|
||||||
|
progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCompleted(galleryID)) {
|
||||||
|
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 {
|
override fun getBody(): String {
|
||||||
return s
|
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) {
|
data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) {
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(tag: String) : Tag {
|
fun parse(tag: String) : Tag {
|
||||||
if (tag.first() == '-') {
|
if (tag.firstOrNull() == '-') {
|
||||||
tag.substring(1).split(Regex(":"), 2).let {
|
tag.substring(1).split(Regex(":"), 2).let {
|
||||||
return when(it.size) {
|
return when(it.size) {
|
||||||
2 -> Tag(it[0], it[1], true)
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode() = toString().hashCode()
|
||||||
return super.hashCode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
class Tags(val tags: MutableSet<Tag> = mutableSetOf()) : MutableSet<Tag> by tags {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(tags: String) : Tags {
|
fun parse(tags: String) : Tags {
|
||||||
@@ -77,20 +75,13 @@ class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
|||||||
Tag.parse(it)
|
Tag.parse(it)
|
||||||
else
|
else
|
||||||
null
|
null
|
||||||
}
|
}.filterNotNull().toMutableSet()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
tag?.forEach {
|
|
||||||
if (it != null)
|
|
||||||
add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun contains(element: String): Boolean {
|
fun contains(element: String): Boolean {
|
||||||
forEach {
|
tags.forEach {
|
||||||
if (it.toString() == element)
|
if (it.toString() == element)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -99,23 +90,22 @@ class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun add(element: String): Boolean {
|
fun add(element: String): Boolean {
|
||||||
return super.add(Tag.parse(element))
|
return tags.add(Tag.parse(element))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(element: String) {
|
fun remove(element: String) {
|
||||||
filter { it.toString() == element }.forEach {
|
tags.filter { it.toString() == element }.forEach {
|
||||||
remove(it)
|
tags.remove(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeByArea(area: String, isNegative: Boolean? = null) {
|
fun removeByArea(area: String, isNegative: Boolean? = null) {
|
||||||
filter { it.area == area && (if(isNegative == null) true else (it.isNegative == isNegative)) }.forEach {
|
tags.filter { it.area == area && (if(isNegative == null) true else (it.isNegative == isNegative)) }.forEach {
|
||||||
remove(it)
|
tags.remove(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.andrognito.patternlockview.PatternLockView
|
import com.andrognito.patternlockview.PatternLockView
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.activity_lock.*
|
import kotlinx.android.synthetic.main.activity_lock.*
|
||||||
@@ -38,7 +37,9 @@ import xyz.quaver.pupil.ui.fragment.PINLockFragment
|
|||||||
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
|
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
|
||||||
import xyz.quaver.pupil.util.Lock
|
import xyz.quaver.pupil.util.Lock
|
||||||
import xyz.quaver.pupil.util.LockManager
|
import xyz.quaver.pupil.util.LockManager
|
||||||
|
import xyz.quaver.pupil.util.Preferences
|
||||||
|
|
||||||
|
private var lastUnlocked = 0L
|
||||||
class LockActivity : AppCompatActivity() {
|
class LockActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var lockManager: LockManager
|
private lateinit var lockManager: LockManager
|
||||||
@@ -52,6 +53,7 @@ class LockActivity : AppCompatActivity() {
|
|||||||
val result = lockManager.check(it)
|
val result = lockManager.check(it)
|
||||||
|
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
|
lastUnlocked = System.currentTimeMillis()
|
||||||
setResult(Activity.RESULT_OK)
|
setResult(Activity.RESULT_OK)
|
||||||
finish()
|
finish()
|
||||||
} else
|
} else
|
||||||
@@ -86,6 +88,7 @@ class LockActivity : AppCompatActivity() {
|
|||||||
val result = lockManager.check(it)
|
val result = lockManager.check(it)
|
||||||
|
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
|
lastUnlocked = System.currentTimeMillis()
|
||||||
setResult(Activity.RESULT_OK)
|
setResult(Activity.RESULT_OK)
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
@@ -157,6 +160,7 @@ class LockActivity : AppCompatActivity() {
|
|||||||
override fun onAuthenticationSucceeded(
|
override fun onAuthenticationSucceeded(
|
||||||
result: BiometricPrompt.AuthenticationResult) {
|
result: BiometricPrompt.AuthenticationResult) {
|
||||||
super.onAuthenticationSucceeded(result)
|
super.onAuthenticationSucceeded(result)
|
||||||
|
lastUnlocked = System.currentTimeMillis()
|
||||||
setResult(RESULT_OK)
|
setResult(RESULT_OK)
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
@@ -185,6 +189,7 @@ class LockActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mode = intent.getStringExtra("mode")
|
mode = intent.getStringExtra("mode")
|
||||||
|
val force = intent.getBooleanExtra("force", false)
|
||||||
|
|
||||||
when(mode) {
|
when(mode) {
|
||||||
null -> {
|
null -> {
|
||||||
@@ -194,8 +199,15 @@ class LockActivity : AppCompatActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (System.currentTimeMillis() - lastUnlocked < 5*60*1000 && !force) {
|
||||||
|
lastUnlocked = System.currentTimeMillis()
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lock_fingerprint", false)
|
Preferences["lock_fingerprint"]
|
||||||
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
) {
|
) {
|
||||||
lock_fingerprint.apply {
|
lock_fingerprint.apply {
|
||||||
|
|||||||
@@ -19,62 +19,49 @@
|
|||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.*
|
import android.text.InputType
|
||||||
import android.text.style.AlignmentSpan
|
import android.view.*
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|
||||||
import com.arlib.floatingsearchview.FloatingSearchView
|
import com.arlib.floatingsearchview.FloatingSearchView
|
||||||
import com.arlib.floatingsearchview.FloatingSearchViewDayNight
|
import com.arlib.floatingsearchview.FloatingSearchViewDayNight
|
||||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import com.arlib.floatingsearchview.util.view.SearchInputView
|
import com.arlib.floatingsearchview.util.view.SearchInputView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import com.google.android.material.navigation.NavigationView
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import kotlinx.android.synthetic.main.activity_main_content.*
|
import kotlinx.android.synthetic.main.activity_main_content.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.builtins.list
|
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.doSearch
|
import xyz.quaver.hitomi.doSearch
|
||||||
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
||||||
import xyz.quaver.hitomi.getSuggestionsForQuery
|
import xyz.quaver.hitomi.getSuggestionsForQuery
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.*
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.services.DownloadService
|
||||||
import xyz.quaver.pupil.types.TagSuggestion
|
import xyz.quaver.pupil.types.*
|
||||||
import xyz.quaver.pupil.types.Tags
|
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
|
||||||
import xyz.quaver.pupil.ui.dialog.GalleryDialog
|
import xyz.quaver.pupil.ui.dialog.GalleryDialog
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
import xyz.quaver.pupil.util.download.Cache
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
import java.io.File
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity :
|
||||||
|
BaseActivity(),
|
||||||
|
FloatingSearchView.OnMenuItemClickListener,
|
||||||
|
NavigationView.OnNavigationItemSelectedListener
|
||||||
|
{
|
||||||
|
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
SEARCH,
|
SEARCH,
|
||||||
@@ -82,13 +69,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
DOWNLOAD,
|
DOWNLOAD,
|
||||||
FAVORITE
|
FAVORITE
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SortMode {
|
enum class SortMode {
|
||||||
NEWEST,
|
NEWEST,
|
||||||
POPULAR
|
POPULAR
|
||||||
}
|
}
|
||||||
|
|
||||||
private val galleries = ArrayList<GalleryBlock>()
|
private val galleries = ArrayList<Int>()
|
||||||
|
|
||||||
private var query = ""
|
private var query = ""
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -98,72 +85,48 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setText(query, TextView.BufferType.EDITABLE)
|
setText(query, TextView.BufferType.EDITABLE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private var queryStack = mutableListOf<String>()
|
||||||
|
|
||||||
private var mode = Mode.SEARCH
|
private var mode = Mode.SEARCH
|
||||||
private var sortMode = SortMode.NEWEST
|
private var sortMode = SortMode.NEWEST
|
||||||
|
|
||||||
private val REQUEST_SETTINGS = 45162
|
|
||||||
private val REQUEST_LOCK = 561
|
|
||||||
|
|
||||||
private var galleryIDs: Deferred<List<Int>>? = null
|
private var galleryIDs: Deferred<List<Int>>? = null
|
||||||
private var totalItems = 0
|
private var totalItems = 0
|
||||||
private var loadingJob: Job? = null
|
private var loadingJob: Job? = null
|
||||||
private var currentPage = 0
|
private var currentPage = 0
|
||||||
|
|
||||||
private lateinit var histories: Histories
|
|
||||||
private lateinit var favorites: Histories
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val lockManager = try {
|
if (intent.action == Intent.ACTION_VIEW) {
|
||||||
LockManager(this)
|
intent.dataString?.let { url ->
|
||||||
} catch (e: Exception) {
|
restore(url,
|
||||||
android.app.AlertDialog.Builder(this).apply {
|
onFailure = {
|
||||||
setTitle(R.string.warning)
|
Snackbar.make(this.main_recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
||||||
setMessage(R.string.lock_corrupted)
|
}, onSuccess = {
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
Snackbar.make(this.main_recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
|
||||||
finish()
|
}
|
||||||
}
|
)
|
||||||
}.show()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lockManager.isNotEmpty())
|
|
||||||
startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK)
|
|
||||||
|
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
|
|
||||||
if (Locale.getDefault().language == "ko") {
|
|
||||||
if (!preference.getBoolean("https_block_alert", false)) {
|
|
||||||
android.app.AlertDialog.Builder(this).apply {
|
|
||||||
setTitle(R.string.https_block_alert_title)
|
|
||||||
setMessage(R.string.https_block_alert)
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
preference.edit().putBoolean("https_block_alert", true).apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(application as Pupil) {
|
|
||||||
this@MainActivity.histories = histories
|
|
||||||
this@MainActivity.favorites = favorites
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
if (Preferences["download_folder", ""].isEmpty())
|
||||||
|
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
|
||||||
|
|
||||||
checkUpdate(this)
|
checkUpdate(this)
|
||||||
|
|
||||||
initView()
|
initView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
when {
|
when {
|
||||||
main_drawer_layout.isDrawerOpen(GravityCompat.START) -> main_drawer_layout.closeDrawer(GravityCompat.START)
|
main_drawer_layout.isDrawerOpen(GravityCompat.START) -> main_drawer_layout.closeDrawer(GravityCompat.START)
|
||||||
query.isNotEmpty() -> runOnUiThread {
|
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
|
||||||
query = ""
|
query = queryStack.last()
|
||||||
|
|
||||||
cancelFetch()
|
cancelFetch()
|
||||||
clearGalleries()
|
clearGalleries()
|
||||||
@@ -180,22 +143,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
|
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
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 onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
val perPage = Preferences["per_page", "25"].toInt()
|
||||||
val perPage = preference.getString("per_page", "25")!!.toIntOrNull() ?: 25
|
|
||||||
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
|
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
|
||||||
|
|
||||||
return when(keyCode) {
|
return when(keyCode) {
|
||||||
@@ -232,9 +181,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
when(requestCode) {
|
when(requestCode) {
|
||||||
REQUEST_SETTINGS -> {
|
R.id.request_settings.normalizeID() -> {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
cancelFetch()
|
cancelFetch()
|
||||||
clearGalleries()
|
clearGalleries()
|
||||||
@@ -242,10 +190,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
loadBlocks()
|
loadBlocks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
REQUEST_LOCK -> {
|
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||||
if (resultCode != Activity.RESULT_OK)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,80 +213,19 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
//NavigationView
|
//NavigationView
|
||||||
main_nav_view.setNavigationItemSelectedListener {
|
main_nav_view.setNavigationItemSelectedListener(this)
|
||||||
runOnUiThread {
|
|
||||||
main_drawer_layout.closeDrawers()
|
|
||||||
|
|
||||||
when(it.itemId) {
|
|
||||||
R.id.main_drawer_home -> {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
currentPage = 0
|
|
||||||
query = ""
|
|
||||||
mode = Mode.SEARCH
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
R.id.main_drawer_history -> {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
currentPage = 0
|
|
||||||
query = ""
|
|
||||||
mode = Mode.HISTORY
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
R.id.main_drawer_downloads -> {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
currentPage = 0
|
|
||||||
query = ""
|
|
||||||
mode = Mode.DOWNLOAD
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
R.id.main_drawer_favorite -> {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
currentPage = 0
|
|
||||||
query = ""
|
|
||||||
mode = Mode.FAVORITE
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
R.id.main_drawer_help -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
|
|
||||||
}
|
|
||||||
R.id.main_drawer_github -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github))))
|
|
||||||
}
|
|
||||||
R.id.main_drawer_homepage -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page))))
|
|
||||||
}
|
|
||||||
R.id.main_drawer_email -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
|
|
||||||
}
|
|
||||||
R.id.main_drawer_kakaotalk -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
with(main_fab_cancel) {
|
with(main_fab_cancel) {
|
||||||
setImageResource(R.drawable.cancel)
|
setImageResource(R.drawable.cancel)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
DownloadWorker.getInstance(context).stop()
|
DownloadService.cancel(this@MainActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(main_fab_jump) {
|
with(main_fab_jump) {
|
||||||
setImageResource(R.drawable.ic_jump)
|
setImageResource(R.drawable.ic_jump)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
val perPage = Preferences["per_page", "25"].toInt()
|
||||||
val perPage = preference.getString("per_page", "25")!!.toIntOrNull() ?: 25
|
|
||||||
val editText = EditText(context)
|
val editText = EditText(context)
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
@@ -377,13 +261,24 @@ class MainActivity : AppCompatActivity() {
|
|||||||
if (it?.isEmpty() == false) {
|
if (it?.isEmpty() == false) {
|
||||||
val galleryID = it.random()
|
val galleryID = it.random()
|
||||||
|
|
||||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
|
GalleryDialog(
|
||||||
putExtra("galleryID", galleryID)
|
this@MainActivity,
|
||||||
}
|
Glide.with(this@MainActivity),
|
||||||
|
galleryID
|
||||||
|
).apply {
|
||||||
|
onChipClickedHandler.add {
|
||||||
|
runOnUiThread {
|
||||||
|
query = it.toQuery()
|
||||||
|
currentPage = 0
|
||||||
|
|
||||||
startActivity(intent)
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
histories.add(galleryID)
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,8 +302,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|
||||||
histories.add(galleryID)
|
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
@@ -436,19 +329,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onDownloadClickedHandler = { position ->
|
onDownloadClickedHandler = { position ->
|
||||||
val galleryID = galleries[position].id
|
val galleryID = galleries[position]
|
||||||
val worker = DownloadWorker.getInstance(context)
|
if (Preferences["cache_disable"])
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
|
|
||||||
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
|
||||||
else {
|
else {
|
||||||
if (worker.progress.indexOfKey(galleryID) >= 0 && Cache(context).isDownloading(galleryID)) { //download in progress
|
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
|
||||||
Cache(context).setDownloading(galleryID, false)
|
DownloadService.cancel(this@MainActivity, galleryID)
|
||||||
worker.cancel(galleryID)
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Cache(context).setDownloading(galleryID, true)
|
DownloadManager.getInstance(context).addDownloadFolder(galleryID)
|
||||||
|
DownloadService.download(this@MainActivity, galleryID)
|
||||||
worker.queue.add(galleryID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,48 +346,41 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDeleteClickedHandler = { position ->
|
onDeleteClickedHandler = { position ->
|
||||||
val galleryID = galleries[position].id
|
val galleryID = galleries[position]
|
||||||
|
DownloadService.delete(this@MainActivity, galleryID)
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
histories.remove(galleryID)
|
||||||
DownloadWorker.getInstance(context).cancel(galleryID)
|
|
||||||
|
|
||||||
Cache(context).getCachedGallery(galleryID).deleteRecursively()
|
if (this@MainActivity.mode != Mode.SEARCH)
|
||||||
|
runOnUiThread {
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
|
||||||
histories.remove(galleryID)
|
completeFlag.put(galleryID, false)
|
||||||
|
|
||||||
if (this@MainActivity.mode != Mode.SEARCH)
|
|
||||||
runOnUiThread {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
|
|
||||||
completeFlag.put(galleryID, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAllItems()
|
closeAllItems()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ItemClickSupport.addTo(this)
|
ItemClickSupport.addTo(this).apply {
|
||||||
.setOnItemClickListener { _, position, v ->
|
onItemClickListener = listener@{ _, position, v ->
|
||||||
if (v !is CardView)
|
if (v !is CardView)
|
||||||
return@setOnItemClickListener
|
return@listener
|
||||||
|
|
||||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
||||||
val gallery = galleries[position]
|
intent.putExtra("galleryID", galleries[position])
|
||||||
intent.putExtra("galleryID", gallery.id)
|
|
||||||
|
|
||||||
//TODO: Maybe sprinkling some transitions will be nice :D
|
//TODO: Maybe sprinkling some transitions will be nice :D
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
histories.add(gallery.id)
|
onItemLongClickListener = listener@{ _, position, v ->
|
||||||
}.setOnItemLongClickListener { _, position, v ->
|
|
||||||
|
|
||||||
if (v !is CardView)
|
if (v !is CardView)
|
||||||
return@setOnItemLongClickListener true
|
return@listener false
|
||||||
|
|
||||||
val galleryID = galleries[position].id
|
val galleryID = galleries[position]
|
||||||
|
|
||||||
GalleryDialog(
|
GalleryDialog(
|
||||||
this@MainActivity,
|
this@MainActivity,
|
||||||
@@ -520,11 +403,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var origin = 0f
|
var origin = 0f
|
||||||
var target = -1
|
var target = -1
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
val perPage = Preferences["per_page", "25"].toInt()
|
||||||
val perPage = preferences.getString("per_page", "25")!!.toInt()
|
|
||||||
setOnTouchListener { _, event ->
|
setOnTouchListener { _, event ->
|
||||||
when(event.action) {
|
when(event.action) {
|
||||||
MotionEvent.ACTION_UP -> {
|
MotionEvent.ACTION_UP -> {
|
||||||
@@ -725,37 +608,24 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isFavorite = false
|
||||||
|
private val defaultSuggestions: List<SearchSuggestion>
|
||||||
|
get() = when {
|
||||||
|
isFavorite -> {
|
||||||
|
favoriteTags.map {
|
||||||
|
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
|
||||||
|
} + FavoriteHistorySwitch(getString(R.string.search_show_histories))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
searchHistory.map {
|
||||||
|
Suggestion(it)
|
||||||
|
}.takeLast(20) + FavoriteHistorySwitch(getString(R.string.search_show_tags))
|
||||||
|
}
|
||||||
|
}.reversed()
|
||||||
|
|
||||||
private var suggestionJob : Job? = null
|
private var suggestionJob : Job? = null
|
||||||
private fun setupSearchBar() {
|
private fun setupSearchBar() {
|
||||||
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
|
||||||
//Change upper case letters to lower case
|
|
||||||
searchInputView.addTextChangedListener(object: TextWatcher {
|
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun afterTextChanged(s: Editable?) {
|
|
||||||
s ?: return
|
|
||||||
|
|
||||||
if (s.any { it.isUpperCase() })
|
|
||||||
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
|
|
||||||
|
|
||||||
with(main_searchview as FloatingSearchViewDayNight) {
|
with(main_searchview as FloatingSearchViewDayNight) {
|
||||||
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
|
|
||||||
val serializer = Tag.serializer().list
|
|
||||||
|
|
||||||
if (!favoritesFile.exists()) {
|
|
||||||
favoritesFile.createNewFile()
|
|
||||||
favoritesFile.writeText(json.stringify(serializer, Tags(listOf())))
|
|
||||||
}
|
|
||||||
|
|
||||||
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
|
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
|
||||||
override fun onMenuOpened() {
|
override fun onMenuOpened() {
|
||||||
(this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
|
(this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
|
||||||
@@ -766,176 +636,55 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setOnMenuItemClickListener {
|
onHistoryDeleteClickedListener = {
|
||||||
when(it.itemId) {
|
searchHistory.remove(it)
|
||||||
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
|
swapSuggestions(defaultSuggestions)
|
||||||
R.id.main_menu_thin -> {
|
|
||||||
main_recyclerview.apply {
|
|
||||||
(adapter as GalleryBlockAdapter).apply {
|
|
||||||
isThin = !isThin
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter = adapter // Force to redraw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
R.id.main_menu_sort_newest -> {
|
|
||||||
sortMode = SortMode.NEWEST
|
|
||||||
it.isChecked = true
|
|
||||||
|
|
||||||
runOnUiThread {
|
|
||||||
currentPage = 0
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
R.id.main_menu_sort_popular -> {
|
|
||||||
sortMode = SortMode.POPULAR
|
|
||||||
it.isChecked = true
|
|
||||||
|
|
||||||
runOnUiThread {
|
|
||||||
currentPage = 0
|
|
||||||
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
onFavoriteHistorySwitchClickListener = {
|
||||||
|
isFavorite = !isFavorite
|
||||||
|
swapSuggestions(defaultSuggestions)
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnMenuItemClickListener(this@MainActivity)
|
||||||
|
|
||||||
setOnQueryChangeListener { _, query ->
|
setOnQueryChangeListener { _, query ->
|
||||||
this@MainActivity.query = query
|
this@MainActivity.query = query
|
||||||
|
|
||||||
suggestionJob?.cancel()
|
suggestionJob?.cancel()
|
||||||
|
|
||||||
clearSuggestions()
|
|
||||||
|
|
||||||
if (query.isEmpty() or query.endsWith(' ')) {
|
if (query.isEmpty() or query.endsWith(' ')) {
|
||||||
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map {
|
swapSuggestions(defaultSuggestions)
|
||||||
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
|
|
||||||
})
|
|
||||||
|
|
||||||
return@setOnQueryChangeListener
|
return@setOnQueryChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString())))
|
||||||
|
|
||||||
val currentQuery = query.split(" ").last().replace('_', ' ')
|
val currentQuery = query.split(" ").last().replace('_', ' ')
|
||||||
|
|
||||||
suggestionJob = CoroutineScope(Dispatchers.IO).launch {
|
suggestionJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val suggestions = ArrayList(getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) })
|
val suggestions = kotlin.runCatching {
|
||||||
|
getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }.toMutableList()
|
||||||
|
}.getOrElse { mutableListOf() }
|
||||||
|
|
||||||
suggestions.filter {
|
suggestions.filter {
|
||||||
val tag = "${it.n}:${it.s.replace(Regex("\\s"), "_")}"
|
val tag = "${it.n}:${it.s.replace(Regex("\\s"), "_")}"
|
||||||
Tags(json.parse(serializer, favoritesFile.readText())).contains(tag)
|
favoriteTags.contains(Tag.parse(tag))
|
||||||
}.reversed().forEach {
|
}.reversed().forEach {
|
||||||
suggestions.remove(it)
|
suggestions.remove(it)
|
||||||
suggestions.add(0, it)
|
suggestions.add(0, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
swapSuggestions(suggestions)
|
swapSuggestions(if (suggestions.isNotEmpty()) suggestions else listOf(NoResultSuggestion(getText(R.string.main_no_result).toString())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnBindSuggestionCallback { suggestionView, leftIcon, textView, item, _ ->
|
|
||||||
item as TagSuggestion
|
|
||||||
|
|
||||||
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
|
|
||||||
|
|
||||||
val color = TypedValue()
|
|
||||||
theme.resolveAttribute(R.attr.colorControlNormal, color, true)
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
|
|
||||||
if (Tags(json.parse(serializer, favoritesFile.readText())).contains(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 favorites = Tags(json.parse(serializer, favoritesFile.readText()))
|
|
||||||
|
|
||||||
if (favorites.contains(tag)) {
|
|
||||||
setImageResource(R.drawable.ic_star_empty)
|
|
||||||
favorites.remove(tag)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context,
|
|
||||||
R.drawable.avd_star
|
|
||||||
))
|
|
||||||
(drawable as Animatable).start()
|
|
||||||
|
|
||||||
favorites.add(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
favoritesFile.writeText(json.stringify(serializer, favorites))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.t == -1) {
|
|
||||||
textView.text = item.s
|
|
||||||
} else {
|
|
||||||
val text = "${item.s}\n ${item.t}"
|
|
||||||
|
|
||||||
val len = text.length
|
|
||||||
val left = item.s.length
|
|
||||||
|
|
||||||
textView.text = SpannableString(text).apply {
|
|
||||||
val s = AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE)
|
|
||||||
setSpan(s, left, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
setSpan(SetLineOverlap(true), 1, len-2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
setSpan(SetLineOverlap(false), len-1, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setOnSearchListener(object : FloatingSearchView.OnSearchListener {
|
|
||||||
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
|
|
||||||
if (searchSuggestion !is TagSuggestion)
|
|
||||||
return
|
|
||||||
|
|
||||||
with(searchInputView.text) {
|
|
||||||
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length)
|
|
||||||
append("${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")} ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSearchAction(currentQuery: String?) {
|
|
||||||
//Do search on onFocusCleared()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener {
|
setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener {
|
||||||
override fun onFocus() {
|
override fun onFocus() {
|
||||||
if (query.isEmpty() or query.endsWith(' '))
|
if (query.isEmpty() or query.endsWith(' '))
|
||||||
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map {
|
swapSuggestions(defaultSuggestions)
|
||||||
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFocusCleared() {
|
override fun onFocusCleared() {
|
||||||
@@ -955,6 +704,113 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onActionMenuItemSelected(item: MenuItem?) {
|
||||||
|
when(item?.itemId) {
|
||||||
|
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), R.id.request_settings.normalizeID())
|
||||||
|
R.id.main_menu_thin -> {
|
||||||
|
main_recyclerview.apply {
|
||||||
|
(adapter as GalleryBlockAdapter).apply {
|
||||||
|
isThin = !isThin
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = adapter // Force to redraw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R.id.main_menu_sort_newest -> {
|
||||||
|
sortMode = SortMode.NEWEST
|
||||||
|
item.isChecked = true
|
||||||
|
|
||||||
|
runOnUiThread {
|
||||||
|
currentPage = 0
|
||||||
|
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R.id.main_menu_sort_popular -> {
|
||||||
|
sortMode = SortMode.POPULAR
|
||||||
|
item.isChecked = true
|
||||||
|
|
||||||
|
runOnUiThread {
|
||||||
|
currentPage = 0
|
||||||
|
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||||
|
runOnUiThread {
|
||||||
|
main_drawer_layout.closeDrawers()
|
||||||
|
|
||||||
|
when(item.itemId) {
|
||||||
|
R.id.main_drawer_home -> {
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
currentPage = 0
|
||||||
|
query = ""
|
||||||
|
queryStack.clear()
|
||||||
|
mode = Mode.SEARCH
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
R.id.main_drawer_history -> {
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
currentPage = 0
|
||||||
|
query = ""
|
||||||
|
queryStack.clear()
|
||||||
|
mode = Mode.HISTORY
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
R.id.main_drawer_downloads -> {
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
currentPage = 0
|
||||||
|
query = ""
|
||||||
|
queryStack.clear()
|
||||||
|
mode = Mode.DOWNLOAD
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
R.id.main_drawer_favorite -> {
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
currentPage = 0
|
||||||
|
query = ""
|
||||||
|
queryStack.clear()
|
||||||
|
mode = Mode.FAVORITE
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
R.id.main_drawer_help -> {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
|
||||||
|
}
|
||||||
|
R.id.main_drawer_github -> {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github))))
|
||||||
|
}
|
||||||
|
R.id.main_drawer_homepage -> {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page))))
|
||||||
|
}
|
||||||
|
R.id.main_drawer_email -> {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
|
||||||
|
}
|
||||||
|
R.id.main_drawer_kakaotalk -> {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private fun cancelFetch() {
|
private fun cancelFetch() {
|
||||||
galleryIDs?.cancel()
|
galleryIDs?.cancel()
|
||||||
loadingJob?.cancel()
|
loadingJob?.cancel()
|
||||||
@@ -976,8 +832,29 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchGalleries(query: String, sortMode: SortMode) {
|
private fun fetchGalleries(query: String, sortMode: SortMode) {
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
val defaultQuery: String = Preferences["default_query"]
|
||||||
val defaultQuery = preference.getString("default_query", "")!!
|
|
||||||
|
if (query.isNotBlank())
|
||||||
|
searchHistory.add(query)
|
||||||
|
|
||||||
|
if (query != queryStack.lastOrNull()) {
|
||||||
|
queryStack.remove(query)
|
||||||
|
queryStack.add(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.isNotEmpty() && mode != Mode.SEARCH) {
|
||||||
|
Snackbar.make(this@MainActivity.main_recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply {
|
||||||
|
setAction(android.R.string.ok) {
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
currentPage = 0
|
||||||
|
mode = Mode.SEARCH
|
||||||
|
queryStack.clear()
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
galleryIDs = null
|
galleryIDs = null
|
||||||
|
|
||||||
@@ -1004,34 +881,28 @@ class MainActivity : AppCompatActivity() {
|
|||||||
Mode.HISTORY -> {
|
Mode.HISTORY -> {
|
||||||
when {
|
when {
|
||||||
query.isEmpty() -> {
|
query.isEmpty() -> {
|
||||||
histories.toList().also {
|
histories.reversed().also {
|
||||||
totalItems = it.size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val result = doSearch(query).sorted()
|
val result = doSearch(query).sorted()
|
||||||
histories.filter { result.binarySearch(it) >= 0 }.also {
|
histories.reversed().filter { result.binarySearch(it) >= 0 }.also {
|
||||||
totalItems = it.size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode.DOWNLOAD -> {
|
Mode.DOWNLOAD -> {
|
||||||
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
|
val downloads = DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList()
|
||||||
file.isDirectory && file.name.toIntOrNull() != null
|
|
||||||
}?.sortedByDescending {
|
|
||||||
it.lastModified()
|
|
||||||
}?.map {
|
|
||||||
it.name.toInt()
|
|
||||||
} ?: emptyList()
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
query.isEmpty() -> downloads.also {
|
query.isEmpty() -> downloads.reversed().also {
|
||||||
totalItems = it.size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val result = doSearch(query).sorted()
|
val result = doSearch(query).sorted()
|
||||||
downloads.filter { result.binarySearch(it) >= 0 }.also {
|
downloads.reversed().filter { result.binarySearch(it) >= 0 }.also {
|
||||||
totalItems = it.size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1039,12 +910,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
Mode.FAVORITE -> {
|
Mode.FAVORITE -> {
|
||||||
when {
|
when {
|
||||||
query.isEmpty() -> favorites.toList().also {
|
query.isEmpty() -> favorites.reversed().also {
|
||||||
totalItems = it.size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val result = doSearch(query).sorted()
|
val result = doSearch(query).sorted()
|
||||||
favorites.filter { result.binarySearch(it) >= 0 }.also {
|
favorites.reversed().filter { result.binarySearch(it) >= 0 }.also {
|
||||||
totalItems = it.size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1055,8 +926,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadBlocks() {
|
private fun loadBlocks() {
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
val perPage = Preferences["per_page", "25"].toInt()
|
||||||
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
|
|
||||||
|
|
||||||
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val galleryIDs = try {
|
val galleryIDs = try {
|
||||||
@@ -1081,16 +951,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
for (chunk in chunks)
|
for (chunk in chunks)
|
||||||
chunk.map { galleryID ->
|
chunk.map { galleryID ->
|
||||||
async {
|
async {
|
||||||
Cache(this@MainActivity).getGalleryBlock(galleryID)
|
Cache.getInstance(this@MainActivity, galleryID).getGalleryBlock()?.let {
|
||||||
|
galleryID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.forEach {
|
}.forEach {
|
||||||
val galleryBlock = it.await()
|
it.await()?.also {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
main_progressbar.hide()
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
galleries.add(it)
|
||||||
main_progressbar.hide()
|
|
||||||
|
|
||||||
if (galleryBlock != null) {
|
|
||||||
galleries.add(galleryBlock)
|
|
||||||
main_recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
|
main_recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1098,4 +968,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLowMemory() {
|
||||||
|
super.onLowMemory()
|
||||||
|
Glide.get(this).onLowMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTrimMemory(level: Int) {
|
||||||
|
super.onTrimMemory(level)
|
||||||
|
Glide.get(this).onTrimMemory(level)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,14 +18,16 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -36,6 +38,7 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
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.*
|
||||||
import kotlinx.android.synthetic.main.activity_reader.view.*
|
import kotlinx.android.synthetic.main.activity_reader.view.*
|
||||||
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
||||||
@@ -43,16 +46,19 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.pupil.Pupil
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
import xyz.quaver.pupil.adapters.ReaderAdapter
|
||||||
import xyz.quaver.pupil.util.Histories
|
import xyz.quaver.pupil.favorites
|
||||||
import xyz.quaver.pupil.util.download.Cache
|
import xyz.quaver.pupil.histories
|
||||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
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 java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
import kotlin.concurrent.timer
|
||||||
|
|
||||||
class ReaderActivity : AppCompatActivity() {
|
class ReaderActivity : BaseActivity() {
|
||||||
|
|
||||||
private var galleryID = 0
|
private var galleryID = 0
|
||||||
private var currentPage = 0
|
private var currentPage = 0
|
||||||
@@ -70,30 +76,34 @@ 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 val timer = Timer()
|
||||||
|
private var autoTimer: Timer? = null
|
||||||
|
|
||||||
private val snapHelper = PagerSnapHelper()
|
private val snapHelper = PagerSnapHelper()
|
||||||
|
|
||||||
private var menu: Menu? = null
|
private var menu: Menu? = null
|
||||||
|
|
||||||
private lateinit var favorites: Histories
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_reader)
|
||||||
|
|
||||||
title = getString(R.string.reader_loading)
|
title = getString(R.string.reader_loading)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
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)
|
handleIntent(intent)
|
||||||
|
cache = Cache.getInstance(this, galleryID)
|
||||||
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
|
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
|
||||||
|
|
||||||
if (galleryID == 0) {
|
if (galleryID == 0) {
|
||||||
@@ -101,11 +111,10 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
initView()
|
if (Preferences["cache_disable"]) {
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("cache_disable", false)) {
|
|
||||||
reader_download_progressbar.visibility = View.GONE
|
reader_download_progressbar.visibility = View.GONE
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val reader = Cache(this@ReaderActivity).getReader(galleryID)
|
val reader = cache.getReader()
|
||||||
|
|
||||||
launch(Dispatchers.Main) initDownloader@{
|
launch(Dispatchers.Main) initDownloader@{
|
||||||
if (reader == null) {
|
if (reader == null) {
|
||||||
@@ -115,6 +124,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
return@initDownloader
|
return@initDownloader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
histories.add(galleryID)
|
||||||
(reader_recyclerview.adapter as ReaderAdapter).apply {
|
(reader_recyclerview.adapter as ReaderAdapter).apply {
|
||||||
this.reader = reader
|
this.reader = reader
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
@@ -132,6 +142,8 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
initDownloader()
|
initDownloader()
|
||||||
|
|
||||||
|
initView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
@@ -157,19 +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()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
menuInflater.inflate(R.menu.reader, menu)
|
menuInflater.inflate(R.menu.reader, menu)
|
||||||
|
|
||||||
@@ -184,14 +183,14 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when(item?.itemId) {
|
when(item.itemId) {
|
||||||
R.id.reader_menu_page_indicator -> {
|
R.id.reader_menu_page_indicator -> {
|
||||||
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
|
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
|
||||||
with(view.dialog_number_picker) {
|
with(view.dialog_number_picker) {
|
||||||
minValue=1
|
minValue = 1
|
||||||
maxValue=Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.size ?: 0
|
maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0
|
||||||
value=currentPage
|
value = currentPage
|
||||||
}
|
}
|
||||||
val dialog = AlertDialog.Builder(this).apply {
|
val dialog = AlertDialog.Builder(this).apply {
|
||||||
setView(view)
|
setView(view)
|
||||||
@@ -226,8 +225,11 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
timer.cancel()
|
timer.cancel()
|
||||||
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
|
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
|
||||||
|
|
||||||
if (!Cache(this).isDownloading(galleryID))
|
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
|
||||||
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)
|
DownloadService.cancel(this, galleryID)
|
||||||
|
|
||||||
|
if (downloader != null)
|
||||||
|
unbindService(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
@@ -263,32 +265,33 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initDownloader() {
|
private fun initDownloader() {
|
||||||
val worker = DownloadWorker.getInstance(this).apply {
|
DownloadService.download(this, galleryID, true)
|
||||||
cancel(galleryID)
|
bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
|
||||||
queue.add(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
timer.schedule(1000, 1000) {
|
timer.schedule(1000, 1000) {
|
||||||
if (worker.progress.indexOfKey(galleryID) < 0) //loading
|
val downloader = downloader ?: return@schedule
|
||||||
|
|
||||||
|
if (downloader.progress.indexOfKey(galleryID) < 0) //loading
|
||||||
return@schedule
|
return@schedule
|
||||||
|
|
||||||
if (worker.progress[galleryID] == null) { //Gallery not found
|
if (downloader.progress[galleryID] == null) { //Gallery not found
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
Snackbar
|
Snackbar
|
||||||
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
histories.add(galleryID)
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||||
reader_download_progressbar.progress = worker.progress[galleryID]?.count { it.isInfinite() } ?: 0
|
reader_download_progressbar.progress = downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
|
||||||
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||||
|
|
||||||
if (title == getString(R.string.reader_loading)) {
|
if (title == getString(R.string.reader_loading)) {
|
||||||
val reader = Cache(this@ReaderActivity).getReaderOrNull(galleryID)
|
val reader = cache.metadata.reader
|
||||||
|
|
||||||
if (reader != null) {
|
if (reader != null) {
|
||||||
|
|
||||||
with (reader_recyclerview.adapter as ReaderAdapter) {
|
with (reader_recyclerview.adapter as ReaderAdapter) {
|
||||||
this.reader = reader
|
this.reader = reader
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
@@ -306,7 +309,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (worker.progress[galleryID]?.all { it.isInfinite() } == true) { //Download finished
|
if (downloader.isCompleted(galleryID)) { //Download finished
|
||||||
reader_download_progressbar.visibility = View.GONE
|
reader_download_progressbar.visibility = View.GONE
|
||||||
|
|
||||||
animateDownloadFAB(false)
|
animateDownloadFAB(false)
|
||||||
@@ -317,7 +320,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
with(reader_recyclerview) {
|
with(reader_recyclerview) {
|
||||||
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, this@ReaderActivity).apply {
|
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
|
||||||
onItemClickListener = {
|
onItemClickListener = {
|
||||||
if (isScroll) {
|
if (isScroll) {
|
||||||
isScroll = false
|
isScroll = false
|
||||||
@@ -352,18 +355,19 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
with(reader_fab_download) {
|
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 {
|
setOnClickListener {
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
|
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
|
||||||
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
|
||||||
else {
|
else {
|
||||||
if (Cache(context).isDownloading(galleryID)) {
|
val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
|
||||||
Cache(context).setDownloading(galleryID, false)
|
|
||||||
|
|
||||||
|
if (downloadManager.isDownloading(galleryID)) {
|
||||||
|
downloadManager.deleteDownloadFolder(galleryID)
|
||||||
animateDownloadFAB(false)
|
animateDownloadFAB(false)
|
||||||
} else {
|
} else {
|
||||||
Cache(context).setDownloading(galleryID, true)
|
downloadManager.addDownloadFolder(galleryID)
|
||||||
animateDownloadFAB(true)
|
animateDownloadFAB(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,9 +377,31 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
with(reader_fab_retry) {
|
with(reader_fab_retry) {
|
||||||
setImageResource(R.drawable.refresh)
|
setImageResource(R.drawable.refresh)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
DownloadWorker.getInstance(context).let {
|
downloader?.cancel(galleryID)
|
||||||
it.cancel(galleryID)
|
downloader?.download(galleryID)
|
||||||
it.queue.add(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 {
|
||||||
|
autoTimer?.cancel()
|
||||||
|
autoTimer = null
|
||||||
|
setImageResource(R.drawable.clock_start)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,10 +423,22 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||||
supportActionBar?.hide()
|
supportActionBar?.hide()
|
||||||
this@ReaderActivity.reader_fab.visibility = View.INVISIBLE
|
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 {
|
} else {
|
||||||
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
||||||
supportActionBar?.show()
|
supportActionBar?.show()
|
||||||
this@ReaderActivity.reader_fab.visibility = View.VISIBLE
|
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
|
window.attributes = this
|
||||||
@@ -415,7 +453,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
reader_recyclerview.layoutManager = LinearLayoutManager(this)
|
reader_recyclerview.layoutManager = LinearLayoutManager(this)
|
||||||
} else {
|
} else {
|
||||||
snapHelper.attachToRecyclerView(reader_recyclerview)
|
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)
|
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
||||||
@@ -428,8 +466,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
|
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
|
||||||
override fun onAnimationEnd(drawable: Drawable?) {
|
override fun onAnimationEnd(drawable: Drawable?) {
|
||||||
val worker = DownloadWorker.getInstance(context)
|
if (downloader?.isCompleted(galleryID) == true) // If download is finished, stop animating
|
||||||
if (worker.progress[galleryID]?.all { it.isInfinite() } == true) // If download is finished, stop animating
|
|
||||||
post {
|
post {
|
||||||
setImageResource(R.drawable.ic_download)
|
setImageResource(R.drawable.ic_download)
|
||||||
labelText = getString(R.string.reader_fab_download_cancel)
|
labelText = getString(R.string.reader_fab_download_cancel)
|
||||||
@@ -450,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -22,34 +22,25 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.settings_activity.*
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.builtins.list
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
|
||||||
import xyz.quaver.pupil.Pupil
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.favorites
|
||||||
import xyz.quaver.pupil.ui.fragment.LockSettingsFragment
|
import xyz.quaver.pupil.ui.fragment.LockSettingsFragment
|
||||||
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import java.io.File
|
import xyz.quaver.pupil.util.normalizeID
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : BaseActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
window.setFlags(
|
|
||||||
WindowManager.LayoutParams.FLAG_SECURE,
|
|
||||||
WindowManager.LayoutParams.FLAG_SECURE)
|
|
||||||
|
|
||||||
setContentView(R.layout.settings_activity)
|
setContentView(R.layout.settings_activity)
|
||||||
supportFragmentManager
|
supportFragmentManager
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
@@ -58,155 +49,24 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
when (item.itemId) {
|
||||||
|
|
||||||
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) {
|
|
||||||
android.R.id.home -> onBackPressed()
|
android.R.id.home -> onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
when(requestCode) {
|
|
||||||
REQUEST_LOCK -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
supportFragmentManager
|
|
||||||
.beginTransaction()
|
|
||||||
.replace(R.id.settings, LockSettingsFragment())
|
|
||||||
.addToBackStack("Lock")
|
|
||||||
.commitAllowingStateLoss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
REQUEST_RESTORE -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
val uri = data?.data ?: return
|
|
||||||
|
|
||||||
try {
|
|
||||||
val str = contentResolver.openInputStream(uri).use { inputStream ->
|
|
||||||
inputStream!!
|
|
||||||
|
|
||||||
inputStream.readBytes().toString(Charset.defaultCharset())
|
|
||||||
}
|
|
||||||
|
|
||||||
(application as Pupil).favorites.addAll(json.parse(Int.serializer().list, str).also {
|
|
||||||
Snackbar.make(
|
|
||||||
window.decorView,
|
|
||||||
getString(R.string.settings_restore_successful, it.size),
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
})
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Snackbar.make(
|
|
||||||
window.decorView,
|
|
||||||
R.string.settings_restore_failed,
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
REQUEST_DOWNLOAD_FOLDER -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
data?.data?.also { uri ->
|
|
||||||
val takeFlags: Int =
|
|
||||||
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
|
||||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
|
||||||
|
|
||||||
val file = uri.toFile(this)
|
|
||||||
|
|
||||||
if (file?.canWrite() != true)
|
|
||||||
Snackbar.make(
|
|
||||||
settings,
|
|
||||||
R.string.settings_dl_location_not_writable,
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
else
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
|
||||||
.putString("dl_location", file.canonicalPath)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
REQUEST_DOWNLOAD_FOLDER_OLD -> {
|
|
||||||
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
|
||||||
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
|
||||||
|
|
||||||
if (!File(directory).canWrite())
|
|
||||||
Snackbar.make(
|
|
||||||
settings,
|
|
||||||
R.string.settings_dl_location_not_writable,
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
else
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
|
||||||
.putString("dl_location", File(directory).canonicalPath)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
REQUEST_IMPORT_OLD_GALLERIES -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
data?.data?.also { uri ->
|
|
||||||
val takeFlags: Int =
|
|
||||||
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
|
||||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
|
||||||
|
|
||||||
val file = uri.toFile(this)
|
|
||||||
|
|
||||||
if (file?.canRead() != true)
|
|
||||||
Snackbar.make(
|
|
||||||
settings,
|
|
||||||
resources.getText(R.string.import_old_galleries_folder_not_readable),
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
else
|
|
||||||
importOldGalleries(this, file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
REQUEST_IMPORT_OLD_GALLERIES_OLD -> {
|
|
||||||
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
|
||||||
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
|
||||||
|
|
||||||
if (!File(directory).canRead())
|
|
||||||
Snackbar.make(
|
|
||||||
settings,
|
|
||||||
resources.getText(R.string.import_old_galleries_folder_not_readable),
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
else {
|
|
||||||
importOldGalleries(this, File(directory))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
REQUEST_WRITE_PERMISSION_AND_SAF -> {
|
R.id.request_write_permission_and_saf.normalizeID() -> {
|
||||||
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
|
startActivityForResult(intent, R.id.request_download_folder.normalizeID())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.appcompat.app.AlertDialog
|
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.*
|
||||||
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.types.Tags
|
import xyz.quaver.pupil.types.Tags
|
||||||
|
import xyz.quaver.pupil.util.Preferences
|
||||||
|
|
||||||
class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||||
|
|
||||||
@@ -82,9 +82,8 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
private fun build() : View {
|
private fun build() : View {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val tags = Tags.parse(
|
val tags = Tags.parse(
|
||||||
preferences.getString("default_query", "") ?: ""
|
Preferences["default_query"]
|
||||||
)
|
)
|
||||||
|
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
||||||
|
|||||||
@@ -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,138 +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.Manifest
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.RadioButton
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import kotlinx.android.synthetic.main.item_dl_location.view.*
|
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.util.*
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
|
||||||
|
|
||||||
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
private val buttons = mutableListOf<Pair<RadioButton, File?>>()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
setTitle(R.string.settings_dl_location)
|
|
||||||
|
|
||||||
setView(build())
|
|
||||||
|
|
||||||
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ -> }
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun build() : View {
|
|
||||||
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
|
|
||||||
|
|
||||||
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
|
||||||
|
|
||||||
externalFilesDirs.forEachIndexed { index, dir ->
|
|
||||||
|
|
||||||
dir ?: return@forEachIndexed
|
|
||||||
|
|
||||||
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
|
|
||||||
location_type.text = context.getString(when (index) {
|
|
||||||
0 -> R.string.settings_dl_location_internal
|
|
||||||
else -> R.string.settings_dl_location_removable
|
|
||||||
})
|
|
||||||
location_available.text = context.getString(
|
|
||||||
R.string.settings_dl_location_available,
|
|
||||||
byteToString(dir.freeSpace)
|
|
||||||
)
|
|
||||||
setOnClickListener {
|
|
||||||
buttons.forEach { pair ->
|
|
||||||
pair.first.isChecked = false
|
|
||||||
}
|
|
||||||
button.performClick()
|
|
||||||
preference.edit().putString("dl_location", dir.canonicalPath).apply()
|
|
||||||
}
|
|
||||||
buttons.add(button to dir)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
|
|
||||||
location_type.text = context.getString(R.string.settings_dl_location_custom)
|
|
||||||
setOnClickListener {
|
|
||||||
buttons.forEach { pair ->
|
|
||||||
pair.first.isChecked = false
|
|
||||||
}
|
|
||||||
button.performClick()
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
|
|
||||||
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_PERMISSION_AND_SAF)
|
|
||||||
else {
|
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
|
||||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
|
|
||||||
}
|
|
||||||
|
|
||||||
dismiss()
|
|
||||||
} else { // Can't use SAF on old Androids!
|
|
||||||
val config = DirectoryChooserConfig.builder()
|
|
||||||
.newDirectoryName("Pupil")
|
|
||||||
.allowNewDirectoryNameModification(true)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
|
|
||||||
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER_OLD)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buttons.add(button to null)
|
|
||||||
})
|
|
||||||
|
|
||||||
externalFilesDirs.indexOfFirst {
|
|
||||||
it.canonicalPath == getDownloadDirectory(context).canonicalPath
|
|
||||||
}.let { index ->
|
|
||||||
if (index < 0)
|
|
||||||
buttons.first().first.isChecked = true
|
|
||||||
else
|
|
||||||
buttons[index].first.isChecked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -30,7 +30,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
import com.google.android.material.chip.Chip
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.dialog_gallery.*
|
import kotlinx.android.synthetic.main.dialog_gallery.*
|
||||||
import kotlinx.android.synthetic.main.dialog_gallery_details.view.*
|
import kotlinx.android.synthetic.main.dialog_gallery_details.view.*
|
||||||
@@ -38,30 +37,24 @@ import kotlinx.android.synthetic.main.dialog_gallery_dotindicator.view.*
|
|||||||
import kotlinx.android.synthetic.main.item_gallery_details.view.*
|
import kotlinx.android.synthetic.main.item_gallery_details.view.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import xyz.quaver.hitomi.Gallery
|
import xyz.quaver.hitomi.Gallery
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.getGallery
|
import xyz.quaver.hitomi.getGallery
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.Pupil
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||||
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
||||||
|
import xyz.quaver.pupil.histories
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
import xyz.quaver.pupil.ui.ReaderActivity
|
||||||
|
import xyz.quaver.pupil.ui.view.TagChip
|
||||||
import xyz.quaver.pupil.util.ItemClickSupport
|
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
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
|
|
||||||
class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : Dialog(context) {
|
class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : Dialog(context) {
|
||||||
|
|
||||||
private val languages = context.resources.getStringArray(R.array.languages).map {
|
|
||||||
it.split("|").let { split ->
|
|
||||||
Pair(split[0], split[1])
|
|
||||||
}
|
|
||||||
}.toMap()
|
|
||||||
|
|
||||||
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -81,7 +74,7 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
||||||
putExtra("galleryID", galleryID)
|
putExtra("galleryID", galleryID)
|
||||||
})
|
})
|
||||||
(context.applicationContext as Pupil).histories.add(galleryID)
|
histories.add(galleryID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +123,7 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
|
|
||||||
private fun addDetails(gallery: Gallery) {
|
private fun addDetails(gallery: Gallery) {
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
||||||
gallery_details.setText(R.string.gallery_details)
|
gallery_details.setText(R.string.gallery_details)
|
||||||
|
|
||||||
@@ -165,28 +158,7 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
|
|
||||||
content.forEach { tag ->
|
content.forEach { tag ->
|
||||||
gallery_details_tags.addView(
|
gallery_details_tags.addView(
|
||||||
Chip(context).apply {
|
TagChip(context, tag).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.gender_male)
|
|
||||||
}
|
|
||||||
"female" -> {
|
|
||||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
|
||||||
ContextCompat.getDrawable(context, R.drawable.gender_female)
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
text = when (tag.area) {
|
|
||||||
"language" -> languages[tag.tag]
|
|
||||||
else -> tag.tag.wordCapitalize()
|
|
||||||
}
|
|
||||||
|
|
||||||
setEnsureMinTouchTargetSize(false)
|
|
||||||
|
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
onChipClickedHandler.forEach { handler ->
|
onChipClickedHandler.forEach { handler ->
|
||||||
handler.invoke(tag)
|
handler.invoke(tag)
|
||||||
@@ -229,7 +201,7 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
|
|
||||||
private fun addRelated(gallery: Gallery) {
|
private fun addRelated(gallery: Gallery) {
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
val galleries = ArrayList<GalleryBlock>()
|
val galleries = ArrayList<Int>()
|
||||||
|
|
||||||
val adapter = GalleryBlockAdapter(glide, galleries).apply {
|
val adapter = GalleryBlockAdapter(glide, galleries).apply {
|
||||||
onChipClickedHandler.add { tag ->
|
onChipClickedHandler.add { tag ->
|
||||||
@@ -239,19 +211,6 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.dialog_gallery_details, gallery_contents, false).apply {
|
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
||||||
gallery_details.setText(R.string.gallery_related)
|
gallery_details.setText(R.string.gallery_related)
|
||||||
|
|
||||||
@@ -259,18 +218,18 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = LinearLayoutManager(context)
|
||||||
this.adapter = adapter
|
this.adapter = adapter
|
||||||
|
|
||||||
ItemClickSupport.addTo(this)
|
ItemClickSupport.addTo(this).apply {
|
||||||
.setOnItemClickListener { _, position, _ ->
|
onItemClickListener = { _, position, _ ->
|
||||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
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(
|
GalleryDialog(
|
||||||
context,
|
context,
|
||||||
glide,
|
glide,
|
||||||
galleries[position].id
|
galleries[position]
|
||||||
).apply {
|
).apply {
|
||||||
onChipClickedHandler.add { tag ->
|
onChipClickedHandler.add { tag ->
|
||||||
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
||||||
@@ -279,12 +238,25 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}.let {
|
}.let {
|
||||||
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
||||||
}
|
}
|
||||||
}.let {
|
}.let {
|
||||||
gallery_contents.addView(it)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -24,13 +24,13 @@ import android.content.Context
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.adapters.MirrorAdapter
|
import xyz.quaver.pupil.adapters.MirrorAdapter
|
||||||
|
import xyz.quaver.pupil.util.Preferences
|
||||||
|
|
||||||
class MirrorDialog(context: Context) : AlertDialog(context) {
|
class MirrorDialog(context: Context) : AlertDialog(context) {
|
||||||
|
|
||||||
@@ -82,10 +82,7 @@ class MirrorDialog(context: Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onItemMoved = {
|
onItemMoved = {
|
||||||
PreferenceManager.getDefaultSharedPreferences(context)
|
Preferences["mirrors"] = it.joinToString(">")
|
||||||
.edit()
|
|
||||||
.putString("mirrors", it.joinToString(">"))
|
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,23 +27,23 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import kotlinx.android.synthetic.main.dialog_proxy.view.*
|
import kotlinx.android.synthetic.main.dialog_proxy.view.*
|
||||||
import xyz.quaver.proxy
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import xyz.quaver.pupil.R
|
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.ProxyInfo
|
||||||
import xyz.quaver.pupil.util.getProxyInfo
|
import xyz.quaver.pupil.util.getProxyInfo
|
||||||
import xyz.quaver.pupil.util.json
|
import xyz.quaver.pupil.util.proxyInfo
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
|
|
||||||
class ProxyDialog(context: Context) : Dialog(context) {
|
class ProxyDialog(context: Context) : Dialog(context) {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val view = build()
|
setContentView(build())
|
||||||
|
|
||||||
setTitle(R.string.settings_proxy_title)
|
|
||||||
setContentView(view)
|
|
||||||
|
|
||||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -51,7 +51,7 @@ class ProxyDialog(context: Context) : Dialog(context) {
|
|||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
private fun build() : View {
|
private fun build() : View {
|
||||||
val proxyInfo = getProxyInfo(context)
|
val proxyInfo = getProxyInfo()
|
||||||
|
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null)
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null)
|
||||||
|
|
||||||
@@ -116,12 +116,12 @@ class ProxyDialog(context: Context) : Dialog(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ProxyInfo(type, addr, port, username, password).let {
|
ProxyInfo(type, addr, port, username, password).let {
|
||||||
|
Preferences["proxy"] = Json.encodeToString(it)
|
||||||
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putString("proxy",
|
clientBuilder
|
||||||
json.stringify(ProxyInfo.serializer(), it)
|
.proxyInfo(it)
|
||||||
).apply()
|
clientHolder = null
|
||||||
|
client
|
||||||
proxy = it.proxy()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
|
|||||||
@@ -24,15 +24,14 @@ import android.widget.Toast
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.ui.LockActivity
|
import xyz.quaver.pupil.ui.LockActivity
|
||||||
import xyz.quaver.pupil.util.Lock
|
import xyz.quaver.pupil.util.Lock
|
||||||
import xyz.quaver.pupil.util.LockManager
|
import xyz.quaver.pupil.util.LockManager
|
||||||
|
import xyz.quaver.pupil.util.Preferences
|
||||||
|
|
||||||
class LockSettingsFragment :
|
class LockSettingsFragment : PreferenceFragmentCompat() {
|
||||||
PreferenceFragmentCompat() {
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
@@ -54,7 +53,7 @@ class LockSettingsFragment :
|
|||||||
if (lockManager.isEmpty()) {
|
if (lockManager.isEmpty()) {
|
||||||
(findPreference<Preference>("lock_fingerprint") as SwitchPreferenceCompat).isChecked = false
|
(findPreference<Preference>("lock_fingerprint") as SwitchPreferenceCompat).isChecked = false
|
||||||
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("lock_fingerprint", false).apply()
|
Preferences["lock_fingerprint"] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,11 +74,11 @@ class LockSettingsFragment :
|
|||||||
setTitle(R.string.warning)
|
setTitle(R.string.warning)
|
||||||
setMessage(R.string.settings_lock_remove_message)
|
setMessage(R.string.settings_lock_remove_message)
|
||||||
|
|
||||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
lockManager.remove(Lock.Type.PATTERN)
|
lockManager.remove(Lock.Type.PATTERN)
|
||||||
onResume()
|
onResume()
|
||||||
}
|
}
|
||||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
}.show()
|
}.show()
|
||||||
} else {
|
} else {
|
||||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||||
@@ -108,11 +107,11 @@ class LockSettingsFragment :
|
|||||||
setTitle(R.string.warning)
|
setTitle(R.string.warning)
|
||||||
setMessage(R.string.settings_lock_remove_message)
|
setMessage(R.string.settings_lock_remove_message)
|
||||||
|
|
||||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
lockManager.remove(Lock.Type.PIN)
|
lockManager.remove(Lock.Type.PIN)
|
||||||
onResume()
|
onResume()
|
||||||
}
|
}
|
||||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
}.show()
|
}.show()
|
||||||
} else {
|
} else {
|
||||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -18,34 +18,29 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui.fragment
|
package xyz.quaver.pupil.ui.fragment
|
||||||
|
|
||||||
import android.Manifest
|
import android.app.Activity
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
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 com.google.android.material.snackbar.Snackbar
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
import kotlinx.serialization.decodeFromString
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
import kotlinx.serialization.json.Json
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.io.FileX
|
||||||
|
import xyz.quaver.io.util.getChild
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.favorites
|
||||||
import xyz.quaver.pupil.ui.LockActivity
|
import xyz.quaver.pupil.ui.LockActivity
|
||||||
import xyz.quaver.pupil.ui.SettingsActivity
|
import xyz.quaver.pupil.ui.SettingsActivity
|
||||||
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
|
import xyz.quaver.pupil.ui.dialog.*
|
||||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
|
|
||||||
import xyz.quaver.pupil.ui.dialog.MirrorDialog
|
|
||||||
import xyz.quaver.pupil.ui.dialog.ProxyDialog
|
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
import java.io.File
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
class SettingsFragment :
|
class SettingsFragment :
|
||||||
PreferenceFragmentCompat(),
|
PreferenceFragmentCompat(),
|
||||||
@@ -53,8 +48,6 @@ class SettingsFragment :
|
|||||||
Preference.OnPreferenceChangeListener,
|
Preference.OnPreferenceChangeListener,
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
lateinit var sharedPreference: SharedPreferences
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
@@ -73,12 +66,6 @@ class SettingsFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDirSize(dir: File) : String {
|
|
||||||
val size = dir.walk().map { it.length() }.sum()
|
|
||||||
|
|
||||||
return getString(R.string.settings_clear_summary, byteToString(size))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceClick(preference: Preference?): Boolean {
|
override fun onPreferenceClick(preference: Preference?): Boolean {
|
||||||
with (preference) {
|
with (preference) {
|
||||||
this ?: return false
|
this ?: return false
|
||||||
@@ -87,63 +74,22 @@ class SettingsFragment :
|
|||||||
"app_version" -> {
|
"app_version" -> {
|
||||||
checkUpdate(activity as SettingsActivity, true)
|
checkUpdate(activity as SettingsActivity, true)
|
||||||
}
|
}
|
||||||
"delete_cache" -> {
|
"download_folder" -> {
|
||||||
val dir = File(requireContext().cacheDir, "imageCache")
|
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
|
||||||
|
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_clear_cache_alert_message)
|
|
||||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
|
||||||
if (dir.exists())
|
|
||||||
dir.deleteRecursively()
|
|
||||||
|
|
||||||
summary = getDirSize(dir)
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
"delete_downloads" -> {
|
|
||||||
val dir = getDownloadDirectory(requireContext())
|
|
||||||
|
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_clear_downloads_alert_message)
|
|
||||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
|
||||||
if (dir.exists())
|
|
||||||
dir.deleteRecursively()
|
|
||||||
|
|
||||||
summary = getDirSize(dir)
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
"clear_history" -> {
|
|
||||||
val histories = (requireContext().applicationContext as Pupil).histories
|
|
||||||
|
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_clear_history_alert_message)
|
|
||||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
|
||||||
histories.clear()
|
|
||||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
"dl_location" -> {
|
|
||||||
DownloadLocationDialog(requireActivity()).show()
|
|
||||||
}
|
}
|
||||||
"default_query" -> {
|
"default_query" -> {
|
||||||
DefaultQueryDialog(requireContext()).apply {
|
DefaultQueryDialog(requireContext()).apply {
|
||||||
onPositiveButtonClickListener = { newTags ->
|
onPositiveButtonClickListener = { newTags ->
|
||||||
sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
|
Preferences["default_query"] = newTags.toString()
|
||||||
summary = newTags.toString()
|
summary = newTags.toString()
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
"app_lock" -> {
|
"app_lock" -> {
|
||||||
val intent = Intent(requireContext(), LockActivity::class.java)
|
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||||
activity?.startActivityForResult(intent, REQUEST_LOCK)
|
putExtra("force", true)
|
||||||
|
}
|
||||||
|
startActivityForResult(intent, R.id.request_lock.normalizeID())
|
||||||
}
|
}
|
||||||
"mirrors" -> {
|
"mirrors" -> {
|
||||||
MirrorDialog(requireContext())
|
MirrorDialog(requireContext())
|
||||||
@@ -153,54 +99,9 @@ class SettingsFragment :
|
|||||||
ProxyDialog(requireContext())
|
ProxyDialog(requireContext())
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
"nomedia" -> {
|
|
||||||
File(getDownloadDirectory(context), ".nomedia").createNewFile()
|
|
||||||
}
|
|
||||||
"backup" -> {
|
|
||||||
File(ContextCompat.getDataDir(requireContext()), "favorites.json").copyTo(
|
|
||||||
File(getDownloadDirectory(requireContext()), "favorites.json"),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
"restore" -> {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
}
|
|
||||||
|
|
||||||
activity?.startActivityForResult(intent, REQUEST_RESTORE)
|
|
||||||
}
|
|
||||||
"old_import_galleries" -> {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
|
|
||||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
|
|
||||||
ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_PERMISSION_AND_SAF)
|
|
||||||
else {
|
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
|
||||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
activity?.startActivityForResult(intent, REQUEST_IMPORT_OLD_GALLERIES)
|
|
||||||
}
|
|
||||||
} else { // Can't use SAF on old Androids!
|
|
||||||
val config = DirectoryChooserConfig.builder()
|
|
||||||
.newDirectoryName("Pupil")
|
|
||||||
.allowNewDirectoryNameModification(true)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val intent = Intent(requireContext(), DirectoryChooserActivity::class.java).apply {
|
|
||||||
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
activity?.startActivityForResult(intent, REQUEST_IMPORT_OLD_GALLERIES_OLD)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"user_id" -> {
|
"user_id" -> {
|
||||||
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||||
ClipData.newPlainText("user_id", sharedPreference.getString("user_id", ""))
|
ClipData.newPlainText("user_id", Preferences.get<String>("user_id"))
|
||||||
)
|
)
|
||||||
Toast.makeText(context, R.string.settings_user_id_toast, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.settings_user_id_toast, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -216,6 +117,18 @@ class SettingsFragment :
|
|||||||
this ?: return false
|
this ?: return false
|
||||||
|
|
||||||
when (key) {
|
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" -> {
|
"dark_mode" -> {
|
||||||
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
|
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
|
||||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
@@ -237,10 +150,13 @@ class SettingsFragment :
|
|||||||
|
|
||||||
when (key) {
|
when (key) {
|
||||||
"proxy" -> {
|
"proxy" -> {
|
||||||
summary = getProxyInfo(requireContext()).type.name
|
summary = context?.let { getProxyInfo().type.name }
|
||||||
}
|
}
|
||||||
"dl_location" -> {
|
"download_folder" -> {
|
||||||
summary = getDownloadDirectory(requireContext()).canonicalPath
|
summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath
|
||||||
|
}
|
||||||
|
"download_folder_name" -> {
|
||||||
|
summary = Preferences["download_folder_name", "[-id-] -title-"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,12 +165,16 @@ class SettingsFragment :
|
|||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
||||||
|
|
||||||
sharedPreference = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
Preferences.registerOnSharedPreferenceChangeListener(this)
|
||||||
sharedPreference.registerOnSharedPreferenceChangeListener(this)
|
|
||||||
|
|
||||||
initPreferences()
|
initPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
Preferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
private fun initPreferences() {
|
private fun initPreferences() {
|
||||||
for (i in 0 until preferenceScreen.preferenceCount) {
|
for (i in 0 until preferenceScreen.preferenceCount) {
|
||||||
|
|
||||||
@@ -264,7 +184,7 @@ class SettingsFragment :
|
|||||||
else
|
else
|
||||||
listOf(this)
|
listOf(this)
|
||||||
}.forEach { preference ->
|
}.forEach { preference ->
|
||||||
with (preference) {
|
with (preference) with@{
|
||||||
|
|
||||||
when (key) {
|
when (key) {
|
||||||
"app_version" -> {
|
"app_version" -> {
|
||||||
@@ -274,31 +194,29 @@ class SettingsFragment :
|
|||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"delete_cache" -> {
|
"download_folder_name" -> {
|
||||||
val dir = File(requireContext().cacheDir, "imageCache")
|
summary = Preferences["download_folder_name", "[-id-] -title-"]
|
||||||
summary = getDirSize(dir)
|
|
||||||
|
setOnPreferenceClickListener {
|
||||||
|
DownloadFolderNameDialogFragment().show(requireActivity().supportFragmentManager, "Download Location Dialog")
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"download_folder" -> {
|
||||||
|
summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"delete_downloads" -> {
|
"nomedia" -> {
|
||||||
val dir = getDownloadDirectory(requireContext())
|
(this as SwitchPreferenceCompat).isChecked = kotlin.runCatching {
|
||||||
summary = getDirSize(dir)
|
DownloadManager.getInstance(context).downloadFolder.getChild(".nomedia").exists()
|
||||||
|
}.getOrDefault(false)
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceChangeListener = this@SettingsFragment
|
||||||
}
|
|
||||||
"clear_history" -> {
|
|
||||||
val histories = (requireActivity().application as Pupil).histories
|
|
||||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"dl_location" -> {
|
|
||||||
summary = getDownloadDirectory(requireContext()).canonicalPath
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
}
|
||||||
"default_query" -> {
|
"default_query" -> {
|
||||||
summary = sharedPreference.getString("default_query", "") ?: ""
|
summary = Preferences.get<String>("default_query")
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
@@ -323,33 +241,45 @@ class SettingsFragment :
|
|||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"proxy" -> {
|
"proxy" -> {
|
||||||
summary = getProxyInfo(requireContext()).type.name
|
summary = getProxyInfo().type.name
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"dark_mode" -> {
|
"dark_mode" -> {
|
||||||
onPreferenceChangeListener = this@SettingsFragment
|
onPreferenceChangeListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"nomedia" -> {
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"backup" -> {
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"restore" -> {
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"old_import_galleries" -> {
|
"old_import_galleries" -> {
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"user_id" -> {
|
"user_id" -> {
|
||||||
summary = sharedPreference.getString("user_id", "")
|
summary = Preferences.get<String>("user_id")
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
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,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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -25,23 +25,24 @@ import android.util.SparseArray
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.Reader
|
||||||
import xyz.quaver.proxy
|
|
||||||
import xyz.quaver.pupil.util.getCachedGallery
|
import xyz.quaver.pupil.util.getCachedGallery
|
||||||
import xyz.quaver.pupil.util.getDownloadDirectory
|
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||||
import xyz.quaver.pupil.util.isParentOf
|
import xyz.quaver.pupil.util.isParentOf
|
||||||
import xyz.quaver.pupil.util.json
|
import xyz.quaver.readBytes
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.locks.Lock
|
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use downloader.Cache instead")
|
||||||
class Cache(context: Context) : ContextWrapper(context) {
|
class Cache(context: Context) : ContextWrapper(context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -49,20 +50,6 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
private val readers = SparseArray<Reader?>()
|
private val readers = SparseArray<Reader?>()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val locks = SparseArray<Lock>()
|
|
||||||
private fun lock(galleryID: Int) {
|
|
||||||
synchronized(locks) {
|
|
||||||
if (locks.indexOfKey(galleryID) < 0)
|
|
||||||
locks.put(galleryID, ReentrantLock())
|
|
||||||
}
|
|
||||||
|
|
||||||
locks[galleryID].lock()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun unlock(galleryID: Int) {
|
|
||||||
locks[galleryID]?.unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
// Search in this order
|
// Search in this order
|
||||||
@@ -79,7 +66,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
json.parse(Metadata.serializer(), file.readText())
|
Json.decodeFromString(file.readText())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
//File corrupted
|
//File corrupted
|
||||||
file.delete()
|
file.delete()
|
||||||
@@ -96,7 +83,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
it.createNewFile()
|
it.createNewFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
file.writeText(json.stringify(Metadata.serializer(), metadata))
|
file.writeText(Json.encodeToString(metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getThumbnail(galleryID: Int): String? {
|
suspend fun getThumbnail(galleryID: Int): String? {
|
||||||
@@ -105,11 +92,12 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
val thumbnail = if (metadata?.thumbnail == null)
|
val thumbnail = if (metadata?.thumbnail == null)
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
|
val thumbnail = getGalleryBlock(galleryID)?.thumbnails?.firstOrNull() ?: return@withContext null
|
||||||
try {
|
try {
|
||||||
Base64.encodeToString(URL(thumbnails?.firstOrNull()).openConnection(proxy).getInputStream().use {
|
val data = URL(thumbnail).readBytes().apply {
|
||||||
it.readBytes()
|
if (isEmpty()) return@withContext null
|
||||||
}, Base64.DEFAULT)
|
}
|
||||||
|
Base64.encodeToString(data, Base64.DEFAULT)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -134,7 +122,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val galleryBlock = if (metadata?.galleryBlock == null) {
|
val galleryBlock = if (metadata?.galleryBlock == null) {
|
||||||
CoroutineScope(Dispatchers.IO).async {
|
withContext(Dispatchers.IO) {
|
||||||
var galleryBlock: GalleryBlock? = null
|
var galleryBlock: GalleryBlock? = null
|
||||||
|
|
||||||
for (source in sources) {
|
for (source in sources) {
|
||||||
@@ -149,7 +137,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
galleryBlock
|
galleryBlock
|
||||||
}.await() ?: return null
|
} ?: return null
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
metadata.galleryBlock
|
metadata.galleryBlock
|
||||||
@@ -175,11 +163,9 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
|
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
|
||||||
).let {
|
).let {
|
||||||
if (mirrors.isNotEmpty())
|
if (mirrors.isNotEmpty())
|
||||||
it.toSortedMap(
|
it.toSortedMap{ o1, o2 ->
|
||||||
Comparator { o1, o2 ->
|
mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
|
||||||
mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
@@ -194,7 +180,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
retval = try {
|
retval = try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
withTimeoutOrNull(1000) {
|
withTimeoutOrNull(1000) {
|
||||||
source.value.invoke()
|
source.value.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -37,17 +37,17 @@ import xyz.quaver.Code
|
|||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.Reader
|
||||||
import xyz.quaver.hitomi.getReferer
|
import xyz.quaver.hitomi.getReferer
|
||||||
import xyz.quaver.hitomi.imageUrlFromImage
|
import xyz.quaver.hitomi.imageUrlFromImage
|
||||||
import xyz.quaver.hiyobi.cookie
|
|
||||||
import xyz.quaver.hiyobi.createImgList
|
import xyz.quaver.hiyobi.createImgList
|
||||||
import xyz.quaver.hiyobi.user_agent
|
|
||||||
import xyz.quaver.proxy
|
|
||||||
import xyz.quaver.pupil.R
|
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.ui.ReaderActivity
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use DownloadService instead")
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
|
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
|
||||||
|
|
||||||
@@ -86,7 +86,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun source(source: Source) = object: ForwardingSource(source) {
|
private fun source(source: Source) = object: ForwardingSource(source) {
|
||||||
|
|
||||||
var totalBytesRead = 0L
|
var totalBytesRead = 0L
|
||||||
|
|
||||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||||
@@ -100,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
|
//endregion
|
||||||
|
|
||||||
//region Singleton
|
//region Singleton
|
||||||
@@ -135,34 +152,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
private val loop = loop()
|
private val loop = loop()
|
||||||
private val worker = SparseArray<Job?>()
|
private val worker = SparseArray<Job?>()
|
||||||
|
|
||||||
val interceptor = Interceptor { chain ->
|
|
||||||
val request = chain.request()
|
|
||||||
var response = chain.proceed(request)
|
|
||||||
|
|
||||||
var retry = 5
|
|
||||||
while (!response.isSuccessful && retry > 0) {
|
|
||||||
response = chain.proceed(request)
|
|
||||||
retry--
|
|
||||||
}
|
|
||||||
|
|
||||||
response.newBuilder()
|
|
||||||
.body(response.body()?.let {
|
|
||||||
ProgressResponseBody(request.tag(), it, progressListener)
|
|
||||||
}).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
val client =
|
|
||||||
OkHttpClient.Builder()
|
|
||||||
.connectTimeout(0, TimeUnit.SECONDS)
|
|
||||||
.addInterceptor(interceptor)
|
|
||||||
.readTimeout(0, TimeUnit.SECONDS)
|
|
||||||
.dispatcher(Dispatcher().apply {
|
|
||||||
maxRequests = 4
|
|
||||||
maxRequestsPerHost = 4
|
|
||||||
})
|
|
||||||
.proxy(proxy)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
queue.clear()
|
queue.clear()
|
||||||
|
|
||||||
@@ -179,6 +168,11 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
}.forEach {
|
}.forEach {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
|
client.dispatcher().runningCalls().filter {
|
||||||
|
it.request().tag() is Pair<*, *>
|
||||||
|
}.forEach {
|
||||||
|
it.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
progress.clear()
|
progress.clear()
|
||||||
notification.clear()
|
notification.clear()
|
||||||
@@ -194,6 +188,11 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
}.forEach {
|
}.forEach {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
|
client.dispatcher().runningCalls().filter {
|
||||||
|
((it.request().tag() as Pair<*, *>).first as Int) == galleryID
|
||||||
|
}.forEach {
|
||||||
|
it.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
progress.remove(galleryID)
|
progress.remove(galleryID)
|
||||||
notification.remove(galleryID)
|
notification.remove(galleryID)
|
||||||
@@ -219,8 +218,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
}
|
}
|
||||||
Code.HIYOBI -> {
|
Code.HIYOBI -> {
|
||||||
url(createImgList(galleryID, reader, lowQuality)[index].path)
|
url(createImgList(galleryID, reader, lowQuality)[index].path)
|
||||||
addHeader("User-Agent", user_agent)
|
|
||||||
addHeader("Cookie", cookie)
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
//shouldn't be called anyway
|
//shouldn't be called anyway
|
||||||
@@ -275,23 +272,22 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
if (e.message?.contains("cancel", true) != false)
|
if (e.message?.contains("cancel", true) != false)
|
||||||
return
|
return
|
||||||
|
|
||||||
Log.i("PUPILD", "FAIL ${call.request().tag()} (${e.message})")
|
|
||||||
FirebaseCrashlytics.getInstance().apply {
|
|
||||||
log("FAIL ${call.request().tag()} (${e.message})")
|
|
||||||
setCustomKey("POS", "FAIL")
|
|
||||||
recordException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel(galleryID)
|
cancel(galleryID)
|
||||||
queue.add(galleryID)
|
queue.add(galleryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
if (response.code() != 200) {
|
||||||
|
response.close()
|
||||||
|
onFailure(call, IOException())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val ext = call.request().url().encodedPath().split('.').last()
|
val ext = call.request().url().encodedPath().split('.').last()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response.body().use {
|
response.body()!!.use {
|
||||||
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it!!.byteStream())
|
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream())
|
||||||
}
|
}
|
||||||
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
|
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ import kotlinx.serialization.Serializable
|
|||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.Reader
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use downloader.Cache.Metadata instead")
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Metadata(
|
data class Metadata(
|
||||||
val thumbnail: String? = null,
|
var thumbnail: String? = null,
|
||||||
val galleryBlock: GalleryBlock? = null,
|
var galleryBlock: GalleryBlock? = null,
|
||||||
val reader: Reader? = null,
|
var reader: Reader? = null,
|
||||||
val isDownloading: Boolean? = null
|
var isDownloading: Boolean? = null
|
||||||
) {
|
) {
|
||||||
constructor(
|
constructor(
|
||||||
metadata: Metadata?,
|
metadata: Metadata?,
|
||||||
|
|||||||
230
app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
metadata.imageList?.forEach { imageName ->
|
||||||
|
imageName ?: return@forEach
|
||||||
|
val target = downloadFolder.getChild(imageName)
|
||||||
|
val source = cacheFolder.getChild(imageName)
|
||||||
|
|
||||||
|
if (!source.exists())
|
||||||
|
return@forEach
|
||||||
|
|
||||||
|
kotlin.runCatching {
|
||||||
|
target.createNewFile()
|
||||||
|
source.readBytes()?.let { target.writeBytes(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val cacheMetadata = cacheFolder.getChild(".metadata")
|
||||||
|
val downloadMetadata = downloadFolder.getChild(".metadata")
|
||||||
|
|
||||||
|
if (cacheMetadata.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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,20 +18,17 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.storage.StorageManager
|
import android.os.storage.StorageManager
|
||||||
import android.provider.DocumentsContract
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.core.net.toUri
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.lang.reflect.Array
|
import java.lang.reflect.Array
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use downloader.Cache instead")
|
||||||
fun getCachedGallery(context: Context, galleryID: Int) =
|
fun getCachedGallery(context: Context, galleryID: Int) =
|
||||||
File(getDownloadDirectory(context), galleryID.toString()).let {
|
File(getDownloadDirectory(context), galleryID.toString()).let {
|
||||||
if (it.exists())
|
if (it.exists())
|
||||||
@@ -40,178 +37,17 @@ fun getCachedGallery(context: Context, galleryID: Int) =
|
|||||||
File(context.cacheDir, "imageCache/$galleryID")
|
File(context.cacheDir, "imageCache/$galleryID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use downloader.Cache instead")
|
||||||
fun getDownloadDirectory(context: Context) =
|
fun getDownloadDirectory(context: Context) =
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
|
Preferences.get<String>("dl_location").let {
|
||||||
if (it != null && !it.startsWith("content"))
|
if (it.isNotEmpty() && !it.startsWith("content"))
|
||||||
File(it)
|
File(it)
|
||||||
else
|
else
|
||||||
context.getExternalFilesDir(null)!!
|
context.getExternalFilesDir(null)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use FileX instead")
|
||||||
if (to.parentFile?.exists() == false)
|
|
||||||
to.parentFile!!.mkdirs()
|
|
||||||
|
|
||||||
if (!to.exists())
|
|
||||||
to.createNewFile()
|
|
||||||
|
|
||||||
FileOutputStream(to).use { out ->
|
|
||||||
|
|
||||||
with(openConnection()) {
|
|
||||||
val fileSize = contentLength.toLong()
|
|
||||||
|
|
||||||
getInputStream().use {
|
|
||||||
|
|
||||||
var bytesCopied: Long = 0
|
|
||||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
|
||||||
|
|
||||||
var bytes = it.read(buffer)
|
|
||||||
while (bytes >= 0) {
|
|
||||||
out.write(buffer, 0, bytes)
|
|
||||||
bytesCopied += bytes
|
|
||||||
onDownloadProgress?.invoke(bytesCopied, fileSize)
|
|
||||||
bytes = it.read(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getExtSdCardPaths(context: Context) =
|
|
||||||
ContextCompat.getExternalFilesDirs(context, null).drop(1).map {
|
|
||||||
it.absolutePath.substringBeforeLast("/Android/data").let { path ->
|
|
||||||
runCatching {
|
|
||||||
File(path).canonicalPath
|
|
||||||
}.getOrElse {
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const val PRIMARY_VOLUME_NAME = "primary"
|
|
||||||
fun getVolumePath(context: Context, volumeID: String?): String? {
|
|
||||||
return runCatching {
|
|
||||||
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
|
||||||
val storageVolumeClass = Class.forName("android.os.storage.StorageVolume")
|
|
||||||
|
|
||||||
val getVolumeList = storageVolumeClass.javaClass.getMethod("getVolumeList")
|
|
||||||
val getUUID = storageVolumeClass.getMethod("getUuid")
|
|
||||||
val getPath = storageVolumeClass.getMethod("getPath")
|
|
||||||
val isPrimary = storageVolumeClass.getMethod("isPrimary")
|
|
||||||
|
|
||||||
val result = getVolumeList.invoke(storageManager)!!
|
|
||||||
|
|
||||||
val length = Array.getLength(result)
|
|
||||||
|
|
||||||
for (i in 0 until length) {
|
|
||||||
val storageVolumeElement = Array.get(result, i)
|
|
||||||
val uuid = getUUID.invoke(storageVolumeElement) as? String
|
|
||||||
val primary = isPrimary.invoke(storageVolumeElement) as? Boolean
|
|
||||||
|
|
||||||
// primary volume?
|
|
||||||
if (primary == true && volumeID == PRIMARY_VOLUME_NAME)
|
|
||||||
return@runCatching getPath.invoke(storageVolumeElement) as? String
|
|
||||||
|
|
||||||
// other volumes?
|
|
||||||
if (volumeID == uuid) {
|
|
||||||
return@runCatching getPath.invoke(storageVolumeElement) as? String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return@runCatching null
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Credits go to https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri/36162691#36162691
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
fun getVolumeIdFromTreeUri(uri: Uri) =
|
|
||||||
DocumentsContract.getTreeDocumentId(uri).split(':').let {
|
|
||||||
if (it.isNotEmpty())
|
|
||||||
it[0]
|
|
||||||
else
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
fun getDocumentPathFromTreeUri(uri: Uri) =
|
|
||||||
DocumentsContract.getTreeDocumentId(uri).split(':').let {
|
|
||||||
if (it.size >= 2)
|
|
||||||
it[1]
|
|
||||||
else
|
|
||||||
File.separator
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFullPathFromTreeUri(context: Context, uri: Uri) : String? {
|
|
||||||
val volumePath = getVolumePath(context, getVolumeIdFromTreeUri(uri) ?: return null).let {
|
|
||||||
it ?: return File.separator
|
|
||||||
|
|
||||||
if (it.endsWith(File.separator))
|
|
||||||
it.dropLast(1)
|
|
||||||
else
|
|
||||||
it
|
|
||||||
}
|
|
||||||
|
|
||||||
val documentPath = getDocumentPathFromTreeUri(uri).let {
|
|
||||||
if (it.endsWith(File.separator))
|
|
||||||
it.dropLast(1)
|
|
||||||
else
|
|
||||||
it
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (documentPath.isNotEmpty()) {
|
|
||||||
if (documentPath.startsWith(File.separator))
|
|
||||||
volumePath + documentPath
|
|
||||||
else
|
|
||||||
volumePath + File.separator + documentPath
|
|
||||||
} else
|
|
||||||
volumePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Huge thanks to avluis(https://github.com/avluis)
|
|
||||||
// This code is originated from Hentoid(https://github.com/avluis/Hentoid) under Apache-2.0 license.
|
|
||||||
fun Uri.toFile(context: Context): File? {
|
|
||||||
val path = this.path ?: return null
|
|
||||||
|
|
||||||
val pathSeparator = path.indexOf(':')
|
|
||||||
val folderName = path.substring(pathSeparator+1)
|
|
||||||
|
|
||||||
// Determine whether the designated file is
|
|
||||||
// - on a removable media (e.g. SD card, OTG)
|
|
||||||
// or
|
|
||||||
// - on the internal phone memory
|
|
||||||
val removableMediaFolderRoots = getExtSdCardPaths(context)
|
|
||||||
|
|
||||||
/* First test is to compare root names with known roots of removable media
|
|
||||||
* In many cases, the SD card root name is shared between pre-SAF (File) and SAF (DocumentFile) frameworks
|
|
||||||
* (e.g. /storage/3437-3934 vs. /tree/3437-3934)
|
|
||||||
* This is what the following block is trying to do
|
|
||||||
*/
|
|
||||||
for (s in removableMediaFolderRoots) {
|
|
||||||
val sRoot = s.substring(s.lastIndexOf(File.separatorChar))
|
|
||||||
val root = path.substring(0, pathSeparator).let {
|
|
||||||
it.substring(it.lastIndexOf(File.separatorChar))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sRoot.equals(root, true)) {
|
|
||||||
return File(s + File.separatorChar + folderName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* In some other cases, there is no common name (e.g. /storage/sdcard1 vs. /tree/3437-3934)
|
|
||||||
* We can use a slower method to translate the Uri obtained with SAF into a pre-SAF path
|
|
||||||
* and compare it to the known removable media volume names
|
|
||||||
*/
|
|
||||||
val root = getFullPathFromTreeUri(context, this)
|
|
||||||
|
|
||||||
for (s in removableMediaFolderRoots) {
|
|
||||||
if (root?.startsWith(s) == true) {
|
|
||||||
return File(root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun File.isParentOf(another: File) =
|
fun File.isParentOf(another: File) =
|
||||||
another.absolutePath.startsWith(this.absolutePath)
|
another.absolutePath.startsWith(this.absolutePath)
|
||||||
@@ -1,96 +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.KSerializer
|
|
||||||
import kotlinx.serialization.builtins.list
|
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class Histories(private val file: File) : ArrayList<Int>() {
|
|
||||||
|
|
||||||
val serializer: KSerializer<List<Int>> = Int.serializer().list
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (!file.exists())
|
|
||||||
file.parentFile?.mkdirs()
|
|
||||||
|
|
||||||
try {
|
|
||||||
load()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun load() : Histories {
|
|
||||||
return apply {
|
|
||||||
super.clear()
|
|
||||||
super.addAll(
|
|
||||||
json.parse(
|
|
||||||
serializer,
|
|
||||||
file.bufferedReader().use { it.readText() }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save() {
|
|
||||||
file.writeText(json.stringify(serializer, this))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(element: Int): Boolean {
|
|
||||||
load()
|
|
||||||
|
|
||||||
if (contains(element))
|
|
||||||
super.remove(element)
|
|
||||||
|
|
||||||
super.add(0, element)
|
|
||||||
|
|
||||||
save()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addAll(elements: Collection<Int>): Boolean {
|
|
||||||
load()
|
|
||||||
|
|
||||||
for (e in elements) {
|
|
||||||
if (contains(e))
|
|
||||||
super.remove(e)
|
|
||||||
super.add(0, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
save()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun remove(element: Int): Boolean {
|
|
||||||
load()
|
|
||||||
val retval = super.remove(element)
|
|
||||||
save()
|
|
||||||
|
|
||||||
return retval
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clear() {
|
|
||||||
super.clear()
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,9 @@ import android.content.Context
|
|||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.builtins.list
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ fun hashWithSalt(password: String): Pair<String, String> {
|
|||||||
return Pair(hash(password+salt), salt)
|
return Pair(hash(password+salt), salt)
|
||||||
}
|
}
|
||||||
|
|
||||||
val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
const val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Lock(val type: Type, val hash: String, val salt: String) {
|
data class Lock(val type: Type, val hash: String, val salt: String) {
|
||||||
@@ -80,7 +82,7 @@ class LockManager(base: Context): ContextWrapper(base) {
|
|||||||
lock.writeText("[]")
|
lock.writeText("[]")
|
||||||
}
|
}
|
||||||
|
|
||||||
locks = ArrayList(json.parse(Lock.serializer().list, lock.readText()))
|
locks = Json.decodeFromString(lock.readText())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun save() {
|
private fun save() {
|
||||||
@@ -89,7 +91,7 @@ class LockManager(base: Context): ContextWrapper(base) {
|
|||||||
if (!lock.exists())
|
if (!lock.exists())
|
||||||
lock.createNewFile()
|
lock.createNewFile()
|
||||||
|
|
||||||
lock.writeText(json.stringify(Lock.serializer().list, locks?.toList() ?: listOf()))
|
lock.writeText(Json.encodeToString(locks?.toList() ?: listOf()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(lock: Lock) {
|
fun add(lock: Lock) {
|
||||||
|
|||||||
@@ -19,6 +19,18 @@
|
|||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
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 java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
@@ -33,15 +45,15 @@ fun String.wordCapitalize() : String {
|
|||||||
return result.joinToString(" ")
|
return result.joinToString(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun byteToString(byte: Long, precision : Int = 1) : String {
|
private val suffix = listOf(
|
||||||
|
"B",
|
||||||
|
"kB",
|
||||||
|
"MB",
|
||||||
|
"GB",
|
||||||
|
"TB" //really?
|
||||||
|
)
|
||||||
|
|
||||||
val suffix = listOf(
|
fun byteToString(byte: Long, precision : Int = 1) : String {
|
||||||
"B",
|
|
||||||
"kB",
|
|
||||||
"MB",
|
|
||||||
"GB",
|
|
||||||
"TB" //really?
|
|
||||||
)
|
|
||||||
var size = byte.toDouble(); var suffixIndex = 0
|
var size = byte.toDouble(); var suffixIndex = 0
|
||||||
|
|
||||||
while (size >= 1024) {
|
while (size >= 1024) {
|
||||||
@@ -50,5 +62,70 @@ fun byteToString(byte: Long, precision : Int = 1) : String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return "%.${precision}f ${suffix[suffixIndex]}".format(size)
|
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
|
||||||
@@ -19,8 +19,10 @@
|
|||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Authenticator
|
import okhttp3.Authenticator
|
||||||
import okhttp3.Credentials
|
import okhttp3.Credentials
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
@@ -35,29 +37,22 @@ data class ProxyInfo(
|
|||||||
val password: String? = null
|
val password: String? = null
|
||||||
) {
|
) {
|
||||||
fun proxy() : Proxy {
|
fun proxy() : Proxy {
|
||||||
return if (host == null || port == null)
|
return if (host.isNullOrBlank() || port == null)
|
||||||
return Proxy.NO_PROXY
|
return Proxy.NO_PROXY
|
||||||
else
|
else
|
||||||
Proxy(type, InetSocketAddress.createUnresolved(host, port))
|
Proxy(type, InetSocketAddress.createUnresolved(host, port))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authenticator() = Authenticator { _, response ->
|
fun authenticator(): Authenticator? = if (username.isNullOrBlank() || password.isNullOrBlank()) null else
|
||||||
val credential = Credentials.basic(username ?: "", password ?: "")
|
Authenticator { _, response ->
|
||||||
|
val credential = Credentials.basic(username, password)
|
||||||
|
|
||||||
response.request().newBuilder()
|
response.request().newBuilder()
|
||||||
.header("Proxy-Authorization", credential)
|
.header("Proxy-Authorization", credential)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getProxy(context: Context) =
|
fun getProxyInfo(): ProxyInfo =
|
||||||
getProxyInfo(context).proxy()
|
Json.decodeFromString(Preferences["proxy", Json.encodeToString(ProxyInfo(Proxy.Type.DIRECT))])
|
||||||
|
|
||||||
fun getProxyInfo(context: Context) =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).getString("proxy", null).let {
|
|
||||||
if (it == null)
|
|
||||||
ProxyInfo(Proxy.Type.DIRECT)
|
|
||||||
else
|
|
||||||
json.parse(ProxyInfo.serializer(), it)
|
|
||||||
}
|
|
||||||
@@ -21,60 +21,70 @@ package xyz.quaver.pupil.util
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.URLUtil
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.coroutines.*
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.serialization.json.boolean
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.serialization.json.content
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.*
|
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 ru.noties.markwon.Markwon
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.Reader
|
||||||
import xyz.quaver.hitomi.getGalleryBlock
|
import xyz.quaver.hitomi.getGalleryBlock
|
||||||
import xyz.quaver.hitomi.getReader
|
import xyz.quaver.hitomi.getReader
|
||||||
import xyz.quaver.proxy
|
import xyz.quaver.io.FileX
|
||||||
import xyz.quaver.pupil.BroadcastReciever
|
import xyz.quaver.io.util.getChild
|
||||||
|
import xyz.quaver.io.util.*
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.util.download.Cache
|
import xyz.quaver.pupil.client
|
||||||
import xyz.quaver.pupil.util.download.Metadata
|
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.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
fun getReleases(url: String) : JsonArray {
|
fun getReleases(url: String) : JsonArray {
|
||||||
return try {
|
return try {
|
||||||
URL(url).readText().let {
|
URL(url).readText().let {
|
||||||
json.parse(JsonArray.serializer(), it)
|
Json.parseToJsonElement(it).jsonArray
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
JsonArray(emptyList())
|
JsonArray(emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkUpdate(context: Context, url: String) : JsonObject? {
|
fun checkUpdate(url: String) : JsonObject? {
|
||||||
val releases = getReleases(url)
|
val releases = getReleases(url)
|
||||||
|
|
||||||
if (releases.isEmpty())
|
if (releases.isEmpty())
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return releases.firstOrNull {
|
return releases.firstOrNull {
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("beta", false))
|
Preferences["beta"] || it.jsonObject["prerelease"]?.jsonPrimitive?.booleanOrNull == false
|
||||||
true
|
|
||||||
else
|
|
||||||
it.jsonObject["prerelease"]?.boolean == false
|
|
||||||
}?.let {
|
}?.let {
|
||||||
if (it.jsonObject["tag_name"]?.content == BuildConfig.VERSION_NAME)
|
if (it.jsonObject["tag_name"]?.jsonPrimitive?.contentOrNull == BuildConfig.VERSION_NAME)
|
||||||
null
|
null
|
||||||
else
|
else
|
||||||
it.jsonObject
|
it.jsonObject
|
||||||
@@ -83,13 +93,12 @@ fun checkUpdate(context: Context, url: String) : JsonObject? {
|
|||||||
|
|
||||||
fun getApkUrl(releases: JsonObject) : String? {
|
fun getApkUrl(releases: JsonObject) : String? {
|
||||||
return releases["assets"]?.jsonArray?.firstOrNull {
|
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 {
|
}.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: Context, force: Boolean = false) {
|
fun checkUpdate(context: Context, force: Boolean = false) {
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
@@ -99,7 +108,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
|||||||
return
|
return
|
||||||
|
|
||||||
fun extractReleaseNote(update: JsonObject, locale: Locale) : String {
|
fun extractReleaseNote(update: JsonObject, locale: Locale) : String {
|
||||||
val markdown = update["body"]!!.content
|
val markdown = update["body"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
val target = when(locale.language) {
|
val target = when(locale.language) {
|
||||||
"ko" -> "한국어"
|
"ko" -> "한국어"
|
||||||
@@ -137,12 +146,12 @@ fun checkUpdate(context: Context, 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 {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
val update =
|
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
|
val url = getApkUrl(update) ?: return@launch
|
||||||
|
|
||||||
@@ -150,15 +159,13 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
|||||||
setTitle(R.string.update_title)
|
setTitle(R.string.update_title)
|
||||||
val msg = extractReleaseNote(update, Locale.getDefault())
|
val msg = extractReleaseNote(update, Locale.getDefault())
|
||||||
setMessage(Markwon.create(context).toMarkdown(msg))
|
setMessage(Markwon.create(context).toMarkdown(msg))
|
||||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
//Cancel any download queued before
|
//Cancel any download queued before
|
||||||
|
|
||||||
val id = preference.getLong("update_download_id", -1)
|
val id: Long = Preferences["update_download_id"]
|
||||||
|
|
||||||
if (id != -1L)
|
if (id != -1L)
|
||||||
downloadManager.remove(id)
|
downloadManager.remove(id)
|
||||||
@@ -172,10 +179,10 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
|||||||
.setDestinationUri(Uri.fromFile(target))
|
.setDestinationUri(Uri.fromFile(target))
|
||||||
|
|
||||||
downloadManager.enqueue(request).also {
|
downloadManager.enqueue(request).also {
|
||||||
preference.edit().putLong("update_download_id", it).apply()
|
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)
|
if (!force)
|
||||||
preferences.edit()
|
preferences.edit()
|
||||||
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
||||||
@@ -189,147 +196,141 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cancelImport = false
|
fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<Int>) -> Unit)? = null) {
|
||||||
@SuppressLint("RestrictedApi")
|
if (!URLUtil.isValidUrl(url)) {
|
||||||
fun importOldGalleries(context: Context, folder: File) = CoroutineScope(Dispatchers.IO).launch {
|
onFailure?.invoke(IllegalArgumentException())
|
||||||
val client = OkHttpClient.Builder()
|
return
|
||||||
.connectTimeout(0, TimeUnit.SECONDS)
|
}
|
||||||
.readTimeout(0, TimeUnit.SECONDS)
|
|
||||||
.proxy(proxy)
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.get()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val cancelIntent = Intent(context, BroadcastReciever::class.java).apply {
|
client.newCall(request).enqueue(object: Callback {
|
||||||
action = BroadcastReciever.ACTION_CANCEL_IMPORT
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
putExtra(BroadcastReciever.EXTRA_IMPORT_NOTIFICATION_ID, 0)
|
onFailure?.invoke(e)
|
||||||
}
|
|
||||||
val pendingIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, 0)
|
|
||||||
|
|
||||||
val notificationManager = NotificationManagerCompat.from(context)
|
|
||||||
val notificationBuilder = NotificationCompat.Builder(context, "import").apply {
|
|
||||||
setContentTitle(context.getText(R.string.import_old_galleries_notification))
|
|
||||||
setProgress(0, 0, true)
|
|
||||||
setSmallIcon(R.drawable.ic_notification)
|
|
||||||
addAction(0, context.getText(android.R.string.cancel), pendingIntent)
|
|
||||||
setOngoing(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager.notify(0, notificationBuilder.build())
|
|
||||||
|
|
||||||
if (!folder.isDirectory)
|
|
||||||
return@launch
|
|
||||||
|
|
||||||
val galleryRegex = Regex("""[0-9]+$""")
|
|
||||||
val imageRegex = Regex("""^[0-9]+\..+$""")
|
|
||||||
var size = 0
|
|
||||||
fun setProgress(progress: Int) {
|
|
||||||
notificationBuilder.apply {
|
|
||||||
setContentText(
|
|
||||||
context.getString(
|
|
||||||
R.string.import_old_galleries_notification_text,
|
|
||||||
progress,
|
|
||||||
size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setProgress(size, progress, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.notify(0, notificationBuilder.build())
|
override fun onResponse(call: Call, response: Response) {
|
||||||
}
|
|
||||||
|
|
||||||
folder.listFiles { _, name ->
|
|
||||||
galleryRegex.matches(name)
|
|
||||||
}?.also {
|
|
||||||
size = it.size
|
|
||||||
setProgress(0)
|
|
||||||
}?.forEachIndexed { index, gallery ->
|
|
||||||
if (cancelImport)
|
|
||||||
return@forEachIndexed
|
|
||||||
|
|
||||||
setProgress(index)
|
|
||||||
|
|
||||||
val galleryID = gallery.name.toIntOrNull() ?: return@forEachIndexed
|
|
||||||
|
|
||||||
File(getDownloadDirectory(context), galleryID.toString()).mkdirs()
|
|
||||||
|
|
||||||
val reader = async {
|
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
json.parse(Reader.serializer(), File(gallery, "reader.json").readText())
|
Json.decodeFromString<List<Int>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let {
|
||||||
}.getOrElse {
|
favorites.addAll(it)
|
||||||
getReader(galleryID)
|
onSuccess?.invoke(it)
|
||||||
}
|
|
||||||
}
|
|
||||||
val galleryBlock = async {
|
|
||||||
kotlin.runCatching {
|
|
||||||
json.parse(GalleryBlock.serializer(), File(gallery, "galleryBlock.json").readText())
|
|
||||||
}.getOrElse {
|
|
||||||
getGalleryBlock(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Suppress("NAME_SHADOWING")
|
|
||||||
val thumbnail = async thumbnail@{
|
|
||||||
val galleryBlock = galleryBlock.await()
|
|
||||||
|
|
||||||
Base64.encodeToString(try {
|
|
||||||
File(gallery, "thumbnail.jpg").readBytes()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
val url = galleryBlock?.thumbnails?.firstOrNull()
|
|
||||||
|
|
||||||
if (url == null)
|
|
||||||
null
|
|
||||||
else {
|
|
||||||
val request = Request.Builder().url(url).build()
|
|
||||||
|
|
||||||
var done = false
|
|
||||||
var result: ByteArray? = null
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
|
||||||
override fun onFailure(call: Call?, e: IOException?) {
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call?, response: Response?) {
|
|
||||||
result = response?.body()?.use {
|
|
||||||
it.bytes()
|
|
||||||
}
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!done)
|
|
||||||
yield()
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
} ?: return@thumbnail null, Base64.DEFAULT)
|
}.onFailure { onFailure?.invoke(it) }
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
Cache(context).setCachedMetadata(galleryID,
|
private var job: Job? = null
|
||||||
Metadata(
|
private val receiver = object: BroadcastReceiver() {
|
||||||
thumbnail.await(),
|
val ACTION_CANCEL = "ACTION_IMPORT_CANCEL"
|
||||||
galleryBlock.await(),
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
reader.await()
|
context ?: return
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
File(gallery, "images").listFiles { _, name ->
|
when (intent?.action) {
|
||||||
imageRegex.matches(name)
|
ACTION_CANCEL -> {
|
||||||
}?.forEach {
|
job?.cancel()
|
||||||
if (cancelImport)
|
NotificationManagerCompat.from(context).cancel(R.id.notification_id_import)
|
||||||
return@forEach
|
context.unregisterReceiver(this)
|
||||||
|
}
|
||||||
@Suppress("NAME_SHADOWING")
|
|
||||||
val index = it.nameWithoutExtension.toIntOrNull() ?: return@forEach
|
|
||||||
|
|
||||||
Cache(context).putImage(galleryID, index, it.extension, it.inputStream())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
|
||||||
|
registerReceiver(receiver, IntentFilter().apply { addAction(receiver.ACTION_CANCEL) })
|
||||||
|
|
||||||
notificationBuilder.apply {
|
val notificationManager = NotificationManagerCompat.from(this)
|
||||||
setContentText(context.getText(R.string.import_old_galleries_notification_done))
|
val action = NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
||||||
setProgress(0, 0, false)
|
PendingIntent.getBroadcast(this, R.id.notification_import_cancel_action.normalizeID(), Intent(receiver.ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
setOngoing(false)
|
).build()
|
||||||
mActions.clear()
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
notificationManager.notify(0, notificationBuilder.build())
|
|
||||||
|
|
||||||
cancelImport = false
|
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 197 B |
|
Before Width: | Height: | Size: 585 B |
|
Before Width: | Height: | Size: 361 B |
|
Before Width: | Height: | Size: 470 B |
|
Before Width: | Height: | Size: 145 B |
|
Before Width: | Height: | Size: 366 B |
|
Before Width: | Height: | Size: 224 B |
|
Before Width: | Height: | Size: 236 B |
|
Before Width: | Height: | Size: 226 B |
|
Before Width: | Height: | Size: 700 B |
|
Before Width: | Height: | Size: 370 B |
|
Before Width: | Height: | Size: 416 B |
|
Before Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 602 B |
|
Before Width: | Height: | Size: 687 B |
@@ -1,8 +1,4 @@
|
|||||||
<!-- drawable/account_group.xml -->
|
<!--drawable/account_group.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:height="24dp"
|
<path android:fillColor="?attr/colorControlNormal" android:pathData="M12 5.5A3.5 3.5 0 0 1 15.5 9a3.5 3.5 0 0 1-3.5 3.5A3.5 3.5 0 0 1 8.5 9 3.5 3.5 0 0 1 12 5.5M5 8c0.56 0 1.08 0.15 1.53 0.42-0.15 1.43 0.27 2.85 1.13 3.96C7.16 13.34 6.16 14 5 14a3 3 0 0 1-3-3 3 3 0 0 1 3-3m14 0a3 3 0 0 1 3 3 3 3 0 0 1-3 3c-1.16 0-2.16-0.66-2.66-1.62 0.86-1.11 1.28-2.53 1.13-3.96C17.92 8.15 18.44 8 19 8M5.5 18.25c0-2.07 2.91-3.75 6.5-3.75s6.5 1.68 6.5 3.75V20h-13v-1.75M0 20v-1.5c0-1.39 1.89-2.56 4.45-2.9-0.59 0.68-0.95 1.62-0.95 2.65V20H0m24 0h-3.5v-1.75c0-1.03-0.36-1.97-0.95-2.65 2.56 0.34 4.45 1.51 4.45 2.9V20z"/>
|
||||||
android:width="24dp"
|
</vector>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="?attr/colorControlNormal" android:pathData="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
<!-- drawable/account_star.xml -->
|
<!--drawable/account_star.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:height="24dp"
|
<path android:fillColor="?attr/colorControlNormal" android:pathData="M15 14c-2.67 0-8 1.33-8 4v2h16v-2c0-2.67-5.33-4-8-4m0-2a4 4 0 0 0 4-4 4 4 0 0 0-4-4 4 4 0 0 0-4 4 4 4 0 0 0 4 4M5 13.28l2.45 1.49-0.65-2.81L9 10.08 6.11 9.83 5 7.19 3.87 9.83 1 10.08l2.18 1.88-0.68 2.81L5 13.28z"/>
|
||||||
android:width="24dp"
|
</vector>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="?attr/colorControlNormal" android:pathData="M15,14C12.33,14 7,15.33 7,18V20H23V18C23,15.33 17.67,14 15,14M15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12M5,13.28L7.45,14.77L6.8,11.96L9,10.08L6.11,9.83L5,7.19L3.87,9.83L1,10.08L3.18,11.96L2.5,14.77L5,13.28Z" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
<!-- drawable/arrow_right.xml -->
|
<!--drawable/arrow_right.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:height="24dp"
|
<path android:fillColor="#fff" android:pathData="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11H4z"/>
|
||||||
android:width="24dp"
|
</vector>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="#fff" android:pathData="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
@@ -1,37 +1,14 @@
|
|||||||
<animated-vector
|
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" xmlns:tools="http://schemas.android.com/tools" tools:ignore="newApi">
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
tools:ignore="newApi">
|
|
||||||
<aapt:attr name="android:drawable">
|
<aapt:attr name="android:drawable">
|
||||||
<vector
|
<vector android:name="vector" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:name="vector"
|
<path android:name="path" android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z" android:fillColor="@color/material_orange_500"/>
|
||||||
android:width="24dp"
|
<clip-path android:name="clip" android:pathData="M 2 21 L 2 21 L 22 21 L 22 21 Z"/>
|
||||||
android:height="24dp"
|
<path android:name="path_1" android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z" android:fillColor="@color/material_orange_500"/>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:name="path"
|
|
||||||
android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z"
|
|
||||||
android:fillColor="@color/material_orange_500"/>
|
|
||||||
<clip-path
|
|
||||||
android:name="clip"
|
|
||||||
android:pathData="M 2 21 L 2 21 L 22 21 L 22 21 Z"/>
|
|
||||||
<path
|
|
||||||
android:name="path_1"
|
|
||||||
android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z"
|
|
||||||
android:fillColor="@color/material_orange_500"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
</aapt:attr>
|
</aapt:attr>
|
||||||
<target android:name="clip">
|
<target android:name="clip">
|
||||||
<aapt:attr name="android:animation">
|
<aapt:attr name="android:animation">
|
||||||
<objectAnimator
|
<objectAnimator android:propertyName="pathData" android:duration="500" android:valueFrom="M 2 21 L 2 21 L 22 21 L 22 21 Z" android:valueTo="M 2 2 L 2 21 L 22 21 L 22 2 Z" android:valueType="pathType" android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||||
android:propertyName="pathData"
|
|
||||||
android:duration="500"
|
|
||||||
android:valueFrom="M 2 21 L 2 21 L 22 21 L 22 21 Z"
|
|
||||||
android:valueTo="M 2 2 L 2 21 L 22 21 L 22 2 Z"
|
|
||||||
android:valueType="pathType"
|
|
||||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
|
||||||
</aapt:attr>
|
</aapt:attr>
|
||||||
</target>
|
</target>
|
||||||
</animated-vector>
|
</animated-vector>
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
<!-- drawable/backspace_outline.xml -->
|
<!--drawable/backspace_outline.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:height="24dp"
|
<path android:fillColor="#000" android:pathData="M19 15.59L17.59 17 14 13.41 10.41 17 9 15.59 12.59 12 9 8.41 10.41 7 14 10.59 17.59 7 19 8.41 15.41 12 19 15.59M22 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H7c-0.69 0-1.23-0.36-1.59-0.89L0 12l5.41-8.12C5.77 3.35 6.31 3 7 3h15m0 2H7l-4.72 7L7 19h15V5z"/>
|
||||||
android:width="24dp"
|
</vector>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="#000" android:pathData="M19,15.59L17.59,17L14,13.41L10.41,17L9,15.59L12.59,12L9,8.41L10.41,7L14,10.59L17.59,7L19,8.41L15.41,12L19,15.59M22,3A2,2 0 0,1 24,5V19A2,2 0 0,1 22,21H7C6.31,21 5.77,20.64 5.41,20.11L0,12L5.41,3.88C5.77,3.35 6.31,3 7,3H22M22,5H7L2.28,12L7,19H22V5Z" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
<!-- drawable/book_open.xml -->
|
<!--drawable/book_open.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:height="24dp"
|
<path android:fillColor="?attr/colorControlNormal" android:pathData="M13 12h7v1.5h-7m0-4h7V11h-7m0 3.5h7V16h-7m8-12H3a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2m0 15h-9V6h9"/>
|
||||||
android:width="24dp"
|
</vector>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="?attr/colorControlNormal" android:pathData="M13,12H20V13.5H13M13,9.5H20V11H13M13,14.5H20V16H13M21,4H3A2,2 0 0,0 1,6V19A2,2 0 0,0 3,21H21A2,2 0 0,0 23,19V6A2,2 0 0,0 21,4M21,19H12V6H21" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
<!-- drawable/brush.xml -->
|
<!--drawable/brush.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:height="24dp"
|
<path android:fillColor="?attr/colorControlNormal" android:pathData="M20.71 4.63l-1.34-1.34c-0.37-0.39-1.02-0.39-1.41 0L9 12.25 11.75 15l8.96-8.96c0.39-0.39 0.39-1.04 0-1.41M7 14a3 3 0 0 0-3 3c0 1.31-1.16 2-2 2 0.92 1.22 2.5 2 4 2a4 4 0 0 0 4-4 3 3 0 0 0-3-3z"/>
|
||||||
android:width="24dp"
|
</vector>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="?attr/colorControlNormal" android:pathData="M20.71,4.63L19.37,3.29C19,2.9 18.35,2.9 17.96,3.29L9,12.25L11.75,15L20.71,6.04C21.1,5.65 21.1,5 20.71,4.63M7,14A3,3 0 0,0 4,17C4,18.31 2.84,19 2,19C2.92,20.22 4.5,21 6,21A4,4 0 0,0 10,17A3,3 0 0,0 7,14Z" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
<!-- drawable/cancel.xml -->
|
<!--drawable/cancel.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:height="24dp"
|
<path android:fillColor="#fff" android:pathData="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2m0 2a8 8 0 0 0-8 8c0 1.85 0.63 3.55 1.68 4.91L16.91 5.68C15.55 4.63 13.85 4 12 4m0 16a8 8 0 0 0 8-8c0-1.85-0.63-3.55-1.68-4.91L7.09 18.32C8.45 19.37 10.15 20 12 20z"/>
|
||||||
android:width="24dp"
|
</vector>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="#fff" android:pathData="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,13.85 4.63,15.55 5.68,16.91L16.91,5.68C15.55,4.63 13.85,4 12,4M12,20A8,8 0 0,0 20,12C20,10.15 19.37,8.45 18.32,7.09L7.09,18.32C8.45,19.37 10.15,20 12,20Z" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
4
app/src/main/res/drawable/clock_end.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!--drawable/clock_end.xml-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#fff" android:pathData="M12 1C8.14 1 5 4.14 5 8a7 7 0 0 0 7 7c3.86 0 7-3.13 7-7 0-3.86-3.14-7-7-7m0 2.15c2.67 0 4.85 2.17 4.85 4.85 0 2.68-2.18 4.85-4.85 4.85A4.85 4.85 0 0 1 7.15 8 4.85 4.85 0 0 1 12 3.15M11 5v3.69l3.19 1.84 0.75-1.3-2.44-1.41V5M15 16v3H3v2h12v3l4-4m0 0v4h2v-8h-2"/>
|
||||||
|
</vector>
|
||||||
4
app/src/main/res/drawable/clock_start.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!--drawable/clock_start.xml-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#fff" android:pathData="M12 1C8.14 1 5 4.14 5 8a7 7 0 0 0 7 7c3.86 0 7-3.13 7-7 0-3.86-3.14-7-7-7m0 2.15c2.67 0 4.85 2.17 4.85 4.85 0 2.68-2.18 4.85-4.85 4.85A4.85 4.85 0 0 1 7.15 8 4.85 4.85 0 0 1 12 3.15M11 5v3.69l3.19 1.84 0.75-1.3-2.44-1.41V5M4 16v8h2v-3h12v3l4-4-4-4v3H6v-3"/>
|
||||||
|
</vector>
|
||||||
4
app/src/main/res/drawable/close.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!--drawable/close.xml-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="?attr/colorControlNormal" android:pathData="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
|
||||||
|
</vector>
|
||||||
4
app/src/main/res/drawable/delete.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!--drawable/delete.xml-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="?attr/colorControlNormal" android:pathData="M19 4h-3.5l-1-1h-5l-1 1H5v2h14M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6v12z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
<!-- drawable/fingerprint.xml -->
|
<!--drawable/fingerprint.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
<path android:fillColor="#fff" android:pathData="M17.81 4.47c-0.08 0-0.16-0.02-0.23-0.06C15.66 3.42 14 3 12 3c-1.97 0-3.85 0.47-5.56 1.41C6.2 4.54 5.9 4.45 5.76 4.21c-0.13-0.24-0.04-0.55 0.2-0.68C7.82 2.5 9.86 2 12 2s4 0.47 6.04 1.5c0.25 0.15 0.34 0.45 0.21 0.69-0.09 0.18-0.25 0.28-0.44 0.28M3.5 9.72c-0.1 0-0.2-0.03-0.29-0.09C3 9.47 2.93 9.16 3.09 8.93c0.99-1.4 2.25-2.5 3.75-3.27C10 4.04 14 4.03 17.15 5.65c1.5 0.77 2.76 1.85 3.75 3.25 0.16 0.22 0.1 0.54-0.12 0.7-0.23 0.16-0.54 0.11-0.7-0.1-0.9-1.28-2.04-2.27-3.39-2.96-2.87-1.47-6.54-1.47-9.4 0.01-1.36 0.7-2.5 1.7-3.4 2.95C3.81 9.65 3.66 9.72 3.5 9.72m6.25 12.07c-0.13 0-0.25-0.05-0.35-0.15-0.87-0.87-1.34-1.43-2.01-2.64-0.69-1.23-1.05-2.73-1.05-4.34 0-2.97 2.54-5.39 5.66-5.39s5.66 2.42 5.66 5.39a0.5 0.5 0 0 1-0.5 0.5 0.5 0.5 0 0 1-0.5-0.5c0-2.42-2.09-4.39-4.66-4.39-2.57 0-4.66 1.97-4.66 4.39 0 1.44 0.32 2.77 0.93 3.84 0.64 1.16 1.08 1.65 1.85 2.43 0.19 0.2 0.19 0.51 0 0.71-0.12 0.1-0.24 0.15-0.37 0.15m7.17-1.85c-1.19 0-2.24-0.3-3.1-0.89-1.49-1.01-2.38-2.65-2.38-4.39a0.5 0.5 0 0 1 0.5-0.5 0.5 0.5 0 0 1 0.5 0.5c0 1.41 0.72 2.74 1.94 3.56 0.71 0.48 1.54 0.71 2.54 0.71 0.24 0 0.64-0.03 1.04-0.1 0.27-0.05 0.54 0.13 0.58 0.41 0.05 0.26-0.13 0.53-0.41 0.58-0.57 0.11-1.07 0.12-1.21 0.12M14.91 22h-0.13c-1.59-0.46-2.63-1.05-3.72-2.12-1.4-1.38-2.17-3.24-2.17-5.22 0-1.62 1.38-2.94 3.08-2.94 1.7 0 3.08 1.32 3.08 2.94 0 1.07 0.95 1.94 2.08 1.94 1.15 0 2.08-0.87 2.08-1.94 0-3.77-3.25-6.83-7.25-6.83-2.84 0-5.46 1.58-6.61 4.03-0.39 0.81-0.59 1.76-0.59 2.8 0 0.78 0.07 2.01 0.67 3.61 0.1 0.26-0.03 0.55-0.29 0.64C4.88 19 4.59 18.87 4.5 18.62 4 17.31 3.77 16 3.77 14.66c0-1.2 0.23-2.29 0.68-3.24 1.33-2.79 4.28-4.6 7.51-4.6 4.54 0 8.25 3.51 8.25 7.83 0 1.62-1.38 2.94-3.08 2.94-1.7 0-3.08-1.32-3.08-2.94 0-1.07-0.93-1.94-2.08-1.94s-2.08 0.87-2.08 1.94c0 1.71 0.66 3.31 1.87 4.51 0.95 0.94 1.86 1.46 3.27 1.84 0.27 0.08 0.42 0.36 0.35 0.62-0.05 0.23-0.26 0.38-0.47 0.38z" tools:ignore="VectorPath"/>
|
||||||
android:height="24dp"
|
</vector>
|
||||||
android:width="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="#fff" android:pathData="M17.81,4.47C17.73,4.47 17.65,4.45 17.58,4.41C15.66,3.42 14,3 12,3C10.03,3 8.15,3.47 6.44,4.41C6.2,4.54 5.9,4.45 5.76,4.21C5.63,3.97 5.72,3.66 5.96,3.53C7.82,2.5 9.86,2 12,2C14.14,2 16,2.47 18.04,3.5C18.29,3.65 18.38,3.95 18.25,4.19C18.16,4.37 18,4.47 17.81,4.47M3.5,9.72C3.4,9.72 3.3,9.69 3.21,9.63C3,9.47 2.93,9.16 3.09,8.93C4.08,7.53 5.34,6.43 6.84,5.66C10,4.04 14,4.03 17.15,5.65C18.65,6.42 19.91,7.5 20.9,8.9C21.06,9.12 21,9.44 20.78,9.6C20.55,9.76 20.24,9.71 20.08,9.5C19.18,8.22 18.04,7.23 16.69,6.54C13.82,5.07 10.15,5.07 7.29,6.55C5.93,7.25 4.79,8.25 3.89,9.5C3.81,9.65 3.66,9.72 3.5,9.72M9.75,21.79C9.62,21.79 9.5,21.74 9.4,21.64C8.53,20.77 8.06,20.21 7.39,19C6.7,17.77 6.34,16.27 6.34,14.66C6.34,11.69 8.88,9.27 12,9.27C15.12,9.27 17.66,11.69 17.66,14.66A0.5,0.5 0 0,1 17.16,15.16A0.5,0.5 0 0,1 16.66,14.66C16.66,12.24 14.57,10.27 12,10.27C9.43,10.27 7.34,12.24 7.34,14.66C7.34,16.1 7.66,17.43 8.27,18.5C8.91,19.66 9.35,20.15 10.12,20.93C10.31,21.13 10.31,21.44 10.12,21.64C10,21.74 9.88,21.79 9.75,21.79M16.92,19.94C15.73,19.94 14.68,19.64 13.82,19.05C12.33,18.04 11.44,16.4 11.44,14.66A0.5,0.5 0 0,1 11.94,14.16A0.5,0.5 0 0,1 12.44,14.66C12.44,16.07 13.16,17.4 14.38,18.22C15.09,18.7 15.92,18.93 16.92,18.93C17.16,18.93 17.56,18.9 17.96,18.83C18.23,18.78 18.5,18.96 18.54,19.24C18.59,19.5 18.41,19.77 18.13,19.82C17.56,19.93 17.06,19.94 16.92,19.94M14.91,22C14.87,22 14.82,22 14.78,22C13.19,21.54 12.15,20.95 11.06,19.88C9.66,18.5 8.89,16.64 8.89,14.66C8.89,13.04 10.27,11.72 11.97,11.72C13.67,11.72 15.05,13.04 15.05,14.66C15.05,15.73 16,16.6 17.13,16.6C18.28,16.6 19.21,15.73 19.21,14.66C19.21,10.89 15.96,7.83 11.96,7.83C9.12,7.83 6.5,9.41 5.35,11.86C4.96,12.67 4.76,13.62 4.76,14.66C4.76,15.44 4.83,16.67 5.43,18.27C5.53,18.53 5.4,18.82 5.14,18.91C4.88,19 4.59,18.87 4.5,18.62C4,17.31 3.77,16 3.77,14.66C3.77,13.46 4,12.37 4.45,11.42C5.78,8.63 8.73,6.82 11.96,6.82C16.5,6.82 20.21,10.33 20.21,14.65C20.21,16.27 18.83,17.59 17.13,17.59C15.43,17.59 14.05,16.27 14.05,14.65C14.05,13.58 13.12,12.71 11.97,12.71C10.82,12.71 9.89,13.58 9.89,14.65C9.89,16.36 10.55,17.96 11.76,19.16C12.71,20.1 13.62,20.62 15.03,21C15.3,21.08 15.45,21.36 15.38,21.62C15.33,21.85 15.12,22 14.91,22Z"
|
|
||||||
tools:ignore="VectorPath" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
<!-- drawable/gender_female.xml -->
|
<!--drawable/gender_female.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:height="24dp"
|
<path android:fillColor="?attr/colorControlNormal" android:pathData="M12 4a6 6 0 0 1 6 6c0 2.97-2.16 5.44-5 5.92V18h2v2h-2v2h-2v-2H9v-2h2v-2.08C8.16 15.44 6 12.97 6 10a6 6 0 0 1 6-6m0 2a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4z"/>
|
||||||
android:width="24dp"
|
</vector>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="?attr/colorControlNormal" android:pathData="M12,4A6,6 0 0,1 18,10C18,12.97 15.84,15.44 13,15.92V18H15V20H13V22H11V20H9V18H11V15.92C8.16,15.44 6,12.97 6,10A6,6 0 0,1 12,4M12,6A4,4 0 0,0 8,10A4,4 0 0,0 12,14A4,4 0 0,0 16,10A4,4 0 0,0 12,6Z" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
4
app/src/main/res/drawable/gender_female_white.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!--drawable/gender_female.xml-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFF" android:pathData="M12 4a6 6 0 0 1 6 6c0 2.97-2.16 5.44-5 5.92V18h2v2h-2v2h-2v-2H9v-2h2v-2.08C8.16 15.44 6 12.97 6 10a6 6 0 0 1 6-6m0 2a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
<!-- drawable/gender_male.xml -->
|
<!--drawable/gender_male.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:height="24dp"
|
<path android:fillColor="?attr/colorControlNormal" android:pathData="M9 9c1.29 0 2.5 0.41 3.47 1.11L17.58 5H13V3h8v8h-2V6.41l-5.11 5.09c0.7 1 1.11 2.2 1.11 3.5a6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6m0 2a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4z"/>
|
||||||
android:width="24dp"
|
</vector>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="?attr/colorControlNormal" android:pathData="M9,9C10.29,9 11.5,9.41 12.47,10.11L17.58,5H13V3H21V11H19V6.41L13.89,11.5C14.59,12.5 15,13.7 15,15A6,6 0 0,1 9,21A6,6 0 0,1 3,15A6,6 0 0,1 9,9M9,11A4,4 0 0,0 5,15A4,4 0 0,0 9,19A4,4 0 0,0 13,15A4,4 0 0,0 9,11Z" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
4
app/src/main/res/drawable/gender_male_white.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!--drawable/gender_male.xml-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#FFF" android:pathData="M9 9c1.29 0 2.5 0.41 3.47 1.11L17.58 5H13V3h8v8h-2V6.41l-5.11 5.09c0.7 1 1.11 2.2 1.11 3.5a6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6m0 2a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
<!-- drawable/github-circle.xml -->
|
<!--drawable/github-circle.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:height="24dp"
|
<path android:fillColor="#000" android:pathData="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5 0.5 0.08 0.66-0.23 0.66-0.5v-1.69c-2.77 0.6-3.36-1.34-3.36-1.34-0.46-1.16-1.11-1.47-1.11-1.47-0.91-0.62 0.07-0.6 0.07-0.6 1 0.07 1.53 1.03 1.53 1.03 0.87 1.52 2.34 1.07 2.91 0.83 0.09-0.65 0.35-1.09 0.63-1.34-2.22-0.25-4.55-1.11-4.55-4.92 0-1.11 0.38-2 1.03-2.71-0.1-0.25-0.45-1.29 0.1-2.64 0 0 0.84-0.27 2.75 1.02 0.79-0.22 1.65-0.33 2.5-0.33 0.85 0 1.71 0.11 2.5 0.33 1.91-1.29 2.75-1.02 2.75-1.02 0.55 1.35 0.2 2.39 0.1 2.64 0.65 0.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91 0.36 0.31 0.69 0.92 0.69 1.85V21c0 0.27 0.16 0.59 0.67 0.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2z"/>
|
||||||
android:width="24dp"
|
</vector>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="#000" android:pathData="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
4
app/src/main/res/drawable/history.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!--drawable/history.xml-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="?attr/colorControlNormal" android:pathData="M13.5 8H12v5l4.28 2.54 0.72-1.21-3.5-2.08V8M13 3a9 9 0 0 0-9 9H1l3.96 4.03L9 12H6a7 7 0 0 1 7-7 7 7 0 0 1 7 7 7 7 0 0 1-7 7c-1.93 0-3.68-0.79-4.94-2.06l-1.42 1.42C8.27 20 10.5 21 13 21a9 9 0 0 0 9-9 9 9 0 0 0-9-9"/>
|
||||||
|
</vector>
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
<vector android:height="24dp"
|
<vector android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
<path android:fillColor="#fff" android:pathData="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path android:fillColor="#fff" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,74 +1,19 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--~ 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/>.-->
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:aapt="http://schemas.android.com/aapt" tools:ignore="NewApi">
|
||||||
~ 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/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<animated-vector
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
tools:ignore="NewApi">
|
|
||||||
<aapt:attr name="android:drawable">
|
<aapt:attr name="android:drawable">
|
||||||
<vector
|
<vector android:name="vector" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:name="vector"
|
<path android:name="path" android:pathData="M 19 9 L 15 9 L 15 3 L 9 3 L 9 9 L 5 9 L 12 16 L 19 9 Z" android:fillColor="#fff" android:strokeWidth="1"/>
|
||||||
android:width="24dp"
|
<path android:name="path_2" android:pathData="M 5 19 L 19 19" android:fillColor="#fff" android:strokeColor="#fff" android:strokeWidth="2"/>
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:name="path"
|
|
||||||
android:pathData="M 19 9 L 15 9 L 15 3 L 9 3 L 9 9 L 5 9 L 12 16 L 19 9 Z"
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:strokeWidth="1"/>
|
|
||||||
<path
|
|
||||||
android:name="path_2"
|
|
||||||
android:pathData="M 5 19 L 19 19"
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:strokeColor="#fff"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:strokeLineCap="butt"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
</aapt:attr>
|
</aapt:attr>
|
||||||
<target android:name="path_2">
|
<target android:name="path_2">
|
||||||
<aapt:attr name="android:animation">
|
<aapt:attr name="android:animation">
|
||||||
<set>
|
<set>
|
||||||
<objectAnimator
|
<objectAnimator android:propertyName="trimPathEnd" android:duration="500" android:valueFrom="0" android:valueTo="0.8" android:valueType="floatType" android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||||
android:propertyName="trimPathEnd"
|
<objectAnimator android:propertyName="trimPathStart" android:startOffset="500" android:duration="500" android:valueFrom="0" android:valueTo="0.8" android:valueType="floatType" android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||||
android:duration="500"
|
<objectAnimator android:propertyName="trimPathOffset" android:duration="1000" android:valueFrom="0" android:valueTo="0.2" android:valueType="floatType" android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="0.8"
|
|
||||||
android:valueType="floatType"
|
|
||||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
|
||||||
<objectAnimator
|
|
||||||
android:propertyName="trimPathStart"
|
|
||||||
android:startOffset="500"
|
|
||||||
android:duration="500"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="0.8"
|
|
||||||
android:valueType="floatType"
|
|
||||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
|
||||||
<objectAnimator
|
|
||||||
android:propertyName="trimPathOffset"
|
|
||||||
android:duration="1000"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="0.2"
|
|
||||||
android:valueType="floatType"
|
|
||||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
|
||||||
</set>
|
</set>
|
||||||
</aapt:attr>
|
</aapt:attr>
|
||||||
</target>
|
</target>
|
||||||
</animated-vector>
|
</animated-vector>
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
<!-- drawable/export.xml -->
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:height="24dp"
|
|
||||||
android:width="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="#fff" android:pathData="M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,14 +1,4 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
||||||
android:width="24dp"
|
<path android:fillColor="#fff" android:pathData="M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73 0.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/>
|
||||||
android:height="24dp"
|
<path android:fillColor="#fff" android:pathData="M8.5 15a1.5 1.5 0 1 1 1.5 1.5A1.5 1.5 0 0 1 8.5 15z"/>
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:pathData="M18.4,10.6C16.55,8.99 14.15,8 11.5,8c-4.65,0 -8.58,3.03 -9.96,7.22L3.9,16c1.05,-3.19 4.05,-5.5 7.6,-5.5 1.95,0 3.73,0.72 5.12,1.88L13,16h9V7l-3.6,3.6z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:pathData="M8.5,15
|
|
||||||
a1.5,1.5 0 1,1 1.5,1.5
|
|
||||||
a1.5,1.5 0 0,1 -1.5,-1.5 Z"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
<!-- drawable/message.xml -->
|
<!--drawable/message.xml-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
android:height="24dp"
|
<path android:fillColor="#000" android:pathData="M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4c0-1.11-0.9-2-2-2z"/>
|
||||||
android:width="24dp"
|
</vector>
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path android:fillColor="#000" android:pathData="M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4C22,2.89 21.1,2 20,2Z" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
||||||
android:width="24dp"
|
<path android:fillColor="#FF000000" android:pathData="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||