Compare commits

...

84 Commits

Author SHA1 Message Date
tom5079
439a8e93ec App built 2020-09-09 11:25:53 +09:00
tom5079
83801feee9 Bug fix 2020-09-09 11:23:22 +09:00
tom5079
8a6860c96e Import cleanup 2020-09-09 09:29:33 +09:00
tom5079
5c959f2987 Bug fix 2020-09-09 09:29:33 +09:00
tom5079
4e4397287a Implemented fast scroll 2020-09-09 09:22:17 +09:00
tom5079
fe02abc9e8 Bug fix 2020-09-08 20:04:12 +09:00
tom5079
59347ab317 Bug fix 2020-09-08 19:16:15 +09:00
tom5079
f408a91176 Bug fix 2020-09-07 10:00:10 +09:00
tom5079
6f6956ce27 Fixed DownloadLocationDialogFragment keep showing up when any button is clicked 2020-09-06 17:19:18 +09:00
tom5079
4ecad8eccc Fixed migration 2020-09-05 18:31:53 +09:00
tom5079
486fbe46a0 Fixed migration 2020-09-05 18:11:20 +09:00
tom5079
1ddb636dd0 Fixed migration 2020-09-05 18:00:15 +09:00
tom5079
081c890b4e Bug fix 2020-09-05 12:51:41 +09:00
tom5079
86d528ba13 v5.0-beta1 2020-09-05 09:13:03 +09:00
tom5079
6bda3cb75a Performance improvement 2020-09-05 08:57:10 +09:00
tom5079
12d8949c9e Fixed context not attached error 2020-09-05 08:31:30 +09:00
tom5079
ffc7c2aa67 Bug fix 2020-09-04 11:05:29 +09:00
tom5079
5ec67488eb Bug fix 2020-09-04 07:51:55 +09:00
tom5079
be64703d3c Minor bug fix 2020-09-03 18:28:40 +09:00
tom5079
705925a050 Migration bug fix 2020-09-03 15:49:04 +09:00
tom5079
29665be34d Added Migration 2020-09-03 10:54:07 +09:00
tom5079
1edf986acf Added Download Folder Dialog 2020-09-02 21:59:17 +09:00
tom5079
37be8ccf7f Bug fix 2020-09-02 20:15:26 +09:00
tom5079
ead68b5201 Moved storage managing settings to another fragment 2020-09-02 13:28:05 +09:00
tom5079
4409664698 nomedia 2020-09-02 13:05:36 +09:00
tom5079
fc6bc7965c Added download folder name change 2020-09-02 12:58:31 +09:00
tom5079
f70eccb1da Created TagChip.kt 2020-09-02 12:29:34 +09:00
tom5079
861994e804 Fixed crash when download folder is changed 2020-09-02 12:15:37 +09:00
tom5079
2b8facfb97 End service when completed
Auto redownload
2020-09-02 11:03:46 +09:00
tom5079
9583897ada working 2020-09-02 00:13:25 +09:00
tom5079
7704c96955 what i got so far 2020-09-01 18:07:16 +09:00
tom5079
c96d609803 Created new cache/downloader 2020-08-31 11:41:12 +09:00
tom5079
aa0e5000ab Deleted redundant code 2020-08-30 20:12:06 +09:00
tom5079
7ca4418a50 Added global preferences object 2020-08-30 19:52:51 +09:00
tom5079
fdd9b02388 (Almost) Full OkHTTP implementation 2020-08-28 22:43:47 +09:00
tom5079
ece127e982 Fixed downloader not fully cancelled after DownloadWorker.cancel() is called 2020-08-28 21:51:44 +09:00
tom5079
5488e14f32 Moved history adding code to ReaderActivity.kt 2020-08-26 23:47:07 +09:00
tom5079
3558d826fb Implemented new favorite backup/restore feature 2020-08-26 23:40:36 +09:00
tom5079
68c94d1d8b Removed old features 2020-08-26 20:00:30 +09:00
tom5079
1a4ae5dfc6 Dependency update 2020-08-26 19:19:26 +09:00
tom5079
1a95afe266 Implemented Right-to-Left pageturn 2020-08-26 18:47:27 +09:00
tom5079
6579db3cc8 Added Search all galleries feature 2020-08-26 18:36:59 +09:00
tom5079
ceac01533a Updated History.kt to use kotlin's delegate feature 2020-08-26 18:01:12 +09:00
tom5079
216914882c Separated libpupil to standalone repository
Migrated to Kotlin 1.4
2020-08-23 20:26:23 +09:00
tom5079
735dbab695 Build optimization 2020-08-10 01:53:11 +09:00
tom5079
dbaab152ef Build optimization 2020-08-10 01:32:56 +09:00
tom5079
9da1b30984 i'm dumb 2020-08-08 16:29:04 +09:00
tom5079
9415ab4ef9 Fixed some images crashing
Added auto pageturn timer
2020-08-08 15:55:17 +09:00
tom5079
647294daf2 what were i thinking?! 2020-08-04 17:50:07 +09:00
tom5079
6ebc386474 Fixed app crashing when deleting cache/download 2020-08-04 12:14:14 +09:00
tom5079
3e657bdc09 Merge branch 'dev' 2020-08-03 21:54:28 +09:00
tom5079
0b0adb76a1 Merge branch 'master' into dev
# Conflicts:
#	app/build.gradle
#	app/release/app-release.apk
#	app/release/output-metadata.json
2020-08-03 21:54:14 +09:00
tom5079
17b3e010aa fuck git 2020-08-03 21:49:04 +09:00
tom5079
20003acd73 App built
resolves #98
2020-08-03 21:34:24 +09:00
tom5079
2ab7672092 Search Backtracking Added 2020-08-03 21:31:39 +09:00
tom5079
c317abe64b Open Gallery Info dialog instead of opening up the gallery when opening a random gallery 2020-08-03 21:19:08 +09:00
tom5079
bc33ce1ebc Fix Settings opening up too late if the download folder is too big 2020-08-03 21:14:47 +09:00
Pupil
684c5cf38b Merge pull request #105 from tom5079/dev
4.19-hotfix2
2020-08-03 02:10:17 +09:00
tom5079
c34e15f0a1 Fixed image corruption when not using cache 2020-08-03 02:06:37 +09:00
Pupil
bad004f892 Merge pull request #104 from tom5079/dev
Version 4.19-hotfix1
2020-08-02 17:48:32 +09:00
tom5079
828d3de020 Fixed app crashing between android version 5.0 and 5.1 2020-08-02 17:45:24 +09:00
tom5079
132b3b9be1 Unable to fetch thumbnails fixed 2020-07-29 10:53:50 +09:00
Pupil
388bc6fda5 Merge pull request #103 from tom5079/Pupil-99
resolves #99
2020-07-29 10:28:01 +09:00
tom5079
a93edc184d resolves #99 2020-07-23 22:42:38 +09:00
tom5079
08672d10ac oops 2020-07-23 20:48:24 +09:00
tom5079
b563dae3a8 App rebuilt 2020-07-23 20:37:38 +09:00
tom5079
917f9672dd bug fix - unable to run on older devices
Version 4.18.4
2020-07-23 20:32:45 +09:00
Pupil
9ddb19530b Merge pull request #101 from tom5079/dev
Version 4.18.3
2020-07-12 20:09:12 +09:00
tom5079
431e56a9f1 Version 4.18.3 2020-07-12 20:07:59 +09:00
Pupil
71093aac4c Update README.md 2020-07-12 19:53:28 +09:00
tom5079
47c9e8127e Remove logic for resetting low quality setting 2020-07-07 10:21:30 +09:00
tom5079
24b801b346 resolves #52 2020-07-05 19:45:55 +09:00
tom5079
70608c3ed9 resolves #52 2020-07-05 19:40:14 +09:00
tom5079
f185196e85 Version Update 2020-07-05 19:33:56 +09:00
tom5079
a8766a8bbe Drawer logo fixed 2020-06-23 20:37:20 +09:00
Pupil
27a8c93cfe Merge pull request #97 from tom5079/Pupil-95
Pupil 95
2020-06-23 18:49:57 +09:00
tom5079
a3cd29fda9 Merge remote-tracking branch 'origin/Pupil-95' into Pupil-95 2020-06-23 18:48:24 +09:00
tom5079
adda8ab640 Pupil-95 Search tag icon not visible in light theme 2020-06-23 18:48:13 +09:00
tom5079
1538ea5fc8 Merge remote-tracking branch 'origin/dev' into dev 2020-06-23 18:47:58 +09:00
tom5079
2367a97a54 App built 2020-06-23 18:47:48 +09:00
tom5079
090ec0e4af Auto-Retry
Bug fix
2020-06-23 18:47:48 +09:00
tom5079
de7f552e5c Pupil-95 Search tag icon not visible in light theme 2020-06-23 18:40:43 +09:00
tom5079
d763f5dca0 App built 2020-06-22 09:52:25 +09:00
tom5079
9f41116241 Auto-Retry
Bug fix
2020-06-22 09:48:04 +09:00
131 changed files with 3331 additions and 3225 deletions

3
.gitignore vendored
View File

@@ -16,4 +16,5 @@
/gh-pages /gh-pages
#Private files #Private files
**/google-services.json **/google-services.json
**/credentials.json

View File

@@ -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">

View File

@@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value=" Copyright &amp;#36;today.year tom5079&#10;&#10; Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10; you may not use this file except in compliance with the License.&#10; You may obtain a copy of the License at&#10;&#10; http://www.apache.org/licenses/LICENSE-2.0&#10;&#10; Unless required by applicable law or agreed to in writing, software&#10; distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10; See the License for the specific language governing permissions and&#10; limitations under the License." />
<option name="myName" value="Apache" />
</copyright>
</component>

View File

@@ -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
View File

@@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="tom50">
<words>
<w>hitomi</w>
</words>
</dictionary>
</component>

1
.idea/gradle.xml generated
View File

@@ -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
View 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>

View File

@@ -1,3 +0,0 @@
<component name="DependencyValidationManager">
<scope name="libpupil" pattern="file[libpupil]:*/" />
</component>

View File

@@ -4,7 +4,6 @@
*Pupil, Hitomi.la viewer for Android* *Pupil, Hitomi.la viewer for Android*
[![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v) [![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v)
I can speak English, Japanese and Korean. If you have any questions, head over to my discord server or DM me!
# Screenshot # Screenshot
![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.png?raw=true) ![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.png?raw=true)

View File

@@ -14,32 +14,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 54 versionCode 57
versionName "4.18.1" versionName "5.0-beta7"
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,46 +59,55 @@ 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:$kotlin_version"
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-alpha01' implementation 'com.google.android.material:material:1.3.0-alpha02'
implementation 'com.google.firebase:firebase-core:17.4.3' implementation 'com.google.firebase:firebase-core:17.5.0'
implementation 'com.google.firebase:firebase-analytics:17.4.3' implementation 'com.google.firebase:firebase-analytics:17.5.0'
implementation 'com.google.firebase:firebase-crashlytics:17.1.0' implementation 'com.google.firebase:firebase-crashlytics:17.2.1'
implementation 'com.google.firebase:firebase-perf:19.0.7' implementation 'com.google.firebase:firebase-perf:19.0.8'
implementation 'com.github.arimorty:floatingsearchview:2.1.1' implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4' implementation 'com.github.clans:fab:1.6.4'
//implementation 'com.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") {
transitive = false transitive = false
} }
implementation 'com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2'
implementation 'net.rdrei.android.dirchooser:library:3.2@aar' implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
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.5") {
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
} }

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -11,8 +11,8 @@
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"properties": [], "properties": [],
"versionCode": 54, "versionCode": 57,
"versionName": "54", "versionName": "5.0-beta7",
"enabled": true, "enabled": true,
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }

View File

@@ -21,7 +21,6 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import android.util.Log 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 androidx.test.rule.ActivityTestRule
@@ -29,6 +28,7 @@ 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.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader import xyz.quaver.hiyobi.getReader
@@ -36,6 +36,8 @@ import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.download.Cache import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.getDownloadDirectory
import java.io.InputStreamReader
import java.net.URL import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@@ -58,8 +60,10 @@ class ExampleInstrumentedTest {
val activityTestRule = ActivityTestRule(LockActivity::class.java) val activityTestRule = ActivityTestRule(LockActivity::class.java)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
ContextCompat.getExternalFilesDirs(appContext, null).forEachIndexed { index, file -> Runtime.getRuntime().exec("du -hs " + getDownloadDirectory(appContext)).let {
Log.i("PUPILD", "$index: ${file?.absolutePath}") InputStreamReader(it.inputStream).readLines().forEach { res ->
Log.i("PUPILD", res)
}
} }
} }
@@ -99,7 +103,7 @@ class ExampleInstrumentedTest {
while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) { while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) {
Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null") Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null")
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) if (worker.progress[galleryID]?.all { it.isInfinite() } == true)
break break
} }
@@ -118,4 +122,9 @@ class ExampleInstrumentedTest {
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null") Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null")
} }
@Test
fun test_suggestion() {
getSuggestionsForQuery("female:l")
}
} }

View File

@@ -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>

View File

@@ -18,68 +18,114 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import android.app.Application
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import 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.util.*
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: GalleryList
lateinit var favorites: Histories private set
lateinit var favorites: GalleryList
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()
} }
} catch (e: Exception) { } catch (e: Exception) {
preference.edit().remove("dl_location").apply() Preferences.remove("download_folder")
} }
if (!preference.getBoolean("low_quality_reset", false)) { histories = GalleryList(File(ContextCompat.getDataDir(this), "histories.json"))
preference.edit() favorites = GalleryList(File(ContextCompat.getDataDir(this), "favorites.json"))
.putBoolean("low_quality", true)
.putBoolean("low_quality_reset", true)
.apply()
}
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json")) if (Preferences["new_history"]) {
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json")) 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)
@@ -102,6 +148,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)
@@ -109,7 +162,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)
@@ -117,8 +170,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
}) })

View File

@@ -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) }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -18,19 +18,20 @@
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
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.google.android.material.snackbar.Snackbar import com.bumptech.glide.request.RequestListener
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.bumptech.glide.request.target.Target
import kotlinx.android.synthetic.main.activity_reader.view.*
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
@@ -39,30 +40,31 @@ 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.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) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() { private val galleryID: Int) : 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
@@ -71,11 +73,12 @@ 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 = RecyclerView.LayoutParams.MATCH_PARENT
@@ -98,9 +101,8 @@ class ReaderAdapter(private val glide: RequestManager,
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 ->
@@ -112,10 +114,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 {
@@ -128,54 +127,49 @@ class ReaderAdapter(private val glide: RequestManager,
.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() .fitCenter()
.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
glide.clear(holder.view.image) glide.clear(holder.view.image)
if (progress?.isNaN() == true) { holder.view.reader_item_progressbar.progress =
FirebaseCrashlytics.getInstance().recordException( if (progress?.isInfinite() == true)
DownloadWorker.getInstance(holder.view.context).exception[galleryID]?.get(position)!! 100
) else
progress?.roundToInt() ?: 0
glide holder.view.image.setImageDrawable(null)
.load(R.drawable.image_broken_variant)
.into(holder.view.image)
Snackbar.make(holder.view.reader_layout, R.string.reader_error_retry, Snackbar.LENGTH_SHORT).apply {
setAction(android.R.string.no) { }
setAction(android.R.string.yes) {
downloadWorker!!.cancel(galleryID)
downloadWorker!!.queue.add(galleryID)
}
}.show()
return
} else {
holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true)
100
else
progress?.roundToInt() ?: 0
holder.view.image.setImageDrawable(null)
}
timer.schedule(1000) { timer.schedule(1000) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {

View File

@@ -22,9 +22,10 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.RequestManager import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.engine.DiskCacheStrategy
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
class ThumbnailAdapter(private val glide: RequestManager, private val thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() { class ThumbnailAdapter(private val glide: RequestManager, var thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() {
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view) class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
@@ -35,6 +36,7 @@ class ThumbnailAdapter(private val glide: RequestManager, private val thumbnails
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
glide glide
.load(thumbnails[position]) .load(thumbnails[position])
.diskCacheStrategy(DiskCacheStrategy.NONE)
.apply { .apply {
if (BuildConfig.CENSOR) if (BuildConfig.CENSOR)
override(5, 8) override(5, 8)

View File

@@ -0,0 +1,50 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.RequestManager
import kotlin.math.min
class ThumbnailPageAdapter(private val glide: RequestManager, private val thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailPageAdapter.ViewHolder>() {
class ViewHolder(val view: RecyclerView) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(RecyclerView(parent.context).apply {
layoutManager = GridLayoutManager(parent.context, 3)
adapter = ThumbnailAdapter(glide, listOf())
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
})
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
(holder.view.adapter as ThumbnailAdapter).apply {
thumbnails = this@ThumbnailPageAdapter.thumbnails.slice(9*position until min(9*position+9, this@ThumbnailPageAdapter.thumbnails.size))
notifyDataSetChanged()
holder.view.layoutManager?.scrollToPosition(itemCount-1)
}
}
override fun getItemCount() = if (thumbnails.isEmpty()) 0 else thumbnails.size/9 + if (thumbnails.size%9 != 0) 1 else 0
}

View File

@@ -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
} }
} }
} }

View File

@@ -0,0 +1,410 @@
/*
* 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 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 xyz.quaver.pupil.util.startForegroundServiceCompat
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.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) {
context.startForegroundServiceCompat(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 {
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)
}
}

View File

@@ -67,7 +67,7 @@ data class Tag(val area: String?, val tag: String, val isNegative: Boolean = fal
} }
} }
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 +77,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 +92,25 @@ 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() }
} }
} }

View File

@@ -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,6 +37,7 @@ 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
class LockActivity : AppCompatActivity() { class LockActivity : AppCompatActivity() {
@@ -195,7 +195,7 @@ class LockActivity : AppCompatActivity() {
} }
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 {

View File

@@ -26,6 +26,7 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.* import android.text.*
import android.text.style.AlignmentSpan import android.text.style.AlignmentSpan
import android.util.TypedValue
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@@ -38,7 +39,6 @@ import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.preference.PreferenceManager
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.FloatingSearchView import com.arlib.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.FloatingSearchViewDayNight import com.arlib.floatingsearchview.FloatingSearchViewDayNight
@@ -46,25 +46,29 @@ import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView import com.arlib.floatingsearchview.util.view.SearchInputView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics import 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 kotlinx.serialization.decodeFromString
import xyz.quaver.hitomi.GalleryBlock import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
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.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.types.TagSuggestion import xyz.quaver.pupil.types.TagSuggestion
import xyz.quaver.pupil.types.Tags import xyz.quaver.pupil.types.Tags
import xyz.quaver.pupil.ui.dialog.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.io.File
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -81,13 +85,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) {
@@ -97,20 +101,16 @@ 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)
@@ -130,39 +130,36 @@ class MainActivity : AppCompatActivity() {
} }
if (lockManager.isNotEmpty()) if (lockManager.isNotEmpty())
startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK) startActivityForResult(Intent(this, LockActivity::class.java), R.id.request_lock.normalizeID())
val preference = PreferenceManager.getDefaultSharedPreferences(this) if (intent.action == Intent.ACTION_VIEW) {
intent.dataString?.let { url ->
if (Locale.getDefault().language == "ko") { restore(favorites, url,
if (!preference.getBoolean("https_block_alert", false)) { onFailure = {
android.app.AlertDialog.Builder(this).apply { Snackbar.make(this.main_recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
setTitle(R.string.https_block_alert_title) }, onSuccess = {
setMessage(R.string.https_block_alert) Snackbar.make(this.main_recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
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,9 +177,7 @@ class MainActivity : AppCompatActivity() {
} }
override fun onResume() { override fun onResume() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this) if (Preferences["security_mode"])
if (preferences.getBoolean("security_mode", false))
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE) WindowManager.LayoutParams.FLAG_SECURE)
@@ -193,8 +188,7 @@ class MainActivity : AppCompatActivity() {
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val 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) {
@@ -231,9 +225,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()
@@ -241,10 +234,11 @@ class MainActivity : AppCompatActivity() {
loadBlocks() loadBlocks()
} }
} }
REQUEST_LOCK -> { R.id.request_lock.normalizeID() -> {
if (resultCode != Activity.RESULT_OK) if (resultCode != Activity.RESULT_OK)
finish() finish()
} }
else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }
@@ -277,6 +271,7 @@ class MainActivity : AppCompatActivity() {
clearGalleries() clearGalleries()
currentPage = 0 currentPage = 0
query = "" query = ""
queryStack.clear()
mode = Mode.SEARCH mode = Mode.SEARCH
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
@@ -286,6 +281,7 @@ class MainActivity : AppCompatActivity() {
clearGalleries() clearGalleries()
currentPage = 0 currentPage = 0
query = "" query = ""
queryStack.clear()
mode = Mode.HISTORY mode = Mode.HISTORY
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
@@ -295,6 +291,7 @@ class MainActivity : AppCompatActivity() {
clearGalleries() clearGalleries()
currentPage = 0 currentPage = 0
query = "" query = ""
queryStack.clear()
mode = Mode.DOWNLOAD mode = Mode.DOWNLOAD
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
@@ -304,6 +301,7 @@ class MainActivity : AppCompatActivity() {
clearGalleries() clearGalleries()
currentPage = 0 currentPage = 0
query = "" query = ""
queryStack.clear()
mode = Mode.FAVORITE mode = Mode.FAVORITE
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
@@ -332,15 +330,14 @@ class MainActivity : AppCompatActivity() {
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 {
@@ -376,13 +373,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()
} }
} }
} }
@@ -406,8 +414,6 @@ class MainActivity : AppCompatActivity() {
} }
startActivity(intent) startActivity(intent)
histories.add(galleryID)
} }
}.show() }.show()
} }
@@ -435,17 +441,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 (Cache(context).isDownloading(galleryID)) //download in progress if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
worker.cancel(galleryID) DownloadService.cancel(this@MainActivity, galleryID)
}
else { else {
Cache(context).setDownloading(galleryID, true) DownloadManager.getInstance(context).addDownloadFolder(galleryID)
DownloadService.download(this@MainActivity, galleryID)
worker.queue.add(galleryID)
} }
} }
@@ -453,48 +458,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,
@@ -517,11 +515,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 -> {
@@ -746,11 +744,10 @@ class MainActivity : AppCompatActivity() {
with(main_searchview as FloatingSearchViewDayNight) { with(main_searchview as FloatingSearchViewDayNight) {
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json") val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
val serializer = Tag.serializer().list
if (!favoritesFile.exists()) { if (!favoritesFile.exists()) {
favoritesFile.createNewFile() favoritesFile.createNewFile()
favoritesFile.writeText(json.stringify(serializer, Tags(listOf()))) favoritesFile.writeText("[]")
} }
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener { setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
@@ -765,7 +762,7 @@ class MainActivity : AppCompatActivity() {
setOnMenuItemClickListener { setOnMenuItemClickListener {
when(it.itemId) { when(it.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS) R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), R.id.request_settings.normalizeID())
R.id.main_menu_thin -> { R.id.main_menu_thin -> {
main_recyclerview.apply { main_recyclerview.apply {
(adapter as GalleryBlockAdapter).apply { (adapter as GalleryBlockAdapter).apply {
@@ -812,7 +809,7 @@ class MainActivity : AppCompatActivity() {
clearSuggestions() clearSuggestions()
if (query.isEmpty() or query.endsWith(' ')) { if (query.isEmpty() or query.endsWith(' ')) {
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map { swapSuggestions(Tags(Json.decodeFromString(favoritesFile.readText())).map {
TagSuggestion(it.tag, -1, "", it.area ?: "tag") TagSuggestion(it.tag, -1, "", it.area ?: "tag")
}) })
@@ -822,11 +819,13 @@ class MainActivity : AppCompatActivity() {
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) Tags(Json.decodeFromString(favoritesFile.readText())).contains(tag)
}.reversed().forEach { }.reversed().forEach {
suggestions.remove(it) suggestions.remove(it)
suggestions.add(0, it) suggestions.add(0, it)
@@ -843,6 +842,9 @@ class MainActivity : AppCompatActivity() {
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}" val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
val color = TypedValue()
theme.resolveAttribute(R.attr.colorControlNormal, color, true)
leftIcon.setImageDrawable( leftIcon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
resources, resources,
@@ -856,12 +858,12 @@ class MainActivity : AppCompatActivity() {
"artist" -> R.drawable.brush "artist" -> R.drawable.brush
else -> R.drawable.tag else -> R.drawable.tag
}, },
null) context.theme)
) )
with(suggestionView.findViewById<ImageView>(R.id.right_icon)) { with(suggestionView.findViewById<ImageView>(R.id.right_icon)) {
if (Tags(json.parse(serializer, favoritesFile.readText())).contains(tag)) if (Tags(Json.decodeFromString(favoritesFile.readText())).contains(tag))
setImageResource(R.drawable.ic_star_filled) setImageResource(R.drawable.ic_star_filled)
else else
setImageResource(R.drawable.ic_star_empty) setImageResource(R.drawable.ic_star_empty)
@@ -872,7 +874,7 @@ class MainActivity : AppCompatActivity() {
isClickable = true isClickable = true
setOnClickListener { setOnClickListener {
val favorites = Tags(json.parse(serializer, favoritesFile.readText())) val favorites = Tags(Json.decodeFromString(favoritesFile.readText()))
if (favorites.contains(tag)) { if (favorites.contains(tag)) {
setImageResource(R.drawable.ic_star_empty) setImageResource(R.drawable.ic_star_empty)
@@ -887,7 +889,7 @@ class MainActivity : AppCompatActivity() {
favorites.add(tag) favorites.add(tag)
} }
favoritesFile.writeText(json.stringify(serializer, favorites)) favoritesFile.writeText(Json.encodeToString(favorites.tags))
} }
} }
@@ -927,7 +929,7 @@ class MainActivity : AppCompatActivity() {
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(Tags(Json.decodeFromString(favoritesFile.readText())).map {
TagSuggestion(it.tag, -1, "", it.area ?: "tag") TagSuggestion(it.tag, -1, "", it.area ?: "tag")
}) })
} }
@@ -970,8 +972,26 @@ 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 != 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
@@ -998,29 +1018,23 @@ 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 -> {
@@ -1033,7 +1047,7 @@ 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 -> {
@@ -1049,8 +1063,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 {
@@ -1075,16 +1088,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)
} }
} }

View File

@@ -18,10 +18,13 @@
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
@@ -33,7 +36,6 @@ import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide
import com.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 kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
@@ -43,14 +45,17 @@ 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 : AppCompatActivity() {
@@ -70,42 +75,48 @@ 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( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
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) {
onBackPressed() onBackPressed()
return return
} }
if (Preferences["cache_disable"]) {
initView()
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 +126,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 +144,8 @@ class ReaderActivity : AppCompatActivity() {
} }
} else } else
initDownloader() initDownloader()
initView()
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
@@ -158,9 +172,7 @@ class ReaderActivity : AppCompatActivity() {
} }
override fun onResume() { override fun onResume() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this) if (Preferences["security_mode"])
if (preferences.getBoolean("security_mode", false))
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE) WindowManager.LayoutParams.FLAG_SECURE)
@@ -184,14 +196,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 +238,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,31 +278,33 @@ class ReaderActivity : AppCompatActivity() {
} }
private fun initDownloader() { private fun initDownloader() {
val worker = DownloadWorker.getInstance(this).apply { DownloadService.download(this, galleryID, true)
queue.add(galleryID) bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
}
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.isFinite() } ?: 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()
@@ -305,7 +322,7 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) { //Download finished if (downloader.isCompleted(galleryID)) { //Download finished
reader_download_progressbar.visibility = View.GONE reader_download_progressbar.visibility = View.GONE
animateDownloadFAB(false) animateDownloadFAB(false)
@@ -316,7 +333,7 @@ class ReaderActivity : AppCompatActivity() {
private fun initView() { private fun initView() {
with(reader_recyclerview) { with(reader_recyclerview) {
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID).apply { adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
onItemClickListener = { onItemClickListener = {
if (isScroll) { if (isScroll) {
isScroll = false isScroll = false
@@ -351,18 +368,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)
} }
} }
@@ -372,9 +390,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)
} }
} }
} }
@@ -414,7 +454,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)
@@ -427,8 +467,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.isFinite() } == 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)

View File

@@ -22,23 +22,19 @@ 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 : AppCompatActivity() {
@@ -59,9 +55,7 @@ class SettingsActivity : AppCompatActivity() {
} }
override fun onResume() { override fun onResume() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this) if (Preferences["security_mode"])
if (preferences.getBoolean("security_mode", false))
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE) WindowManager.LayoutParams.FLAG_SECURE)
@@ -70,8 +64,8 @@ class SettingsActivity : AppCompatActivity() {
super.onResume() super.onResume()
} }
override fun onOptionsItemSelected(item: MenuItem?): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item?.itemId) { when (item.itemId) {
android.R.id.home -> onBackPressed() android.R.id.home -> onBackPressed()
} }
@@ -80,7 +74,7 @@ class SettingsActivity : AppCompatActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) { when(requestCode) {
REQUEST_LOCK -> { R.id.request_lock.normalizeID() -> {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
@@ -89,7 +83,7 @@ class SettingsActivity : AppCompatActivity() {
.commitAllowingStateLoss() .commitAllowingStateLoss()
} }
} }
REQUEST_RESTORE -> { R.id.request_restore.normalizeID() -> {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
val uri = data?.data ?: return val uri = data?.data ?: return
@@ -100,10 +94,10 @@ class SettingsActivity : AppCompatActivity() {
inputStream.readBytes().toString(Charset.defaultCharset()) inputStream.readBytes().toString(Charset.defaultCharset())
} }
(application as Pupil).favorites.addAll(json.parse(Int.serializer().list, str).also { favorites.addAll(Json.decodeFromString<List<Int>>(str).also {
Snackbar.make( Snackbar.make(
window.decorView, window.decorView,
getString(R.string.settings_restore_successful, it.size), getString(R.string.settings_restore_success, it.size),
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
).show() ).show()
}) })
@@ -116,83 +110,6 @@ class SettingsActivity : AppCompatActivity() {
} }
} }
} }
REQUEST_DOWNLOAD_FOLDER -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
val takeFlags: Int =
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
contentResolver.takePersistableUriPermission(uri, takeFlags)
val file = uri.toFile(this)
if (file?.canWrite() != true)
Snackbar.make(
settings,
R.string.settings_dl_location_not_writable,
Snackbar.LENGTH_LONG
).show()
else
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", file.canonicalPath)
.apply()
}
}
}
REQUEST_DOWNLOAD_FOLDER_OLD -> {
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canWrite())
Snackbar.make(
settings,
R.string.settings_dl_location_not_writable,
Snackbar.LENGTH_LONG
).show()
else
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", File(directory).canonicalPath)
.apply()
}
}
REQUEST_IMPORT_OLD_GALLERIES -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
val takeFlags: Int =
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
contentResolver.takePersistableUriPermission(uri, takeFlags)
val file = uri.toFile(this)
if (file?.canRead() != true)
Snackbar.make(
settings,
resources.getText(R.string.import_old_galleries_folder_not_readable),
Snackbar.LENGTH_LONG
).show()
else
importOldGalleries(this, file)
}
}
}
REQUEST_IMPORT_OLD_GALLERIES_OLD -> {
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canRead())
Snackbar.make(
settings,
resources.getText(R.string.import_old_galleries_folder_not_readable),
Snackbar.LENGTH_LONG
).show()
else {
importOldGalleries(this, File(directory))
}
}
}
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }
@@ -200,13 +117,13 @@ class SettingsActivity : AppCompatActivity() {
@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())
} }
} }
} }

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)?.canonicalPath ?: ""
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)
}
}
}

View File

@@ -26,41 +26,35 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout.LayoutParams import android.widget.LinearLayout.LayoutParams
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import 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.gallery_details.view.* import kotlinx.android.synthetic.main.dialog_gallery_details.view.*
import kotlinx.android.synthetic.main.dialog_gallery_dotindicator.view.*
import kotlinx.android.synthetic.main.item_gallery_details.view.* import kotlinx.android.synthetic.main.item_gallery_details.view.*
import kotlinx.coroutines.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.ThumbnailAdapter 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?) {
@@ -80,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)
} }
} }
@@ -129,8 +123,8 @@ 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.gallery_details, gallery_contents, false).apply { inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_details) gallery_details.setText(R.string.gallery_details)
listOf( listOf(
@@ -164,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)
@@ -206,15 +179,21 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
private fun addThumbnails(gallery: Gallery) { private fun addThumbnails(gallery: Gallery) {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply { inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_thumbnails) gallery_details.setText(R.string.gallery_thumbnails)
RecyclerView(context).apply { val pager = ViewPager2(context).apply {
layoutManager = GridLayoutManager(context, 3) adapter = ThumbnailPageAdapter(glide, gallery.thumbnails)
adapter = ThumbnailAdapter(glide, gallery.thumbnails)
}.let {
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
} }
gallery_details_contents.addView(
pager,
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
)
LayoutInflater.from(context).inflate(R.layout.dialog_gallery_dotindicator, gallery_details_contents)
gallery_dotindicator.setViewPager2(pager)
}.let { }.let {
gallery_contents.addView(it) gallery_contents.addView(it)
} }
@@ -222,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 ->
@@ -232,38 +211,25 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
} }
} }
CoroutineScope(Dispatchers.Main).launch { inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery.related.forEachIndexed { i, galleryID ->
async(Dispatchers.IO) {
Cache(context).getGalleryBlock(galleryID)
}.let {
val galleryBlock = it.await() ?: return@let
galleries.add(galleryBlock)
adapter.notifyItemInserted(i)
}
}
}
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_related) gallery_details.setText(R.string.gallery_related)
RecyclerView(context).apply { RecyclerView(context).apply {
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) }
@@ -272,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()
}
}
} }
} }

View File

@@ -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()
} }
} }
} }

View File

@@ -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()

View File

@@ -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 {

View File

@@ -0,0 +1,101 @@
/*
* 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) {
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(favorites, 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
}
}
}

View File

@@ -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()
}
}

View File

@@ -18,34 +18,22 @@
package xyz.quaver.pupil.ui.fragment package xyz.quaver.pupil.ui.fragment
import android.Manifest
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.material.snackbar.Snackbar import xyz.quaver.io.FileX
import net.rdrei.android.dirchooser.DirectoryChooserActivity import xyz.quaver.io.util.getChild
import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.pupil.Pupil
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.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
class SettingsFragment : class SettingsFragment :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
@@ -53,8 +41,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 +59,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 +67,20 @@ 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(requireActivity().supportFragmentManager, "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)
activity?.startActivityForResult(intent, REQUEST_LOCK) activity?.startActivityForResult(intent, R.id.request_lock.normalizeID())
} }
"mirrors" -> { "mirrors" -> {
MirrorDialog(requireContext()) MirrorDialog(requireContext())
@@ -153,54 +90,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 +108,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 +141,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 +156,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 +175,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 +185,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,27 +232,18 @@ 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
} }
} }

View File

@@ -0,0 +1,66 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.view
import android.content.Context
import 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
class TagChip(context: Context, val tag: Tag) : Chip(context) {
private val languages = context.resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
Pair(split[0], split[1])
}
}.toMap()
init {
val tag = tag.let {
when {
it.area != null -> it
else -> Tag("tag", tag.tag)
}
}
chipIcon = when(tag.area) {
"male" -> {
setChipBackgroundColorResource(R.color.material_blue_700)
setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.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)
}
}

View File

@@ -18,65 +18,65 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import kotlinx.serialization.KSerializer import kotlinx.serialization.decodeFromString
import kotlinx.serialization.builtins.list import kotlinx.serialization.encodeToString
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.json.Json
import java.io.File import java.io.File
class Histories(private val file: File) : ArrayList<Int>() { class GalleryList(private val file: File, private val list: MutableSet<Int> = mutableSetOf()) : MutableSet<Int> by list {
val serializer: KSerializer<List<Int>> = Int.serializer().list
init { init {
if (!file.exists()) if (!file.exists()) {
file.parentFile?.mkdirs() file.parentFile?.mkdirs()
try {
load()
} catch (e: Exception) {
save() save()
} }
load()
} }
fun load() : Histories { fun load() {
return apply { synchronized(this) {
super.clear() list.clear()
addAll( kotlin.runCatching {
json.parse( Json.decodeFromString<List<Int>>(file.bufferedReader().use { it.readText() })
serializer, }.onSuccess {
file.bufferedReader().use { it.readText() } list.addAll(it)
) }
)
} }
} }
fun save() { fun save() {
file.writeText(json.stringify(serializer, this)) synchronized(this) {
file.writeText(Json.encodeToString(list.toList()))
}
} }
override fun add(element: Int): Boolean { override fun add(element: Int): Boolean {
load() load()
if (contains(element)) return list.add(element).also {
super.remove(element) save()
}
}
super.add(0, element) override fun addAll(elements: Collection<Int>): Boolean {
load()
save() return list.addAll(elements).also {
save()
return true }
} }
override fun remove(element: Int): Boolean { override fun remove(element: Int): Boolean {
load() load()
val retval = super.remove(element)
save()
return retval return list.remove(element).also {
save()
}
} }
override fun clear() { override fun clear() {
super.clear() list.clear()
save() save()
} }
} }

View File

@@ -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);
}
}

View 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()
}
}

View 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()
}
}

View File

@@ -21,28 +21,28 @@ package xyz.quaver.pupil.util.download
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.Base64 import android.util.Base64
import android.util.Log
import android.util.SparseArray 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 {
@@ -50,26 +50,12 @@ 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
// Download -> Cache // Download -> Cache
fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also { fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also {
if (!it.exists() && !preference.getBoolean("cache_disable", false)) if (!it.exists())
it.mkdirs() it.mkdirs()
} }
@@ -80,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()
@@ -97,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? {
@@ -106,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
} }
@@ -135,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) {
@@ -150,7 +137,7 @@ class Cache(context: Context) : ContextWrapper(context) {
} }
galleryBlock galleryBlock
}.await() ?: return null } ?: return null
} }
else else
metadata.galleryBlock metadata.galleryBlock
@@ -176,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
} }
@@ -195,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) {
@@ -289,17 +274,17 @@ class Cache(context: Context) : ContextWrapper(context) {
if (download.isParentOf(cache)) if (download.isParentOf(cache))
return@launch return@launch
Log.i("PUPILD", "MOVING ${cache.canonicalPath} --> ${download.canonicalPath}") FirebaseCrashlytics.getInstance().log("MOVING ${cache.canonicalPath} --> ${download.canonicalPath}")
cache.copyRecursively(download, true) { file, err -> cache.copyRecursively(download, true) { file, err ->
Log.i("PUPILD", "MOVING ERROR ${file.canonicalPath} ${err.message}") FirebaseCrashlytics.getInstance().log("MOVING ERROR ${file.canonicalPath} ${err.message}")
OnErrorAction.SKIP OnErrorAction.SKIP
} }
Log.i("PUPILD", "MOVED ${cache.canonicalPath}") FirebaseCrashlytics.getInstance().log("MOVED ${cache.canonicalPath}")
Log.i("PUPILD", "DELETING ${cache.canonicalPath}") FirebaseCrashlytics.getInstance().log("DELETING ${cache.canonicalPath}")
cache.deleteRecursively() cache.deleteRecursively()
Log.i("PUPILD", "DELETED ${cache.canonicalPath}") FirebaseCrashlytics.getInstance().log("DELETED ${cache.canonicalPath}")
} }
} }

View File

@@ -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
@@ -128,54 +145,13 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
* SECONDARY VALUE * SECONDARY VALUE
* 0 <= value < 100 -> Download in progress * 0 <= value < 100 -> Download in progress
* Float.POSITIVE_INFINITY -> Download completed * Float.POSITIVE_INFINITY -> Download completed
* Float.NaN -> Exception
*/ */
val progress = SparseArray<MutableList<Float>?>() val progress = SparseArray<MutableList<Float>?>()
/* val notification = SparseArray<NotificationCompat.Builder?>()
* KEY
* primary galleryID
* secondary index
* PRIMARY VALUE
* MutableList -> Download in progress / Loading
* null -> Gallery doesn't exist
* SECONDARY VALUE
* Throwable -> Exception
* null -> Download in progress / Loading
*/
val exception = SparseArray<MutableList<Throwable?>?>()
val notification = SparseArray<NotificationCompat.Builder>()
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()
@@ -192,9 +168,13 @@ 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()
exception.clear()
notification.clear() notification.clear()
notificationManager.cancelAll() notificationManager.cancelAll()
} }
@@ -208,17 +188,18 @@ 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)
exception.remove(galleryID)
notification.remove(galleryID) notification.remove(galleryID)
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID)
if (progress.indexOfKey(galleryID) >= 0)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
} }
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true fun isCompleted(galleryID: Int) = progress[galleryID]?.all { it.isInfinite() } == true
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) { private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
val lowQuality = preferences.getBoolean("low_quality", false) val lowQuality = preferences.getBoolean("low_quality", false)
@@ -237,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
@@ -248,8 +227,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
}.build() }.build()
client.newCall(request).enqueue(callback) client.newCall(request).enqueue(callback)
Log.i("PUPILD", "DOWNLOADING ($galleryID, $index) from ${request.url()}")
} }
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch { private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
@@ -258,7 +235,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
//gallery doesn't exist //gallery doesn't exist
if (reader == null) { if (reader == null) {
progress.put(galleryID, null) progress.put(galleryID, null)
exception.put(galleryID, null)
Cache(this@DownloadWorker).setDownloading(galleryID, false) Cache(this@DownloadWorker).setDownloading(galleryID, false)
return@launch return@launch
@@ -272,12 +248,11 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
else else
0F 0F
}.toMutableList()) }.toMutableList())
exception.put(galleryID, reader.galleryInfo.files.map { null }.toMutableList())
if (notification[galleryID] == null) if (notification[galleryID] == null)
initNotification(galleryID) initNotification(galleryID)
notification[galleryID].setContentTitle(reader.galleryInfo.title) notification[galleryID]?.setContentTitle(reader.galleryInfo.title)
notify(galleryID) notify(galleryID)
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
@@ -294,35 +269,19 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
for (i in reader.galleryInfo.files.indices) { for (i in reader.galleryInfo.files.indices) {
val callback = object : Callback { val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
Log.i("PUPILD", "FAIL ${call.request().tag()} (${e.message})") if (e.message?.contains("cancel", true) != false)
if (e.message?.contains("cancel", true) != true) return
FirebaseCrashlytics.getInstance().recordException(e)
progress[galleryID]?.set(i, Float.NaN) cancel(galleryID)
exception[galleryID]?.set(i, e) queue.add(galleryID)
notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
}
}
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
Log.i("PUPILD", "OK ${call.request().tag()}")
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)
@@ -338,47 +297,29 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
} }
} }
} }
Log.i("PUPILD", "SUCCESS ${call.request().tag()}")
} catch (e: Exception) { } catch (e: Exception) {
FirebaseCrashlytics.getInstance().apply {
progress[galleryID]?.set(i, Float.NaN) log("FAIL ON OK ${call.request().tag()} (${e.message})")
exception[galleryID]?.set(i, e) setCustomKey("POS", "FAIL ON OK")
recordException(e)
notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
}
} }
File(Cache(this@DownloadWorker).getCachedGallery(galleryID), "%05d.$ext".format(i)).delete() File(Cache(this@DownloadWorker).getCachedGallery(galleryID), "%05d.$ext".format(i)).delete()
Log.i("PUPILD", "FAIL ON OK ${call.request().tag()} (${e.message})") cancel(galleryID)
queue.add(galleryID)
} }
} }
} }
if (progress[galleryID]?.get(i)?.isFinite() == true) { if (progress[galleryID]?.get(i)?.isFinite() == true)
queueDownload(galleryID, reader, i, callback) queueDownload(galleryID, reader, i, callback)
Log.i("PUPILD", "$galleryID QUEUED $i")
} else {
Log.i("PUPILD", "$galleryID SKIPPED $i (${progress[galleryID]?.get(i)})")
}
} }
} }
private fun notify(galleryID: Int) { private fun notify(galleryID: Int) {
val max = progress[galleryID]?.size ?: 0 val max = progress[galleryID]?.size ?: 0
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0 val progress = progress[galleryID]?.count { it.isInfinite() } ?: 0
Log.i("PUPILD", "NOTIFY $galleryID ${isCompleted(galleryID)} $progress/$max")
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
notification[galleryID] notification[galleryID]
@@ -394,7 +335,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
?.setContentText("$progress/$max") ?.setContentText("$progress/$max")
if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null) if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null)
notificationManager.notify(galleryID, notification[galleryID].build()) notification[galleryID]?.let { notificationManager.notify(galleryID, it.build()) }
else else
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID)
} }
@@ -432,9 +373,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
initNotification(galleryID) initNotification(galleryID)
if (Cache(this@DownloadWorker).isDownloading(galleryID)) if (Cache(this@DownloadWorker).isDownloading(galleryID))
notificationManager.notify(galleryID, notification[galleryID].build()) notification[galleryID]?.let { notificationManager.notify(galleryID, it.build()) }
Log.i("PUPILD", "QUEUED $galleryID")
worker.put(galleryID, download(galleryID)) worker.put(galleryID, download(galleryID))
queue.poll() queue.poll()

View File

@@ -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?,

View File

@@ -0,0 +1,229 @@
/*
* 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
@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().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()
}
}

View File

@@ -0,0 +1,135 @@
/*
* 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()
file.writeText("{}")
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) {
if (downloadFolderMap.containsKey(galleryID))
return
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) {
if (!downloadFolderMap.containsKey(galleryID))
return
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))
}
}
}
}

View File

@@ -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)

View File

@@ -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) {

View File

@@ -19,6 +19,17 @@
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 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 +44,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 +61,77 @@ 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)
}
}
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
format.let {
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
}
}
fun Context.startForegroundServiceCompat(service: Intent) {
if (Build.VERSION.SDK_INT >= 26)
startForegroundService(service)
else
startService(service)
}
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

View File

@@ -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)
}

View File

@@ -21,60 +21,69 @@ 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.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 +92,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 +107,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 +145,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 +158,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 +178,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 +195,138 @@ fun checkUpdate(context: Context, force: Boolean = false) {
} }
} }
var cancelImport = false fun restore(favorites: GalleryList, 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.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 as? FileX)?.isDirectory == true && !downloadFolderMap.values.contains(folder.name)
}
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 {
if (folder !is FileX) return@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
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 B

View File

@@ -4,5 +4,5 @@
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#fff" 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" /> <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> </vector>

View File

@@ -4,5 +4,5 @@
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#fff" 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" /> <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> </vector>

View File

@@ -4,5 +4,5 @@
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#fff" 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" /> <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> </vector>

View File

@@ -4,5 +4,5 @@
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#fff" 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" /> <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> </vector>

View File

@@ -0,0 +1,8 @@
<!-- 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 12,15C15.86,15 19,11.87 19,8C19,4.14 15.86,1 12,1M12,3.15C14.67,3.15 16.85,5.32 16.85,8C16.85,10.68 14.67,12.85 12,12.85A4.85,4.85 0 0,1 7.15,8A4.85,4.85 0 0,1 12,3.15M11,5V8.69L14.19,10.53L14.94,9.23L12.5,7.82V5M15,16V19H3V21H15V24L19,20M19,20V24H21V16H19" />
</vector>

View File

@@ -0,0 +1,8 @@
<!-- 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 12,15C15.86,15 19,11.87 19,8C19,4.14 15.86,1 12,1M12,3.15C14.67,3.15 16.85,5.32 16.85,8C16.85,10.68 14.67,12.85 12,12.85A4.85,4.85 0 0,1 7.15,8A4.85,4.85 0 0,1 12,3.15M11,5V8.69L14.19,10.53L14.94,9.23L12.5,7.82V5M4,16V24H6V21H18V24L22,20L18,16V19H6V16" />
</vector>

View File

@@ -4,5 +4,5 @@
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#fff" 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" /> <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> </vector>

View File

@@ -0,0 +1,8 @@
<!-- 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 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>

View File

@@ -4,5 +4,5 @@
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#fff" 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" /> <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> </vector>

View File

@@ -0,0 +1,8 @@
<!-- 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,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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tintMode="multiply">
<path
android:name="path_1"
android:pathData="M 0 12 L 24 12"
android:fillColor="#000"
android:strokeColor="#80d8ff"
android:strokeWidth="24"/>
<path
android:name="path"
android:pathData="M 0 12 L 24 12"
android:fillColor="#000"
android:strokeColor="#0091ea"
android:strokeWidth="24"/>
</vector>

View File

@@ -0,0 +1,38 @@
<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">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tintMode="multiply">
<path
android:name="path_1"
android:pathData="M 0 12 L 24 12"
android:fillColor="#000"
android:strokeColor="#80d8ff"
android:strokeWidth="24"/>
<path
android:name="path"
android:pathData="M 0 12 L 24 12"
android:fillColor="#000"
android:strokeColor="#0091ea"
android:strokeWidth="24"/>
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="trimPathEnd"
android:duration="1000"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -1,8 +0,0 @@
<!-- drawable/sort_variant.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="M3,13H15V11H3M3,6V8H21V6M3,18H9V16H3V18Z" />
</vector>

View File

@@ -4,5 +4,5 @@
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.44 21.77,11.94 21.41,11.58Z" /> <path android:fillColor="?attr/colorControlNormal" android:pathData="M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.44 21.77,11.94 21.41,11.58Z" />
</vector> </vector>

View File

@@ -4,5 +4,5 @@
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M12.87,15.07L10.33,12.56L10.36,12.53C12.1,10.59 13.34,8.36 14.07,6H17V4H10V2H8V4H1V6H12.17C11.5,7.92 10.44,9.75 9,11.35C8.07,10.32 7.3,9.19 6.69,8H4.69C5.42,9.63 6.42,11.17 7.67,12.56L2.58,17.58L4,19L9,14L12.11,17.11L12.87,15.07M18.5,10H16.5L12,22H14L15.12,19H19.87L21,22H23L18.5,10M15.88,17L17.5,12.67L19.12,17H15.88Z" /> <path android:fillColor="?attr/colorControlNormal" android:pathData="M12.87,15.07L10.33,12.56L10.36,12.53C12.1,10.59 13.34,8.36 14.07,6H17V4H10V2H8V4H1V6H12.17C11.5,7.92 10.44,9.75 9,11.35C8.07,10.32 7.3,9.19 6.69,8H4.69C5.42,9.63 6.42,11.17 7.67,12.56L2.58,17.58L4,19L9,14L12.11,17.11L12.87,15.07M18.5,10H16.5L12,22H14L15.12,19H19.87L21,22H23L18.5,10M15.88,17L17.5,12.67L19.12,17H15.88Z" />
</vector> </vector>

View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:visibility="invisible"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent">
<View
android:layout_width="match_parent"
android:layout_height="64dp"
android:visibility="invisible"
android:background="@color/transparent"
app:layout_scrollFlags="scroll|enterAlways"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.ContentLoadingProgressBar
style="?android:attr/progressBarStyle"
android:id="@+id/main_progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"/>
<TextView
android:id="@+id/main_noresult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/main_no_result"
android:visibility="invisible"/>
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:layout_width="match_parent"
android:layout_height="match_parent"
app:handleHeight="100dp"
app:addLastItemPadding="true"
app:popupDrawable="@color/transparent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/main_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="64dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/main_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_cancel"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_jump"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_jump_title"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_random"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_random"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_open_gallery_by_id"
app:fab_size="mini"/>
</com.github.clans.fab.FloatingActionMenu>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.arlib.floatingsearchview.FloatingSearchViewDayNight
android:id="@+id/main_searchview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:floatingSearch_backgroundColor="?android:attr/colorBackgroundFloating"
app:floatingSearch_leftActionColor="?attr/colorControlNormal"
app:floatingSearch_menuItemIconColor="?attr/colorControlNormal"
app:floatingSearch_actionMenuOverflowColor="?attr/colorControlNormal"
app:floatingSearch_clearBtnColor="?attr/colorControlNormal"
app:floatingSearch_viewTextColor="?android:attr/textColorPrimary"
app:floatingSearch_suggestionRightIconColor="@color/material_orange_500"
app:floatingSearch_searchBarMarginLeft="8dp"
app:floatingSearch_searchBarMarginRight="8dp"
app:floatingSearch_searchBarMarginTop="8dp"
app:floatingSearch_searchHint="@string/search_hint"
app:floatingSearch_suggestionsListAnimDuration="250"
app:floatingSearch_showSearchKey="true"
app:floatingSearch_leftActionMode="showHamburger"
app:floatingSearch_menu="@menu/main"
app:floatingSearch_dismissOnOutsideTouch="true"
app:floatingSearch_close_search_on_keyboard_dismiss="true"
tools:ignore="NewApi" />
</RelativeLayout>

View File

@@ -17,9 +17,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/>.
--> -->
<RelativeLayout <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -65,18 +63,22 @@
android:text="@string/main_no_result" android:text="@string/main_no_result"
android:visibility="invisible"/> android:visibility="invisible"/>
<androidx.recyclerview.widget.RecyclerView <com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:id="@+id/main_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingTop="64dp" app:handleHeight="100dp"
android:clipToPadding="false" app:addLastItemPadding="true"
app:fastScrollEnabled="true" app:popupDrawable="@color/transparent">
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable" <androidx.recyclerview.widget.RecyclerView
app:fastScrollHorizontalTrackDrawable="@drawable/track_drawable" android:id="@+id/main_recyclerview"
app:fastScrollVerticalTrackDrawable="@drawable/track_drawable" android:layout_width="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> android:layout_height="match_parent"
android:paddingTop="64dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
<com.github.clans.fab.FloatingActionMenu <com.github.clans.fab.FloatingActionMenu
android:id="@+id/main_fab" android:id="@+id/main_fab"
@@ -122,12 +124,6 @@
android:id="@+id/main_searchview" android:id="@+id/main_searchview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:floatingSearch_backgroundColor="?android:attr/colorBackgroundFloating"
app:floatingSearch_leftActionColor="?attr/colorControlNormal"
app:floatingSearch_menuItemIconColor="?attr/colorControlNormal"
app:floatingSearch_actionMenuOverflowColor="?attr/colorControlNormal"
app:floatingSearch_clearBtnColor="?attr/colorControlNormal"
app:floatingSearch_viewTextColor="?android:attr/textColorPrimary"
app:floatingSearch_suggestionRightIconColor="@color/material_orange_500" app:floatingSearch_suggestionRightIconColor="@color/material_orange_500"
app:floatingSearch_searchBarMarginLeft="8dp" app:floatingSearch_searchBarMarginLeft="8dp"
app:floatingSearch_searchBarMarginRight="8dp" app:floatingSearch_searchBarMarginRight="8dp"

View File

@@ -26,16 +26,20 @@
android:background="@color/dark_gray" android:background="@color/dark_gray"
tools:context=".ui.ReaderActivity"> tools:context=".ui.ReaderActivity">
<androidx.recyclerview.widget.RecyclerView <com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:id="@+id/reader_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:fastScrollEnabled="true" app:handleHeight="100dp"
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable" app:addLastItemPadding="true"
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable" app:popupDrawable="@color/transparent">
app:fastScrollHorizontalTrackDrawable="@drawable/track_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/track_drawable" <androidx.recyclerview.widget.RecyclerView
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> android:id="@+id/reader_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -82,6 +86,13 @@
app:fab_label="@string/reader_fab_retry" app:fab_label="@string/reader_fab_retry"
app:fab_size="mini"/> app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/reader_fab_auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/reader_fab_auto"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/reader_fab_fullscreen" android:id="@+id/reader_fab_fullscreen"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -108,7 +108,6 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/default_query_dialog_loli_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="16dp">
<TextView
style="?android:textAppearanceLarge"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_download_folder_name"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"/>
<EditText
android:id="@+id/edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/message"
android:layout_margin="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/ok_button"
style="?borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok"
app:layout_constraintTop_toBottomOf="@id/edittext"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -18,6 +18,7 @@
--> -->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/gallery_layout" android:id="@+id/gallery_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -47,7 +48,8 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/gallery_title"/> app:layout_constraintRight_toLeftOf="@id/gallery_title"
tools:ignore="ContentDescription" />
<TextView <TextView
android:id="@+id/gallery_title" android:id="@+id/gallery_title"
@@ -72,7 +74,6 @@
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
<View <View
android:id="@+id/gallery_padding"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/gallery_artist" app:layout_constraintTop_toBottomOf="@id/gallery_artist"

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<com.tbuonomo.viewpagerdotsindicator.DotsIndicator
android:id="@+id/gallery_dotindicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
app:dotsSize="8dp"/>
</RelativeLayout>

View File

@@ -20,6 +20,7 @@
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
@@ -34,8 +35,7 @@
app:show_mode="pull_out"> app:show_mode="pull_out">
<LinearLayout <LinearLayout
android:id="@+id/galleryblock_secondary" android:layout_width="wrap_content"
android:layout_width="wrap_content"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView <TextView
@@ -50,7 +50,8 @@
android:text="@string/main_download" android:text="@string/main_download"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:focusable="true" android:focusable="true"
android:clickable="true"/> android:clickable="true"
tools:ignore="UnusedAttribute" />
<TextView <TextView
android:id="@+id/galleryblock_delete" android:id="@+id/galleryblock_delete"
@@ -64,7 +65,8 @@
android:text="@string/main_delete" android:text="@string/main_delete"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:focusable="true" android:focusable="true"
android:clickable="true"/> android:clickable="true"
tools:ignore="UnusedAttribute" />
</LinearLayout> </LinearLayout>
@@ -75,7 +77,8 @@
android:orientation="vertical" android:orientation="vertical"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:focusable="true" android:focusable="true"
android:clickable="true"> android:clickable="true"
tools:ignore="UnusedAttribute">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -18,6 +18,7 @@
--> -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -44,6 +45,7 @@
app:tint="?attr/colorControlNormal" app:tint="?attr/colorControlNormal"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/> app:layout_constraintBottom_toBottomOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -19,11 +19,14 @@
<ImageView <ImageView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height" android:layout_height="wrap_content"
android:background="@drawable/side_nav_bar" android:src="@drawable/side_nav_bar"
android:adjustViewBounds="true"
android:paddingBottom="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
android:gravity="bottom"/> android:gravity="bottom"
tools:ignore="ContentDescription" />

View File

@@ -23,9 +23,7 @@
<item android:id="@+id/main_menu_thin" <item android:id="@+id/main_menu_thin"
android:title="@string/main_menu_thin"/> android:title="@string/main_menu_thin"/>
<item <item android:title="@string/main_menu_sort">
android:id="@+id/main_menu_sort"
android:title="@string/main_menu_sort">
<menu> <menu>
<group android:checkableBehavior="single"> <group android:checkableBehavior="single">
<item android:id="@+id/main_menu_sort_newest" <item android:id="@+id/main_menu_sort_newest"

Some files were not shown because too many files have changed in this diff Show More