Compare commits
220 Commits
5.0.3-hotf
...
5.2.14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4fbd21731 | ||
|
|
8be64745fc | ||
|
|
b66f376729 | ||
|
|
cc40416e1e | ||
|
|
5073352366 | ||
|
|
9ae12a2c4c | ||
|
|
843b8412a9 | ||
|
|
4f67578371 | ||
|
|
37f2227093 | ||
|
|
1833c0bde5 | ||
|
|
aa3aeca3f2 | ||
|
|
152d4e248f | ||
|
|
7461c8d201 | ||
|
|
0902fdf981 | ||
|
|
0fd2cf4fd7 | ||
|
|
679558106f | ||
|
|
e498efc493 | ||
|
|
74bbc71741 | ||
|
|
502b4890e3 | ||
|
|
dfb60461e4 | ||
|
|
bd6bc418e6 | ||
|
|
a284143ce1 | ||
|
|
1f1c782772 | ||
|
|
5c0f5fe333 | ||
|
|
748e023fde | ||
|
|
30104bacd2 | ||
|
|
f33d1a1bfa | ||
|
|
3c08331441 | ||
|
|
3eaa38247b | ||
|
|
304ce643f9 | ||
|
|
b4ad994f95 | ||
|
|
03c5cfa791 | ||
|
|
e8056072b8 | ||
|
|
d134639a5f | ||
|
|
b4745d76b8 | ||
|
|
c5fd674020 | ||
|
|
9b821dd7cb | ||
|
|
1b441f6aea | ||
|
|
213902c854 | ||
|
|
2054922586 | ||
|
|
a17b7355f5 | ||
|
|
066a1e1f3a | ||
|
|
b10cbfbd63 | ||
|
|
fcd72bb8bd | ||
|
|
37cd99731c | ||
|
|
ed97773f24 | ||
|
|
0424ba3e87 | ||
|
|
9539c4e7bf | ||
|
|
248b378f01 | ||
|
|
1c40575665 | ||
|
|
ac67c648be | ||
|
|
42cc026acc | ||
|
|
23a74edfad | ||
|
|
5da1804f17 | ||
|
|
75f0c35017 | ||
|
|
0e6b02d260 | ||
|
|
d5a0ce55f0 | ||
|
|
09fc6fe8ef | ||
|
|
ff30be879a | ||
|
|
309fe4d831 | ||
|
|
dff0c817a7 | ||
|
|
04313981d4 | ||
|
|
810cb4d13a | ||
|
|
969e32e744 | ||
|
|
980909df9b | ||
|
|
e6753088a4 | ||
|
|
cbdb6cb63a | ||
|
|
3cdf1a899e | ||
|
|
c796be5de5 | ||
|
|
db301cb0c3 | ||
|
|
f00421ef23 | ||
|
|
b324654967 | ||
|
|
aa10ada3ee | ||
|
|
10c97987fb | ||
|
|
b532615bbd | ||
|
|
3066f41af3 | ||
|
|
0c401c6741 | ||
|
|
1a21d1c937 | ||
|
|
525b49a5c9 | ||
|
|
34c074bf7b | ||
|
|
b4dc961cdc | ||
|
|
93374d2cfe | ||
|
|
4009b10549 | ||
|
|
db1864205f | ||
|
|
bf39ccabbd | ||
|
|
0e8e7767ee | ||
|
|
5b6c86e34f | ||
|
|
6bbaca3686 | ||
|
|
35eae90df1 | ||
|
|
488d43e076 | ||
|
|
7c5e93c171 | ||
|
|
a20ef783e1 | ||
|
|
8ae0dce0ed | ||
|
|
44aea606b7 | ||
|
|
a05dc8c661 | ||
|
|
1f80e36017 | ||
|
|
1efca40744 | ||
|
|
86e3131afa | ||
|
|
4910b4a4b0 | ||
|
|
9c7320c0a0 | ||
|
|
02c17c3b75 | ||
|
|
49a47f4b4f | ||
|
|
68280f4a62 | ||
|
|
0e3669b247 | ||
|
|
4c9aa29d46 | ||
|
|
66fbf10f2d | ||
|
|
15ad806eb8 | ||
|
|
b7f80b9c82 | ||
|
|
9b511d2f8f | ||
|
|
6ebce2deb3 | ||
|
|
95dade13f4 | ||
|
|
ba4449d003 | ||
|
|
7632fe5e86 | ||
|
|
2c56bcacee | ||
|
|
c8202db3c6 | ||
|
|
223d689b0c | ||
|
|
4f0e7d9696 | ||
|
|
f4ce911de9 | ||
|
|
d0ad7effa0 | ||
|
|
a032beecbf | ||
|
|
46ec9e48d9 | ||
|
|
26bcef1cc0 | ||
|
|
bfb2f44f8f | ||
|
|
28b19b6774 | ||
|
|
8d72f4a3aa | ||
|
|
9c62e0399d | ||
|
|
65ea09854e | ||
|
|
9f9a4c81b3 | ||
|
|
d567b30f4b | ||
|
|
6d7c4ce0ab | ||
|
|
e062b8f9e9 | ||
|
|
08403b7a4e | ||
|
|
c6ed5d35e7 | ||
|
|
dba3460b60 | ||
|
|
f07f624fcf | ||
|
|
48ff2f328f | ||
|
|
9ae2423a40 | ||
|
|
2bc3c78c75 | ||
|
|
18e9fe75fb | ||
|
|
880a741a44 | ||
|
|
2c6ddcc64b | ||
|
|
8f2e757b77 | ||
|
|
ff177955b3 | ||
|
|
8bb8066a98 | ||
|
|
2747ddbf65 | ||
|
|
b939e9424d | ||
|
|
fb9dea5d1e | ||
|
|
da4d5d711b | ||
|
|
331cbec5f1 | ||
|
|
7f02284285 | ||
|
|
ac2c3a6d97 | ||
|
|
c3bc80fec6 | ||
|
|
09779a0710 | ||
|
|
e82c6ef866 | ||
|
|
861ae9be64 | ||
|
|
96108bc1ec | ||
|
|
016f217db0 | ||
|
|
0688294f18 | ||
|
|
9ad008255d | ||
|
|
4c5a862dd6 | ||
|
|
b165a2308f | ||
|
|
8757b08cd2 | ||
|
|
3800543fba | ||
|
|
02ef60c818 | ||
|
|
88f3b30266 | ||
|
|
9203dc0112 | ||
|
|
4c683bec68 | ||
|
|
0cfd1eb453 | ||
|
|
19744dab37 | ||
|
|
12d58e5aa7 | ||
|
|
e46dd37a26 | ||
|
|
49c3ebc36b | ||
|
|
11e9bc2235 | ||
|
|
3029b3bf0e | ||
|
|
9a6c6f67ce | ||
|
|
a6ed0baef2 | ||
|
|
d3b43d80da | ||
|
|
46d4316d49 | ||
|
|
ade2864351 | ||
|
|
365fc56e9d | ||
|
|
54a5cd21ea | ||
|
|
38c0399b09 | ||
|
|
2b67858453 | ||
|
|
87fdbdbb6e | ||
|
|
bab77a4116 | ||
|
|
d20756ab96 | ||
|
|
dc75a753c3 | ||
|
|
4712d47903 | ||
|
|
c5561801e1 | ||
|
|
5c259fa07a | ||
|
|
60e8b18702 | ||
|
|
a8317824a9 | ||
|
|
0c3c78cc72 | ||
|
|
cfd4a8faac | ||
|
|
7f3fb0db0d | ||
|
|
385d3f0d1b | ||
|
|
8fa6bd12a2 | ||
|
|
57c2004e46 | ||
|
|
c6b069bbfb | ||
|
|
c18bffd08f | ||
|
|
47ec181439 | ||
|
|
90ad40b1b7 | ||
|
|
4d3f20cf98 | ||
|
|
86df9d52bc | ||
|
|
1bd025e070 | ||
|
|
86ee239c71 | ||
|
|
27d0c01e1f | ||
|
|
7a9507be01 | ||
|
|
1490035893 | ||
|
|
a6afcb0ed0 | ||
|
|
ea7e8584cb | ||
|
|
608c6e6a1d | ||
|
|
bb2c91145f | ||
|
|
db074df0f7 | ||
|
|
f7c45df9a6 | ||
|
|
44e3d16cd6 | ||
|
|
a973cdfe0b | ||
|
|
fca42c79a8 | ||
|
|
f236775599 | ||
|
|
360decd37c |
23
.github/workflows/watchdiff.yml
vendored
Normal file
23
.github/workflows/watchdiff.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# This is a basic workflow that is manually triggered
|
||||||
|
|
||||||
|
name: Watch hitomi.la file changes
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "*/10 * * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
watchdiff:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: watchdiff
|
||||||
|
- name: Download files
|
||||||
|
run: ./fetch.sh
|
||||||
|
- name: Commit and Push
|
||||||
|
id: push
|
||||||
|
run: |
|
||||||
|
git config --global user.name 'Watchdiff bot'
|
||||||
|
git config --global user.email 'tom5079@naver.com'
|
||||||
|
{ git add . && git commit -m "File update" && git push; } | tail -1 | grep -q "nothing to commit"
|
||||||
6
.idea/compiler.xml
generated
Normal file
6
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="11" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
2
.idea/copyright/profiles_settings.xml
generated
2
.idea/copyright/profiles_settings.xml
generated
@@ -1,7 +1,7 @@
|
|||||||
<component name="CopyrightManager">
|
<component name="CopyrightManager">
|
||||||
<settings>
|
<settings>
|
||||||
<module2copyright>
|
<module2copyright>
|
||||||
<element module="Pupil" copyright="GPL" />
|
<element module="Project Files" copyright="GPL" />
|
||||||
</module2copyright>
|
</module2copyright>
|
||||||
</settings>
|
</settings>
|
||||||
</component>
|
</component>
|
||||||
4
.idea/gradle.xml
generated
4
.idea/gradle.xml
generated
@@ -4,9 +4,11 @@
|
|||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="PLATFORM" />
|
<option name="testRunner" value="GRADLE" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleHome" value="/usr/share/java/gradle" />
|
||||||
|
<option name="gradleJvm" value="11" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
|
|||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="AndroidLintNewerVersionAvailable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
25
.idea/jarRepositories.xml
generated
25
.idea/jarRepositories.xml
generated
@@ -61,5 +61,30 @@
|
|||||||
<option name="name" value="MavenLocal" />
|
<option name="name" value="MavenLocal" />
|
||||||
<option name="url" value="file:/$USER_HOME$/.m2/repository" />
|
<option name="url" value="file:/$USER_HOME$/.m2/repository" />
|
||||||
</remote-repository>
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="maven3" />
|
||||||
|
<option name="name" value="maven3" />
|
||||||
|
<option name="url" value="https://dl.bintray.com/tom5079/maven" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="maven3" />
|
||||||
|
<option name="name" value="maven3" />
|
||||||
|
<option name="url" value="http://dl.bintray.com/piasy/maven" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="maven2" />
|
||||||
|
<option name="name" value="maven2" />
|
||||||
|
<option name="url" value="https://guardian.github.io/maven/repo-releases/" />
|
||||||
|
</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="maven2" />
|
||||||
|
<option name="name" value="maven2" />
|
||||||
|
<option name="url" value="https://oss.sonatype.org/content/repositories/snapshots" />
|
||||||
|
</remote-repository>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
9
.idea/misc.xml
generated
9
.idea/misc.xml
generated
@@ -1,6 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
<component name="DesignSurface">
|
||||||
|
<option name="filePathToZoomLevelMap">
|
||||||
|
<map>
|
||||||
|
<entry key="app/src/main/res/layout/reader_activity.xml" value="0.14351851851851852" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
12
.idea/runConfigurations.xml
generated
12
.idea/runConfigurations.xml
generated
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RunConfigurationProducerService">
|
|
||||||
<option name="ignoredProducers">
|
|
||||||
<set>
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
21
README.md
21
README.md
@@ -1,18 +1,12 @@
|
|||||||
# Pupil
|
|
||||||
|
|
||||||

|

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

|
||||||
|
[](https://github.com/tom5079/Pupil/releases/download/5.2.14/Pupil-v5.2.14.apk)
|
||||||
|
[](https://discord.gg/Stj4b5v)
|
||||||
|
|
||||||
# Screenshot
|
# Features
|
||||||

|

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

|
|
||||||
*Reader Screen*
|
|
||||||
|
|
||||||
Images are censored to be SFW
|
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
@@ -26,4 +20,7 @@ or Build app yourself
|
|||||||
|
|
||||||
# Contribution
|
# Contribution
|
||||||
|
|
||||||
Any kind of contribution is appriciated. Feel free to leave PR!
|
Any kind of contribution is appreciated. Feel free to leave PR!
|
||||||
|
|
||||||
|
## Tag Translation
|
||||||
|
Head over to [tags branch](https://github.com/tom5079/Pupil/tree/tags)
|
||||||
|
|||||||
176
app/build.gradle
176
app/build.gradle
@@ -1,42 +1,58 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: "com.android.application"
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: "kotlin-android"
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: "kotlin-kapt"
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: "kotlin-parcelize"
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: "kotlinx-serialization"
|
||||||
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
|
apply plugin: "com.google.android.gms.oss-licenses-plugin"
|
||||||
|
|
||||||
if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
|
if (file("google-services.json").exists()) {
|
||||||
logger.lifecycle("Firebase Enabled")
|
logger.lifecycle("Firebase Enabled")
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: "com.google.gms.google-services"
|
||||||
apply plugin: 'com.google.firebase.crashlytics'
|
apply plugin: "com.google.firebase.crashlytics"
|
||||||
apply plugin: 'com.google.firebase.firebase-perf'
|
apply plugin: "com.google.firebase.firebase-perf"
|
||||||
} else {
|
} else {
|
||||||
logger.lifecycle("Firebase Disabled")
|
logger.lifecycle("Firebase Disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
okhttp_version = "3.12.12"
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
all {
|
||||||
|
resolutionStrategy {
|
||||||
|
eachDependency { DependencyResolveDetails details ->
|
||||||
|
if (details.requested.group == "com.squareup.okhttp3" && details.requested.name == "okhttp") {
|
||||||
|
// OkHttp drops support before 5.0 since 3.13.0
|
||||||
|
details.useVersion okhttp_version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 31
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "xyz.quaver.pupil"
|
applicationId "xyz.quaver.pupil"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 30
|
targetSdkVersion 31
|
||||||
versionCode 59
|
versionCode 69
|
||||||
versionName "5.0.3-hotfix2"
|
versionName "5.2.14"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled true
|
defaultConfig.minSdkVersion 21
|
||||||
shrinkResources true
|
|
||||||
|
minifyEnabled false
|
||||||
|
shrinkResources false
|
||||||
|
|
||||||
debuggable true
|
debuggable true
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
versionNameSuffix "-DEBUG"
|
versionNameSuffix "-DEBUG"
|
||||||
|
|
||||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
|
|
||||||
ext.enableCrashlytics = false
|
ext.enableCrashlytics = false
|
||||||
ext.alwaysUpdateBuildId = false
|
ext.alwaysUpdateBuildId = false
|
||||||
}
|
}
|
||||||
@@ -44,75 +60,83 @@ android {
|
|||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
|
|
||||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
|
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
buildToolsVersion = '29.0.3'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
|
||||||
//implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC"
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation "androidx.appcompat:appcompat:1.4.0"
|
||||||
implementation "androidx.activity:activity-ktx:1.2.0-alpha08"
|
implementation "androidx.activity:activity-ktx:1.4.0"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha08'
|
implementation "androidx.fragment:fragment-ktx:1.4.0"
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation "androidx.constraintlayout:constraintlayout:2.1.2"
|
||||||
implementation "androidx.biometric:biometric:1.0.1"
|
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
implementation "androidx.biometric:biometric:1.1.0"
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||||
implementation 'com.google.firebase:firebase-core:17.5.0'
|
|
||||||
implementation 'com.google.firebase:firebase-analytics:17.5.0'
|
|
||||||
implementation 'com.google.firebase:firebase-crashlytics:17.2.1'
|
|
||||||
implementation 'com.google.firebase:firebase-perf:19.0.8'
|
|
||||||
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
|
|
||||||
implementation 'com.google.android.gms:play-services-mlkit-face-detection:16.1.1'
|
|
||||||
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
|
||||||
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:okhttp3-integration:4.11.0") {
|
|
||||||
transitive = false
|
|
||||||
}
|
|
||||||
implementation 'com.github.bumptech.glide:annotations:4.11.0'
|
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
|
||||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
|
||||||
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
|
|
||||||
transitive = false
|
|
||||||
}
|
|
||||||
implementation 'com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2'
|
|
||||||
implementation 'com.gu:option:1.3'
|
|
||||||
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
|
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
|
||||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
|
||||||
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
|
|
||||||
implementation "ru.noties.markwon:core:3.1.0"
|
|
||||||
implementation ("xyz.quaver:libpupil:1.6") {
|
|
||||||
exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-serialization-core-jvm'
|
|
||||||
}
|
|
||||||
implementation "xyz.quaver:documentfilex:0.2.15"
|
|
||||||
testImplementation 'junit:junit:4.13'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
|
||||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
androidExtensions {
|
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||||
experimental = true
|
|
||||||
|
implementation "com.google.android.material:material:1.4.0"
|
||||||
|
|
||||||
|
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
||||||
|
implementation "com.google.firebase:firebase-analytics-ktx"
|
||||||
|
implementation "com.google.firebase:firebase-crashlytics-ktx"
|
||||||
|
implementation "com.google.firebase:firebase-perf-ktx"
|
||||||
|
|
||||||
|
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
|
||||||
|
implementation "com.google.android.gms:play-services-mlkit-face-detection:16.2.1"
|
||||||
|
|
||||||
|
implementation "com.github.clans:fab:1.6.4"
|
||||||
|
|
||||||
|
//implementation "com.quiph.ui:recyclerviewfastscroller:0.2.1"
|
||||||
|
|
||||||
|
implementation 'com.github.piasy:BigImageViewer:1.8.1'
|
||||||
|
implementation 'com.github.piasy:FrescoImageLoader:1.8.1'
|
||||||
|
implementation 'com.github.piasy:FrescoImageViewFactory:1.8.1'
|
||||||
|
implementation 'com.facebook.fresco:imagepipeline-okhttp3:2.6.0'
|
||||||
|
|
||||||
|
//noinspection GradleDependency
|
||||||
|
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
|
|
||||||
|
implementation "com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2"
|
||||||
|
|
||||||
|
implementation "net.rdrei.android.dirchooser:library:3.2@aar"
|
||||||
|
implementation "com.gu:option:1.3"
|
||||||
|
|
||||||
|
implementation "com.andrognito.patternlockview:patternlockview:1.0.0"
|
||||||
|
//implementation "com.andrognito.pinlockview:pinlockview:2.1.0"
|
||||||
|
|
||||||
|
implementation "ru.noties.markwon:core:3.1.0"
|
||||||
|
|
||||||
|
implementation "org.jsoup:jsoup:1.14.3"
|
||||||
|
|
||||||
|
implementation "com.google.guava:guava:31.0.1-android"
|
||||||
|
|
||||||
|
implementation "xyz.quaver:documentfilex:0.7.2"
|
||||||
|
implementation "xyz.quaver:floatingsearchview:1.1.7"
|
||||||
|
|
||||||
|
testImplementation "junit:junit:4.13.2"
|
||||||
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0"
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
||||||
|
androidTestImplementation "androidx.test:rules:1.4.0"
|
||||||
|
androidTestImplementation "androidx.test:runner:1.4.0"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
17
app/proguard-rules.pro
vendored
17
app/proguard-rules.pro
vendored
@@ -22,21 +22,6 @@
|
|||||||
|
|
||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
|
||||||
-keep class * extends com.bumptech.glide.module.AppGlideModule {
|
|
||||||
<init>(...);
|
|
||||||
}
|
|
||||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
|
||||||
**[] $VALUES;
|
|
||||||
public *;
|
|
||||||
}
|
|
||||||
-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
|
|
||||||
*** rewind();
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
|
||||||
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
|
|
||||||
|
|
||||||
-keepattributes *Annotation*, InnerClasses
|
-keepattributes *Annotation*, InnerClasses
|
||||||
-dontnote kotlinx.serialization.SerializationKt
|
-dontnote kotlinx.serialization.SerializationKt
|
||||||
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; } # <-- change package name to your app's
|
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; } # <-- change package name to your app's
|
||||||
@@ -48,4 +33,4 @@
|
|||||||
}
|
}
|
||||||
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
|
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
|
||||||
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
|
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
|
||||||
-keep class xyz.quaver.pupil.util.Preferences
|
-keep class com.hippo.quickjs.** { *; }
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 3,
|
||||||
"artifactType": {
|
"artifactType": {
|
||||||
"type": "APK",
|
"type": "APK",
|
||||||
"kind": "Directory"
|
"kind": "Directory"
|
||||||
@@ -10,11 +10,11 @@
|
|||||||
{
|
{
|
||||||
"type": "SINGLE",
|
"type": "SINGLE",
|
||||||
"filters": [],
|
"filters": [],
|
||||||
"properties": [],
|
"attributes": [],
|
||||||
"versionCode": 59,
|
"versionCode": 69,
|
||||||
"versionName": "5.0.3-hotfix2",
|
"versionName": "5.2.14",
|
||||||
"enabled": true,
|
|
||||||
"outputFile": "app-release.apk"
|
"outputFile": "app-release.apk"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"elementType": "File"
|
||||||
}
|
}
|
||||||
@@ -20,10 +20,18 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.*
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import xyz.quaver.pupil.hitomi.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
@@ -32,10 +40,113 @@ import org.junit.runner.RunWith
|
|||||||
*/
|
*/
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ExampleInstrumentedTest {
|
class ExampleInstrumentedTest {
|
||||||
|
@Before
|
||||||
|
fun init() {
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
WebView.setWebContentsDebuggingEnabled(true)
|
||||||
|
|
||||||
|
webView = WebView(appContext).apply {
|
||||||
|
with (settings) {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
domStorageEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent = settings.userAgentString
|
||||||
|
|
||||||
|
webViewClient = object: WebViewClient() {
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
webViewReady = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceivedError(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?,
|
||||||
|
error: WebResourceError?
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webChromeClient = object: WebChromeClient() {
|
||||||
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||||
|
return super.onConsoleMessage(consoleMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addJavascriptInterface(object {
|
||||||
|
@JavascriptInterface
|
||||||
|
fun onResult(uid: String, result: String) {
|
||||||
|
_webViewFlow.tryEmit(uid to result)
|
||||||
|
}
|
||||||
|
@JavascriptInterface
|
||||||
|
fun onError(uid: String, message: String) {
|
||||||
|
_webViewFlow.tryEmit(uid to null)
|
||||||
|
}
|
||||||
|
}, "Callback")
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadWhenFailedOrUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun useAppContext() {
|
fun test_getGalleryIDsFromNozomi() {
|
||||||
// Context of the app under test.
|
runBlocking {
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val result = getGalleryIDsFromNozomi(null, "boten", "all")
|
||||||
|
|
||||||
|
Log.d("PUPILD", "getGalleryIDsFromNozomi: ${result.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getGalleryIDsForQuery() {
|
||||||
|
runBlocking {
|
||||||
|
val result = getGalleryIDsForQuery("female:crotch tattoo")
|
||||||
|
|
||||||
|
Log.d("PUPILD", "getGalleryIDsForQuery: ${result.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getSuggestionsForQuery() {
|
||||||
|
runBlocking {
|
||||||
|
val result = getSuggestionsForQuery("fem")
|
||||||
|
|
||||||
|
Log.d("PUPILD", "getSuggestionsForQuery: ${result.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_urlFromUrlFromHash() {
|
||||||
|
runBlocking {
|
||||||
|
val galleryInfo = getGalleryInfo(2102416)
|
||||||
|
|
||||||
|
val result = galleryInfo.files.map {
|
||||||
|
imageUrlFromImage(2102416, it, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("PUPILD", result.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getGalleryInfo() {
|
||||||
|
runBlocking {
|
||||||
|
val galleryInfo = getGalleryInfo(2102416)
|
||||||
|
|
||||||
|
Log.d("PUPILD", galleryInfo.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getGalleryBlock() {
|
||||||
|
runBlocking {
|
||||||
|
val block = getGalleryBlock(2102731)
|
||||||
|
|
||||||
|
Log.d("PUPILD", block.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,10 +6,11 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="23"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="21" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="23"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||||
@@ -58,7 +59,8 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.ReaderActivity"
|
android:name=".ui.ReaderActivity"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
android:parentActivityName=".ui.MainActivity">
|
android:parentActivityName=".ui.MainActivity"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -222,7 +224,8 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
android:theme="@style/NoActionBarAppTheme">
|
android:theme="@style/NoActionBarAppTheme"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|||||||
@@ -18,32 +18,43 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
import android.app.*
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.*
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
|
||||||
|
import com.github.piasy.biv.BigImageViewer
|
||||||
|
import com.github.piasy.biv.loader.fresco.FrescoImageLoader
|
||||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
import com.google.android.gms.security.ProviderInstaller
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import okhttp3.Interceptor
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.*
|
||||||
import okhttp3.Response
|
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
|
import xyz.quaver.pupil.hitomi.evaluationContext
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import xyz.quaver.setClient
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.URL
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@@ -66,12 +77,148 @@ var clientHolder: OkHttpClient? = null
|
|||||||
val client: OkHttpClient
|
val client: OkHttpClient
|
||||||
get() = clientHolder ?: clientBuilder.build().also {
|
get() = clientHolder ?: clientBuilder.build().also {
|
||||||
clientHolder = it
|
clientHolder = it
|
||||||
setClient(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
lateinit var webView: WebView
|
||||||
|
val _webViewFlow = MutableSharedFlow<Pair<String, String?>>()
|
||||||
|
val webViewFlow = _webViewFlow.asSharedFlow()
|
||||||
|
var webViewReady = false
|
||||||
|
var webViewFailed = false
|
||||||
|
private var reloadJob: Job? = null
|
||||||
|
|
||||||
|
fun reloadWebView() {
|
||||||
|
if (reloadJob?.isActive == true) return
|
||||||
|
|
||||||
|
reloadJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
webViewReady = false
|
||||||
|
webViewFailed = false
|
||||||
|
|
||||||
|
evaluationContext.cancelChildren(CancellationException("reload"))
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
URL(
|
||||||
|
if (isDebugBuild)
|
||||||
|
"https://tom5079.github.io/Pupil/hitomi-dev.html"
|
||||||
|
else
|
||||||
|
"https://tom5079.github.io/Pupil/hitomi.html"
|
||||||
|
).readText()
|
||||||
|
}.onFailure {
|
||||||
|
webViewFailed = true
|
||||||
|
}.getOrNull()?.let { html ->
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
webView.loadDataWithBaseURL(
|
||||||
|
"https://hitomi.la/",
|
||||||
|
html,
|
||||||
|
"text/html",
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var htmlVersion: String = ""
|
||||||
|
fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
while (true) {
|
||||||
|
if (
|
||||||
|
webViewFailed ||
|
||||||
|
runCatching {
|
||||||
|
URL(
|
||||||
|
if (isDebugBuild)
|
||||||
|
"https://tom5079.github.io/Pupil/hitomi-dev.html.ver"
|
||||||
|
else
|
||||||
|
"https://tom5079.github.io/Pupil/hitomi.html.ver"
|
||||||
|
).readText()
|
||||||
|
}.getOrNull().let { version ->
|
||||||
|
(!version.isNullOrEmpty() && version != htmlVersion).also {
|
||||||
|
if (it) htmlVersion = version!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
reloadWebView()
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(if (webViewReady && !webViewFailed) 10000 else 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDebugBuild: Boolean = false
|
||||||
|
lateinit var userAgent: String
|
||||||
|
|
||||||
class Pupil : Application() {
|
class Pupil : Application() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
lateinit var instance: Pupil
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
instance = this
|
||||||
|
isDebugBuild = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
|
||||||
|
|
||||||
|
if (isDebugBuild) WebView.setWebContentsDebuggingEnabled(true)
|
||||||
|
|
||||||
|
webView = WebView(this).apply {
|
||||||
|
with (settings) {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
domStorageEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent = settings.userAgentString
|
||||||
|
|
||||||
|
webViewClient = object: WebViewClient() {
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
webViewReady = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceivedError(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?,
|
||||||
|
error: WebResourceError?
|
||||||
|
) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
FirebaseCrashlytics.getInstance().log(
|
||||||
|
"onReceivedError: ${error?.description}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webChromeClient = object: WebChromeClient() {
|
||||||
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||||
|
FirebaseCrashlytics.getInstance().log(
|
||||||
|
"onConsoleMessage: ${consoleMessage?.message()} (${consoleMessage?.sourceId()}:${consoleMessage?.lineNumber()})"
|
||||||
|
)
|
||||||
|
|
||||||
|
return super.onConsoleMessage(consoleMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addJavascriptInterface(object {
|
||||||
|
@JavascriptInterface
|
||||||
|
fun onResult(uid: String, result: String) {
|
||||||
|
CoroutineScope(Dispatchers.Unconfined).launch {
|
||||||
|
_webViewFlow.emit(uid to result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@JavascriptInterface
|
||||||
|
fun onError(uid: String, message: String) {
|
||||||
|
CoroutineScope(Dispatchers.Unconfined).launch {
|
||||||
|
_webViewFlow.emit(uid to null)
|
||||||
|
}
|
||||||
|
Toast.makeText(this@Pupil, message, Toast.LENGTH_LONG).show()
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(
|
||||||
|
Exception(message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, "Callback")
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadWhenFailedOrUpdate()
|
||||||
|
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||||
|
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
@@ -90,15 +237,24 @@ class Pupil : Application() {
|
|||||||
.readTimeout(0, TimeUnit.SECONDS)
|
.readTimeout(0, TimeUnit.SECONDS)
|
||||||
.proxyInfo(proxyInfo)
|
.proxyInfo(proxyInfo)
|
||||||
.addInterceptor { chain ->
|
.addInterceptor { chain ->
|
||||||
val request = chain.request()
|
val request = chain.request().newBuilder()
|
||||||
|
.header("User-Agent", userAgent)
|
||||||
|
.header("Referer", "https://hitomi.la/")
|
||||||
|
.build()
|
||||||
|
|
||||||
val tag = request.tag() ?: return@addInterceptor chain.proceed(request)
|
val tag = request.tag() ?: return@addInterceptor chain.proceed(request)
|
||||||
|
|
||||||
interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request)
|
interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request)
|
||||||
|
}.apply {
|
||||||
|
(Preferences.get<String>("max_concurrent_download").toIntOrNull() ?: 0).let {
|
||||||
|
if (it != 0)
|
||||||
|
dispatcher(Dispatcher(Executors.newFixedThreadPool(it)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Preferences.get<String>("download_folder").also {
|
Preferences.get<String>("download_folder").also {
|
||||||
if (it.startsWith("content") && Build.VERSION.SDK_INT > 19)
|
if (it.startsWith("content://"))
|
||||||
contentResolver.takePersistableUriPermission(
|
contentResolver.takePersistableUriPermission(
|
||||||
Uri.parse(it),
|
Uri.parse(it),
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
@@ -106,34 +262,28 @@ class Pupil : Application() {
|
|||||||
|
|
||||||
if (!FileX(this, it).canWrite())
|
if (!FileX(this, it).canWrite())
|
||||||
throw Exception()
|
throw Exception()
|
||||||
|
|
||||||
DownloadManager.getInstance(this).migrate()
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Preferences.remove("download_folder")
|
Preferences.remove("download_folder")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Preferences["reset_secure", false]) {
|
||||||
|
Preferences["security_mode"] = false
|
||||||
|
Preferences["reset_secure"] = true
|
||||||
|
}
|
||||||
|
|
||||||
histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0)
|
histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0)
|
||||||
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0)
|
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0)
|
||||||
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
|
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
|
||||||
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
|
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
|
||||||
|
|
||||||
if (Preferences["new_history"]) {
|
favoriteTags.filter { it.tag.contains('_') }.forEach {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
favoriteTags.remove(it)
|
||||||
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)*/
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProviderInstaller.installIfNeeded(this)
|
ProviderInstaller.installIfNeeded(this)
|
||||||
@@ -143,6 +293,15 @@ class Pupil : Application() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BigImageViewer.initialize(
|
||||||
|
FrescoImageLoader.with(
|
||||||
|
this,
|
||||||
|
OkHttpImagePipelineConfigFactory
|
||||||
|
.newBuilder(this, client)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +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
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.Registry
|
|
||||||
import com.bumptech.glide.annotation.GlideModule
|
|
||||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
|
||||||
import com.bumptech.glide.module.AppGlideModule
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
@GlideModule
|
|
||||||
class PupilGlideModule : AppGlideModule() {
|
|
||||||
|
|
||||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
|
||||||
registry.append(
|
|
||||||
GlideUrl::class.java,
|
|
||||||
InputStream::class.java,
|
|
||||||
OkHttpUrlLoader.Factory(client)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -18,347 +18,294 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
package xyz.quaver.pupil.adapters
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.util.Log
|
|
||||||
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.Toast
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
|
||||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||||
import com.bumptech.glide.RequestManager
|
|
||||||
import com.bumptech.glide.load.DataSource
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
|
||||||
import com.bumptech.glide.request.RequestListener
|
|
||||||
import com.bumptech.glide.request.target.Target
|
|
||||||
import com.daimajia.swipe.SwipeLayout
|
import com.daimajia.swipe.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 kotlinx.android.synthetic.main.item_galleryblock.view.*
|
import com.github.piasy.biv.loader.ImageLoader
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import xyz.quaver.hitomi.getReader
|
|
||||||
import xyz.quaver.io.util.getChild
|
import xyz.quaver.io.util.getChild
|
||||||
import xyz.quaver.pupil.BuildConfig
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.databinding.GalleryblockItemBinding
|
||||||
|
import xyz.quaver.pupil.favoriteTags
|
||||||
import xyz.quaver.pupil.favorites
|
import xyz.quaver.pupil.favorites
|
||||||
|
import xyz.quaver.pupil.hitomi.getGallery
|
||||||
|
import xyz.quaver.pupil.hitomi.getGalleryInfo
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
import xyz.quaver.pupil.ui.view.TagChip
|
import xyz.quaver.pupil.ui.view.ProgressCard
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
import java.util.*
|
import java.io.File
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
|
|
||||||
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
||||||
|
|
||||||
enum class ViewType {
|
var updateAll = true
|
||||||
NEXT,
|
var thin: Boolean = Preferences["thin"]
|
||||||
GALLERY,
|
|
||||||
PREV
|
|
||||||
}
|
|
||||||
|
|
||||||
val timer = Timer()
|
inner class GalleryViewHolder(val binding: GalleryblockItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
private var galleryID: Int = 0
|
||||||
var isThin = false
|
|
||||||
|
|
||||||
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
var timerTask: TimerTask? = null
|
|
||||||
|
|
||||||
private fun updateProgress(context: Context, galleryID: Int) {
|
|
||||||
val cache = Cache.getInstance(context, galleryID)
|
|
||||||
|
|
||||||
|
init {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
if (cache.metadata.reader == null || Preferences["cache_disable"]) {
|
while (updateAll) {
|
||||||
view.galleryblock_progressbar_layout.visibility = View.GONE
|
updateProgress(itemView.context)
|
||||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
delay(1000)
|
||||||
return@launch
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateProgress(context: Context) = CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
with(binding.galleryblockCard) {
|
||||||
|
val imageList = Cache.getInstance(context, galleryID).metadata.imageList
|
||||||
|
|
||||||
|
if (imageList == null) {
|
||||||
|
max = 0
|
||||||
|
return@with
|
||||||
}
|
}
|
||||||
|
|
||||||
with(view.galleryblock_progressbar) {
|
progress = imageList.count { it != null }
|
||||||
val imageList = cache.metadata.imageList!!
|
max = imageList.size
|
||||||
|
|
||||||
progress = imageList.filterNotNull().size
|
this@GalleryViewHolder.binding.galleryblockId.setOnClickListener {
|
||||||
max = imageList.size
|
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||||
|
ClipData.newPlainText("gallery_id", galleryID.toString())
|
||||||
with(view.galleryblock_progressbar_layout) {
|
)
|
||||||
if (visibility == View.GONE)
|
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress == max) {
|
|
||||||
val downloadManager = DownloadManager.getInstance(context)
|
|
||||||
|
|
||||||
if (completeFlag.get(galleryID, false)) {
|
|
||||||
with(view.galleryblock_progress_complete) {
|
|
||||||
setImageResource(
|
|
||||||
if (downloadManager.getDownloadFolder(galleryID) != null)
|
|
||||||
R.drawable.ic_progressbar
|
|
||||||
else R.drawable.ic_progressbar_cache
|
|
||||||
)
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
with(view.galleryblock_progress_complete) {
|
|
||||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context,
|
|
||||||
if (downloadManager.getDownloadFolder(galleryID) != null)
|
|
||||||
R.drawable.ic_progressbar_complete
|
|
||||||
else R.drawable.ic_progressbar_complete_cache
|
|
||||||
).apply {
|
|
||||||
this?.start()
|
|
||||||
})
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
completeFlag.put(galleryID, true)
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type = if (!imageList.contains(null)) {
|
||||||
|
val downloadManager = DownloadManager.getInstance(context)
|
||||||
|
|
||||||
|
if (downloadManager.getDownloadFolder(galleryID) == null)
|
||||||
|
ProgressCard.Type.CACHE
|
||||||
|
else
|
||||||
|
ProgressCard.Type.DOWNLOAD
|
||||||
|
} else
|
||||||
|
ProgressCard.Type.LOADING
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(galleryID: Int) {
|
fun bind(galleryID: Int) {
|
||||||
val cache = Cache.getInstance(view.context, galleryID)
|
this.galleryID = galleryID
|
||||||
|
updateProgress(itemView.context)
|
||||||
|
|
||||||
val galleryBlock = cache.metadata.galleryBlock ?: return
|
val cache = Cache.getInstance(itemView.context, galleryID)
|
||||||
|
|
||||||
with(view) {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val resources = context.resources
|
val galleryBlock = cache.getGalleryBlock() ?: return@launch
|
||||||
val languages = resources.getStringArray(R.array.languages).map {
|
|
||||||
it.split("|").let { split ->
|
|
||||||
Pair(split[0], split[1])
|
|
||||||
}
|
|
||||||
}.toMap()
|
|
||||||
|
|
||||||
val artists = galleryBlock.artists
|
launch(Dispatchers.Main) {
|
||||||
val series = galleryBlock.series
|
val resources = itemView.context.resources
|
||||||
|
val languages = resources.getStringArray(R.array.languages).map {
|
||||||
|
it.split("|").let { split ->
|
||||||
|
Pair(split[0], split[1])
|
||||||
|
}
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
if (isThin)
|
val artists = galleryBlock.artists
|
||||||
galleryblock_thumbnail.layoutParams.width = context.resources.getDimensionPixelSize(
|
val series = galleryBlock.series
|
||||||
R.dimen.galleryblock_thumbnail_thin
|
|
||||||
)
|
|
||||||
|
|
||||||
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
|
binding.galleryblockThumbnail.apply {
|
||||||
it.start()
|
setOnClickListener {
|
||||||
})
|
itemView.performClick()
|
||||||
|
}
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
setOnLongClickListener {
|
||||||
val thumbnail = cache.getThumbnail()
|
itemView.performLongClick()
|
||||||
|
}
|
||||||
glide
|
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
|
||||||
.load(thumbnail)
|
setImageLoaderCallback(object: ImageLoader.Callback {
|
||||||
.skipMemoryCache(true)
|
override fun onFail(error: Exception?) {
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
Cache.delete(context, galleryID)
|
||||||
.error(R.drawable.image_broken_variant)
|
|
||||||
.listener(object: RequestListener<Drawable> {
|
|
||||||
override fun onLoadFailed(
|
|
||||||
e: GlideException?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<Drawable>?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
Cache.getInstance(context, galleryID).let {
|
|
||||||
it.cacheFolder.getChild(".thumbnail").let { if (it.exists()) it.delete() }
|
|
||||||
it.downloadFolder?.getChild(".thumbnail")?.let { if (it.exists()) it.delete() }
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResourceReady(
|
override fun onCacheHit(imageType: Int, image: File?) {}
|
||||||
resource: Drawable?,
|
override fun onCacheMiss(imageType: Int, image: File?) {}
|
||||||
model: Any?,
|
override fun onFinish() {}
|
||||||
target: Target<Drawable>?,
|
override fun onProgress(progress: Int) {}
|
||||||
dataSource: DataSource?,
|
override fun onStart() {}
|
||||||
isFirstResource: Boolean
|
override fun onSuccess(image: File?) {}
|
||||||
): Boolean = false
|
|
||||||
})
|
})
|
||||||
.apply {
|
ssiv?.recycle()
|
||||||
if (BuildConfig.CENSOR)
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
override(5, 8)
|
cache.getThumbnail().let { launch(Dispatchers.Main) {
|
||||||
}.let { launch(Dispatchers.Main) { it.into(galleryblock_thumbnail) } }
|
showImage(it)
|
||||||
}
|
} }
|
||||||
|
}
|
||||||
if (timerTask == null)
|
|
||||||
timerTask = timer.schedule(0, 1000) {
|
|
||||||
updateProgress(context, galleryID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
galleryblock_title.text = galleryBlock.title
|
binding.galleryblockTitle.text = galleryBlock.title
|
||||||
with(galleryblock_artist) {
|
with(binding.galleryblockArtist) {
|
||||||
text = artists.joinToString(", ") { it.wordCapitalize() }
|
text = artists.joinToString { it.wordCapitalize() }
|
||||||
visibility = when {
|
visibility = when {
|
||||||
artists.isNotEmpty() -> View.VISIBLE
|
artists.isNotEmpty() -> View.VISIBLE
|
||||||
else -> View.GONE
|
else -> View.GONE
|
||||||
}
|
}
|
||||||
}
|
|
||||||
with(galleryblock_series) {
|
|
||||||
text =
|
|
||||||
resources.getString(
|
|
||||||
R.string.galleryblock_series,
|
|
||||||
series.joinToString(", ") { it.wordCapitalize() })
|
|
||||||
visibility = when {
|
|
||||||
series.isNotEmpty() -> View.VISIBLE
|
|
||||||
else -> View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
galleryblock_type.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
|
|
||||||
with(galleryblock_language) {
|
|
||||||
text =
|
|
||||||
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
|
|
||||||
visibility = when {
|
|
||||||
galleryBlock.language.isNotEmpty() -> View.VISIBLE
|
|
||||||
else -> View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
galleryblock_tag_group.removeAllViews()
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
val gallery = runCatching {
|
||||||
galleryBlock.relatedTags.map {
|
getGallery(galleryID)
|
||||||
TagChip(context, Tag.parse(it)).apply {
|
}.getOrNull()
|
||||||
setOnClickListener { view ->
|
|
||||||
for (callback in onChipClickedHandler)
|
if (gallery?.groups?.isNotEmpty() != true)
|
||||||
callback.invoke((view as TagChip).tag)
|
return@launch
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
text = context.getString(
|
||||||
|
R.string.galleryblock_artist_with_group,
|
||||||
|
artists.joinToString { it.wordCapitalize() },
|
||||||
|
gallery.groups.joinToString { it.wordCapitalize() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.let { launch(Dispatchers.Main) { it.forEach { galleryblock_tag_group.addView(it) } } }
|
|
||||||
}
|
|
||||||
|
|
||||||
galleryblock_id.text = galleryBlock.id.toString()
|
|
||||||
galleryblock_pagecount.text = "-"
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val pageCount = kotlin.runCatching {
|
|
||||||
getReader(galleryBlock.id).galleryInfo.files.size
|
|
||||||
}.getOrNull() ?: return@launch
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
galleryblock_pagecount.text = context.getString(R.string.galleryblock_pagecount, pageCount)
|
|
||||||
}
|
}
|
||||||
}
|
with(binding.galleryblockSeries) {
|
||||||
|
text =
|
||||||
|
resources.getString(
|
||||||
|
R.string.galleryblock_series,
|
||||||
|
series.joinToString(", ") { it.wordCapitalize() })
|
||||||
|
visibility = when {
|
||||||
|
series.isNotEmpty() -> View.VISIBLE
|
||||||
|
else -> View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.galleryblockType.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
|
||||||
|
with(binding.galleryblockLanguage) {
|
||||||
|
text =
|
||||||
|
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
|
||||||
|
visibility = when {
|
||||||
|
galleryBlock.language.isNotEmpty() -> View.VISIBLE
|
||||||
|
else -> View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
with(galleryblock_favorite) {
|
with(binding.galleryblockTagGroup) {
|
||||||
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
|
onClickListener = {
|
||||||
setOnClickListener {
|
onChipClickedHandler.forEach { callback ->
|
||||||
when {
|
callback.invoke(it)
|
||||||
favorites.contains(galleryBlock.id) -> {
|
|
||||||
favorites.remove(galleryBlock.id)
|
|
||||||
|
|
||||||
setImageResource(R.drawable.ic_star_empty)
|
|
||||||
}
|
}
|
||||||
else -> {
|
}
|
||||||
favorites.add(galleryBlock.id)
|
|
||||||
|
|
||||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
|
tags.clear()
|
||||||
this ?: return@apply
|
|
||||||
|
|
||||||
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
override fun onAnimationEnd(drawable: Drawable?) {
|
tags.addAll(
|
||||||
setImageResource(R.drawable.ic_star_filled)
|
galleryBlock.relatedTags.sortedBy {
|
||||||
|
val tag = Tag.parse(it)
|
||||||
|
|
||||||
|
if (favoriteTags.contains(tag))
|
||||||
|
-1
|
||||||
|
else
|
||||||
|
when(Tag.parse(it).area) {
|
||||||
|
"female" -> 0
|
||||||
|
"male" -> 1
|
||||||
|
else -> 2
|
||||||
}
|
}
|
||||||
})
|
}.map {
|
||||||
start()
|
Tag.parse(it)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.galleryblockId.text = galleryBlock.id.toString()
|
||||||
|
binding.galleryblockPagecount.text = "-"
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val pageCount = kotlin.runCatching {
|
||||||
|
getGalleryInfo(galleryBlock.id).files.size
|
||||||
|
}.getOrNull() ?: return@launch
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
binding.galleryblockPagecount.text = itemView.context.getString(R.string.galleryblock_pagecount, pageCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with(binding.galleryblockFavorite) {
|
||||||
|
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
|
||||||
|
setOnClickListener {
|
||||||
|
when {
|
||||||
|
favorites.contains(galleryBlock.id) -> {
|
||||||
|
favorites.remove(galleryBlock.id)
|
||||||
|
|
||||||
|
setImageResource(R.drawable.ic_star_empty)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
favorites.add(galleryBlock.id)
|
||||||
|
|
||||||
|
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
|
||||||
|
this ?: return@apply
|
||||||
|
|
||||||
|
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
|
||||||
|
override fun onAnimationEnd(drawable: Drawable?) {
|
||||||
|
setImageResource(R.drawable.ic_star_filled)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
start()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Make some views invisible to make it thinner
|
|
||||||
if (isThin) {
|
|
||||||
galleryblock_language.visibility = View.GONE
|
|
||||||
galleryblock_type.visibility = View.GONE
|
|
||||||
galleryblock_tag_group.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make some views invisible to make it thinner
|
||||||
|
if (thin) {
|
||||||
|
binding.galleryblockTagGroup.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class NextViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
|
|
||||||
class PrevViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
|
|
||||||
|
|
||||||
class ViewHolderFactory {
|
|
||||||
companion object {
|
|
||||||
fun getLayoutID(type: Int): Int {
|
|
||||||
return when(ViewType.values()[type]) {
|
|
||||||
ViewType.NEXT -> R.layout.item_next
|
|
||||||
ViewType.PREV -> R.layout.item_prev
|
|
||||||
ViewType.GALLERY -> R.layout.item_galleryblock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val completeFlag = SparseBooleanArray()
|
|
||||||
|
|
||||||
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
||||||
var onDownloadClickedHandler: ((Int) -> Unit)? = null
|
var onDownloadClickedHandler: ((Int) -> Unit)? = null
|
||||||
var onDeleteClickedHandler: ((Int) -> Unit)? = null
|
var onDeleteClickedHandler: ((Int) -> Unit)? = null
|
||||||
|
|
||||||
var showNext = false
|
|
||||||
var showPrev = false
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
return GalleryViewHolder(GalleryblockItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
fun getViewHolder(type: Int, view: View): RecyclerView.ViewHolder {
|
|
||||||
return when(ViewType.values()[type]) {
|
|
||||||
ViewType.NEXT -> NextViewHolder(view as LinearLayout)
|
|
||||||
ViewType.PREV -> PrevViewHolder(view as LinearLayout)
|
|
||||||
ViewType.GALLERY -> GalleryViewHolder(view as CardView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getViewHolder(
|
|
||||||
viewType,
|
|
||||||
LayoutInflater.from(parent.context).inflate(
|
|
||||||
ViewHolderFactory.getLayoutID(viewType),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
if (holder is GalleryViewHolder) {
|
if (holder is GalleryViewHolder) {
|
||||||
val galleryID = galleries[position-(if (showPrev) 1 else 0)]
|
val galleryID = galleries[position]
|
||||||
|
|
||||||
holder.bind(galleryID)
|
holder.bind(galleryID)
|
||||||
|
|
||||||
with(holder.view.galleryblock_primary) {
|
holder.binding.galleryblockCard.binding.download.setOnClickListener {
|
||||||
setOnClickListener {
|
|
||||||
holder.view.performClick()
|
|
||||||
}
|
|
||||||
setOnLongClickListener {
|
|
||||||
holder.view.performLongClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.view.galleryblock_download.setOnClickListener {
|
|
||||||
onDownloadClickedHandler?.invoke(position)
|
onDownloadClickedHandler?.invoke(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.view.galleryblock_delete.setOnClickListener {
|
holder.binding.galleryblockCard.binding.delete.setOnClickListener {
|
||||||
onDeleteClickedHandler?.invoke(position)
|
onDeleteClickedHandler?.invoke(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
mItemManger.bindView(holder.view, position)
|
mItemManger.bindView(holder.binding.root, position)
|
||||||
|
|
||||||
holder.view.galleryblock_swipe_layout.addSwipeListener(object: SwipeLayout.SwipeListener {
|
holder.binding.galleryblockCard.binding.swipeLayout.addSwipeListener(object: SwipeLayout.SwipeListener {
|
||||||
override fun onStartOpen(layout: SwipeLayout?) {
|
override fun onStartOpen(layout: SwipeLayout?) {
|
||||||
mItemManger.closeAllExcept(layout)
|
mItemManger.closeAllExcept(layout)
|
||||||
|
|
||||||
holder.view.galleryblock_download.text =
|
holder.binding.galleryblockCard.binding.download.text =
|
||||||
if (DownloadManager.getInstance(holder.view.context).isDownloading(galleryID))
|
if (DownloadManager.getInstance(holder.binding.root.context).isDownloading(galleryID))
|
||||||
holder.view.context.getString(android.R.string.cancel)
|
holder.binding.root.context.getString(android.R.string.cancel)
|
||||||
else
|
else
|
||||||
holder.view.context.getString(R.string.main_download)
|
holder.binding.root.context.getString(R.string.main_download)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClose(layout: SwipeLayout?) {}
|
override fun onClose(layout: SwipeLayout?) {}
|
||||||
@@ -370,27 +317,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
override fun getItemCount() = galleries.size
|
||||||
super.onViewDetachedFromWindow(holder)
|
|
||||||
|
|
||||||
if (holder is GalleryViewHolder) {
|
override fun getSwipeLayoutResourceId(position: Int) = R.id.swipe_layout
|
||||||
holder.timerTask?.cancel()
|
|
||||||
holder.timerTask = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() =
|
|
||||||
galleries.size +
|
|
||||||
(if (showNext) 1 else 0) +
|
|
||||||
(if (showPrev) 1 else 0)
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
|
||||||
return when {
|
|
||||||
showPrev && position == 0 -> ViewType.PREV
|
|
||||||
showNext && position == galleries.size+(if (showPrev) 1 else 0) -> ViewType.NEXT
|
|
||||||
else -> ViewType.GALLERY
|
|
||||||
}.ordinal
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSwipeLayoutResourceId(position: Int) = R.id.galleryblock_swipe_layout
|
|
||||||
}
|
}
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import kotlinx.android.synthetic.main.item_mirrors.view.*
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
|
||||||
|
|
||||||
val mirrors = context.resources.getStringArray(R.array.mirrors).map {
|
|
||||||
it.split('|').let { split ->
|
|
||||||
Pair(split.first(), split.last())
|
|
||||||
}
|
|
||||||
}.toMap()
|
|
||||||
|
|
||||||
val list = mirrors.keys.toMutableList().apply {
|
|
||||||
Preferences.get<String>("mirrors")
|
|
||||||
.split(">")
|
|
||||||
.reversed()
|
|
||||||
.forEach {
|
|
||||||
if (this.contains(it)) {
|
|
||||||
this.remove(it)
|
|
||||||
this.add(0, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onItemMove : ((Int, Int) -> Unit) = { from, to ->
|
|
||||||
Collections.swap(list, from, to)
|
|
||||||
notifyItemMoved(from, to)
|
|
||||||
onItemMoved?.invoke(list)
|
|
||||||
}
|
|
||||||
var onStartDrag : ((ViewHolder) -> Unit)? = null
|
|
||||||
var onItemMoved : ((List<String>) -> (Unit))? = null
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
with(holder.view) {
|
|
||||||
mirror_name.text = mirrors[list.elementAt(position)]
|
|
||||||
mirror_button.setOnTouchListener { _, event ->
|
|
||||||
if (event.action == MotionEvent.ACTION_DOWN)
|
|
||||||
onStartDrag?.invoke(holder)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
return LayoutInflater.from(parent.context).inflate(
|
|
||||||
R.layout.item_mirrors, parent, false
|
|
||||||
).let {
|
|
||||||
ViewHolder(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = mirrors.size
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -18,188 +18,233 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
package xyz.quaver.pupil.adapters
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.net.Uri
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import com.bumptech.glide.load.DataSource
|
import com.facebook.drawee.backends.pipeline.Fresco
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.facebook.drawee.controller.BaseControllerListener
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
import com.facebook.drawee.drawable.ScalingUtils
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.facebook.drawee.interfaces.DraweeController
|
||||||
import com.bumptech.glide.load.model.LazyHeaders
|
import com.facebook.drawee.view.SimpleDraweeView
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.facebook.imagepipeline.image.ImageInfo
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.github.piasy.biv.view.BigImageView
|
||||||
import kotlinx.android.synthetic.main.item_reader.view.*
|
import com.github.piasy.biv.view.ImageShownCallback
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import com.github.piasy.biv.view.ImageViewFactory
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import xyz.quaver.Code
|
|
||||||
import xyz.quaver.hitomi.Reader
|
|
||||||
import xyz.quaver.hitomi.getReferer
|
|
||||||
import xyz.quaver.hitomi.imageUrlFromImage
|
|
||||||
import xyz.quaver.hiyobi.createImgList
|
|
||||||
import xyz.quaver.pupil.BuildConfig
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
import xyz.quaver.pupil.databinding.ReaderItemBinding
|
||||||
|
import xyz.quaver.pupil.hitomi.GalleryInfo
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
import xyz.quaver.pupil.ui.ReaderActivity
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import java.util.*
|
import java.io.File
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class ReaderAdapter(private val activity: ReaderActivity,
|
class ReaderAdapter(
|
||||||
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
private val activity: ReaderActivity,
|
||||||
|
private val galleryID: Int
|
||||||
var reader: Reader? = null
|
) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||||
val timer = Timer()
|
var galleryInfo: GalleryInfo? = null
|
||||||
|
|
||||||
private val glide = Glide.with(activity)
|
|
||||||
|
|
||||||
var isFullScreen = false
|
var isFullScreen = false
|
||||||
|
|
||||||
var onItemClickListener : ((Int) -> (Unit))? = null
|
var onItemClickListener : (() -> (Unit))? = null
|
||||||
|
|
||||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
init {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
with (binding.image) {
|
||||||
return LayoutInflater.from(parent.context).inflate(
|
setImageViewFactory(FrescoImageViewFactory().apply {
|
||||||
R.layout.item_reader, parent, false
|
updateView = { imageInfo ->
|
||||||
).let {
|
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||||
ViewHolder(it)
|
dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private var cache: Cache? = null
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
holder.view as ConstraintLayout
|
|
||||||
|
|
||||||
if (cache == null)
|
|
||||||
cache = Cache.getInstance(holder.view.context, galleryID)
|
|
||||||
|
|
||||||
if (isFullScreen) {
|
|
||||||
holder.view.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
|
||||||
} else {
|
|
||||||
holder.view.layoutParams.height = ConstraintLayout.LayoutParams.WRAP_CONTENT
|
|
||||||
|
|
||||||
(holder.view.progress_layout.layoutParams as ConstraintLayout.LayoutParams)
|
|
||||||
.dimensionRatio = "${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
|
||||||
onItemClickListener?.invoke(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.view.setOnClickListener {
|
|
||||||
onItemClickListener?.invoke(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.view.reader_index.text = (position+1).toString()
|
|
||||||
|
|
||||||
if (Preferences["cache_disable"]) {
|
|
||||||
val lowQuality: Boolean = Preferences["low_quality"]
|
|
||||||
|
|
||||||
val url = when (reader!!.code) {
|
|
||||||
Code.HITOMI ->
|
|
||||||
GlideUrl(
|
|
||||||
imageUrlFromImage(
|
|
||||||
galleryID,
|
|
||||||
reader!!.galleryInfo.files[position],
|
|
||||||
!lowQuality
|
|
||||||
)
|
|
||||||
, LazyHeaders.Builder().addHeader("Referer", getReferer(galleryID)).build())
|
|
||||||
Code.HIYOBI ->
|
|
||||||
GlideUrl(createImgList(galleryID, reader!!, lowQuality)[position].path)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
holder.view.image.post {
|
|
||||||
glide
|
|
||||||
.load(url!!)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.skipMemoryCache(false)
|
|
||||||
.error(R.drawable.image_broken_variant)
|
|
||||||
.apply {
|
|
||||||
if (BuildConfig.CENSOR)
|
|
||||||
override(5, 8)
|
|
||||||
else
|
|
||||||
override(
|
|
||||||
holder.view.context.resources.displayMetrics.widthPixels,
|
|
||||||
holder.view.context.resources.getDimensionPixelSize(R.dimen.reader_max_height)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.error(R.drawable.image_broken_variant)
|
})
|
||||||
.into(holder.view.image)
|
setImageShownCallback(object : ImageShownCallback {
|
||||||
|
override fun onMainImageShown() {
|
||||||
|
binding.image.mainView.let { v ->
|
||||||
|
when (v) {
|
||||||
|
is SubsamplingScaleImageView ->
|
||||||
|
if (!isFullScreen) binding.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onThumbnailShown() {}
|
||||||
|
})
|
||||||
|
|
||||||
|
setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
|
||||||
|
setOnClickListener {
|
||||||
|
onItemClickListener?.invoke()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
onItemClickListener?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(position: Int) {
|
||||||
|
if (cache == null)
|
||||||
|
cache = Cache.getInstance(itemView.context, galleryID)
|
||||||
|
|
||||||
|
if (!isFullScreen) {
|
||||||
|
binding.root.setBackgroundResource(R.drawable.reader_item_boundary)
|
||||||
|
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||||
|
height = 0
|
||||||
|
dimensionRatio =
|
||||||
|
"${galleryInfo!!.files[position].width}:${galleryInfo!!.files[position].height}"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||||
|
height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
||||||
|
dimensionRatio = null
|
||||||
|
}
|
||||||
|
binding.root.background = null
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.readerIndex.text = (position+1).toString()
|
||||||
|
|
||||||
val image = cache!!.getImage(position)
|
val image = cache!!.getImage(position)
|
||||||
val progress = activity.downloader?.progress?.get(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
|
binding.progressGroup.visibility = View.INVISIBLE
|
||||||
|
binding.image.showImage(image.uri)
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
glide
|
|
||||||
.load(image.uri)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.skipMemoryCache(true)
|
|
||||||
.apply {
|
|
||||||
if (BuildConfig.CENSOR)
|
|
||||||
override(5, 8)
|
|
||||||
else
|
|
||||||
override(
|
|
||||||
holder.view.context.resources.displayMetrics.widthPixels,
|
|
||||||
holder.view.context.resources.getDimensionPixelSize(R.dimen.reader_max_height)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.error(R.drawable.image_broken_variant)
|
|
||||||
.listener(object: RequestListener<Drawable> {
|
|
||||||
override fun onLoadFailed(
|
|
||||||
e: GlideException?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<Drawable>?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
cache!!.metadata.imageList?.set(position, null)
|
|
||||||
image.delete()
|
|
||||||
DownloadService.cancel(holder.view.context, galleryID)
|
|
||||||
DownloadService.download(holder.view.context, galleryID, true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: Drawable?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<Drawable>?,
|
|
||||||
dataSource: DataSource?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
) = false
|
|
||||||
}).let { launch(Dispatchers.Main) { it.into(holder.view.image) } }
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
holder.view.reader_item_progressbar.visibility = View.VISIBLE
|
binding.progressGroup.visibility = View.VISIBLE
|
||||||
|
binding.readerItemProgressbar.progress =
|
||||||
glide.clear(holder.view.image)
|
|
||||||
|
|
||||||
holder.view.reader_item_progressbar.progress =
|
|
||||||
if (progress?.isInfinite() == true)
|
if (progress?.isInfinite() == true)
|
||||||
100
|
100
|
||||||
else
|
else
|
||||||
progress?.roundToInt() ?: 0
|
progress?.roundToInt() ?: 0
|
||||||
|
|
||||||
holder.view.image.setImageDrawable(null)
|
clear()
|
||||||
|
|
||||||
timer.schedule(1000) {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
delay(1000)
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
binding.image.mainView.let {
|
||||||
|
when (it) {
|
||||||
|
is SubsamplingScaleImageView ->
|
||||||
|
it.recycle()
|
||||||
|
is SimpleDraweeView ->
|
||||||
|
it.controller = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
return ViewHolder(ReaderItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cache: Cache? = null
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = galleryInfo?.files?.size ?: 0
|
||||||
|
|
||||||
|
override fun onViewRecycled(holder: ViewHolder) {
|
||||||
|
holder.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class FrescoImageViewFactory : ImageViewFactory() {
|
||||||
|
var updateView: ((ImageInfo) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun createAnimatedImageView(
|
||||||
|
context: Context, imageType: Int,
|
||||||
|
initScaleType: Int
|
||||||
|
): View {
|
||||||
|
val view = SimpleDraweeView(context)
|
||||||
|
view.hierarchy.actualImageScaleType = scaleType(initScaleType)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadAnimatedContent(
|
||||||
|
view: View, imageType: Int,
|
||||||
|
imageFile: File
|
||||||
|
) {
|
||||||
|
if (view is SimpleDraweeView) {
|
||||||
|
val controller: DraweeController = Fresco.newDraweeControllerBuilder()
|
||||||
|
.setUri(Uri.parse("file://" + imageFile.absolutePath))
|
||||||
|
.setAutoPlayAnimations(true)
|
||||||
|
.setControllerListener(object: BaseControllerListener<ImageInfo>() {
|
||||||
|
override fun onIntermediateImageSet(id: String?, imageInfo: ImageInfo?) {
|
||||||
|
imageInfo?.let { updateView?.invoke(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
|
||||||
|
imageInfo?.let { updateView?.invoke(it) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
view.controller = controller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createThumbnailView(
|
||||||
|
context: Context,
|
||||||
|
scaleType: ImageView.ScaleType, willLoadFromNetwork: Boolean
|
||||||
|
): View {
|
||||||
|
return if (willLoadFromNetwork) {
|
||||||
|
val thumbnailView = SimpleDraweeView(context)
|
||||||
|
thumbnailView.hierarchy.actualImageScaleType = scaleType(scaleType)
|
||||||
|
thumbnailView
|
||||||
|
} else {
|
||||||
|
super.createThumbnailView(context, scaleType, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadThumbnailContent(view: View, thumbnail: Uri) {
|
||||||
|
if (view is SimpleDraweeView) {
|
||||||
|
val controller: DraweeController = Fresco.newDraweeControllerBuilder()
|
||||||
|
.setUri(thumbnail)
|
||||||
|
.build()
|
||||||
|
view.controller = controller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scaleType(value: Int): ScalingUtils.ScaleType {
|
||||||
|
return when (value) {
|
||||||
|
BigImageView.INIT_SCALE_TYPE_CENTER -> ScalingUtils.ScaleType.CENTER
|
||||||
|
BigImageView.INIT_SCALE_TYPE_CENTER_CROP -> ScalingUtils.ScaleType.CENTER_CROP
|
||||||
|
BigImageView.INIT_SCALE_TYPE_CENTER_INSIDE -> ScalingUtils.ScaleType.CENTER_INSIDE
|
||||||
|
BigImageView.INIT_SCALE_TYPE_FIT_END -> ScalingUtils.ScaleType.FIT_END
|
||||||
|
BigImageView.INIT_SCALE_TYPE_FIT_START -> ScalingUtils.ScaleType.FIT_START
|
||||||
|
BigImageView.INIT_SCALE_TYPE_FIT_XY -> ScalingUtils.ScaleType.FIT_XY
|
||||||
|
BigImageView.INIT_SCALE_TYPE_FIT_CENTER -> ScalingUtils.ScaleType.FIT_CENTER
|
||||||
|
else -> ScalingUtils.ScaleType.FIT_CENTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scaleType(scaleType: ImageView.ScaleType): ScalingUtils.ScaleType {
|
||||||
|
return when (scaleType) {
|
||||||
|
ImageView.ScaleType.CENTER -> ScalingUtils.ScaleType.CENTER
|
||||||
|
ImageView.ScaleType.CENTER_CROP -> ScalingUtils.ScaleType.CENTER_CROP
|
||||||
|
ImageView.ScaleType.CENTER_INSIDE -> ScalingUtils.ScaleType.CENTER_INSIDE
|
||||||
|
ImageView.ScaleType.FIT_END -> ScalingUtils.ScaleType.FIT_END
|
||||||
|
ImageView.ScaleType.FIT_START -> ScalingUtils.ScaleType.FIT_START
|
||||||
|
ImageView.ScaleType.FIT_XY -> ScalingUtils.ScaleType.FIT_XY
|
||||||
|
ImageView.ScaleType.FIT_CENTER -> ScalingUtils.ScaleType.FIT_CENTER
|
||||||
|
else -> ScalingUtils.ScaleType.FIT_CENTER
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,32 +18,35 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
package xyz.quaver.pupil.adapters
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.RequestManager
|
import com.github.piasy.biv.view.BigImageView
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.BuildConfig
|
|
||||||
|
|
||||||
class ThumbnailAdapter(private val glide: RequestManager, var thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() {
|
class ThumbnailAdapter(var thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() {
|
||||||
|
|
||||||
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
|
class ViewHolder(val view: BigImageView) : RecyclerView.ViewHolder(view) {
|
||||||
|
fun clear() {
|
||||||
|
view.ssiv?.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
return ViewHolder(ImageView(parent.context))
|
return ViewHolder(BigImageView(parent.context).apply {
|
||||||
|
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
glide
|
holder.view.showImage(Uri.parse(thumbnails[position]))
|
||||||
.load(thumbnails[position])
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.apply {
|
|
||||||
if (BuildConfig.CENSOR)
|
|
||||||
override(5, 8)
|
|
||||||
}
|
|
||||||
.into(holder.view)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = thumbnails.size
|
override fun getItemCount() = thumbnails.size
|
||||||
|
|
||||||
|
override fun onViewRecycled(holder: ViewHolder) {
|
||||||
|
holder.clear()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -21,17 +21,19 @@ package xyz.quaver.pupil.adapters
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.RequestManager
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class ThumbnailPageAdapter(private val glide: RequestManager, private val thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailPageAdapter.ViewHolder>() {
|
class ThumbnailPageAdapter(private val thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailPageAdapter.ViewHolder>() {
|
||||||
|
|
||||||
class ViewHolder(val view: RecyclerView) : RecyclerView.ViewHolder(view)
|
class ViewHolder(val view: RecyclerView) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
return ViewHolder(RecyclerView(parent.context).apply {
|
return ViewHolder(RecyclerView(parent.context).apply {
|
||||||
layoutManager = GridLayoutManager(parent.context, 3)
|
val layoutManager = GridLayoutManager(parent.context, 3)
|
||||||
adapter = ThumbnailAdapter(glide, listOf())
|
val adapter = ThumbnailAdapter(listOf())
|
||||||
|
|
||||||
|
this.layoutManager = layoutManager
|
||||||
|
this.adapter = adapter
|
||||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -41,7 +43,7 @@ class ThumbnailPageAdapter(private val glide: RequestManager, private val thumbn
|
|||||||
thumbnails = this@ThumbnailPageAdapter.thumbnails.slice(9*position until min(9*position+9, this@ThumbnailPageAdapter.thumbnails.size))
|
thumbnails = this@ThumbnailPageAdapter.thumbnails.slice(9*position until min(9*position+9, this@ThumbnailPageAdapter.thumbnails.size))
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
|
|
||||||
holder.view.layoutManager?.scrollToPosition(itemCount-1)
|
(holder.view.layoutManager as GridLayoutManager).scrollToPosition(8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
46
app/src/main/java/xyz/quaver/pupil/hitomi/Utils.kt
Normal file
46
app/src/main/java/xyz/quaver/pupil/hitomi/Utils.kt
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 tom5079
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Request
|
||||||
|
import xyz.quaver.pupil.client
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kotlinx.serialization.json.Json object for global use
|
||||||
|
* properties should not be changed
|
||||||
|
*
|
||||||
|
* @see [https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.json/-json/index.html]
|
||||||
|
*/
|
||||||
|
val json = Json {
|
||||||
|
isLenient = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
allowSpecialFloatingPointValues = true
|
||||||
|
useArrayPolymorphism = true
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias HeaderSetter = (Request.Builder) -> Request.Builder
|
||||||
|
fun URL.readText(settings: HeaderSetter? = null): String {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(this).let {
|
||||||
|
settings?.invoke(it) ?: it
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return client.newCall(request).execute().also{ if (it.code() != 200) throw IOException() }.body()?.use { it.string() } ?: throw IOException()
|
||||||
|
}
|
||||||
142
app/src/main/java/xyz/quaver/pupil/hitomi/common.kt
Normal file
142
app/src/main/java/xyz/quaver/pupil/hitomi/common.kt
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 tom5079
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.hitomi
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.google.common.collect.ConcurrentHashMultiset
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.transformWhile
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import xyz.quaver.json
|
||||||
|
import xyz.quaver.pupil.*
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
const val protocol = "https:"
|
||||||
|
|
||||||
|
val evaluationContext = Dispatchers.Main + Job()
|
||||||
|
|
||||||
|
suspend fun WebView.evaluate(script: String): String = coroutineScope {
|
||||||
|
var result: String? = null
|
||||||
|
|
||||||
|
while (result == null) {
|
||||||
|
try {
|
||||||
|
result = withContext(evaluationContext) {
|
||||||
|
while (webViewFailed || !webViewReady) yield()
|
||||||
|
|
||||||
|
suspendCoroutine { continuation ->
|
||||||
|
evaluateJavascript(script) {
|
||||||
|
continuation.resume(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
if (e.message != "reload") result = "null"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
suspend fun WebView.evaluatePromise(
|
||||||
|
script: String,
|
||||||
|
then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result))).catch(err => Callback.onError(%uid, JSON.stringify(error)))"
|
||||||
|
): String = coroutineScope {
|
||||||
|
var result: String? = null
|
||||||
|
|
||||||
|
while (result == null) {
|
||||||
|
try {
|
||||||
|
result = withContext(evaluationContext) {
|
||||||
|
while (webViewFailed || !webViewReady) yield()
|
||||||
|
|
||||||
|
val uid = UUID.randomUUID().toString()
|
||||||
|
|
||||||
|
val flow: Flow<Pair<String, String?>> = webViewFlow.transformWhile { (currentUid, result) ->
|
||||||
|
if (currentUid == uid) {
|
||||||
|
emit(currentUid to result)
|
||||||
|
}
|
||||||
|
currentUid != uid
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
evaluateJavascript((script + then).replace("%uid", "'$uid'"), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
flow.first().second
|
||||||
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
if (e.message != "reload") result = "null"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||||
|
suspend fun getGalleryInfo(galleryID: Int): GalleryInfo {
|
||||||
|
val result = webView.evaluatePromise("get_gallery_info($galleryID)")
|
||||||
|
|
||||||
|
return json.decodeFromString(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
//common.js
|
||||||
|
const val domain = "ltn.hitomi.la"
|
||||||
|
const val galleryblockdir = "galleryblock"
|
||||||
|
const val nozomiextension = ".nozomi"
|
||||||
|
|
||||||
|
val String?.js: String
|
||||||
|
get() = if (this == null) "null" else "'$this'"
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null): String {
|
||||||
|
val result = webView.evaluate(
|
||||||
|
"""
|
||||||
|
url_from_url_from_hash(
|
||||||
|
${galleryID.toString().js},
|
||||||
|
${Json.encodeToString(image)},
|
||||||
|
${dir.js}, ${ext.js}, ${base.js}
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
return Json.decodeFromString(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
|
||||||
|
return when {
|
||||||
|
noWebp ->
|
||||||
|
urlFromUrlFromHash(galleryID, image)
|
||||||
|
// image.hasavif != 0 ->
|
||||||
|
// urlFromUrlFromHash(galleryID, image, "avif", null, "a")
|
||||||
|
image.haswebp != 0 ->
|
||||||
|
urlFromUrlFromHash(galleryID, image, "webp", null, "a")
|
||||||
|
else ->
|
||||||
|
urlFromUrlFromHash(galleryID, image)
|
||||||
|
}
|
||||||
|
}
|
||||||
80
app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt
Normal file
80
app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 tom5079
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.hitomi
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import xyz.quaver.readText
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLDecoder
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Gallery(
|
||||||
|
val related: List<Int>,
|
||||||
|
val langList: List<Pair<String, String>>,
|
||||||
|
val cover: String,
|
||||||
|
val title: String,
|
||||||
|
val artists: List<String>,
|
||||||
|
val groups: List<String>,
|
||||||
|
val type: String,
|
||||||
|
val language: String,
|
||||||
|
val series: List<String>,
|
||||||
|
val characters: List<String>,
|
||||||
|
val tags: List<String>,
|
||||||
|
val thumbnails: List<String>
|
||||||
|
)
|
||||||
|
suspend fun getGallery(galleryID: Int) : Gallery {
|
||||||
|
val url = Jsoup.parse(URL("https://hitomi.la/galleries/$galleryID.html").readText())
|
||||||
|
.select("link").attr("href")
|
||||||
|
|
||||||
|
val doc = Jsoup.parse(URL(url).readText())
|
||||||
|
|
||||||
|
val related = Regex("\\d+")
|
||||||
|
.findAll(doc.select("script").first()!!.html())
|
||||||
|
.map {
|
||||||
|
it.value.toInt()
|
||||||
|
}.toList()
|
||||||
|
|
||||||
|
val langList = doc.select("#lang-list a").map {
|
||||||
|
Pair(it.text(), "$protocol//hitomi.la${it.attr("href")}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val cover = protocol + doc.selectFirst(".cover img")!!.attr("src")
|
||||||
|
val title = doc.selectFirst(".gallery h1 a")!!.text()
|
||||||
|
val artists = doc.select(".gallery h2 a").map { it.text() }
|
||||||
|
val groups = doc.select(".gallery-info a[href~=^/group/]").map { it.text() }
|
||||||
|
val type = doc.selectFirst(".gallery-info a[href~=^/type/]")!!.text()
|
||||||
|
|
||||||
|
val language = run {
|
||||||
|
val href = doc.select(".gallery-info a[href~=^/index.+\\.html\$]").attr("href")
|
||||||
|
Regex("""index-([^-]+)(-.+)?\.html""").find(href)?.groupValues?.getOrNull(1) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val series = doc.select(".gallery-info a[href~=^/series/]").map { it.text() }
|
||||||
|
val characters = doc.select(".gallery-info a[href~=^/character/]").map { it.text() }
|
||||||
|
|
||||||
|
val tags = doc.select(".gallery-info a[href~=^/tag/]").map {
|
||||||
|
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
||||||
|
href.slice(5 until href.indexOf('-'))
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbnails = getGalleryInfo(galleryID).files.map { galleryInfo ->
|
||||||
|
urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails)
|
||||||
|
}
|
||||||
79
app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt
Normal file
79
app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 tom5079
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.hitomi
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import xyz.quaver.pupil.webView
|
||||||
|
import xyz.quaver.readText
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.util.*
|
||||||
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GalleryBlock(
|
||||||
|
val id: Int,
|
||||||
|
val galleryUrl: String,
|
||||||
|
val thumbnails: List<String>,
|
||||||
|
val title: String,
|
||||||
|
val artists: List<String>,
|
||||||
|
val series: List<String>,
|
||||||
|
val type: String,
|
||||||
|
val language: String,
|
||||||
|
val relatedTags: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock {
|
||||||
|
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
|
||||||
|
|
||||||
|
val html: String = webView.evaluatePromise(
|
||||||
|
"""
|
||||||
|
$.get('$url').always(function(data, status) {
|
||||||
|
if (status === 'success') {
|
||||||
|
Callback.onResult(%uid, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
""".trimIndent(),
|
||||||
|
then = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
val doc = Jsoup.parse(html)
|
||||||
|
|
||||||
|
val galleryUrl = doc.selectFirst("h1 > a")!!.attr("href")
|
||||||
|
|
||||||
|
val thumbnails = doc.select(".dj-img-cont img").map { protocol + it.attr("data-src") }
|
||||||
|
|
||||||
|
val title = doc.selectFirst("h1 > a")!!.text()
|
||||||
|
val artists = doc.select(".artist-list a").map{ it.text() }
|
||||||
|
val series = doc.select(".dj-content a[href~=^/series/]").map { it.text() }
|
||||||
|
val type = doc.selectFirst("a[href~=^/type/]")!!.text()
|
||||||
|
|
||||||
|
val language = run {
|
||||||
|
val href = doc.select("a[href~=^/index.+\\.html\$]").attr("href")
|
||||||
|
Regex("""index-([^-]+)(-.+)?\.html""").find(href)?.groupValues?.getOrNull(1) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val relatedTags = doc.select(".relatedtags a").map {
|
||||||
|
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
||||||
|
href.slice(5 until href.indexOf("-all"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||||
|
}
|
||||||
49
app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt
Normal file
49
app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 tom5079
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.hitomi
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GalleryInfo(
|
||||||
|
val language_localname: String? = null,
|
||||||
|
val language: String? = null,
|
||||||
|
val date: String? = null,
|
||||||
|
val files: List<GalleryFiles>,
|
||||||
|
val id: Int? = null,
|
||||||
|
val type: String? = null,
|
||||||
|
val title: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GalleryFiles(
|
||||||
|
val width: Int,
|
||||||
|
val hash: String,
|
||||||
|
val haswebp: Int = 0,
|
||||||
|
val name: String,
|
||||||
|
val height: Int,
|
||||||
|
val hasavif: Int = 0,
|
||||||
|
val hasavifsmalltn: Int? = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
//Set header `Referer` to reader url to avoid 403 error
|
||||||
|
@Deprecated("", replaceWith = ReplaceWith("getGalleryInfo"))
|
||||||
|
suspend fun getReader(galleryID: Int) : GalleryInfo {
|
||||||
|
return getGalleryInfo(galleryID)
|
||||||
|
}
|
||||||
87
app/src/main/java/xyz/quaver/pupil/hitomi/results.kt
Normal file
87
app/src/main/java/xyz/quaver/pupil/hitomi/results.kt
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 tom5079
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.hitomi
|
||||||
|
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> = coroutineScope {
|
||||||
|
val terms = query
|
||||||
|
.trim()
|
||||||
|
.replace(Regex("""^\?"""), "")
|
||||||
|
.lowercase()
|
||||||
|
.split(Regex("\\s+"))
|
||||||
|
.map {
|
||||||
|
it.replace('_', ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
val positiveTerms = LinkedList<String>()
|
||||||
|
val negativeTerms = LinkedList<String>()
|
||||||
|
|
||||||
|
for (term in terms) {
|
||||||
|
if (term.matches(Regex("^-.+")))
|
||||||
|
negativeTerms.push(term.replace(Regex("^-"), ""))
|
||||||
|
else if (term.isNotBlank())
|
||||||
|
positiveTerms.push(term)
|
||||||
|
}
|
||||||
|
|
||||||
|
val positiveResults = positiveTerms.map {
|
||||||
|
async {
|
||||||
|
runCatching {
|
||||||
|
getGalleryIDsForQuery(it)
|
||||||
|
}.getOrElse { emptySet() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val negativeResults = negativeTerms.mapIndexed { index, it ->
|
||||||
|
async {
|
||||||
|
runCatching {
|
||||||
|
getGalleryIDsForQuery(it)
|
||||||
|
}.getOrElse { emptySet() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val results = when {
|
||||||
|
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all")
|
||||||
|
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all")
|
||||||
|
else -> emptySet()
|
||||||
|
}.toMutableSet()
|
||||||
|
|
||||||
|
fun filterPositive(newResults: Set<Int>) {
|
||||||
|
when {
|
||||||
|
results.isEmpty() -> results.addAll(newResults)
|
||||||
|
else -> results.retainAll(newResults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filterNegative(newResults: Set<Int>) {
|
||||||
|
results.removeAll(newResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
//positive results
|
||||||
|
positiveResults.forEach {
|
||||||
|
filterPositive(it.await())
|
||||||
|
}
|
||||||
|
|
||||||
|
//negative results
|
||||||
|
negativeResults.forEachIndexed { index, it ->
|
||||||
|
filterNegative(it.await())
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
66
app/src/main/java/xyz/quaver/pupil/hitomi/search.kt
Normal file
66
app/src/main/java/xyz/quaver/pupil/hitomi/search.kt
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 tom5079
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.hitomi
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import xyz.quaver.pupil.webView
|
||||||
|
|
||||||
|
//searchlib.js
|
||||||
|
const val extension = ".html"
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
suspend fun getGalleryIDsForQuery(query: String) : Set<Int> {
|
||||||
|
val result = webView.evaluatePromise("get_galleryids_for_query('$query')")
|
||||||
|
|
||||||
|
return Json.decodeFromString(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
suspend fun getSuggestionsForQuery(query: String) : List<Suggestion> {
|
||||||
|
val result = webView.evaluatePromise(
|
||||||
|
"get_suggestions_for_query('$query', ++search_serial)",
|
||||||
|
then = """
|
||||||
|
.then(r => {
|
||||||
|
let [results, results_serial] = r;
|
||||||
|
console.log(results_serial, r, search_serial);
|
||||||
|
if (search_serial !== results_serial) {
|
||||||
|
Callback.onResult(%uid, '[]');
|
||||||
|
} else {
|
||||||
|
Callback.onResult(%uid, JSON.stringify(results));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
return Json.decodeFromString(result) ?: return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
suspend fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> {
|
||||||
|
val jsArea = if (area == null) "null" else "'$area'"
|
||||||
|
|
||||||
|
val json = webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""")
|
||||||
|
|
||||||
|
return Json.decodeFromString(json)
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
@@ -54,7 +55,7 @@ class UpdateBroadcastReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
val uri = downloadManager.query(query).use { cursor ->
|
val uri = downloadManager.query(query).use { cursor ->
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).let {
|
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))?.let {
|
||||||
val uri = Uri.parse(it)
|
val uri = Uri.parse(it)
|
||||||
|
|
||||||
when (uri.scheme) {
|
when (uri.scheme) {
|
||||||
@@ -76,7 +77,7 @@ class UpdateBroadcastReceiver : BroadcastReceiver() {
|
|||||||
val pendingIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW).apply {
|
val pendingIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||||
}, 0)
|
}, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else 0)
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(context, "update")
|
val notification = NotificationCompat.Builder(context, "update")
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ import android.app.PendingIntent
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.SparseArray
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.app.TaskStackBuilder
|
import androidx.core.app.TaskStackBuilder
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.google.common.util.concurrent.RateLimiter
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -38,18 +40,15 @@ import okhttp3.Callback
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import okio.*
|
import okio.*
|
||||||
import xyz.quaver.pupil.PupilInterceptor
|
import xyz.quaver.pupil.*
|
||||||
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 xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.*
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
import xyz.quaver.pupil.util.ellipsize
|
|
||||||
import xyz.quaver.pupil.util.normalizeID
|
|
||||||
import xyz.quaver.pupil.util.requestBuilders
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.log10
|
||||||
|
|
||||||
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
|
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
|
||||||
class DownloadService : Service() {
|
class DownloadService : Service() {
|
||||||
@@ -68,7 +67,7 @@ class DownloadService : Service() {
|
|||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val notification = SparseArray<NotificationCompat.Builder?>()
|
private val notification = ConcurrentHashMap<Int, NotificationCompat.Builder?>()
|
||||||
|
|
||||||
private fun initNotification(galleryID: Int) {
|
private fun initNotification(galleryID: Int) {
|
||||||
val intent = Intent(this, ReaderActivity::class.java)
|
val intent = Intent(this, ReaderActivity::class.java)
|
||||||
@@ -76,7 +75,7 @@ class DownloadService : Service() {
|
|||||||
|
|
||||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||||
addNextIntentWithParentStack(intent)
|
addNextIntentWithParentStack(intent)
|
||||||
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
|
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0)
|
||||||
}
|
}
|
||||||
val action =
|
val action =
|
||||||
NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
||||||
@@ -86,10 +85,10 @@ class DownloadService : Service() {
|
|||||||
Intent(this, DownloadService::class.java)
|
Intent(this, DownloadService::class.java)
|
||||||
.putExtra(KEY_COMMAND, COMMAND_CANCEL)
|
.putExtra(KEY_COMMAND, COMMAND_CANCEL)
|
||||||
.putExtra(KEY_ID, galleryID),
|
.putExtra(KEY_ID, galleryID),
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT),
|
PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0),
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
|
notification[galleryID] = NotificationCompat.Builder(this, "download").apply {
|
||||||
setContentTitle(getString(R.string.reader_loading))
|
setContentTitle(getString(R.string.reader_loading))
|
||||||
setContentText(getString(R.string.reader_notification_text))
|
setContentText(getString(R.string.reader_notification_text))
|
||||||
setSmallIcon(R.drawable.ic_notification)
|
setSmallIcon(R.drawable.ic_notification)
|
||||||
@@ -97,7 +96,7 @@ class DownloadService : Service() {
|
|||||||
addAction(action)
|
addAction(action)
|
||||||
setProgress(0, 0, true)
|
setProgress(0, 0, true)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
})
|
}
|
||||||
|
|
||||||
notify(galleryID)
|
notify(galleryID)
|
||||||
}
|
}
|
||||||
@@ -122,7 +121,7 @@ class DownloadService : Service() {
|
|||||||
.setProgress(max, progress, false)
|
.setProgress(max, progress, false)
|
||||||
.setContentText("$progress/$max")
|
.setContentText("$progress/$max")
|
||||||
|
|
||||||
if (DownloadManager.getInstance(this).getDownloadFolder(galleryID) != null)
|
if (DownloadManager.getInstance(this).getDownloadFolder(galleryID) != null || galleryID == priority)
|
||||||
notification.let { notificationManager.notify(galleryID, it.build()) }
|
notification.let { notificationManager.notify(galleryID, it.build()) }
|
||||||
else
|
else
|
||||||
notificationManager.cancel(galleryID)
|
notificationManager.cancel(galleryID)
|
||||||
@@ -167,14 +166,22 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val rateLimiter = RateLimiter.create(2.0)
|
||||||
|
private val rateLimitHost = Regex("..?\\.hitomi.la")
|
||||||
|
|
||||||
private val interceptor: PupilInterceptor = { chain ->
|
private val interceptor: PupilInterceptor = { chain ->
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
var response = chain.proceed(request)
|
|
||||||
|
|
||||||
var retry = 5
|
var response = chain.proceed(request)
|
||||||
while (!response.isSuccessful && retry > 0) {
|
var limit = 5
|
||||||
|
|
||||||
|
while (!response.isSuccessful) {
|
||||||
|
if (response.code() == 503) {
|
||||||
|
Thread.sleep(200)
|
||||||
|
} else if (--limit > 0)
|
||||||
|
break
|
||||||
|
|
||||||
response = chain.proceed(request)
|
response = chain.proceed(request)
|
||||||
retry--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response.newBuilder()
|
response.newBuilder()
|
||||||
@@ -196,19 +203,18 @@ class DownloadService : Service() {
|
|||||||
* 0 <= value < 100 -> Download in progress
|
* 0 <= value < 100 -> Download in progress
|
||||||
* Float.POSITIVE_INFINITY -> Download completed
|
* Float.POSITIVE_INFINITY -> Download completed
|
||||||
*/
|
*/
|
||||||
val progress = SparseArray<MutableList<Float>?>()
|
val progress = ConcurrentHashMap<Int, MutableList<Float>>()
|
||||||
|
var priority = 0
|
||||||
|
|
||||||
fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
|
fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
|
||||||
|
|
||||||
private val callback = object: Callback {
|
private val callback = object: Callback {
|
||||||
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
|
||||||
if (e.message?.contains("cancel", true) == false) {
|
if (e.message?.contains("cancel", true) == false) {
|
||||||
val galleryID = (call.request().tag() as Tag).galleryID
|
val galleryID = (call.request().tag() as Tag).galleryID
|
||||||
|
|
||||||
// Retry
|
|
||||||
cancel(galleryID)
|
|
||||||
download(galleryID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,11 +223,12 @@ class DownloadService : Service() {
|
|||||||
val ext = call.request().url().encodedPath().split('.').last()
|
val ext = call.request().url().encodedPath().split('.').last()
|
||||||
|
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val image = response.also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } ?: throw Exception()
|
val image = response.also { if (it.code() != 200) throw IOException("$galleryID $index ${response.request().url()} CODE ${it.code()}") }.body()?.use { it.bytes() } ?: throw Exception("Response null")
|
||||||
|
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
Cache.getInstance(this@DownloadService, galleryID).putImage(index, "$index.$ext", image)
|
Cache.getInstance(this@DownloadService, galleryID).putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
|
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
|
||||||
notify(galleryID)
|
notify(galleryID)
|
||||||
@@ -234,10 +241,12 @@ class DownloadService : Service() {
|
|||||||
startId?.let { stopSelf(it) }
|
startId?.let { stopSelf(it) }
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
cancel(galleryID)
|
FirebaseCrashlytics.getInstance().recordException(it)
|
||||||
download(galleryID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTrace()
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,51 +296,35 @@ class DownloadService : Service() {
|
|||||||
fun delete(galleryID: Int, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
|
fun delete(galleryID: Int, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
cancel(galleryID)
|
cancel(galleryID)
|
||||||
DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
|
DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
|
||||||
Cache.delete(galleryID)
|
Cache.delete(this@DownloadService, galleryID)
|
||||||
|
|
||||||
startId?.let { stopSelf(it) }
|
startId?.let { stopSelf(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
|
fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
|
||||||
if (progress.indexOfKey(galleryID) >= 0)
|
if (DownloadManager.getInstance(this@DownloadService).isDownloading(galleryID))
|
||||||
cancel(galleryID)
|
return@launch
|
||||||
|
|
||||||
|
cleanCache(this@DownloadService)
|
||||||
|
|
||||||
val cache = Cache.getInstance(this@DownloadService, galleryID)
|
val cache = Cache.getInstance(this@DownloadService, galleryID)
|
||||||
|
|
||||||
initNotification(galleryID)
|
initNotification(galleryID)
|
||||||
|
|
||||||
val reader = cache.getReader()
|
val galleryInfo = cache.getGalleryInfo()
|
||||||
|
|
||||||
// Gallery doesn't exist
|
// Gallery doesn't exist
|
||||||
if (reader == null) {
|
if (galleryInfo == null) {
|
||||||
delete(galleryID)
|
delete(galleryID)
|
||||||
progress.put(galleryID, null)
|
progress[galleryID] = mutableListOf()
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.put(galleryID, MutableList(reader.galleryInfo.files.size) { 0F })
|
histories.add(galleryID)
|
||||||
|
|
||||||
FirebaseCrashlytics.getInstance().log(
|
progress[galleryID] = MutableList(galleryInfo.files.size) { 0F }
|
||||||
"""
|
|
||||||
GALLERYID: $galleryID
|
|
||||||
CACHE: ${cache.findFile(".metadata")}
|
|
||||||
PATTERN: ${Preferences["download_folder_name", ""]}
|
|
||||||
READER ID: ${reader.galleryInfo.id}
|
|
||||||
READER SIZE: ${reader.galleryInfo.files.size}
|
|
||||||
CACHE READER ID: ${cache.metadata.reader?.galleryInfo?.id}}
|
|
||||||
CACHE READER SIZE: ${cache.metadata.reader?.galleryInfo?.files?.size}
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
|
|
||||||
cache.metadata.imageList?.let {
|
cache.metadata.imageList?.let {
|
||||||
if (progress[galleryID]?.size != it.size) {
|
|
||||||
cache.metadata.imageList?.filterNotNull()?.forEach { file ->
|
|
||||||
cache.findFile(file)?.delete()
|
|
||||||
}
|
|
||||||
cache.metadata.imageList = MutableList(reader.galleryInfo.files.size) { null }
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
|
|
||||||
it.forEachIndexed { index, image ->
|
it.forEachIndexed { index, image ->
|
||||||
progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F)
|
progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F)
|
||||||
}
|
}
|
||||||
@@ -347,7 +340,7 @@ class DownloadService : Service() {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
notification[galleryID]?.setContentTitle(reader.galleryInfo.title?.ellipsize(30))
|
notification[galleryID]?.setContentTitle(galleryInfo.title?.ellipsize(30))
|
||||||
notify(galleryID)
|
notify(galleryID)
|
||||||
|
|
||||||
val queued = mutableSetOf<Int>()
|
val queued = mutableSetOf<Int>()
|
||||||
@@ -361,8 +354,8 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.requestBuilders.forEachIndexed { index, it ->
|
galleryInfo.getRequestBuilders().forEachIndexed { index, it ->
|
||||||
if (progress[galleryID]?.get(index)?.isInfinite() != true) {
|
if (progress[galleryID]?.get(index)?.isInfinite() == false) {
|
||||||
val request = it.tag(Tag(galleryID, index, startId)).build()
|
val request = it.tag(Tag(galleryID, index, startId)).build()
|
||||||
client.newCall(request).enqueue(callback)
|
client.newCall(request).enqueue(callback)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,36 +18,33 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.types
|
package xyz.quaver.pupil.types
|
||||||
|
|
||||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import xyz.quaver.hitomi.Suggestion
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
|
import xyz.quaver.pupil.hitomi.Suggestion
|
||||||
|
import xyz.quaver.pupil.util.translations
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion {
|
data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion {
|
||||||
constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n)
|
constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n)
|
||||||
|
|
||||||
override fun getBody(): String {
|
@IgnoredOnParcel
|
||||||
return s
|
override val body =
|
||||||
}
|
if (translations[s] != null)
|
||||||
|
"${translations[s]} ($s)"
|
||||||
|
else
|
||||||
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Suggestion(val str: String) : SearchSuggestion {
|
class Suggestion(override val body: String) : SearchSuggestion
|
||||||
override fun getBody() = str
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class NoResultSuggestion(val str: String) : SearchSuggestion {
|
class NoResultSuggestion(override val body: String) : SearchSuggestion
|
||||||
override fun getBody() = str
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class LoadingSuggestion(val str: String) : SearchSuggestion {
|
class LoadingSuggestion(override val body: String) : SearchSuggestion
|
||||||
override fun getBody() = str
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY")
|
@Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY")
|
||||||
class FavoriteHistorySwitch(private val body: String) : SearchSuggestion {
|
class FavoriteHistorySwitch(override val body: String) : SearchSuggestion
|
||||||
override fun getBody() = body
|
|
||||||
}
|
|
||||||
@@ -23,6 +23,7 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PersistableBundle
|
import android.os.PersistableBundle
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
@@ -34,6 +35,13 @@ open class BaseActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private var locked: Boolean = true
|
private var locked: Boolean = true
|
||||||
|
|
||||||
|
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK)
|
||||||
|
locked = false
|
||||||
|
else
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
||||||
super.onCreate(savedInstanceState, persistentState)
|
super.onCreate(savedInstanceState, persistentState)
|
||||||
@@ -53,20 +61,7 @@ open class BaseActivity : AppCompatActivity() {
|
|||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
|
||||||
if (locked)
|
if (locked)
|
||||||
startActivityForResult(Intent(this, LockActivity::class.java), R.id.request_lock.normalizeID())
|
lockLauncher.launch(Intent(this, LockActivity::class.java))
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
when(requestCode) {
|
|
||||||
R.id.request_lock.normalizeID() -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK)
|
|
||||||
locked = false
|
|
||||||
else
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -29,10 +29,8 @@ import androidx.biometric.BiometricPrompt
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
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.fragment_pattern_lock.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_pin_lock.*
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.databinding.LockActivityBinding
|
||||||
import xyz.quaver.pupil.ui.fragment.PINLockFragment
|
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
|
||||||
@@ -45,6 +43,8 @@ class LockActivity : AppCompatActivity() {
|
|||||||
private lateinit var lockManager: LockManager
|
private lateinit var lockManager: LockManager
|
||||||
private var mode: String? = null
|
private var mode: String? = null
|
||||||
|
|
||||||
|
private lateinit var binding: LockActivityBinding
|
||||||
|
|
||||||
private val patternLockFragment = PatternLockFragment().apply {
|
private val patternLockFragment = PatternLockFragment().apply {
|
||||||
var lastPass = ""
|
var lastPass = ""
|
||||||
onPatternDrawn = {
|
onPatternDrawn = {
|
||||||
@@ -57,7 +57,7 @@ class LockActivity : AppCompatActivity() {
|
|||||||
setResult(Activity.RESULT_OK)
|
setResult(Activity.RESULT_OK)
|
||||||
finish()
|
finish()
|
||||||
} else
|
} else
|
||||||
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||||
}
|
}
|
||||||
"add_lock" -> {
|
"add_lock" -> {
|
||||||
if (lastPass.isEmpty()) {
|
if (lastPass.isEmpty()) {
|
||||||
@@ -69,7 +69,7 @@ class LockActivity : AppCompatActivity() {
|
|||||||
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
|
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||||
lastPass = ""
|
lastPass = ""
|
||||||
|
|
||||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
||||||
@@ -92,15 +92,15 @@ class LockActivity : AppCompatActivity() {
|
|||||||
setResult(Activity.RESULT_OK)
|
setResult(Activity.RESULT_OK)
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
||||||
setAnimationListener(object: Animation.AnimationListener {
|
setAnimationListener(object: Animation.AnimationListener {
|
||||||
override fun onAnimationEnd(animation: Animation?) {
|
override fun onAnimationEnd(animation: Animation?) {
|
||||||
pin_lock_view.resetPinLockView()
|
binding.pinLockView.resetPinLockView()
|
||||||
pin_lock_view.isEnabled = true
|
binding.pinLockView.isEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationStart(animation: Animation?) {
|
override fun onAnimationStart(animation: Animation?) {
|
||||||
pin_lock_view.isEnabled = false
|
binding.pinLockView.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation?) {
|
override fun onAnimationRepeat(animation: Animation?) {
|
||||||
@@ -114,22 +114,22 @@ class LockActivity : AppCompatActivity() {
|
|||||||
if (lastPass.isEmpty()) {
|
if (lastPass.isEmpty()) {
|
||||||
lastPass = it
|
lastPass = it
|
||||||
|
|
||||||
pin_lock_view.resetPinLockView()
|
binding.pinLockView.resetPinLockView()
|
||||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
||||||
} else {
|
} else {
|
||||||
if (lastPass == it) {
|
if (lastPass == it) {
|
||||||
LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it))
|
LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it))
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
||||||
setAnimationListener(object: Animation.AnimationListener {
|
setAnimationListener(object: Animation.AnimationListener {
|
||||||
override fun onAnimationEnd(animation: Animation?) {
|
override fun onAnimationEnd(animation: Animation?) {
|
||||||
pin_lock_view.resetPinLockView()
|
binding.pinLockView.resetPinLockView()
|
||||||
pin_lock_view.isEnabled = true
|
binding.pinLockView.isEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationStart(animation: Animation?) {
|
override fun onAnimationStart(animation: Animation?) {
|
||||||
pin_lock_view.isEnabled = false
|
binding.pinLockView.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation?) {
|
override fun onAnimationRepeat(animation: Animation?) {
|
||||||
@@ -173,7 +173,8 @@ class LockActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_lock)
|
binding = LockActivityBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
lockManager = try {
|
lockManager = try {
|
||||||
LockManager(this)
|
LockManager(this)
|
||||||
@@ -210,7 +211,7 @@ class LockActivity : AppCompatActivity() {
|
|||||||
Preferences["lock_fingerprint"]
|
Preferences["lock_fingerprint"]
|
||||||
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
) {
|
) {
|
||||||
lock_fingerprint.apply {
|
binding.fingerprintBtn.apply {
|
||||||
isEnabled = true
|
isEnabled = true
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
showBiometricPrompt()
|
showBiometricPrompt()
|
||||||
@@ -219,7 +220,7 @@ class LockActivity : AppCompatActivity() {
|
|||||||
showBiometricPrompt()
|
showBiometricPrompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
lock_pattern.apply {
|
binding.patternBtn.apply {
|
||||||
isEnabled = lockManager.contains(Lock.Type.PATTERN)
|
isEnabled = lockManager.contains(Lock.Type.PATTERN)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
supportFragmentManager.beginTransaction().replace(
|
supportFragmentManager.beginTransaction().replace(
|
||||||
@@ -227,7 +228,7 @@ class LockActivity : AppCompatActivity() {
|
|||||||
).commit()
|
).commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lock_pin.apply {
|
binding.pinBtn.apply {
|
||||||
isEnabled = lockManager.contains(Lock.Type.PIN)
|
isEnabled = lockManager.contains(Lock.Type.PIN)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
supportFragmentManager.beginTransaction().replace(
|
supportFragmentManager.beginTransaction().replace(
|
||||||
@@ -235,7 +236,7 @@ class LockActivity : AppCompatActivity() {
|
|||||||
).commit()
|
).commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lock_password.isEnabled = false
|
binding.passwordBtn.isEnabled = false
|
||||||
|
|
||||||
when (lockManager.locks!!.first().type) {
|
when (lockManager.locks!!.first().type) {
|
||||||
Lock.Type.PIN -> {
|
Lock.Type.PIN -> {
|
||||||
@@ -253,20 +254,20 @@ class LockActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"add_lock" -> {
|
"add_lock" -> {
|
||||||
lock_pattern.isEnabled = false
|
binding.patternBtn.isEnabled = false
|
||||||
lock_pin.isEnabled = false
|
binding.pinBtn.isEnabled = false
|
||||||
lock_fingerprint.isEnabled = false
|
binding.fingerprintBtn.isEnabled = false
|
||||||
lock_password.isEnabled = false
|
binding.passwordBtn.isEnabled = false
|
||||||
|
|
||||||
when(intent.getStringExtra("type")!!) {
|
when(intent.getStringExtra("type")!!) {
|
||||||
"pattern" -> {
|
"pattern" -> {
|
||||||
lock_pattern.isEnabled = true
|
binding.patternBtn.isEnabled = true
|
||||||
supportFragmentManager.beginTransaction().add(
|
supportFragmentManager.beginTransaction().add(
|
||||||
R.id.lock_content, patternLockFragment
|
R.id.lock_content, patternLockFragment
|
||||||
).commit()
|
).commit()
|
||||||
}
|
}
|
||||||
"pin" -> {
|
"pin" -> {
|
||||||
lock_pin.isEnabled = true
|
binding.pinBtn.isEnabled = true
|
||||||
supportFragmentManager.beginTransaction().add(
|
supportFragmentManager.beginTransaction().add(
|
||||||
R.id.lock_content, pinLockFragment
|
R.id.lock_content, pinLockFragment
|
||||||
).commit()
|
).commit()
|
||||||
|
|||||||
@@ -21,46 +21,53 @@ package xyz.quaver.pupil.ui
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.view.*
|
import android.text.util.Linkify
|
||||||
import android.widget.*
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.cardview.widget.CardView
|
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import com.arlib.floatingsearchview.FloatingSearchView
|
import androidx.core.view.ViewCompat
|
||||||
import com.arlib.floatingsearchview.FloatingSearchViewDayNight
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
|
||||||
import com.arlib.floatingsearchview.util.view.SearchInputView
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
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_main.*
|
|
||||||
import kotlinx.android.synthetic.main.activity_main_content.*
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import xyz.quaver.hitomi.doSearch
|
import xyz.quaver.floatingsearchview.FloatingSearchView
|
||||||
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import xyz.quaver.hitomi.getSuggestionsForQuery
|
import xyz.quaver.floatingsearchview.util.view.MenuView
|
||||||
|
import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
||||||
|
import xyz.quaver.pupil.hitomi.doSearch
|
||||||
|
import xyz.quaver.pupil.hitomi.getGalleryIDsFromNozomi
|
||||||
|
import xyz.quaver.pupil.hitomi.getSuggestionsForQuery
|
||||||
import xyz.quaver.pupil.*
|
import xyz.quaver.pupil.*
|
||||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||||
|
import xyz.quaver.pupil.databinding.MainActivityBinding
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
import xyz.quaver.pupil.services.DownloadService
|
||||||
import xyz.quaver.pupil.types.*
|
import xyz.quaver.pupil.types.*
|
||||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
|
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.ui.view.MainView
|
||||||
|
import xyz.quaver.pupil.ui.view.ProgressCard
|
||||||
|
import xyz.quaver.pupil.util.ItemClickSupport
|
||||||
|
import xyz.quaver.pupil.util.Preferences
|
||||||
|
import xyz.quaver.pupil.util.checkUpdate
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
import kotlin.math.abs
|
import xyz.quaver.pupil.util.restore
|
||||||
import kotlin.math.ceil
|
import java.util.regex.Pattern
|
||||||
import kotlin.math.min
|
import kotlin.math.*
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
class MainActivity :
|
class MainActivity :
|
||||||
BaseActivity(),
|
BaseActivity(),
|
||||||
FloatingSearchView.OnMenuItemClickListener,
|
|
||||||
NavigationView.OnNavigationItemSelectedListener
|
NavigationView.OnNavigationItemSelectedListener
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -96,19 +103,20 @@ class MainActivity :
|
|||||||
private var loadingJob: Job? = null
|
private var loadingJob: Job? = null
|
||||||
private var currentPage = 0
|
private var currentPage = 0
|
||||||
|
|
||||||
|
private lateinit var binding: MainActivityBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
binding = MainActivityBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
if (intent.action == Intent.ACTION_VIEW) {
|
if (intent.action == Intent.ACTION_VIEW) {
|
||||||
intent.dataString?.let { url ->
|
intent.dataString?.let { url ->
|
||||||
restore(url,
|
restore(url,
|
||||||
onFailure = {
|
onFailure = {
|
||||||
Snackbar.make(this.main_recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
||||||
}, onSuccess = {
|
}, onSuccess = {
|
||||||
Snackbar.make(this.main_recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
|
Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -117,15 +125,35 @@ class MainActivity :
|
|||||||
if (Preferences["download_folder", ""].isEmpty())
|
if (Preferences["download_folder", ""].isEmpty())
|
||||||
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
|
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
|
||||||
|
|
||||||
checkUpdate(this)
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||||
|
!Preferences["download_folder_ignore_warning", false] &&
|
||||||
|
ContextCompat.getExternalFilesDirs(this, null).filterNotNull().map { Uri.fromFile(it).toString() }
|
||||||
|
.contains(Preferences["download_folder", ""])
|
||||||
|
) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.warning)
|
||||||
|
.setMessage(R.string.unaccessible_download_folder)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
|
||||||
|
}.setNegativeButton(R.string.ignore) { _, _ ->
|
||||||
|
Preferences["download_folder_ignore_warning"] = true
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
initView()
|
initView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
checkUpdate(this)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
when {
|
when {
|
||||||
main_drawer_layout.isDrawerOpen(GravityCompat.START) -> main_drawer_layout.closeDrawer(GravityCompat.START)
|
binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(GravityCompat.START)
|
||||||
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
|
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
|
||||||
query = queryStack.last()
|
query = queryStack.last()
|
||||||
|
|
||||||
@@ -141,7 +169,7 @@ class MainActivity :
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
|
(binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
@@ -181,49 +209,37 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
when(requestCode) {
|
|
||||||
R.id.request_settings.normalizeID() -> {
|
|
||||||
runOnUiThread {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
var prevP1 = 0
|
binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||||
main_appbar_layout.addOnOffsetChangedListener(
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
AppBarLayout.OnOffsetChangedListener { _, p1 ->
|
// -height of the search view < translationY < 0
|
||||||
main_searchview.translationY = p1.toFloat()
|
binding.contents.searchview.translationY =
|
||||||
main_recyclerview.scrollBy(0, prevP1 - p1)
|
min(
|
||||||
|
max(
|
||||||
|
binding.contents.searchview.translationY - dy,
|
||||||
|
-binding.contents.searchview.binding.querySection.root.height.toFloat()
|
||||||
|
), 0F)
|
||||||
|
|
||||||
with(main_fab) {
|
if (dy > 0)
|
||||||
if (prevP1 > p1)
|
binding.contents.fab.hideMenuButton(true)
|
||||||
hideMenuButton(true)
|
else if (dy < 0)
|
||||||
else if (prevP1 < p1)
|
binding.contents.fab.showMenuButton(true)
|
||||||
showMenuButton(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
prevP1 = p1
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
|
Linkify.addLinks(binding.contents.noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) })
|
||||||
|
|
||||||
//NavigationView
|
//NavigationView
|
||||||
main_nav_view.setNavigationItemSelectedListener(this)
|
binding.navView.setNavigationItemSelectedListener(this)
|
||||||
|
|
||||||
with(main_fab_cancel) {
|
with(binding.contents.cancelFab) {
|
||||||
setImageResource(R.drawable.cancel)
|
setImageResource(R.drawable.cancel)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
DownloadService.cancel(this@MainActivity)
|
DownloadService.cancel(this@MainActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(main_fab_jump) {
|
with(binding.contents.jumpFab) {
|
||||||
setImageResource(R.drawable.ic_jump)
|
setImageResource(R.drawable.ic_jump)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
val perPage = Preferences["per_page", "25"].toInt()
|
val perPage = Preferences["per_page", "25"].toInt()
|
||||||
@@ -251,7 +267,7 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(main_fab_random) {
|
with(binding.contents.randomFab) {
|
||||||
setImageResource(R.drawable.shuffle_variant)
|
setImageResource(R.drawable.shuffle_variant)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
@@ -262,11 +278,7 @@ class MainActivity :
|
|||||||
if (it?.isEmpty() == false) {
|
if (it?.isEmpty() == false) {
|
||||||
val galleryID = it.random()
|
val galleryID = it.random()
|
||||||
|
|
||||||
GalleryDialog(
|
GalleryDialog(this@MainActivity, galleryID).apply {
|
||||||
this@MainActivity,
|
|
||||||
Glide.with(this@MainActivity),
|
|
||||||
galleryID
|
|
||||||
).apply {
|
|
||||||
onChipClickedHandler.add {
|
onChipClickedHandler.add {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
query = it.toQuery()
|
query = it.toQuery()
|
||||||
@@ -285,7 +297,7 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(main_fab_id) {
|
with(binding.contents.idFab) {
|
||||||
setImageResource(R.drawable.numeric)
|
setImageResource(R.drawable.numeric)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
val editText = EditText(context).apply {
|
val editText = EditText(context).apply {
|
||||||
@@ -297,17 +309,65 @@ class MainActivity :
|
|||||||
setTitle(R.string.main_open_gallery_by_id)
|
setTitle(R.string.main_open_gallery_by_id)
|
||||||
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton
|
val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton
|
||||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
|
|
||||||
putExtra("galleryID", galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
startActivity(intent)
|
GalleryDialog(this@MainActivity, galleryID).apply {
|
||||||
|
onChipClickedHandler.add {
|
||||||
|
runOnUiThread {
|
||||||
|
query = it.toQuery()
|
||||||
|
currentPage = 0
|
||||||
|
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
with(binding.contents.view) {
|
||||||
|
setOnPageTurnListener(object: MainView.OnPageTurnListener {
|
||||||
|
override fun onPrev(page: Int) {
|
||||||
|
currentPage--
|
||||||
|
|
||||||
|
// disable pageturn until the contents are loaded
|
||||||
|
setCurrentPage(1, false)
|
||||||
|
|
||||||
|
ViewCompat.animate(binding.contents.searchview)
|
||||||
|
.setDuration(100)
|
||||||
|
.setInterpolator(DecelerateInterpolator())
|
||||||
|
.translationY(0F)
|
||||||
|
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNext(page: Int) {
|
||||||
|
currentPage++
|
||||||
|
|
||||||
|
// disable pageturn until the contents are loaded
|
||||||
|
setCurrentPage(1, false)
|
||||||
|
|
||||||
|
ViewCompat.animate(binding.contents.searchview)
|
||||||
|
.setDuration(100)
|
||||||
|
.setInterpolator(DecelerateInterpolator())
|
||||||
|
.translationY(0F)
|
||||||
|
|
||||||
|
cancelFetch()
|
||||||
|
clearGalleries()
|
||||||
|
fetchGalleries(query, sortMode)
|
||||||
|
loadBlocks()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
setupSearchBar()
|
setupSearchBar()
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
fetchGalleries(query, sortMode)
|
fetchGalleries(query, sortMode)
|
||||||
@@ -316,8 +376,8 @@ class MainActivity :
|
|||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
with(main_recyclerview) {
|
with(binding.contents.recyclerview) {
|
||||||
adapter = GalleryBlockAdapter(Glide.with(this@MainActivity), galleries).apply {
|
adapter = GalleryBlockAdapter(galleries).apply {
|
||||||
onChipClickedHandler.add {
|
onChipClickedHandler.add {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
query = it.toQuery()
|
query = it.toQuery()
|
||||||
@@ -331,16 +391,13 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
onDownloadClickedHandler = { position ->
|
onDownloadClickedHandler = { position ->
|
||||||
val galleryID = galleries[position]
|
val galleryID = galleries[position]
|
||||||
if (Preferences["cache_disable"])
|
|
||||||
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
|
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
|
||||||
|
DownloadService.cancel(this@MainActivity, galleryID)
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
|
DownloadManager.getInstance(context).addDownloadFolder(galleryID)
|
||||||
DownloadService.cancel(this@MainActivity, galleryID)
|
DownloadService.download(this@MainActivity, galleryID)
|
||||||
}
|
|
||||||
else {
|
|
||||||
DownloadManager.getInstance(context).addDownloadFolder(galleryID)
|
|
||||||
DownloadService.download(this@MainActivity, galleryID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAllItems()
|
closeAllItems()
|
||||||
@@ -360,14 +417,12 @@ class MainActivity :
|
|||||||
loadBlocks()
|
loadBlocks()
|
||||||
}
|
}
|
||||||
|
|
||||||
completeFlag.put(galleryID, false)
|
|
||||||
|
|
||||||
closeAllItems()
|
closeAllItems()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ItemClickSupport.addTo(this).apply {
|
ItemClickSupport.addTo(this).apply {
|
||||||
onItemClickListener = listener@{ _, position, v ->
|
onItemClickListener = listener@{ _, position, v ->
|
||||||
if (v !is CardView)
|
if (v !is ProgressCard)
|
||||||
return@listener
|
return@listener
|
||||||
|
|
||||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
||||||
@@ -378,16 +433,12 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
onItemLongClickListener = listener@{ _, position, v ->
|
onItemLongClickListener = listener@{ _, position, v ->
|
||||||
if (v !is CardView)
|
if (v !is ProgressCard)
|
||||||
return@listener false
|
return@listener false
|
||||||
|
|
||||||
val galleryID = galleries[position]
|
val galleryID = galleries.getOrNull(position) ?: return@listener true
|
||||||
|
|
||||||
GalleryDialog(
|
GalleryDialog(this@MainActivity, galleryID).apply {
|
||||||
this@MainActivity,
|
|
||||||
Glide.with(this@MainActivity),
|
|
||||||
galleryID
|
|
||||||
).apply {
|
|
||||||
onChipClickedHandler.add {
|
onChipClickedHandler.add {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
query = it.toQuery()
|
query = it.toQuery()
|
||||||
@@ -405,207 +456,6 @@ class MainActivity :
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var origin = 0f
|
|
||||||
var target = -1
|
|
||||||
val perPage = Preferences["per_page", "25"].toInt()
|
|
||||||
setOnTouchListener { _, event ->
|
|
||||||
when(event.action) {
|
|
||||||
MotionEvent.ACTION_UP -> {
|
|
||||||
origin = 0f
|
|
||||||
|
|
||||||
with(main_recyclerview.adapter as GalleryBlockAdapter) {
|
|
||||||
if(showPrev) {
|
|
||||||
showPrev = false
|
|
||||||
|
|
||||||
val prev = main_recyclerview.layoutManager?.getChildAt(0)
|
|
||||||
|
|
||||||
if (prev is LinearLayout) {
|
|
||||||
val icon = prev.findViewById<ImageView>(R.id.icon_prev)
|
|
||||||
prev.layoutParams.height = 1
|
|
||||||
icon.layoutParams.height = 1
|
|
||||||
icon.rotation = 180f
|
|
||||||
}
|
|
||||||
|
|
||||||
prev?.requestLayout()
|
|
||||||
|
|
||||||
notifyItemRemoved(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(showNext) {
|
|
||||||
showNext = false
|
|
||||||
|
|
||||||
val next = main_recyclerview.layoutManager?.let {
|
|
||||||
getChildAt(childCount-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next is LinearLayout) {
|
|
||||||
val icon = next.findViewById<ImageView>(R.id.icon_next)
|
|
||||||
next.layoutParams.height = 1
|
|
||||||
icon.layoutParams.height = 1
|
|
||||||
icon.rotation = 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
next?.requestLayout()
|
|
||||||
|
|
||||||
notifyItemRemoved(itemCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target != -1) {
|
|
||||||
currentPage = target
|
|
||||||
|
|
||||||
runOnUiThread {
|
|
||||||
cancelFetch()
|
|
||||||
clearGalleries()
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
|
|
||||||
target = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MotionEvent.ACTION_DOWN -> origin = event.y
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
|
||||||
if (origin == 0f)
|
|
||||||
origin = event.y
|
|
||||||
|
|
||||||
val dist = event.y - origin
|
|
||||||
|
|
||||||
when {
|
|
||||||
!canScrollVertically(-1) -> {
|
|
||||||
//TOP
|
|
||||||
|
|
||||||
//Scrolling UP
|
|
||||||
if (dist > 0 && currentPage != 0) {
|
|
||||||
with(main_recyclerview.adapter as GalleryBlockAdapter) {
|
|
||||||
if(!showPrev) {
|
|
||||||
showPrev = true
|
|
||||||
notifyItemInserted(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val prev = main_recyclerview.layoutManager?.getChildAt(0)
|
|
||||||
|
|
||||||
if (prev is LinearLayout) {
|
|
||||||
val icon = prev.findViewById<ImageView>(R.id.icon_prev)
|
|
||||||
val text = prev.findViewById<TextView>(R.id.text_prev).apply {
|
|
||||||
text = getString(R.string.main_move, currentPage)
|
|
||||||
}
|
|
||||||
if (dist < 360) {
|
|
||||||
prev.layoutParams.height = (dist/2).roundToInt()
|
|
||||||
icon.layoutParams.height = (dist/2).roundToInt()
|
|
||||||
icon.rotation = dist+180
|
|
||||||
text.layoutParams.width = dist.roundToInt()
|
|
||||||
|
|
||||||
target = -1
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
prev.layoutParams.height = 180
|
|
||||||
icon.layoutParams.height = 180
|
|
||||||
icon.rotation = 180f
|
|
||||||
text.layoutParams.width = LinearLayout.LayoutParams.WRAP_CONTENT
|
|
||||||
|
|
||||||
target = currentPage-1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prev?.requestLayout()
|
|
||||||
|
|
||||||
return@setOnTouchListener true
|
|
||||||
} else {
|
|
||||||
with(main_recyclerview.adapter as GalleryBlockAdapter) {
|
|
||||||
if(showPrev) {
|
|
||||||
showPrev = false
|
|
||||||
|
|
||||||
val prev = main_recyclerview.layoutManager?.getChildAt(0)
|
|
||||||
|
|
||||||
if (prev is LinearLayout) {
|
|
||||||
val icon = prev.findViewById<ImageView>(R.id.icon_prev)
|
|
||||||
prev.layoutParams.height = 1
|
|
||||||
icon.layoutParams.height = 1
|
|
||||||
icon.rotation = 180f
|
|
||||||
}
|
|
||||||
|
|
||||||
prev?.requestLayout()
|
|
||||||
|
|
||||||
notifyItemRemoved(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
!canScrollVertically(1) -> {
|
|
||||||
//BOTTOM
|
|
||||||
|
|
||||||
//Scrolling DOWN
|
|
||||||
if (dist < 0 && currentPage != ceil(totalItems.toDouble()/perPage).roundToInt()-1) {
|
|
||||||
with(main_recyclerview.adapter as GalleryBlockAdapter) {
|
|
||||||
if(!showNext) {
|
|
||||||
showNext = true
|
|
||||||
notifyItemInserted(itemCount-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val next = main_recyclerview.layoutManager?.let {
|
|
||||||
getChildAt(childCount-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val absDist = abs(dist)
|
|
||||||
|
|
||||||
if (next is LinearLayout) {
|
|
||||||
val icon = next.findViewById<ImageView>(R.id.icon_next)
|
|
||||||
val text = next.findViewById<TextView>(R.id.text_next).apply {
|
|
||||||
text = getString(R.string.main_move, currentPage+2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (absDist < 360) {
|
|
||||||
next.layoutParams.height = (absDist/2).roundToInt()
|
|
||||||
icon.layoutParams.height = (absDist/2).roundToInt()
|
|
||||||
icon.rotation = -absDist
|
|
||||||
text.layoutParams.width = absDist.roundToInt()
|
|
||||||
|
|
||||||
target = -1
|
|
||||||
} else {
|
|
||||||
next.layoutParams.height = 180
|
|
||||||
icon.layoutParams.height = 180
|
|
||||||
icon.rotation = 0f
|
|
||||||
text.layoutParams.width = LinearLayout.LayoutParams.WRAP_CONTENT
|
|
||||||
|
|
||||||
target = currentPage+1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next?.requestLayout()
|
|
||||||
|
|
||||||
return@setOnTouchListener true
|
|
||||||
} else {
|
|
||||||
with(main_recyclerview.adapter as GalleryBlockAdapter) {
|
|
||||||
if(showNext) {
|
|
||||||
showNext = false
|
|
||||||
|
|
||||||
val next = main_recyclerview.layoutManager?.let {
|
|
||||||
getChildAt(childCount-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next is LinearLayout) {
|
|
||||||
val icon = next.findViewById<ImageView>(R.id.icon_next)
|
|
||||||
next.layoutParams.height = 1
|
|
||||||
icon.layoutParams.height = 1
|
|
||||||
icon.rotation = 180f
|
|
||||||
}
|
|
||||||
|
|
||||||
next?.requestLayout()
|
|
||||||
|
|
||||||
notifyItemRemoved(itemCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,22 +470,30 @@ class MainActivity :
|
|||||||
else -> {
|
else -> {
|
||||||
searchHistory.map {
|
searchHistory.map {
|
||||||
Suggestion(it)
|
Suggestion(it)
|
||||||
}.takeLast(20) + FavoriteHistorySwitch(getString(R.string.search_show_tags))
|
}.takeLast(10) + FavoriteHistorySwitch(getString(R.string.search_show_tags))
|
||||||
}
|
}
|
||||||
}.reversed()
|
}.reversed()
|
||||||
|
|
||||||
private var suggestionJob : Job? = null
|
private var suggestionJob : Job? = null
|
||||||
private fun setupSearchBar() {
|
private fun setupSearchBar() {
|
||||||
with(main_searchview as FloatingSearchViewDayNight) {
|
with(binding.contents.searchview) {
|
||||||
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
|
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
|
||||||
override fun onMenuOpened() {
|
override fun onMenuOpened() {
|
||||||
(this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
|
(this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuClosed() {
|
override fun onMenuClosed() {
|
||||||
//Do Nothing
|
//Do Nothing
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
findViewById<MenuView>(R.id.menu_view).menuItems.firstOrNull {
|
||||||
|
(it as MenuItem).itemId == R.id.main_menu_thin
|
||||||
|
}?.let {
|
||||||
|
(it as MenuItem).isChecked = Preferences["thin"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onHistoryDeleteClickedListener = {
|
onHistoryDeleteClickedListener = {
|
||||||
searchHistory.remove(it)
|
searchHistory.remove(it)
|
||||||
@@ -646,9 +504,11 @@ class MainActivity :
|
|||||||
swapSuggestions(defaultSuggestions)
|
swapSuggestions(defaultSuggestions)
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnMenuItemClickListener(this@MainActivity)
|
onMenuItemClickListener = {
|
||||||
|
onActionMenuItemSelected(it)
|
||||||
|
}
|
||||||
|
|
||||||
setOnQueryChangeListener { _, query ->
|
onQueryChangeListener = lambda@{ _, query ->
|
||||||
this@MainActivity.query = query
|
this@MainActivity.query = query
|
||||||
|
|
||||||
suggestionJob?.cancel()
|
suggestionJob?.cancel()
|
||||||
@@ -656,12 +516,14 @@ class MainActivity :
|
|||||||
if (query.isEmpty() or query.endsWith(' ')) {
|
if (query.isEmpty() or query.endsWith(' ')) {
|
||||||
swapSuggestions(defaultSuggestions)
|
swapSuggestions(defaultSuggestions)
|
||||||
|
|
||||||
return@setOnQueryChangeListener
|
return@lambda
|
||||||
}
|
}
|
||||||
|
|
||||||
swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString())))
|
swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString())))
|
||||||
|
|
||||||
val currentQuery = query.split(" ").last().replace('_', ' ')
|
val currentQuery = query.split(" ").last()
|
||||||
|
.replace(Regex("^-"), "")
|
||||||
|
.replace('_', ' ')
|
||||||
|
|
||||||
suggestionJob = CoroutineScope(Dispatchers.IO).launch {
|
suggestionJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val suggestions = kotlin.runCatching {
|
val suggestions = kotlin.runCatching {
|
||||||
@@ -682,7 +544,7 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener {
|
onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener {
|
||||||
override fun onFocus() {
|
override fun onFocus() {
|
||||||
if (query.isEmpty() or query.endsWith(' '))
|
if (query.isEmpty() or query.endsWith(' '))
|
||||||
swapSuggestions(defaultSuggestions)
|
swapSuggestions(defaultSuggestions)
|
||||||
@@ -699,19 +561,24 @@ class MainActivity :
|
|||||||
loadBlocks()
|
loadBlocks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
attachNavigationDrawerToMenuButton(main_drawer_layout)
|
attachNavigationDrawerToMenuButton(this@MainActivity.binding.drawer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionMenuItemSelected(item: MenuItem?) {
|
fun onActionMenuItemSelected(item: MenuItem?) {
|
||||||
when(item?.itemId) {
|
when(item?.itemId) {
|
||||||
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), R.id.request_settings.normalizeID())
|
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
|
||||||
R.id.main_menu_thin -> {
|
R.id.main_menu_thin -> {
|
||||||
main_recyclerview.apply {
|
val thin = !item.isChecked
|
||||||
|
|
||||||
|
item.isChecked = thin
|
||||||
|
binding.contents.recyclerview.apply {
|
||||||
(adapter as GalleryBlockAdapter).apply {
|
(adapter as GalleryBlockAdapter).apply {
|
||||||
isThin = !isThin
|
this.thin = thin
|
||||||
|
|
||||||
|
Preferences["thin"] = thin
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter = adapter // Force to redraw
|
adapter = adapter // Force to redraw
|
||||||
@@ -748,7 +615,7 @@ class MainActivity :
|
|||||||
|
|
||||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
main_drawer_layout.closeDrawers()
|
binding.drawer.closeDrawers()
|
||||||
|
|
||||||
when(item.itemId) {
|
when(item.itemId) {
|
||||||
R.id.main_drawer_home -> {
|
R.id.main_drawer_home -> {
|
||||||
@@ -817,19 +684,17 @@ class MainActivity :
|
|||||||
loadingJob?.cancel()
|
loadingJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearGalleries() {
|
private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch {
|
||||||
galleries.clear()
|
galleries.clear()
|
||||||
|
|
||||||
with(main_recyclerview.adapter as GalleryBlockAdapter?) {
|
with(binding.contents.recyclerview.adapter as GalleryBlockAdapter?) {
|
||||||
this ?: return@with
|
this ?: return@with
|
||||||
|
|
||||||
this.completeFlag.clear()
|
|
||||||
this.notifyDataSetChanged()
|
this.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
main_appbar_layout.setExpanded(true)
|
binding.contents.noresult.visibility = View.INVISIBLE
|
||||||
main_noresult.visibility = View.INVISIBLE
|
binding.contents.progressbar.show()
|
||||||
main_progressbar.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchGalleries(query: String, sortMode: SortMode) {
|
private fun fetchGalleries(query: String, sortMode: SortMode) {
|
||||||
@@ -844,7 +709,7 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (query.isNotEmpty() && mode != Mode.SEARCH) {
|
if (query.isNotEmpty() && mode != Mode.SEARCH) {
|
||||||
Snackbar.make(this@MainActivity.main_recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply {
|
Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply {
|
||||||
setAction(android.R.string.ok) {
|
setAction(android.R.string.ok) {
|
||||||
cancelFetch()
|
cancelFetch()
|
||||||
clearGalleries()
|
clearGalleries()
|
||||||
@@ -922,7 +787,7 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,18 +801,21 @@ class MainActivity :
|
|||||||
throw Exception("No result")
|
throw Exception("No result")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e !is CancellationException)
|
||||||
if (e.message != "No result")
|
|
||||||
FirebaseCrashlytics.getInstance().recordException(e)
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
main_noresult.visibility = View.VISIBLE
|
binding.contents.noresult.visibility = View.VISIBLE
|
||||||
main_progressbar.hide()
|
binding.contents.progressbar.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
binding.contents.view.setCurrentPage(currentPage + 1, galleryIDs.size > (currentPage+1)*perPage)
|
||||||
|
}
|
||||||
|
|
||||||
galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks ->
|
galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks ->
|
||||||
for (chunk in chunks)
|
for (chunk in chunks)
|
||||||
chunk.map { galleryID ->
|
chunk.map { galleryID ->
|
||||||
@@ -959,24 +827,14 @@ class MainActivity :
|
|||||||
}.forEach {
|
}.forEach {
|
||||||
it.await()?.also {
|
it.await()?.also {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
main_progressbar.hide()
|
binding.contents.progressbar.hide()
|
||||||
|
|
||||||
galleries.add(it)
|
galleries.add(it)
|
||||||
main_recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
|
binding.contents.recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLowMemory() {
|
|
||||||
super.onLowMemory()
|
|
||||||
Glide.get(this).onLowMemory()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTrimMemory(level: Int) {
|
|
||||||
super.onTrimMemory(level)
|
|
||||||
Glide.get(this).onTrimMemory(level)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,34 +33,27 @@ import android.view.animation.Animation
|
|||||||
import android.view.animation.AnticipateInterpolator
|
import android.view.animation.AnticipateInterpolator
|
||||||
import android.view.animation.OvershootInterpolator
|
import android.view.animation.OvershootInterpolator
|
||||||
import android.view.animation.TranslateAnimation
|
import android.view.animation.TranslateAnimation
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.PagerSnapHelper
|
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 com.google.mlkit.vision.face.Face
|
import com.google.mlkit.vision.face.Face
|
||||||
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
||||||
import kotlinx.android.synthetic.main.activity_reader.*
|
|
||||||
import kotlinx.android.synthetic.main.activity_reader.view.*
|
|
||||||
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
|
||||||
import kotlinx.android.synthetic.main.reader_eye_card.view.*
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import xyz.quaver.Code
|
|
||||||
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.databinding.NumberpickerDialogBinding
|
||||||
|
import xyz.quaver.pupil.databinding.ReaderActivityBinding
|
||||||
import xyz.quaver.pupil.favorites
|
import xyz.quaver.pupil.favorites
|
||||||
import xyz.quaver.pupil.histories
|
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
import xyz.quaver.pupil.services.DownloadService
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.camera
|
import xyz.quaver.pupil.util.camera
|
||||||
@@ -68,8 +61,6 @@ import xyz.quaver.pupil.util.closeCamera
|
|||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
import xyz.quaver.pupil.util.startCamera
|
import xyz.quaver.pupil.util.startCamera
|
||||||
import java.util.*
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
|
|
||||||
class ReaderActivity : BaseActivity() {
|
class ReaderActivity : BaseActivity() {
|
||||||
|
|
||||||
@@ -81,14 +72,19 @@ class ReaderActivity : BaseActivity() {
|
|||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
|
|
||||||
(reader_recyclerview.adapter as ReaderAdapter).isFullScreen = value
|
(binding.recyclerview.adapter as ReaderAdapter).isFullScreen = value
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var cache: Cache
|
private lateinit var cache: Cache
|
||||||
var downloader: DownloadService? = null
|
var downloader: DownloadService? = null
|
||||||
private val conn = object: ServiceConnection {
|
private val conn = object: ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
downloader = (service as DownloadService.Binder).service
|
downloader = (service as DownloadService.Binder).service.also {
|
||||||
|
it.priority = 0
|
||||||
|
|
||||||
|
if (!it.progress.containsKey(galleryID))
|
||||||
|
DownloadService.download(this@ReaderActivity, galleryID, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
@@ -96,7 +92,6 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val timer = Timer()
|
|
||||||
private val snapHelper = PagerSnapHelper()
|
private val snapHelper = PagerSnapHelper()
|
||||||
private var menu: Menu? = null
|
private var menu: Menu? = null
|
||||||
|
|
||||||
@@ -118,12 +113,14 @@ class ReaderActivity : BaseActivity() {
|
|||||||
|
|
||||||
private var cameraEnabled = false
|
private var cameraEnabled = false
|
||||||
private var eyeType: Eye? = null
|
private var eyeType: Eye? = null
|
||||||
private var eyeCount: Int = 0
|
|
||||||
private var eyeTime: Long = 0L
|
private var eyeTime: Long = 0L
|
||||||
|
|
||||||
|
private lateinit var binding: ReaderActivityBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_reader)
|
binding = ReaderActivityBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
title = getString(R.string.reader_loading)
|
title = getString(R.string.reader_loading)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||||
@@ -137,38 +134,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Preferences["cache_disable"]) {
|
initDownloadListener()
|
||||||
reader_download_progressbar.visibility = View.GONE
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val reader = cache.getReader()
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) initDownloader@{
|
|
||||||
if (reader == null) {
|
|
||||||
Snackbar
|
|
||||||
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
|
||||||
.show()
|
|
||||||
return@initDownloader
|
|
||||||
}
|
|
||||||
|
|
||||||
histories.add(galleryID)
|
|
||||||
(reader_recyclerview.adapter as ReaderAdapter).apply {
|
|
||||||
this.reader = reader
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
title = reader.galleryInfo.title ?: ""
|
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.files.size}"
|
|
||||||
|
|
||||||
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
|
|
||||||
when (reader.code) {
|
|
||||||
Code.HITOMI -> R.drawable.hitomi
|
|
||||||
Code.HIYOBI -> R.drawable.ic_hiyobi
|
|
||||||
else -> android.R.color.transparent
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
initDownloader()
|
|
||||||
|
|
||||||
initView()
|
initView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,12 +161,10 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.reader, menu)
|
menuInflater.inflate(R.menu.reader, menu)
|
||||||
|
|
||||||
with(menu?.findItem(R.id.reader_menu_favorite)) {
|
with(menu.findItem(R.id.reader_menu_favorite)) {
|
||||||
this ?: return@with
|
|
||||||
|
|
||||||
if (favorites.contains(galleryID))
|
if (favorites.contains(galleryID))
|
||||||
(icon as Animatable).start()
|
(icon as Animatable).start()
|
||||||
}
|
}
|
||||||
@@ -212,17 +176,19 @@ class ReaderActivity : BaseActivity() {
|
|||||||
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)
|
// TODO: Switch to DialogFragment
|
||||||
with(view.dialog_number_picker) {
|
val binding = NumberpickerDialogBinding.inflate(layoutInflater, binding.root, false)
|
||||||
|
|
||||||
|
with(binding.numberPicker) {
|
||||||
minValue = 1
|
minValue = 1
|
||||||
maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0
|
maxValue = cache.metadata.galleryInfo?.files?.size ?: 0
|
||||||
value = currentPage
|
value = currentPage
|
||||||
}
|
}
|
||||||
val dialog = AlertDialog.Builder(this).apply {
|
val dialog = AlertDialog.Builder(this).apply {
|
||||||
setView(view)
|
setView(binding.root)
|
||||||
}.create()
|
}.create()
|
||||||
view.dialog_ok.setOnClickListener {
|
binding.okButton.setOnClickListener {
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(view.dialog_number_picker.value-1, 0)
|
(this@ReaderActivity.binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(binding.numberPicker.value-1, 0)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +214,8 @@ class ReaderActivity : BaseActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
|
||||||
|
|
||||||
if (cameraEnabled)
|
if (cameraEnabled)
|
||||||
startCamera(this, cameraCallback)
|
startCamera(this, cameraCallback)
|
||||||
}
|
}
|
||||||
@@ -255,19 +223,20 @@ class ReaderActivity : BaseActivity() {
|
|||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
closeCamera()
|
closeCamera()
|
||||||
|
|
||||||
|
if (downloader != null)
|
||||||
|
unbindService(conn)
|
||||||
|
|
||||||
|
downloader?.priority = galleryID
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
timer.cancel()
|
update = false
|
||||||
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
|
|
||||||
|
|
||||||
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
|
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
|
||||||
DownloadService.cancel(this, galleryID)
|
DownloadService.cancel(this, galleryID)
|
||||||
|
|
||||||
if (downloader != null)
|
|
||||||
unbindService(conn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
@@ -289,12 +258,12 @@ class ReaderActivity : BaseActivity() {
|
|||||||
//currentPage is 1-based
|
//currentPage is 1-based
|
||||||
return when(keyCode) {
|
return when(keyCode) {
|
||||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage-2, 0)
|
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-2, 0)
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
|
(binding.recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -302,52 +271,49 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initDownloader() {
|
private var update = true
|
||||||
DownloadService.download(this, galleryID, true)
|
private fun initDownloadListener() {
|
||||||
bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
while (update) {
|
||||||
|
delay(1000)
|
||||||
|
|
||||||
timer.schedule(1000, 1000) {
|
val downloader = downloader ?: continue
|
||||||
val downloader = downloader ?: return@schedule
|
|
||||||
|
|
||||||
if (downloader.progress.indexOfKey(galleryID) < 0) //loading
|
if (!downloader.progress.containsKey(galleryID)) //loading
|
||||||
return@schedule
|
continue
|
||||||
|
|
||||||
if (downloader.progress[galleryID] == null) { //Gallery not found
|
if (downloader.progress[galleryID]?.isEmpty() == true) { //Gallery not found
|
||||||
timer.cancel()
|
update = false
|
||||||
Snackbar
|
Snackbar
|
||||||
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
.make(binding.root, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
||||||
.show()
|
.show()
|
||||||
}
|
|
||||||
|
|
||||||
histories.add(galleryID)
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
runOnUiThread {
|
binding.downloadProgressbar.max = binding.recyclerview.adapter?.itemCount ?: 0
|
||||||
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
binding.downloadProgressbar.progress =
|
||||||
reader_download_progressbar.progress = downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
|
downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
|
||||||
|
|
||||||
if (title == getString(R.string.reader_loading)) {
|
if (title == getString(R.string.reader_loading)) {
|
||||||
val reader = cache.metadata.reader
|
val galleryInfo = cache.metadata.galleryInfo
|
||||||
|
|
||||||
if (reader != null) {
|
if (galleryInfo != null) {
|
||||||
with (reader_recyclerview.adapter as ReaderAdapter) {
|
with(binding.recyclerview.adapter as ReaderAdapter) {
|
||||||
this.reader = reader
|
this.galleryInfo = galleryInfo
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
title = reader.galleryInfo.title
|
title = galleryInfo.title
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.files.size}"
|
menu?.findItem(R.id.reader_menu_page_indicator)?.title =
|
||||||
|
"$currentPage/${galleryInfo.files.size}"
|
||||||
|
|
||||||
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
|
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.hitomi)
|
||||||
when (reader.code) {
|
|
||||||
Code.HITOMI -> R.drawable.hitomi
|
|
||||||
Code.HIYOBI -> R.drawable.ic_hiyobi
|
|
||||||
else -> android.R.color.transparent
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloader.isCompleted(galleryID)) { //Download finished
|
if (downloader.isCompleted(galleryID)) { //Download finished
|
||||||
reader_download_progressbar.visibility = View.GONE
|
binding.downloadProgressbar.visibility = View.GONE
|
||||||
|
|
||||||
animateDownloadFAB(false)
|
animateDownloadFAB(false)
|
||||||
}
|
}
|
||||||
@@ -356,7 +322,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
with(reader_recyclerview) {
|
with(binding.recyclerview) {
|
||||||
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
|
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
|
||||||
onItemClickListener = {
|
onItemClickListener = {
|
||||||
if (isScroll) {
|
if (isScroll) {
|
||||||
@@ -366,7 +332,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
scrollMode(false)
|
scrollMode(false)
|
||||||
fullscreen(true)
|
fullscreen(true)
|
||||||
} else {
|
} else {
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing
|
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0) //Moves to next page because currentPage is 1-based indexing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,9 +342,9 @@ class ReaderActivity : BaseActivity() {
|
|||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
|
||||||
if (dy < 0)
|
if (dy < 0)
|
||||||
this@ReaderActivity.reader_fab.showMenuButton(true)
|
binding.fab.showMenuButton(true)
|
||||||
else if (dy > 0)
|
else if (dy > 0)
|
||||||
this@ReaderActivity.reader_fab.hideMenuButton(true)
|
binding.fab.hideMenuButton(true)
|
||||||
|
|
||||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||||
|
|
||||||
@@ -386,40 +352,35 @@ class ReaderActivity : BaseActivity() {
|
|||||||
return
|
return
|
||||||
currentPage = layoutManager.findFirstVisibleItemPosition()+1
|
currentPage = layoutManager.findFirstVisibleItemPosition()+1
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
|
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
with(reader_fab_download) {
|
with(binding.downloadFab) {
|
||||||
animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //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))
|
val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
|
||||||
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
|
|
||||||
else {
|
|
||||||
val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
|
|
||||||
|
|
||||||
if (downloadManager.isDownloading(galleryID)) {
|
if (downloadManager.isDownloading(galleryID)) {
|
||||||
downloadManager.deleteDownloadFolder(galleryID)
|
downloadManager.deleteDownloadFolder(galleryID)
|
||||||
animateDownloadFAB(false)
|
animateDownloadFAB(false)
|
||||||
} else {
|
} else {
|
||||||
downloadManager.addDownloadFolder(galleryID)
|
downloadManager.addDownloadFolder(galleryID)
|
||||||
DownloadService.download(context, galleryID, true)
|
DownloadService.download(context, galleryID, true)
|
||||||
animateDownloadFAB(true)
|
animateDownloadFAB(true)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(reader_fab_retry) {
|
with(binding.retryFab) {
|
||||||
setImageResource(R.drawable.refresh)
|
setImageResource(R.drawable.refresh)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
DownloadService.download(context, galleryID)
|
DownloadService.download(context, galleryID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(reader_fab_auto) {
|
with(binding.autoFab) {
|
||||||
setImageResource(R.drawable.eye_white)
|
setImageResource(R.drawable.eye_white)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
when {
|
when {
|
||||||
@@ -439,13 +400,13 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(reader_fab_fullscreen) {
|
with(binding.fullscreenFab) {
|
||||||
setImageResource(R.drawable.ic_fullscreen)
|
setImageResource(R.drawable.ic_fullscreen)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
isFullscreen = true
|
isFullscreen = true
|
||||||
fullscreen(isFullscreen)
|
fullscreen(isFullscreen)
|
||||||
|
|
||||||
this@ReaderActivity.reader_fab.close(true)
|
binding.fab.close(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -455,8 +416,8 @@ class ReaderActivity : BaseActivity() {
|
|||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||||
supportActionBar?.hide()
|
supportActionBar?.hide()
|
||||||
this@ReaderActivity.reader_fab.visibility = View.INVISIBLE
|
binding.fab.visibility = View.INVISIBLE
|
||||||
this@ReaderActivity.scroller.let {
|
binding.scroller.let {
|
||||||
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
||||||
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
||||||
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb_horizontal)
|
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb_horizontal)
|
||||||
@@ -465,8 +426,8 @@ class ReaderActivity : BaseActivity() {
|
|||||||
} else {
|
} else {
|
||||||
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
||||||
supportActionBar?.show()
|
supportActionBar?.show()
|
||||||
this@ReaderActivity.reader_fab.visibility = View.VISIBLE
|
binding.fab.visibility = View.VISIBLE
|
||||||
this@ReaderActivity.scroller.let {
|
binding.scroller.let {
|
||||||
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
||||||
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
||||||
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb)
|
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb)
|
||||||
@@ -477,23 +438,27 @@ class ReaderActivity : BaseActivity() {
|
|||||||
window.attributes = this
|
window.attributes = this
|
||||||
}
|
}
|
||||||
|
|
||||||
reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw
|
binding.recyclerview.adapter = binding.recyclerview.adapter // Force to redraw
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scrollMode(isScroll: Boolean) {
|
private fun scrollMode(isScroll: Boolean) {
|
||||||
if (isScroll) {
|
if (isScroll) {
|
||||||
snapHelper.attachToRecyclerView(null)
|
snapHelper.attachToRecyclerView(null)
|
||||||
reader_recyclerview.layoutManager = LinearLayoutManager(this)
|
binding.recyclerview.layoutManager = LinearLayoutManager(this)
|
||||||
} else {
|
} else {
|
||||||
snapHelper.attachToRecyclerView(reader_recyclerview)
|
snapHelper.attachToRecyclerView(binding.recyclerview)
|
||||||
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, Preferences["rtl", false])
|
binding.recyclerview.layoutManager = object: LinearLayoutManager(this, HORIZONTAL, Preferences["rtl", false]) {
|
||||||
|
override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
|
||||||
|
extraLayoutSpace.fill(600)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun animateDownloadFAB(animate: Boolean) {
|
private fun animateDownloadFAB(animate: Boolean) {
|
||||||
with(reader_fab_download) {
|
with(binding.downloadFab) {
|
||||||
if (animate) {
|
if (animate) {
|
||||||
val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
|
val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
|
||||||
|
|
||||||
@@ -522,7 +487,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val cameraCallback: (List<Face>) -> Unit = callback@{ faces ->
|
val cameraCallback: (List<Face>) -> Unit = callback@{ faces ->
|
||||||
eye_card.dot.let {
|
binding.eyeCard.dot.let {
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
delay(50)
|
delay(50)
|
||||||
@@ -532,9 +497,9 @@ class ReaderActivity : BaseActivity() {
|
|||||||
|
|
||||||
if (faces.size != 1)
|
if (faces.size != 1)
|
||||||
ContextCompat.getDrawable(this, R.drawable.eye_off).let {
|
ContextCompat.getDrawable(this, R.drawable.eye_off).let {
|
||||||
with(eye_card) {
|
with(binding.eyeCard) {
|
||||||
left_eye.setImageDrawable(it)
|
leftEye.setImageDrawable(it)
|
||||||
right_eye.setImageDrawable(it)
|
rightEye.setImageDrawable(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return@callback
|
return@callback
|
||||||
@@ -545,16 +510,16 @@ class ReaderActivity : BaseActivity() {
|
|||||||
faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true
|
faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true
|
||||||
)
|
)
|
||||||
|
|
||||||
with(eye_card) {
|
with(binding.eyeCard) {
|
||||||
left_eye.setImageDrawable(
|
leftEye.setImageDrawable(
|
||||||
ContextCompat.getDrawable(
|
ContextCompat.getDrawable(
|
||||||
context,
|
leftEye.context,
|
||||||
if (left) R.drawable.eye else R.drawable.eye_closed
|
if (left) R.drawable.eye else R.drawable.eye_closed
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
right_eye.setImageDrawable(
|
rightEye.setImageDrawable(
|
||||||
ContextCompat.getDrawable(
|
ContextCompat.getDrawable(
|
||||||
context,
|
rightEye.context,
|
||||||
if (right) R.drawable.eye else R.drawable.eye_closed
|
if (right) R.drawable.eye else R.drawable.eye_closed
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -564,29 +529,24 @@ class ReaderActivity : BaseActivity() {
|
|||||||
// Both closed / opened
|
// Both closed / opened
|
||||||
!left.xor(right) -> {
|
!left.xor(right) -> {
|
||||||
eyeType = null
|
eyeType = null
|
||||||
eyeCount = 0
|
|
||||||
eyeTime = 0L
|
eyeTime = 0L
|
||||||
}
|
}
|
||||||
!left -> {
|
!left -> {
|
||||||
if (eyeType != Eye.LEFT) {
|
if (eyeType != Eye.LEFT) {
|
||||||
eyeType = Eye.LEFT
|
eyeType = Eye.LEFT
|
||||||
eyeCount = 0
|
|
||||||
eyeTime = System.currentTimeMillis()
|
eyeTime = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
eyeCount++
|
|
||||||
}
|
}
|
||||||
!right -> {
|
!right -> {
|
||||||
if (eyeType != Eye.RIGHT) {
|
if (eyeType != Eye.RIGHT) {
|
||||||
eyeType = Eye.RIGHT
|
eyeType = Eye.RIGHT
|
||||||
eyeCount = 0
|
|
||||||
eyeTime = System.currentTimeMillis()
|
eyeTime = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
eyeCount++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eyeCount > 3 && System.currentTimeMillis() - eyeTime > 500) {
|
if (eyeType != null && System.currentTimeMillis() - eyeTime > 100) {
|
||||||
(this@ReaderActivity.reader_recyclerview.layoutManager as LinearLayoutManager).let {
|
(binding.recyclerview.layoutManager as LinearLayoutManager).let {
|
||||||
it.scrollToPositionWithOffset(when(eyeType!!) {
|
it.scrollToPositionWithOffset(when(eyeType!!) {
|
||||||
Eye.RIGHT -> {
|
Eye.RIGHT -> {
|
||||||
if (it.reverseLayout) currentPage - 2 else currentPage
|
if (it.reverseLayout) currentPage - 2 else currentPage
|
||||||
@@ -597,18 +557,16 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
eyeType = null
|
eyeTime = System.currentTimeMillis() + 500
|
||||||
eyeCount = 0
|
|
||||||
eyeTime = 0L
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleCamera() {
|
private fun toggleCamera() {
|
||||||
val eyes = this@ReaderActivity.eye_card
|
val eyes = binding.eyeCard.root
|
||||||
when (camera) {
|
when (camera) {
|
||||||
null -> {
|
null -> {
|
||||||
reader_fab_auto.labelText = getString(R.string.reader_fab_auto_cancel)
|
binding.autoFab.labelText = getString(R.string.reader_fab_auto_cancel)
|
||||||
reader_fab_auto.setImageResource(R.drawable.eye_off_white)
|
binding.autoFab.setImageResource(R.drawable.eye_off_white)
|
||||||
eyes.apply {
|
eyes.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
TranslateAnimation(0F, 0F, -100F, 0F).apply {
|
TranslateAnimation(0F, 0F, -100F, 0F).apply {
|
||||||
@@ -621,8 +579,8 @@ class ReaderActivity : BaseActivity() {
|
|||||||
cameraEnabled = true
|
cameraEnabled = true
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
reader_fab_auto.labelText = getString(R.string.reader_fab_auto)
|
binding.autoFab.labelText = getString(R.string.reader_fab_auto)
|
||||||
reader_fab_auto.setImageResource(R.drawable.eye_white)
|
binding.autoFab.setImageResource(R.drawable.eye_white)
|
||||||
eyes.apply {
|
eyes.apply {
|
||||||
TranslateAnimation(0F, 0F, 0F, -100F).apply {
|
TranslateAnimation(0F, 0F, 0F, -100F).apply {
|
||||||
duration = 500
|
duration = 500
|
||||||
@@ -643,14 +601,4 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLowMemory() {
|
|
||||||
super.onLowMemory()
|
|
||||||
Glide.get(this).onLowMemory()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTrimMemory(level: Int) {
|
|
||||||
super.onTrimMemory(level)
|
|
||||||
Glide.get(this).onTrimMemory(level)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -18,24 +18,10 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
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.SettingsFragment
|
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.normalizeID
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
|
|
||||||
class SettingsActivity : BaseActivity() {
|
class SettingsActivity : BaseActivity() {
|
||||||
|
|
||||||
@@ -56,19 +42,4 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
|
||||||
when (requestCode) {
|
|
||||||
R.id.request_write_permission_and_saf.normalizeID() -> {
|
|
||||||
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
|
||||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
startActivityForResult(intent, R.id.request_download_folder.normalizeID())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -18,30 +18,30 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
package xyz.quaver.pupil.ui.dialog
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import kotlinx.android.synthetic.main.dialog_default_query.*
|
import androidx.fragment.app.DialogFragment
|
||||||
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding
|
||||||
import xyz.quaver.pupil.types.Tags
|
import xyz.quaver.pupil.types.Tags
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
|
|
||||||
class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
class DefaultQueryDialog : DialogFragment() {
|
||||||
|
|
||||||
private val languages = context.resources.getStringArray(R.array.languages).map {
|
private val languages: Map<String, String> by lazy {
|
||||||
it.split("|").let { split ->
|
requireContext().resources.getStringArray(R.array.languages).map {
|
||||||
Pair(split[0], split[1])
|
it.split("|").let { split ->
|
||||||
}
|
Pair(split[0], split[1])
|
||||||
}.toMap()
|
}
|
||||||
private val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
|
}.toMap()
|
||||||
|
}
|
||||||
|
private val reverseLanguages: Map<String, String> by lazy {
|
||||||
|
languages.entries.associate { (k, v) -> v to k }
|
||||||
|
}
|
||||||
|
|
||||||
private val excludeBL = "-male:yaoi"
|
private val excludeBL = "-male:yaoi"
|
||||||
private val excludeGuro = listOf("-female:guro", "-male:guro")
|
private val excludeGuro = listOf("-female:guro", "-male:guro")
|
||||||
@@ -49,46 +49,15 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
|
|
||||||
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
|
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
private var _binding: DefaultQueryDialogBinding? = null
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
private val binding get() = _binding!!
|
||||||
setTitle(R.string.default_query_dialog_title)
|
|
||||||
setView(build())
|
|
||||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
|
|
||||||
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
|
|
||||||
|
|
||||||
with(default_query_dialog_language_selector) {
|
private fun initView() {
|
||||||
if (selectedItemPosition != 0)
|
|
||||||
newTags.add("language:${reverseLanguages[selectedItem]}")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (default_query_dialog_BL_checkbox.isChecked)
|
|
||||||
newTags.add(excludeBL)
|
|
||||||
|
|
||||||
if (default_query_dialog_guro_checkbox.isChecked)
|
|
||||||
excludeGuro.forEach { tag ->
|
|
||||||
newTags.add(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (default_query_dialog_loli_checkbox.isChecked)
|
|
||||||
excludeLoli.forEach { tag ->
|
|
||||||
newTags.add(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
onPositiveButtonClickListener?.invoke(newTags)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
private fun build() : View {
|
|
||||||
val tags = Tags.parse(
|
val tags = Tags.parse(
|
||||||
Preferences["default_query"]
|
Preferences["default_query"]
|
||||||
)
|
)
|
||||||
|
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
with(binding.languageSelector) {
|
||||||
|
|
||||||
with(view.default_query_dialog_language_selector) {
|
|
||||||
adapter =
|
adapter =
|
||||||
ArrayAdapter(
|
ArrayAdapter(
|
||||||
context,
|
context,
|
||||||
@@ -111,13 +80,13 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(view.default_query_dialog_BL_checkbox) {
|
with(binding.BLCheckbox) {
|
||||||
isChecked = tags.contains(excludeBL)
|
isChecked = tags.contains(excludeBL)
|
||||||
if (tags.contains(excludeBL))
|
if (tags.contains(excludeBL))
|
||||||
tags.remove(excludeBL)
|
tags.remove(excludeBL)
|
||||||
}
|
}
|
||||||
|
|
||||||
with(view.default_query_dialog_guro_checkbox) {
|
with(binding.guroCheckbox) {
|
||||||
isChecked = excludeGuro.all { tags.contains(it) }
|
isChecked = excludeGuro.all { tags.contains(it) }
|
||||||
if (excludeGuro.all { tags.contains(it) })
|
if (excludeGuro.all { tags.contains(it) })
|
||||||
excludeGuro.forEach {
|
excludeGuro.forEach {
|
||||||
@@ -125,7 +94,7 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(view.default_query_dialog_loli_checkbox) {
|
with(binding.loliCheckbox) {
|
||||||
isChecked = excludeLoli.all { tags.contains(it) }
|
isChecked = excludeLoli.all { tags.contains(it) }
|
||||||
if (excludeLoli.all { tags.contains(it) })
|
if (excludeLoli.all { tags.contains(it) })
|
||||||
excludeLoli.forEach {
|
excludeLoli.forEach {
|
||||||
@@ -133,7 +102,7 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(view.default_query_dialog_edittext) {
|
with(binding.edittext) {
|
||||||
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
|
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
|
||||||
addTextChangedListener(object : TextWatcher {
|
addTextChangedListener(object : TextWatcher {
|
||||||
override fun beforeTextChanged(
|
override fun beforeTextChanged(
|
||||||
@@ -158,8 +127,45 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return view
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
_binding = DefaultQueryDialogBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
initView()
|
||||||
|
|
||||||
|
return AlertDialog.Builder(requireContext()).apply {
|
||||||
|
setTitle(R.string.default_query_dialog_title)
|
||||||
|
setView(binding.root)
|
||||||
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
val newTags = Tags.parse(binding.edittext.text.toString())
|
||||||
|
|
||||||
|
with(binding.languageSelector) {
|
||||||
|
if (selectedItemPosition != 0)
|
||||||
|
newTags.add("language:${reverseLanguages[selectedItem]}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.BLCheckbox.isChecked)
|
||||||
|
newTags.add(excludeBL)
|
||||||
|
|
||||||
|
if (binding.guroCheckbox.isChecked)
|
||||||
|
excludeGuro.forEach { tag ->
|
||||||
|
newTags.add(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.loliCheckbox.isChecked)
|
||||||
|
excludeLoli.forEach { tag ->
|
||||||
|
newTags.add(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositiveButtonClickListener?.invoke(newTags)
|
||||||
|
}
|
||||||
|
}.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,17 +18,17 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
package xyz.quaver.pupil.ui.dialog
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.databinding.DownloadFolderNameDialogBinding
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import xyz.quaver.pupil.util.formatDownloadFolder
|
import xyz.quaver.pupil.util.formatDownloadFolder
|
||||||
@@ -37,38 +37,49 @@ import xyz.quaver.pupil.util.formatMap
|
|||||||
|
|
||||||
class DownloadFolderNameDialogFragment : DialogFragment() {
|
class DownloadFolderNameDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
private var _binding: DownloadFolderNameDialogBinding? = null
|
||||||
private fun build(): View {
|
private val binding get() = _binding!!
|
||||||
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 {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
|
_binding = DownloadFolderNameDialogBinding.inflate(layoutInflater)
|
||||||
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) {
|
initView()
|
||||||
Snackbar.make(this, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
Preferences["download_folder_name"] = edittext.text.toString()
|
return Dialog(requireContext()).apply {
|
||||||
|
setContentView(binding.root)
|
||||||
dismiss()
|
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
override fun onDestroy() {
|
||||||
Dialog(requireContext()).apply {
|
super.onDestroy()
|
||||||
setContentView(build())
|
_binding = null
|
||||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
}
|
||||||
}
|
|
||||||
|
private fun initView() {
|
||||||
|
val galleryID = Cache.instances.let { if (it.size == 0) 1199708 else it.keys.elementAt((0 until it.size).random()) }
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val galleryBlock = Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
|
||||||
|
|
||||||
|
binding.message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
|
||||||
|
binding.edittext.addTextChangedListener {
|
||||||
|
binding.message.text = requireContext().getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
|
||||||
|
binding.okButton.setOnClickListener {
|
||||||
|
val newValue = binding.edittext.text.toString()
|
||||||
|
|
||||||
|
if ((newValue as? String)?.contains("/") != false) {
|
||||||
|
Snackbar.make(binding.root, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
Preferences["download_folder_name"] = binding.edittext.text.toString()
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,70 +18,132 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
package xyz.quaver.pupil.ui.dialog
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.snackbar.Snackbar
|
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.DirectoryChooserActivity
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
|
import xyz.quaver.io.util.toFile
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.databinding.DownloadLocationDialogBinding
|
||||||
|
import xyz.quaver.pupil.databinding.DownloadLocationItemBinding
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.byteToString
|
import xyz.quaver.pupil.util.byteToString
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
import xyz.quaver.pupil.util.migrate
|
|
||||||
import xyz.quaver.pupil.util.normalizeID
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class DownloadLocationDialogFragment : DialogFragment() {
|
class DownloadLocationDialogFragment : DialogFragment() {
|
||||||
private val entries = mutableMapOf<File?, View>()
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
private var _binding: DownloadLocationDialogBinding? = null
|
||||||
private fun build() : View? {
|
private val binding get() = _binding!!
|
||||||
val context = context ?: return null
|
|
||||||
|
|
||||||
val view = layoutInflater.inflate(R.layout.dialog_download_folder, null) as LinearLayout
|
private val entries = mutableMapOf<File?, DownloadLocationItemBinding>()
|
||||||
|
|
||||||
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
private val requestDownloadFolderLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
val context = context ?: return@registerForActivityResult
|
||||||
|
val dialog = dialog ?: return@registerForActivityResult
|
||||||
|
|
||||||
|
it.data?.data?.also { uri ->
|
||||||
|
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||||
|
|
||||||
|
if (kotlin.runCatching { FileX(context, uri).canWrite() }.getOrDefault(false)) {
|
||||||
|
entries[null]?.locationAvailable?.text = uri.toFile(context)?.canonicalPath
|
||||||
|
Preferences["download_folder"] = uri.toString()
|
||||||
|
} else {
|
||||||
|
Snackbar.make(
|
||||||
|
dialog.window!!.decorView.rootView,
|
||||||
|
R.string.settings_download_folder_not_writable,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
|
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
||||||
|
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||||
|
entries[key]!!.button.isChecked = true
|
||||||
|
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val downloadFolder = DownloadManager.getInstance(context ?: return@registerForActivityResult).downloadFolder.canonicalPath
|
||||||
|
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||||
|
if (key == null)
|
||||||
|
entries[key]!!.locationAvailable.text = downloadFolder
|
||||||
|
else {
|
||||||
|
entries[null]!!.button.isChecked = false
|
||||||
|
entries[key]!!.button.isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val requestDownloadFolderOldLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
val context = context ?: return@registerForActivityResult
|
||||||
|
val dialog = dialog ?: return@registerForActivityResult
|
||||||
|
|
||||||
|
if (it.resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
||||||
|
val directory = it.data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
||||||
|
|
||||||
|
if (!File(directory).canWrite()) {
|
||||||
|
Snackbar.make(
|
||||||
|
dialog.window!!.decorView.rootView,
|
||||||
|
R.string.settings_download_folder_not_writable,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
|
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
||||||
|
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||||
|
entries[key]!!.button.isChecked = true
|
||||||
|
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
entries[null]?.locationAvailable?.text = directory
|
||||||
|
Preferences["download_folder"] = File(directory).toURI().toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
val externalFilesDirs = ContextCompat.getExternalFilesDirs(requireContext(), null)
|
||||||
|
|
||||||
externalFilesDirs.forEachIndexed { index, dir ->
|
externalFilesDirs.forEachIndexed { index, dir ->
|
||||||
dir ?: return@forEachIndexed
|
dir ?: return@forEachIndexed
|
||||||
|
|
||||||
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
|
DownloadLocationItemBinding.inflate(layoutInflater, binding.root, true).apply {
|
||||||
location_type.text = context.getString(when (index) {
|
locationType.text = requireContext().getString(when (index) {
|
||||||
0 -> R.string.settings_download_folder_internal
|
0 -> R.string.settings_download_folder_internal
|
||||||
else -> R.string.settings_download_folder_removable
|
else -> R.string.settings_download_folder_removable
|
||||||
})
|
})
|
||||||
location_available.text = context.getString(
|
locationAvailable.text = requireContext().getString(
|
||||||
R.string.settings_download_folder_available,
|
R.string.settings_download_folder_available,
|
||||||
byteToString(dir.freeSpace)
|
byteToString(dir.freeSpace)
|
||||||
)
|
)
|
||||||
setOnClickListener {
|
root.setOnClickListener {
|
||||||
entries.values.forEach {
|
entries.values.forEach { entry ->
|
||||||
it.button.isChecked = false
|
entry.button.isChecked = false
|
||||||
}
|
}
|
||||||
button.performClick()
|
button.performClick()
|
||||||
Preferences["download_folder"] = dir.toUri().toString()
|
Preferences["download_folder"] = dir.toUri().toString()
|
||||||
}
|
}
|
||||||
entries[dir] = this
|
entries[dir] = this
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
|
DownloadLocationItemBinding.inflate(layoutInflater, binding.root, true).apply {
|
||||||
location_type.text = context.getString(R.string.settings_download_folder_custom)
|
locationType.text = requireContext().getString(R.string.settings_download_folder_custom)
|
||||||
setOnClickListener {
|
root.setOnClickListener {
|
||||||
entries.values.forEach {
|
entries.values.forEach { entry ->
|
||||||
it.button.isChecked = false
|
entry.button.isChecked = false
|
||||||
}
|
}
|
||||||
button.performClick()
|
button.performClick()
|
||||||
|
|
||||||
@@ -90,7 +152,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
|
|||||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
startActivityForResult(intent, R.id.request_download_folder.normalizeID())
|
requestDownloadFolderLauncher.launch(intent)
|
||||||
} else { // Can't use SAF on old Androids!
|
} else { // Can't use SAF on old Androids!
|
||||||
val config = DirectoryChooserConfig.builder()
|
val config = DirectoryChooserConfig.builder()
|
||||||
.newDirectoryName("Pupil")
|
.newDirectoryName("Pupil")
|
||||||
@@ -101,96 +163,37 @@ class DownloadLocationDialogFragment : DialogFragment() {
|
|||||||
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
startActivityForResult(intent, R.id.request_download_folder_old.normalizeID())
|
requestDownloadFolderOldLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entries[null] = this
|
entries[null] = this
|
||||||
})
|
}
|
||||||
|
|
||||||
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
val downloadFolder = DownloadManager.getInstance(requireContext()).downloadFolder.canonicalPath
|
||||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||||
entries[key]!!.button.isChecked = true
|
entries[key]!!.button.isChecked = true
|
||||||
if (key == null) entries[key]!!.location_available.text = downloadFolder
|
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val builder = AlertDialog.Builder(requireContext())
|
_binding = DownloadLocationDialogBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
builder
|
initView()
|
||||||
.setTitle(R.string.settings_download_folder)
|
|
||||||
.setView(build())
|
return AlertDialog.Builder(requireContext()).apply {
|
||||||
.setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
|
setTitle(R.string.settings_download_folder)
|
||||||
|
setView(binding.root)
|
||||||
|
setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
|
||||||
if (Preferences["download_folder", ""].isEmpty())
|
if (Preferences["download_folder", ""].isEmpty())
|
||||||
Preferences["download_folder"] = context?.getExternalFilesDir(null)?.toUri()?.toString() ?: ""
|
Preferences["download_folder"] = context.getExternalFilesDir(null)?.toUri()?.toString() ?: ""
|
||||||
|
|
||||||
DownloadManager.getInstance(requireContext()).migrate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isCancelable = false
|
isCancelable = false
|
||||||
|
}.create()
|
||||||
return builder.create()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onDestroy() {
|
||||||
when (requestCode) {
|
super.onDestroy()
|
||||||
R.id.request_download_folder.normalizeID() -> {
|
_binding = null
|
||||||
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).toURI().toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -20,46 +20,47 @@ package xyz.quaver.pupil.ui.dialog
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout.LayoutParams
|
import android.widget.LinearLayout.LayoutParams
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
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 androidx.viewpager2.widget.ViewPager2
|
||||||
import com.bumptech.glide.RequestManager
|
|
||||||
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_details.view.*
|
|
||||||
import kotlinx.android.synthetic.main.dialog_gallery_dotindicator.view.*
|
|
||||||
import kotlinx.android.synthetic.main.item_gallery_details.view.*
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.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.Gallery
|
import xyz.quaver.pupil.hitomi.Gallery
|
||||||
import xyz.quaver.hitomi.getGallery
|
import xyz.quaver.pupil.hitomi.getGallery
|
||||||
import xyz.quaver.pupil.BuildConfig
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||||
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
||||||
import xyz.quaver.pupil.histories
|
import xyz.quaver.pupil.databinding.*
|
||||||
|
import xyz.quaver.pupil.favoriteTags
|
||||||
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.ui.view.TagChip
|
||||||
import xyz.quaver.pupil.util.ItemClickSupport
|
import xyz.quaver.pupil.util.ItemClickSupport
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : AlertDialog(context) {
|
class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(context) {
|
||||||
|
|
||||||
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
||||||
|
|
||||||
|
private lateinit var binding: GalleryDialogBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.dialog_gallery)
|
binding = GalleryDialogBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
window?.attributes.apply {
|
window?.attributes.apply {
|
||||||
this ?: return@apply
|
this ?: return@apply
|
||||||
@@ -68,13 +69,12 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
height = LayoutParams.MATCH_PARENT
|
height = LayoutParams.MATCH_PARENT
|
||||||
}
|
}
|
||||||
|
|
||||||
with(gallery_fab) {
|
with(binding.fab) {
|
||||||
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right))
|
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right))
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
||||||
putExtra("galleryID", galleryID)
|
putExtra("galleryID", galleryID)
|
||||||
})
|
})
|
||||||
histories.add(galleryID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,12 +82,12 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
try {
|
try {
|
||||||
val gallery = getGallery(galleryID)
|
val gallery = getGallery(galleryID)
|
||||||
|
|
||||||
gallery_cover.post {
|
launch (Dispatchers.Main) {
|
||||||
gallery_progressbar.visibility = View.GONE
|
binding.progressbar.visibility = View.GONE
|
||||||
gallery_title.text = gallery.title
|
binding.title.text = gallery.title
|
||||||
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
|
binding.artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
|
||||||
|
|
||||||
with(gallery_type) {
|
with(binding.type) {
|
||||||
text = gallery.type.wordCapitalize()
|
text = gallery.type.wordCapitalize()
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
gallery.type.let {
|
gallery.type.let {
|
||||||
@@ -104,28 +104,26 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
glide
|
binding.cover.showImage(Uri.parse(gallery.cover))
|
||||||
.load(gallery.cover)
|
|
||||||
.apply {
|
|
||||||
if (BuildConfig.CENSOR)
|
|
||||||
override(5, 8)
|
|
||||||
}.into(gallery_cover)
|
|
||||||
|
|
||||||
addDetails(gallery)
|
addDetails(gallery)
|
||||||
addThumbnails(gallery)
|
addThumbnails(gallery)
|
||||||
addRelated(gallery)
|
addRelated(gallery)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Snackbar.make(gallery_layout, R.string.unable_to_connect, Snackbar.LENGTH_INDEFINITE).show()
|
Snackbar.make(binding.root, R.string.unable_to_connect, Snackbar.LENGTH_INDEFINITE).apply {
|
||||||
|
if (Locale.getDefault().language == "ko")
|
||||||
|
setAction(context.getText(R.string.https_text)) {
|
||||||
|
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.https))))
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addDetails(gallery: Gallery) {
|
private fun addDetails(gallery: Gallery) {
|
||||||
val inflater = LayoutInflater.from(context)
|
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
||||||
|
type.setText(R.string.gallery_details)
|
||||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
|
||||||
gallery_details.setText(R.string.gallery_details)
|
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
R.string.gallery_artists,
|
R.string.gallery_artists,
|
||||||
@@ -141,7 +139,18 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
listOf(gallery.language).map { Tag("language", it) },
|
listOf(gallery.language).map { Tag("language", it) },
|
||||||
gallery.series.map { Tag("series", it) },
|
gallery.series.map { Tag("series", it) },
|
||||||
gallery.characters.map { Tag("character", it) },
|
gallery.characters.map { Tag("character", it) },
|
||||||
gallery.tags.map {
|
gallery.tags.sortedBy {
|
||||||
|
val tag = Tag.parse(it)
|
||||||
|
|
||||||
|
if (favoriteTags.contains(tag))
|
||||||
|
-1
|
||||||
|
else
|
||||||
|
when(Tag.parse(it).area) {
|
||||||
|
"female" -> 0
|
||||||
|
"male" -> 1
|
||||||
|
else -> 2
|
||||||
|
}
|
||||||
|
}.map {
|
||||||
Tag.parse(it).let { tag ->
|
Tag.parse(it).let { tag ->
|
||||||
when {
|
when {
|
||||||
tag.area != null -> tag
|
tag.area != null -> tag
|
||||||
@@ -151,13 +160,13 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
).filter {
|
).filter {
|
||||||
(_, content) -> content.isNotEmpty()
|
(_, content) -> content.isNotEmpty()
|
||||||
}.forEach { (title, content) ->
|
}.forEach { (title, content) ->
|
||||||
inflater.inflate(R.layout.item_gallery_details, gallery_details_contents, false).apply {
|
GalleryDialogTagsBinding.inflate(layoutInflater, contents, true).apply {
|
||||||
gallery_details_type.setText(title)
|
type.setText(title)
|
||||||
|
|
||||||
content.forEach { tag ->
|
content.forEach { tag ->
|
||||||
gallery_details_tags.addView(
|
tags.addView(
|
||||||
TagChip(context, tag).apply {
|
TagChip(context, tag).apply {
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
onChipClickedHandler.forEach { handler ->
|
onChipClickedHandler.forEach { handler ->
|
||||||
@@ -167,43 +176,36 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.let {
|
|
||||||
gallery_details_contents.addView(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.let {
|
|
||||||
gallery_contents.addView(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addThumbnails(gallery: Gallery) {
|
private fun addThumbnails(gallery: Gallery) {
|
||||||
val inflater = LayoutInflater.from(context)
|
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
||||||
|
type.setText(R.string.gallery_thumbnails)
|
||||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
|
||||||
gallery_details.setText(R.string.gallery_thumbnails)
|
|
||||||
|
|
||||||
val pager = ViewPager2(context).apply {
|
val pager = ViewPager2(context).apply {
|
||||||
adapter = ThumbnailPageAdapter(glide, gallery.thumbnails)
|
adapter = ThumbnailPageAdapter(gallery.thumbnails)
|
||||||
|
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
gallery_details_contents.addView(
|
contents.addView(
|
||||||
pager,
|
pager,
|
||||||
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||||
)
|
)
|
||||||
|
|
||||||
LayoutInflater.from(context).inflate(R.layout.dialog_gallery_dotindicator, gallery_details_contents)
|
// TODO: Change to direct allocation
|
||||||
|
GalleryDialogDotindicatorBinding.inflate(layoutInflater, contents, true).apply {
|
||||||
gallery_dotindicator.setViewPager2(pager)
|
dotindicator.setViewPager2(pager)
|
||||||
}.let {
|
}
|
||||||
gallery_contents.addView(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addRelated(gallery: Gallery) {
|
private fun addRelated(gallery: Gallery) {
|
||||||
val inflater = LayoutInflater.from(context)
|
val galleries = mutableListOf<Int>()
|
||||||
val galleries = ArrayList<Int>()
|
|
||||||
|
|
||||||
val adapter = GalleryBlockAdapter(glide, galleries).apply {
|
val adapter = GalleryBlockAdapter(galleries).apply {
|
||||||
onChipClickedHandler.add { tag ->
|
onChipClickedHandler.add { tag ->
|
||||||
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
|
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
|
||||||
handler.invoke(tag)
|
handler.invoke(tag)
|
||||||
@@ -211,10 +213,10 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
||||||
gallery_details.setText(R.string.gallery_related)
|
type.setText(R.string.gallery_related)
|
||||||
|
|
||||||
RecyclerView(context).apply {
|
contents.addView(RecyclerView(context).apply {
|
||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = LinearLayoutManager(context)
|
||||||
this.adapter = adapter
|
this.adapter = adapter
|
||||||
|
|
||||||
@@ -223,14 +225,9 @@ 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", galleries[position])
|
putExtra("galleryID", galleries[position])
|
||||||
})
|
})
|
||||||
histories.add(galleries[position])
|
|
||||||
}
|
}
|
||||||
onItemLongClickListener = { _, position, _ ->
|
onItemLongClickListener = { _, position, _ ->
|
||||||
GalleryDialog(
|
GalleryDialog(context, galleries[position]).apply {
|
||||||
context,
|
|
||||||
glide,
|
|
||||||
galleries[position]
|
|
||||||
).apply {
|
|
||||||
onChipClickedHandler.add { tag ->
|
onChipClickedHandler.add { tag ->
|
||||||
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
||||||
}
|
}
|
||||||
@@ -239,22 +236,18 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.let {
|
})
|
||||||
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
|
||||||
}
|
|
||||||
}.let {
|
|
||||||
gallery_contents.addView(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
gallery.related.forEach { galleryID ->
|
gallery.related.forEach { galleryID ->
|
||||||
Cache.getInstance(context, galleryID).getGalleryBlock()?.let {
|
Cache.getInstance(context, galleryID).getGalleryBlock()?.let {
|
||||||
galleries.add(galleryID)
|
galleries.add(galleryID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.adapters.MirrorAdapter
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
|
|
||||||
class MirrorDialog(context: Context) : AlertDialog(context) {
|
|
||||||
|
|
||||||
class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
|
|
||||||
|
|
||||||
var onMoveItem : ((Int, Int) -> (Unit))? = null
|
|
||||||
|
|
||||||
override fun getMovementFlags(
|
|
||||||
recyclerView: RecyclerView,
|
|
||||||
viewHolder: RecyclerView.ViewHolder
|
|
||||||
) = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
|
|
||||||
|
|
||||||
override fun onMove(
|
|
||||||
recyclerView: RecyclerView,
|
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
|
||||||
target: RecyclerView.ViewHolder
|
|
||||||
): Boolean {
|
|
||||||
onMoveItem?.invoke(viewHolder.adapterPosition, target.adapterPosition)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
setTitle(R.string.settings_mirror_title)
|
|
||||||
setView(build())
|
|
||||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun build() : View {
|
|
||||||
return RecyclerView(context).apply recyclerview@{
|
|
||||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
|
||||||
layoutManager = LinearLayoutManager(context)
|
|
||||||
adapter = MirrorAdapter(context).apply adapter@{
|
|
||||||
val itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback().apply {
|
|
||||||
onMoveItem = this@adapter.onItemMove
|
|
||||||
}).apply {
|
|
||||||
attachToRecyclerView(this@recyclerview)
|
|
||||||
}
|
|
||||||
|
|
||||||
onStartDrag = {
|
|
||||||
itemTouchHelper.startDrag(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemMoved = {
|
|
||||||
Preferences["mirrors"] = it.joinToString(">")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -18,59 +18,60 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
package xyz.quaver.pupil.ui.dialog
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import kotlinx.android.synthetic.main.dialog_proxy.view.*
|
import androidx.fragment.app.DialogFragment
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.client
|
import xyz.quaver.pupil.client
|
||||||
import xyz.quaver.pupil.clientBuilder
|
import xyz.quaver.pupil.clientBuilder
|
||||||
import xyz.quaver.pupil.clientHolder
|
import xyz.quaver.pupil.clientHolder
|
||||||
|
import xyz.quaver.pupil.databinding.ProxyDialogBinding
|
||||||
import xyz.quaver.pupil.util.Preferences
|
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.proxyInfo
|
import xyz.quaver.pupil.util.proxyInfo
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
|
|
||||||
class ProxyDialog(context: Context) : AlertDialog(context) {
|
class ProxyDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
private var _binding: ProxyDialogBinding? = null
|
||||||
setContentView(build())
|
private val binding get() = _binding!!
|
||||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
_binding = ProxyDialogBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
initView()
|
||||||
|
|
||||||
|
return AlertDialog.Builder(requireContext()).apply {
|
||||||
|
setView(binding.root)
|
||||||
|
}.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
private fun initView() {
|
||||||
private fun build() : View {
|
|
||||||
val proxyInfo = getProxyInfo()
|
val proxyInfo = getProxyInfo()
|
||||||
|
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null)
|
|
||||||
|
|
||||||
val enabler = { enable: Boolean ->
|
val enabler = { enable: Boolean ->
|
||||||
view?.proxy_addr?.isEnabled = enable
|
binding.addr.isEnabled = enable
|
||||||
view?.proxy_port?.isEnabled = enable
|
binding.port.isEnabled = enable
|
||||||
view?.proxy_username?.isEnabled = enable
|
binding.username.isEnabled = enable
|
||||||
view?.proxy_password?.isEnabled = enable
|
binding.password.isEnabled = enable
|
||||||
|
|
||||||
if (!enable) {
|
if (!enable) {
|
||||||
view?.proxy_addr?.text = null
|
binding.addr.text = null
|
||||||
view?.proxy_port?.text = null
|
binding.port.text = null
|
||||||
view?.proxy_username?.text = null
|
binding.username.text = null
|
||||||
view?.proxy_password?.text = null
|
binding.password.text = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(view.proxy_type_selector) {
|
with(binding.typeSelector) {
|
||||||
adapter = ArrayAdapter(
|
adapter = ArrayAdapter(
|
||||||
context,
|
context,
|
||||||
android.R.layout.simple_spinner_dropdown_item,
|
android.R.layout.simple_spinner_dropdown_item,
|
||||||
@@ -88,29 +89,29 @@ class ProxyDialog(context: Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view.proxy_addr.setText(proxyInfo.host)
|
binding.addr.setText(proxyInfo.host)
|
||||||
view.proxy_port.setText(proxyInfo.port?.toString())
|
binding.port.setText(proxyInfo.port?.toString())
|
||||||
view.proxy_username.setText(proxyInfo.username)
|
binding.username.setText(proxyInfo.username)
|
||||||
view.proxy_password.setText(proxyInfo.password)
|
binding.password.setText(proxyInfo.password)
|
||||||
|
|
||||||
enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT)
|
enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT)
|
||||||
|
|
||||||
view.proxy_cancel.setOnClickListener {
|
binding.cancelButton.setOnClickListener {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
view.proxy_ok.setOnClickListener {
|
binding.okButton.setOnClickListener {
|
||||||
val type = Proxy.Type.values()[view.proxy_type_selector.selectedItemPosition]
|
val type = Proxy.Type.values()[binding.typeSelector.selectedItemPosition]
|
||||||
val addr = view.proxy_addr.text?.toString()
|
val addr = binding.addr.text?.toString()
|
||||||
val port = view.proxy_port.text?.toString()?.toIntOrNull()
|
val port = binding.port.text?.toString()?.toIntOrNull()
|
||||||
val username = view.proxy_username.text?.toString()
|
val username = binding.username.text?.toString()
|
||||||
val password = view.proxy_password.text?.toString()
|
val password = binding.password.text?.toString()
|
||||||
|
|
||||||
if (type != Proxy.Type.DIRECT) {
|
if (type != Proxy.Type.DIRECT) {
|
||||||
if (addr == null || addr.isEmpty())
|
if (addr == null || addr.isEmpty())
|
||||||
view.proxy_addr.error = context.getText(R.string.proxy_dialog_error)
|
binding.addr.error = requireContext().getText(R.string.proxy_dialog_error)
|
||||||
if (port == null)
|
if (port == null)
|
||||||
view.proxy_port.error = context.getText(R.string.proxy_dialog_error)
|
binding.port.error = requireContext().getText(R.string.proxy_dialog_error)
|
||||||
|
|
||||||
if (addr == null || addr.isEmpty() || port == null)
|
if (addr == null || addr.isEmpty() || port == null)
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
@@ -127,8 +128,6 @@ class ProxyDialog(context: Context) : AlertDialog(context) {
|
|||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,7 @@ import xyz.quaver.io.util.deleteRecursively
|
|||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.histories
|
import xyz.quaver.pupil.histories
|
||||||
import xyz.quaver.pupil.util.byteToString
|
import xyz.quaver.pupil.util.byteToString
|
||||||
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -61,6 +62,8 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
|||||||
if (dir.exists())
|
if (dir.exists())
|
||||||
dir.deleteRecursively()
|
dir.deleteRecursively()
|
||||||
|
|
||||||
|
Cache.instances.clear()
|
||||||
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
var size = 0L
|
var size = 0L
|
||||||
@@ -91,7 +94,12 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dir.exists())
|
if (dir.exists())
|
||||||
dir.listFiles()?.forEach { (it as? FileX)?.deleteRecursively() }
|
dir.listFiles()?.forEach {
|
||||||
|
when (it) {
|
||||||
|
is FileX -> it.deleteRecursively()
|
||||||
|
else -> it.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
job = launch {
|
job = launch {
|
||||||
var size = 0L
|
var size = 0L
|
||||||
|
|||||||
@@ -24,30 +24,34 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.andrognito.pinlockview.PinLockListener
|
import com.andrognito.pinlockview.PinLockListener
|
||||||
import kotlinx.android.synthetic.main.fragment_pin_lock.view.*
|
import xyz.quaver.pupil.databinding.PinLockFragmentBinding
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
|
|
||||||
class PINLockFragment : Fragment(), PinLockListener {
|
class PINLockFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: PinLockFragmentBinding? = null
|
||||||
|
val binding get() = _binding!!
|
||||||
|
|
||||||
var onPINEntered: ((String) -> Unit)? = null
|
var onPINEntered: ((String) -> Unit)? = null
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
return inflater.inflate(R.layout.fragment_pin_lock, container, false).apply {
|
_binding = PinLockFragmentBinding.inflate(inflater, container, false)
|
||||||
pin_lock_view.attachIndicatorDots(indicator_dots)
|
|
||||||
pin_lock_view.setPinLockListener(this@PINLockFragment)
|
binding.pinLockView.attachIndicatorDots(binding.indicatorDots)
|
||||||
}
|
binding.pinLockView.setPinLockListener(object: PinLockListener {
|
||||||
|
override fun onComplete(p0: String?) {
|
||||||
|
onPINEntered?.invoke(p0 ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEmpty() {}
|
||||||
|
override fun onPinChange(p0: Int, p1: String?) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onComplete(pin: String?) {
|
override fun onDestroy() {
|
||||||
onPINEntered?.invoke(pin!!)
|
super.onDestroy()
|
||||||
}
|
_binding = null
|
||||||
|
|
||||||
override fun onEmpty() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPinChange(pinLength: Int, intermediatePin: String?) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -26,38 +26,36 @@ import androidx.fragment.app.Fragment
|
|||||||
import com.andrognito.patternlockview.PatternLockView
|
import com.andrognito.patternlockview.PatternLockView
|
||||||
import com.andrognito.patternlockview.listener.PatternLockViewListener
|
import com.andrognito.patternlockview.listener.PatternLockViewListener
|
||||||
import com.andrognito.patternlockview.utils.PatternLockUtils
|
import com.andrognito.patternlockview.utils.PatternLockUtils
|
||||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
|
import xyz.quaver.pupil.databinding.PatternLockFragmentBinding
|
||||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.view.*
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
|
|
||||||
class PatternLockFragment : Fragment(), PatternLockViewListener {
|
class PatternLockFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: PatternLockFragmentBinding? = null
|
||||||
|
val binding get() = _binding!!
|
||||||
|
|
||||||
var onPatternDrawn: ((String) -> Unit)? = null
|
var onPatternDrawn: ((String) -> Unit)? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
return inflater.inflate(R.layout.fragment_pattern_lock, container, false).apply {
|
_binding = PatternLockFragmentBinding.inflate(inflater, container, false)
|
||||||
lock_pattern_view.addPatternLockListener(this@PatternLockFragment)
|
binding.patternLockView.addPatternLockListener(object: PatternLockViewListener {
|
||||||
}
|
override fun onComplete(pattern: MutableList<PatternLockView.Dot>?) {
|
||||||
|
val password = PatternLockUtils.patternToMD5(binding.patternLockView, pattern)
|
||||||
|
onPatternDrawn?.invoke(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {}
|
||||||
|
override fun onProgress(progressPattern: MutableList<PatternLockView.Dot>?) {}
|
||||||
|
override fun onStarted() {}
|
||||||
|
})
|
||||||
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
}
|
_binding = null
|
||||||
|
|
||||||
override fun onComplete(pattern: MutableList<PatternLockView.Dot>?) {
|
|
||||||
val password = PatternLockUtils.patternToMD5(lock_pattern_view, pattern)
|
|
||||||
onPatternDrawn?.invoke(password)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProgress(progressPattern: MutableList<PatternLockView.Dot>?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStarted() {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,25 +22,27 @@ import android.app.Activity
|
|||||||
import android.content.*
|
import android.content.*
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.preference.Preference
|
import androidx.preference.*
|
||||||
import androidx.preference.PreferenceCategory
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.Dispatcher
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
import xyz.quaver.io.util.getChild
|
import xyz.quaver.io.util.getChild
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.favorites
|
import xyz.quaver.pupil.client
|
||||||
|
import xyz.quaver.pupil.clientBuilder
|
||||||
|
import xyz.quaver.pupil.clientHolder
|
||||||
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.*
|
import xyz.quaver.pupil.ui.dialog.*
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
import java.nio.charset.Charset
|
import java.util.*
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class SettingsFragment :
|
class SettingsFragment :
|
||||||
PreferenceFragmentCompat(),
|
PreferenceFragmentCompat(),
|
||||||
@@ -48,6 +50,16 @@ class SettingsFragment :
|
|||||||
Preference.OnPreferenceChangeListener,
|
Preference.OnPreferenceChangeListener,
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
|
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
parentFragmentManager
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.settings, LockSettingsFragment())
|
||||||
|
.addToBackStack("Lock")
|
||||||
|
.commitAllowingStateLoss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
@@ -78,32 +90,27 @@ class SettingsFragment :
|
|||||||
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
|
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
|
||||||
}
|
}
|
||||||
"default_query" -> {
|
"default_query" -> {
|
||||||
DefaultQueryDialog(requireContext()).apply {
|
DefaultQueryDialog().apply {
|
||||||
onPositiveButtonClickListener = { newTags ->
|
onPositiveButtonClickListener = { newTags ->
|
||||||
Preferences["default_query"] = newTags.toString()
|
Preferences["default_query"] = newTags.toString()
|
||||||
summary = newTags.toString()
|
summary = newTags.toString()
|
||||||
}
|
}
|
||||||
}.show()
|
}.show(parentFragmentManager, "Default Query Dialog")
|
||||||
}
|
}
|
||||||
"app_lock" -> {
|
"app_lock" -> {
|
||||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||||
putExtra("force", true)
|
putExtra("force", true)
|
||||||
}
|
}
|
||||||
startActivityForResult(intent, R.id.request_lock.normalizeID())
|
lockLauncher.launch(intent)
|
||||||
}
|
|
||||||
"mirrors" -> {
|
|
||||||
MirrorDialog(requireContext())
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
"proxy" -> {
|
"proxy" -> {
|
||||||
ProxyDialog(requireContext())
|
ProxyDialogFragment().show(parentFragmentManager, "Proxy Dialog")
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
"user_id" -> {
|
"user_id" -> {
|
||||||
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||||
ClipData.newPlainText("user_id", Preferences.get<String>("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.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
@@ -117,6 +124,9 @@ class SettingsFragment :
|
|||||||
this ?: return false
|
this ?: return false
|
||||||
|
|
||||||
when (key) {
|
when (key) {
|
||||||
|
"tag_translation" -> {
|
||||||
|
updateTranslations()
|
||||||
|
}
|
||||||
"nomedia" -> {
|
"nomedia" -> {
|
||||||
val create = (newValue as? Boolean) ?: return false
|
val create = (newValue as? Boolean) ?: return false
|
||||||
|
|
||||||
@@ -158,6 +168,18 @@ class SettingsFragment :
|
|||||||
"download_folder_name" -> {
|
"download_folder_name" -> {
|
||||||
summary = Preferences["download_folder_name", "[-id-] -title-"]
|
summary = Preferences["download_folder_name", "[-id-] -title-"]
|
||||||
}
|
}
|
||||||
|
"max_concurrent_download" -> {
|
||||||
|
val newValue = Preferences.get<String>(key).toIntOrNull() ?: 0
|
||||||
|
|
||||||
|
if (newValue == 0)
|
||||||
|
clientBuilder.dispatcher(Dispatcher())
|
||||||
|
else
|
||||||
|
clientBuilder.dispatcher((Dispatcher(Executors.newFixedThreadPool(newValue))))
|
||||||
|
|
||||||
|
clientHolder = null
|
||||||
|
client
|
||||||
|
}
|
||||||
|
else -> return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,14 +259,32 @@ class SettingsFragment :
|
|||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"mirrors" -> {
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"proxy" -> {
|
"proxy" -> {
|
||||||
summary = getProxyInfo().type.name
|
summary = getProxyInfo().type.name
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
|
"tag_translation" -> {
|
||||||
|
this as ListPreference
|
||||||
|
|
||||||
|
isEnabled = false
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
kotlin.runCatching {
|
||||||
|
val languages = getAvailableLanguages().distinct().toTypedArray()
|
||||||
|
|
||||||
|
entries = languages.map { Locale(it).let { loc -> loc.getDisplayLanguage(loc) } }.toTypedArray()
|
||||||
|
entryValues = languages
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreferenceChangeListener = this@SettingsFragment
|
||||||
|
|
||||||
|
}
|
||||||
"dark_mode" -> {
|
"dark_mode" -> {
|
||||||
onPreferenceChangeListener = this@SettingsFragment
|
onPreferenceChangeListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
@@ -267,19 +307,4 @@ class SettingsFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
when(requestCode) {
|
|
||||||
R.id.request_lock.normalizeID() -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
parentFragmentManager
|
|
||||||
.beginTransaction()
|
|
||||||
.replace(R.id.settings, LockSettingsFragment())
|
|
||||||
.addToBackStack("Lock")
|
|
||||||
.commitAllowingStateLoss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -16,16 +16,16 @@
|
|||||||
* 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 com.arlib.floatingsearchview
|
package xyz.quaver.pupil.ui.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
import android.graphics.PorterDuffColorFilter
|
import android.graphics.PorterDuffColorFilter
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.os.Parcelable
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
@@ -36,32 +36,32 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||||
import com.arlib.floatingsearchview.suggestions.SearchSuggestionsAdapter
|
import xyz.quaver.floatingsearchview.FloatingSearchView
|
||||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import com.arlib.floatingsearchview.util.view.SearchInputView
|
import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.favoriteTags
|
import xyz.quaver.pupil.favoriteTags
|
||||||
import xyz.quaver.pupil.types.*
|
import xyz.quaver.pupil.types.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||||
FloatingSearchView(context, attrs),
|
FloatingSearchView(context, attrs),
|
||||||
FloatingSearchView.OnSearchListener,
|
FloatingSearchView.OnSearchListener,
|
||||||
SearchSuggestionsAdapter.OnBindSuggestionCallback,
|
|
||||||
TextWatcher
|
TextWatcher
|
||||||
{
|
{
|
||||||
|
|
||||||
private val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
private val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
||||||
|
|
||||||
var onHistoryDeleteClickedListener: ((String) -> Unit)? = null
|
var onHistoryDeleteClickedListener: ((String) -> Unit)? = null
|
||||||
var onFavoriteHistorySwitchClickListener: (() -> Unit)? = null
|
var onFavoriteHistorySwitchClickListener: (() -> Unit)? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
|
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or searchInputView.imeOptions
|
||||||
|
|
||||||
searchInputView.addTextChangedListener(this)
|
searchInputView.addTextChangedListener(this)
|
||||||
setOnSearchListener(this)
|
onSearchListener = this
|
||||||
setOnBindSuggestionCallback(this)
|
onBindSuggestionCallback = { binding, item, itemPosition ->
|
||||||
|
onBindSuggestion(binding.root, binding.leftIcon, binding.body, item, itemPosition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||||
@@ -83,7 +83,7 @@ class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, att
|
|||||||
when (searchSuggestion) {
|
when (searchSuggestion) {
|
||||||
is TagSuggestion -> {
|
is TagSuggestion -> {
|
||||||
val tag = "${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")}"
|
val tag = "${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")}"
|
||||||
with(searchInputView.text) {
|
with(searchInputView.text!!) {
|
||||||
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ') + 1, length)
|
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ') + 1, length)
|
||||||
|
|
||||||
if (!this.contains(tag))
|
if (!this.contains(tag))
|
||||||
@@ -91,9 +91,9 @@ class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, att
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Suggestion -> {
|
is Suggestion -> {
|
||||||
with(searchInputView.text) {
|
with(searchInputView.text!!) {
|
||||||
clear()
|
clear()
|
||||||
append(searchSuggestion.str)
|
append(searchSuggestion.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is FavoriteHistorySwitch -> onFavoriteHistorySwitchClickListener?.invoke()
|
is FavoriteHistorySwitch -> onFavoriteHistorySwitchClickListener?.invoke()
|
||||||
@@ -102,16 +102,16 @@ class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, att
|
|||||||
|
|
||||||
override fun onSearchAction(currentQuery: String?) {}
|
override fun onSearchAction(currentQuery: String?) {}
|
||||||
|
|
||||||
override fun onBindSuggestion(
|
fun onBindSuggestion(
|
||||||
suggestionView: View?,
|
suggestionView: View?,
|
||||||
leftIcon: ImageView?,
|
leftIcon: ImageView?,
|
||||||
textView: TextView?,
|
textView: TextView?,
|
||||||
item: SearchSuggestion?,
|
item: SearchSuggestion?,
|
||||||
itemPosition: Int
|
itemPosition: Int
|
||||||
) {
|
) {
|
||||||
when(item) {
|
when(item) {
|
||||||
is TagSuggestion -> {
|
is TagSuggestion -> {
|
||||||
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
|
val tag = "${item.n}:${item.s}"
|
||||||
|
|
||||||
leftIcon?.setImageDrawable(
|
leftIcon?.setImageDrawable(
|
||||||
ResourcesCompat.getDrawable(
|
ResourcesCompat.getDrawable(
|
||||||
@@ -162,9 +162,7 @@ class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, att
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.t == -1) {
|
if (item.t > 0) {
|
||||||
textView?.text = item.s
|
|
||||||
} else {
|
|
||||||
(suggestionView as? LinearLayout)?.let {
|
(suggestionView as? LinearLayout)?.let {
|
||||||
val count = it.findViewById<TextView>(R.id.count)
|
val count = it.findViewById<TextView>(R.id.count)
|
||||||
if (count == null)
|
if (count == null)
|
||||||
@@ -199,7 +197,7 @@ class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, att
|
|||||||
isClickable = true
|
isClickable = true
|
||||||
|
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
onHistoryDeleteClickedListener?.invoke(item.str)
|
onHistoryDeleteClickedListener?.invoke(item.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,10 +213,4 @@ class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, att
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// hack to remove color attributes which should not be reused
|
|
||||||
override fun onSaveInstanceState(): Parcelable? {
|
|
||||||
super.onSaveInstanceState()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
462
app/src/main/java/xyz/quaver/pupil/ui/view/MainView.java
Normal file
462
app/src/main/java/xyz/quaver/pupil/ui/view/MainView.java
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
/*
|
||||||
|
* 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.animation.ValueAnimator;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.Vibrator;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.DecelerateInterpolator;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.view.NestedScrollingChild;
|
||||||
|
import androidx.core.view.NestedScrollingChildHelper;
|
||||||
|
import androidx.core.view.NestedScrollingParent;
|
||||||
|
import androidx.core.view.NestedScrollingParentHelper;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.widget.TextViewCompat;
|
||||||
|
|
||||||
|
import xyz.quaver.pupil.R;
|
||||||
|
|
||||||
|
@SuppressWarnings("NullableProblems")
|
||||||
|
public class MainView extends ViewGroup implements NestedScrollingChild, NestedScrollingParent {
|
||||||
|
|
||||||
|
private static final int PAGE_TURN_LAYOUT_SIZE = 48;
|
||||||
|
private static final int PAGE_TURN_ANIM_DURATION = 500;
|
||||||
|
private static final int PREV_OFFSET = 64;
|
||||||
|
private static final int RIPPLE_GIVE = 4;
|
||||||
|
|
||||||
|
private final float adjustedPageTurnLayoutSize;
|
||||||
|
private final float adjustedPrevOffset;
|
||||||
|
private final float adjustedRippleGive;
|
||||||
|
|
||||||
|
final private NestedScrollingParentHelper mNestedScrollingParentHelper;
|
||||||
|
final private NestedScrollingChildHelper mNestedScrollingChildHelper;
|
||||||
|
|
||||||
|
final private Vibrator mVibrator;
|
||||||
|
|
||||||
|
private View mTarget;
|
||||||
|
|
||||||
|
private TextView mPrev;
|
||||||
|
private TextView mNext;
|
||||||
|
|
||||||
|
private final Paint mRipplePaint = new Paint();
|
||||||
|
private final Rect mRippleBound = new Rect();
|
||||||
|
|
||||||
|
private int mRippleSize = 0;
|
||||||
|
private final int mRippleTargetSize;
|
||||||
|
private final ValueAnimator mRippleAnimator = new ValueAnimator();
|
||||||
|
|
||||||
|
private int mCurrentOverScroll = 0;
|
||||||
|
|
||||||
|
private int mCurrentPage = 1;
|
||||||
|
private boolean mShowPrev;
|
||||||
|
private boolean mShowNext;
|
||||||
|
|
||||||
|
private OnPageTurnListener mOnPageTurnListener;
|
||||||
|
|
||||||
|
public MainView(@NonNull Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MainView(@NonNull Context context, AttributeSet attr) {
|
||||||
|
this(context, attr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MainView(@NonNull Context context, AttributeSet attr, int defStyle) {
|
||||||
|
super(context, attr, defStyle);
|
||||||
|
|
||||||
|
setWillNotDraw(false);
|
||||||
|
|
||||||
|
DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||||
|
|
||||||
|
adjustedPageTurnLayoutSize = PAGE_TURN_LAYOUT_SIZE * metrics.density;
|
||||||
|
adjustedPrevOffset = PREV_OFFSET * metrics.density;
|
||||||
|
adjustedRippleGive = RIPPLE_GIVE * metrics.density;
|
||||||
|
|
||||||
|
mRippleTargetSize = metrics.widthPixels;
|
||||||
|
|
||||||
|
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
|
||||||
|
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
|
||||||
|
|
||||||
|
mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
|
|
||||||
|
mRippleAnimator.addUpdateListener(animation -> {
|
||||||
|
mRippleSize = (int) animation.getAnimatedValue();
|
||||||
|
invalidate();
|
||||||
|
});
|
||||||
|
mRippleAnimator.setDuration(PAGE_TURN_ANIM_DURATION);
|
||||||
|
|
||||||
|
initPageTurnView();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentPage(int currentPage, boolean showNext) {
|
||||||
|
mCurrentPage = currentPage;
|
||||||
|
|
||||||
|
mShowPrev = currentPage > 1;
|
||||||
|
mShowNext = showNext;
|
||||||
|
|
||||||
|
mPrev.setText(getContext().getString(R.string.main_move_to_page, mCurrentPage-1));
|
||||||
|
mNext.setText(getContext().getString(R.string.main_move_to_page, mCurrentPage+1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnPageTurnListener(OnPageTurnListener listener) {
|
||||||
|
mOnPageTurnListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initPageTurnView() {
|
||||||
|
TextView prev = new TextView(getContext());
|
||||||
|
TextView next = new TextView(getContext());
|
||||||
|
|
||||||
|
prev.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
next.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
prev.setCompoundDrawablesWithIntrinsicBounds(R.drawable.navigate_prev, 0, 0, 0);
|
||||||
|
next.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.navigate_next, 0);
|
||||||
|
|
||||||
|
TextViewCompat.setCompoundDrawableTintList(prev, AppCompatResources.getColorStateList(getContext(), R.color.colorAccent));
|
||||||
|
TextViewCompat.setCompoundDrawableTintList(next, AppCompatResources.getColorStateList(getContext(), R.color.colorAccent));
|
||||||
|
|
||||||
|
prev.setVisibility(View.INVISIBLE);
|
||||||
|
next.setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
|
mPrev = prev;
|
||||||
|
mNext = next;
|
||||||
|
|
||||||
|
addView(mPrev);
|
||||||
|
addView(mNext);
|
||||||
|
|
||||||
|
setCurrentPage(1, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTarget() {
|
||||||
|
if (mTarget == null) {
|
||||||
|
for (int i = 0; i < getChildCount(); i++) {
|
||||||
|
View child = getChildAt(i);
|
||||||
|
|
||||||
|
if (!child.equals(mNext) && !child.equals(mPrev)) {
|
||||||
|
mTarget = child;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||||
|
final int width = getMeasuredWidth();
|
||||||
|
final int height = getMeasuredHeight();
|
||||||
|
|
||||||
|
if (getChildCount() == 0)
|
||||||
|
return;
|
||||||
|
if (mTarget == null)
|
||||||
|
ensureTarget();
|
||||||
|
if (mTarget == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mTarget.layout(
|
||||||
|
getPaddingLeft(),
|
||||||
|
getPaddingTop(),
|
||||||
|
width - getPaddingRight(),
|
||||||
|
height - getPaddingBottom()
|
||||||
|
);
|
||||||
|
|
||||||
|
final int prevWidth = mPrev.getMeasuredWidth();
|
||||||
|
mPrev.layout(
|
||||||
|
width / 2 - prevWidth / 2,
|
||||||
|
getPaddingTop() + (int) adjustedPrevOffset,
|
||||||
|
width / 2 + prevWidth / 2,
|
||||||
|
getPaddingTop() + (int) adjustedPrevOffset + mPrev.getMeasuredHeight()
|
||||||
|
);
|
||||||
|
|
||||||
|
final int nextWidth = mNext.getMeasuredWidth();
|
||||||
|
mNext.layout(
|
||||||
|
width / 2 - nextWidth / 2,
|
||||||
|
height - getPaddingBottom() - mNext.getMeasuredHeight(),
|
||||||
|
width / 2 + nextWidth / 2,
|
||||||
|
height - getPaddingBottom()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
if (mTarget == null)
|
||||||
|
ensureTarget();
|
||||||
|
if (mTarget == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mTarget.measure(
|
||||||
|
MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY),
|
||||||
|
MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)
|
||||||
|
);
|
||||||
|
|
||||||
|
mPrev.measure(
|
||||||
|
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST),
|
||||||
|
MeasureSpec.makeMeasureSpec((int) adjustedPageTurnLayoutSize, MeasureSpec.EXACTLY)
|
||||||
|
);
|
||||||
|
|
||||||
|
mNext.measure(
|
||||||
|
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST),
|
||||||
|
MeasureSpec.makeMeasureSpec((int) adjustedPageTurnLayoutSize, MeasureSpec.EXACTLY)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
|
||||||
|
if (mCurrentOverScroll == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mCurrentOverScroll > 0) {
|
||||||
|
mRippleBound.set(
|
||||||
|
getPaddingLeft(),
|
||||||
|
(int) (getPaddingTop() - adjustedRippleGive),
|
||||||
|
getMeasuredWidth() - getPaddingRight(),
|
||||||
|
(int) (getPaddingTop() + adjustedPrevOffset + mPrev.getMeasuredHeight() + adjustedRippleGive)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCurrentOverScroll < 0) {
|
||||||
|
final int height = getMeasuredHeight();
|
||||||
|
mRippleBound.set(
|
||||||
|
getPaddingLeft(),
|
||||||
|
(int) (height - getPaddingBottom() - mNext.getMeasuredHeight() - adjustedRippleGive),
|
||||||
|
getMeasuredWidth() - getPaddingRight(),
|
||||||
|
height - getPaddingBottom()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mRipplePaint.reset();
|
||||||
|
mRipplePaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
|
|
||||||
|
switch (currentNightMode) {
|
||||||
|
case Configuration.UI_MODE_NIGHT_YES:
|
||||||
|
mRipplePaint.setColor(ContextCompat.getColor(getContext(), R.color.material_light_blue_700));
|
||||||
|
break;
|
||||||
|
case Configuration.UI_MODE_NIGHT_NO:
|
||||||
|
mRipplePaint.setColor(ContextCompat.getColor(getContext(), R.color.material_light_blue_300));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawCircle(
|
||||||
|
(mRippleBound.left + mRippleBound.right) / 2F,
|
||||||
|
mCurrentOverScroll > 0 ? mRippleBound.bottom : mRippleBound.top,
|
||||||
|
mRippleSize,
|
||||||
|
mRipplePaint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onOverscroll(int overscroll) {
|
||||||
|
if (mTarget == null)
|
||||||
|
ensureTarget();
|
||||||
|
if (mTarget == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mCurrentOverScroll = overscroll;
|
||||||
|
|
||||||
|
if (overscroll > 0) {
|
||||||
|
mPrev.setVisibility(View.VISIBLE);
|
||||||
|
mNext.setVisibility(View.INVISIBLE);
|
||||||
|
} else if (overscroll < 0) {
|
||||||
|
mPrev.setVisibility(View.INVISIBLE);
|
||||||
|
mNext.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
mPrev.setVisibility(View.INVISIBLE);
|
||||||
|
mNext.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(overscroll) >= adjustedPageTurnLayoutSize) {
|
||||||
|
if (!mRippleAnimator.isStarted() && mRippleSize != mRippleTargetSize) {
|
||||||
|
mVibrator.vibrate(10);
|
||||||
|
|
||||||
|
mRippleAnimator.setIntValues(mRippleSize, mRippleTargetSize);
|
||||||
|
mRippleAnimator.start();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!mRippleAnimator.isStarted() && mRippleSize != 0) {
|
||||||
|
mRippleAnimator.setIntValues(mRippleSize, 0);
|
||||||
|
mRippleAnimator.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float clippedOverScrollTop = (overscroll > 0 ? 1 : -1) * Math.min(Math.abs(overscroll), adjustedPageTurnLayoutSize);
|
||||||
|
mTarget.setTranslationY(clippedOverScrollTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onOverscrollEnd(int overscroll) {
|
||||||
|
if (mTarget == null)
|
||||||
|
ensureTarget();
|
||||||
|
if (mTarget == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mRippleAnimator.cancel();
|
||||||
|
mRippleAnimator.setIntValues(mRippleSize, 0);
|
||||||
|
mRippleAnimator.start();
|
||||||
|
|
||||||
|
mPrev.setVisibility(View.INVISIBLE);
|
||||||
|
mNext.setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
|
ViewCompat.animate(mTarget)
|
||||||
|
.setDuration(PAGE_TURN_ANIM_DURATION)
|
||||||
|
.setInterpolator(new DecelerateInterpolator())
|
||||||
|
.translationY(0);
|
||||||
|
|
||||||
|
if (Math.abs(overscroll) > adjustedPageTurnLayoutSize && mOnPageTurnListener != null) {
|
||||||
|
if (overscroll > 0)
|
||||||
|
mOnPageTurnListener.onPrev(mCurrentPage-1);
|
||||||
|
if (overscroll < 0)
|
||||||
|
mOnPageTurnListener.onNext(mCurrentPage+1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NestedScrollingParent
|
||||||
|
|
||||||
|
private int mTotalUnconsumed = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
|
||||||
|
return isEnabled() && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNestedScrollAccepted(View child, View target, int axes) {
|
||||||
|
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
|
||||||
|
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
|
||||||
|
|
||||||
|
mTotalUnconsumed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
|
||||||
|
if (mTotalUnconsumed != 0 && dy > 0 == mTotalUnconsumed > 0) {
|
||||||
|
if (Math.abs(dy) > Math.abs(mTotalUnconsumed)) {
|
||||||
|
consumed[1] = dy - mTotalUnconsumed;
|
||||||
|
mTotalUnconsumed = 0;
|
||||||
|
} else {
|
||||||
|
mTotalUnconsumed -= dy;
|
||||||
|
consumed[1] = dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOverscroll(mTotalUnconsumed);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int[] parentConsumed = new int[2];
|
||||||
|
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
|
||||||
|
consumed[0] += parentConsumed[0];
|
||||||
|
consumed[1] += parentConsumed[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
|
||||||
|
final int[] mParentOffsetInWindow = new int[2];
|
||||||
|
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
|
||||||
|
|
||||||
|
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
|
||||||
|
|
||||||
|
if (mTotalUnconsumed == 0 && ((dy < 0 && !mShowPrev) || (dy > 0 && !mShowNext)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (dy != 0) {
|
||||||
|
mTotalUnconsumed -= dy;
|
||||||
|
onOverscroll(mTotalUnconsumed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopNestedScroll(View child) {
|
||||||
|
mNestedScrollingParentHelper.onStopNestedScroll(child);
|
||||||
|
|
||||||
|
if (Math.abs(mTotalUnconsumed) > 0) {
|
||||||
|
onOverscrollEnd(mTotalUnconsumed);
|
||||||
|
mTotalUnconsumed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopNestedScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// NestedScrollingChild
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNestedScrollingEnabled(boolean enabled) {
|
||||||
|
mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isNestedScrollingEnabled() {
|
||||||
|
return mNestedScrollingChildHelper.isNestedScrollingEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean startNestedScroll(int axes) {
|
||||||
|
return mNestedScrollingChildHelper.startNestedScroll(axes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopNestedScroll() {
|
||||||
|
mNestedScrollingChildHelper.stopNestedScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNestedScrollingParent() {
|
||||||
|
return mNestedScrollingChildHelper.hasNestedScrollingParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
|
||||||
|
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
|
||||||
|
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
|
||||||
|
return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
|
||||||
|
return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnPageTurnListener {
|
||||||
|
void onPrev(int page);
|
||||||
|
void onNext(int page);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCard.kt
Normal file
72
app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCard.kt
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package xyz.quaver.pupil.ui.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.databinding.ProgressCardViewBinding
|
||||||
|
|
||||||
|
class ProgressCard @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, defStyle: Int = R.attr.cardViewStyle) : CardView(context, attr, defStyle) {
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
LOADING,
|
||||||
|
CACHE,
|
||||||
|
DOWNLOAD
|
||||||
|
}
|
||||||
|
|
||||||
|
var type: Type = Type.LOADING
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
|
||||||
|
when (field) {
|
||||||
|
Type.LOADING -> R.color.colorAccent
|
||||||
|
Type.CACHE -> R.color.material_blue_700
|
||||||
|
Type.DOWNLOAD -> R.color.material_green_a700
|
||||||
|
}.let {
|
||||||
|
val color = ContextCompat.getColor(context, it)
|
||||||
|
DrawableCompat.setTint(binding.progressbar.progressDrawable, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress: Int
|
||||||
|
get() = binding.progressbar.progress
|
||||||
|
set(value) {
|
||||||
|
binding.progressbar.progress = value
|
||||||
|
}
|
||||||
|
var max: Int
|
||||||
|
get() = binding.progressbar.max
|
||||||
|
set(value) {
|
||||||
|
binding.progressbar.max = value
|
||||||
|
|
||||||
|
binding.progressbar.visibility =
|
||||||
|
if (value == 0)
|
||||||
|
GONE
|
||||||
|
else
|
||||||
|
VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
val binding = ProgressCardViewBinding.inflate(LayoutInflater.from(context), this)
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.content.setOnClickListener {
|
||||||
|
performClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.content.setOnLongClickListener {
|
||||||
|
performLongClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
|
||||||
|
if (childCount == 0)
|
||||||
|
super.addView(child, index, params)
|
||||||
|
else
|
||||||
|
binding.content.addView(child, index, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -23,17 +23,19 @@ import android.content.Context
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.favoriteTags
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
|
import xyz.quaver.pupil.util.translations
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class TagChip(context: Context, tag: Tag) : Chip(context) {
|
class TagChip(context: Context, _tag: Tag) : Chip(context) {
|
||||||
|
|
||||||
val tag: Tag =
|
val tag: Tag =
|
||||||
tag.let {
|
_tag.let {
|
||||||
when {
|
when {
|
||||||
it.area != null -> it
|
it.area != null -> it
|
||||||
else -> Tag("tag", tag.tag)
|
else -> Tag("tag", _tag.tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,23 +46,52 @@ class TagChip(context: Context, tag: Tag) : Chip(context) {
|
|||||||
}.toMap()
|
}.toMap()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
chipIcon = when(tag.area) {
|
when(tag.area) {
|
||||||
"male" -> {
|
"male" -> {
|
||||||
setChipBackgroundColorResource(R.color.material_blue_700)
|
setChipBackgroundColorResource(R.color.material_blue_700)
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||||
ContextCompat.getDrawable(context, R.drawable.gender_male_white)
|
setCloseIconTintResource(android.R.color.white)
|
||||||
|
chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_male_white)
|
||||||
}
|
}
|
||||||
"female" -> {
|
"female" -> {
|
||||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
setChipBackgroundColorResource(R.color.material_pink_600)
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||||
ContextCompat.getDrawable(context, R.drawable.gender_female_white)
|
setCloseIconTintResource(android.R.color.white)
|
||||||
|
chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_female_white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (favoriteTags.contains(tag))
|
||||||
|
setChipBackgroundColorResource(R.color.material_orange_500)
|
||||||
|
|
||||||
|
isCloseIconVisible = true
|
||||||
|
closeIcon = ContextCompat.getDrawable(context,
|
||||||
|
if (favoriteTags.contains(tag))
|
||||||
|
R.drawable.ic_star_filled
|
||||||
|
else
|
||||||
|
R.drawable.ic_star_empty
|
||||||
|
)
|
||||||
|
|
||||||
|
setOnCloseIconClickListener {
|
||||||
|
if (favoriteTags.contains(tag)) {
|
||||||
|
favoriteTags.remove(tag)
|
||||||
|
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_empty)
|
||||||
|
|
||||||
|
when(tag.area) {
|
||||||
|
"male" -> setChipBackgroundColorResource(R.color.material_blue_700)
|
||||||
|
"female" -> setChipBackgroundColorResource(R.color.material_pink_600)
|
||||||
|
else -> chipBackgroundColor = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
favoriteTags.add(tag)
|
||||||
|
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_filled)
|
||||||
|
setChipBackgroundColorResource(R.color.material_orange_500)
|
||||||
}
|
}
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
text = when (tag.area) {
|
text = when (tag.area) {
|
||||||
"language" -> languages[tag.tag]
|
"language" -> languages[tag.tag]
|
||||||
else -> tag.tag.wordCapitalize()
|
else -> (translations[tag.tag] ?: tag.tag).wordCapitalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnsureMinTouchTargetSize(false)
|
setEnsureMinTouchTargetSize(false)
|
||||||
|
|||||||
100
app/src/main/java/xyz/quaver/pupil/ui/view/TagChipGroup.kt
Normal file
100
app/src/main/java/xyz/quaver/pupil/ui/view/TagChipGroup.kt
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.ui.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
|
import com.google.android.material.chip.ChipGroup
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.types.Tag
|
||||||
|
import xyz.quaver.pupil.types.Tags
|
||||||
|
|
||||||
|
class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, attrStyle: Int = R.attr.chipGroupStyle, val tags: Tags = Tags()) : ChipGroup(context, attr, attrStyle), MutableSet<Tag> by tags {
|
||||||
|
|
||||||
|
object Defaults {
|
||||||
|
const val maxChipSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxChipSize: Int = Defaults.maxChipSize
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val moreView = Chip(context).apply {
|
||||||
|
text = "…"
|
||||||
|
|
||||||
|
setEnsureMinTouchTargetSize(false)
|
||||||
|
|
||||||
|
setOnClickListener {
|
||||||
|
removeView(this)
|
||||||
|
|
||||||
|
for (i in maxChipSize until tags.size) {
|
||||||
|
val tag = tags.elementAt(i)
|
||||||
|
|
||||||
|
addView(TagChip(context, tag).apply {
|
||||||
|
setOnClickListener {
|
||||||
|
onClickListener?.invoke(tag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var onClickListener: ((Tag) -> Unit)? = null
|
||||||
|
|
||||||
|
private fun applyAttributes(attr: TypedArray) {
|
||||||
|
maxChipSize = attr.getInt(R.styleable.TagChipGroup_maxTag, Defaults.maxChipSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var refreshJob: Job? = null
|
||||||
|
fun refresh() {
|
||||||
|
refreshJob?.cancel()
|
||||||
|
this.removeAllViews()
|
||||||
|
|
||||||
|
refreshJob = CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
tags.take(maxChipSize).map {
|
||||||
|
CoroutineScope(Dispatchers.Default).async {
|
||||||
|
TagChip(context, it).apply {
|
||||||
|
setOnClickListener {
|
||||||
|
onClickListener?.invoke(this.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.forEach {
|
||||||
|
addView(it.await())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxChipSize > 0 && tags.size > maxChipSize)
|
||||||
|
addView(moreView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
applyAttributes(context.obtainStyledAttributes(attr, R.styleable.TagChipGroup))
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -18,9 +18,12 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import kotlinx.serialization.*
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.serializer
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
|
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
|
||||||
@@ -38,27 +41,26 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
|
|||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
fun load() {
|
fun load() {
|
||||||
synchronized(this) {
|
set.clear()
|
||||||
set.clear()
|
kotlin.runCatching {
|
||||||
kotlin.runCatching {
|
Json.decodeFromString(serializer, file.readText())
|
||||||
Json.decodeFromString(serializer, file.readText())
|
}.onSuccess {
|
||||||
}.onSuccess {
|
set.addAll(it)
|
||||||
set.addAll(it)
|
}.onFailure {
|
||||||
}
|
FirebaseCrashlytics.getInstance().recordException(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
fun save() {
|
fun save() {
|
||||||
synchronized(this) {
|
file.writeText(Json.encodeToString(serializer, set.toList()))
|
||||||
file.writeText(Json.encodeToString(serializer, set.toList()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun add(element: T): Boolean {
|
override fun add(element: T): Boolean {
|
||||||
load()
|
|
||||||
|
|
||||||
set.remove(element)
|
set.remove(element)
|
||||||
|
|
||||||
return set.add(element).also {
|
return set.add(element).also {
|
||||||
@@ -66,9 +68,8 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun addAll(elements: Collection<T>): Boolean {
|
override fun addAll(elements: Collection<T>): Boolean {
|
||||||
load()
|
|
||||||
|
|
||||||
set.removeAll(elements)
|
set.removeAll(elements)
|
||||||
|
|
||||||
return set.addAll(elements).also {
|
return set.addAll(elements).also {
|
||||||
@@ -76,14 +77,14 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun remove(element: T): Boolean {
|
override fun remove(element: T): Boolean {
|
||||||
load()
|
|
||||||
|
|
||||||
return set.remove(element).also {
|
return set.remove(element).also {
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun clear() {
|
override fun clear() {
|
||||||
set.clear()
|
set.clear()
|
||||||
save()
|
save()
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.text.style.LineHeightSpan
|
|
||||||
|
|
||||||
class SetLineOverlap(private val overlap: Boolean) : LineHeightSpan {
|
|
||||||
companion object {
|
|
||||||
private var originalBottom = 15
|
|
||||||
private var originalDescent = 13
|
|
||||||
private var overlapSaved = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chooseHeight(
|
|
||||||
text: CharSequence?,
|
|
||||||
start: Int,
|
|
||||||
end: Int,
|
|
||||||
spanstartv: Int,
|
|
||||||
lineHeight: Int,
|
|
||||||
fm: Paint.FontMetricsInt?
|
|
||||||
) {
|
|
||||||
fm ?: return
|
|
||||||
|
|
||||||
if (overlap) {
|
|
||||||
if (overlapSaved) {
|
|
||||||
originalBottom = fm.bottom
|
|
||||||
originalDescent = fm.descent
|
|
||||||
overlapSaved = true
|
|
||||||
}
|
|
||||||
fm.bottom += fm.top
|
|
||||||
fm.descent += fm.top
|
|
||||||
} else {
|
|
||||||
fm.bottom = originalBottom
|
|
||||||
fm.descent = originalDescent
|
|
||||||
overlapSaved = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,38 +20,84 @@ package xyz.quaver.pupil.util.downloader
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.util.SparseArray
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
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.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Request
|
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.FileX
|
||||||
import xyz.quaver.io.util.*
|
import xyz.quaver.io.util.*
|
||||||
import xyz.quaver.pupil.client
|
import xyz.quaver.pupil.client
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.hitomi.GalleryBlock
|
||||||
|
import xyz.quaver.pupil.hitomi.GalleryInfo
|
||||||
|
import xyz.quaver.pupil.hitomi.getGalleryBlock
|
||||||
|
import xyz.quaver.pupil.hitomi.getGalleryInfo
|
||||||
|
import xyz.quaver.pupil.userAgent
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class OldGalleryBlock(
|
||||||
|
val code: String,
|
||||||
|
val id: Int,
|
||||||
|
val galleryUrl: String,
|
||||||
|
val thumbnails: List<String>,
|
||||||
|
val title: String,
|
||||||
|
val artists: List<String>,
|
||||||
|
val series: List<String>,
|
||||||
|
val type: String,
|
||||||
|
val language: String,
|
||||||
|
val relatedTags: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class OldReader(val code: String, val galleryInfo: GalleryInfo)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class OldMetadata(
|
||||||
|
var galleryBlock: OldGalleryBlock? = null,
|
||||||
|
var reader: OldReader? = null,
|
||||||
|
var imageList: MutableList<String?>? = null
|
||||||
|
) {
|
||||||
|
fun copy(): OldMetadata = OldMetadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Metadata(
|
data class Metadata(
|
||||||
var galleryBlock: GalleryBlock? = null,
|
var galleryBlock: GalleryBlock? = null,
|
||||||
var reader: Reader? = null,
|
var galleryInfo: GalleryInfo? = null,
|
||||||
var imageList: MutableList<String?>? = null
|
var imageList: MutableList<String?>? = null
|
||||||
) {
|
) {
|
||||||
fun copy(): Metadata = Metadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
constructor(old: OldMetadata) : this(old.galleryBlock?.let { galleryBlock -> GalleryBlock(
|
||||||
|
galleryBlock.id,
|
||||||
|
galleryBlock.galleryUrl,
|
||||||
|
galleryBlock.thumbnails,
|
||||||
|
galleryBlock.title,
|
||||||
|
galleryBlock.artists,
|
||||||
|
galleryBlock.series,
|
||||||
|
galleryBlock.type,
|
||||||
|
galleryBlock.language,
|
||||||
|
galleryBlock.relatedTags) },
|
||||||
|
old.reader?.galleryInfo,
|
||||||
|
old.imageList
|
||||||
|
)
|
||||||
|
|
||||||
|
fun copy(): Metadata = Metadata(galleryBlock, galleryInfo, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
||||||
}
|
}
|
||||||
|
|
||||||
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
|
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instances = SparseArray<Cache>()
|
val instances = ConcurrentHashMap<Int, Cache>()
|
||||||
|
|
||||||
fun getInstance(context: Context, galleryID: Int) =
|
fun getInstance(context: Context, galleryID: Int) =
|
||||||
instances[galleryID] ?: synchronized(this) {
|
instances[galleryID] ?: synchronized(this) {
|
||||||
@@ -59,9 +105,9 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun delete(galleryID: Int) {
|
fun delete(context: Context, galleryID: Int) {
|
||||||
instances[galleryID]?.cacheFolder?.deleteRecursively()
|
File(context.cacheDir, "imageCache/$galleryID").deleteRecursively()
|
||||||
instances.delete(galleryID)
|
instances.remove(galleryID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +116,12 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
|
|||||||
}
|
}
|
||||||
|
|
||||||
var metadata = kotlin.runCatching {
|
var metadata = kotlin.runCatching {
|
||||||
findFile(".metadata")?.readText()?.let {
|
findFile(".metadata")?.readText()?.let { metadata ->
|
||||||
Json.decodeFromString<Metadata>(it)
|
kotlin.runCatching {
|
||||||
|
Json.decodeFromString<Metadata>(metadata)
|
||||||
|
}.getOrElse {
|
||||||
|
Metadata(Json.decodeFromString<OldMetadata>(metadata))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.getOrNull() ?: Metadata()
|
}.getOrNull() ?: Metadata()
|
||||||
|
|
||||||
@@ -106,143 +156,127 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getGalleryBlock(): GalleryBlock? {
|
suspend fun getGalleryBlock(): GalleryBlock? {
|
||||||
val sources = listOf(
|
|
||||||
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
|
||||||
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
|
||||||
)
|
|
||||||
|
|
||||||
return metadata.galleryBlock
|
return metadata.galleryBlock
|
||||||
?: withContext(Dispatchers.IO) {
|
?: withContext(Dispatchers.IO) {
|
||||||
var galleryBlock: GalleryBlock? = null
|
try {
|
||||||
|
getGalleryBlock(galleryID).also {
|
||||||
for (source in sources) {
|
setMetadata { metadata -> metadata.galleryBlock = it }
|
||||||
galleryBlock = try {
|
}
|
||||||
source.invoke()
|
} catch (e: Exception) { return@withContext null }
|
||||||
} catch (e: Exception) { null }
|
|
||||||
|
|
||||||
if (galleryBlock != null)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
galleryBlock?.also {
|
|
||||||
setMetadata { metadata -> metadata.galleryBlock = it }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
suspend fun getThumbnail(): ByteArray? =
|
suspend fun getThumbnail(): Uri =
|
||||||
findFile(".thumbnail")?.readBytes()
|
findFile(".thumbnail")?.uri
|
||||||
?: getGalleryBlock()?.thumbnails?.firstOrNull()?.let { withContext(Dispatchers.IO) {
|
?: getGalleryBlock()?.thumbnails?.firstOrNull()?.let { withContext(Dispatchers.IO) {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(it)
|
.url(it)
|
||||||
|
.header("Referer", "https://hitomi.la/")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() }
|
client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() }
|
||||||
}.getOrNull()?.also { kotlin.run {
|
}.getOrNull()?.let { thumbnail -> kotlin.runCatching {
|
||||||
cacheFolder.getChild(".thumbnail").writeBytes(it)
|
cacheFolder.getChild(".thumbnail").also {
|
||||||
} }
|
if (!it.exists())
|
||||||
} }
|
it.createNewFile()
|
||||||
|
|
||||||
suspend fun getReader(): Reader? {
|
it.writeBytes(thumbnail)
|
||||||
val mirrors = Preferences.get<String>("mirrors").let { if (it.isEmpty()) emptyList() else it.split('>') }
|
}
|
||||||
|
}.getOrNull()?.uri }
|
||||||
|
} } ?: Uri.EMPTY
|
||||||
|
|
||||||
val sources = mapOf(
|
suspend fun getGalleryInfo(): GalleryInfo? {
|
||||||
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
|
return metadata.galleryInfo
|
||||||
?: withContext(Dispatchers.IO) {
|
?: withContext(Dispatchers.IO) {
|
||||||
var reader: Reader? = null
|
try {
|
||||||
|
getGalleryInfo(galleryID).also {
|
||||||
|
setMetadata { metadata ->
|
||||||
|
metadata.galleryInfo = it
|
||||||
|
|
||||||
for (source in sources) {
|
if (metadata.imageList == null)
|
||||||
reader = try {
|
metadata.imageList = MutableList(it.files.size) { null }
|
||||||
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 }
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getImage(index: Int): FileX? =
|
fun getImage(index: Int): FileX? =
|
||||||
metadata.imageList?.get(index)?.let { findFile(it) }
|
metadata.imageList?.getOrNull(index)?.let { findFile(it) }
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
fun putImage(index: Int, fileName: String, data: ByteArray) {
|
fun putImage(index: Int, fileName: String, data: ByteArray) {
|
||||||
val file = cacheFolder.getChild(fileName)
|
val file = cacheFolder.getChild(fileName)
|
||||||
|
|
||||||
file.createNewFile()
|
if (!file.exists())
|
||||||
|
file.createNewFile()
|
||||||
file.writeBytes(data)
|
file.writeBytes(data)
|
||||||
setMetadata { metadata -> metadata.imageList!![index] = fileName }
|
setMetadata { metadata -> metadata.imageList!![index] = fileName }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val lock = ConcurrentHashMap<Int, Mutex>()
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
|
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val downloadFolder = downloadFolder ?: return@launch
|
val downloadFolder = downloadFolder ?: return@launch
|
||||||
|
|
||||||
if (downloadFolder.getChild(".metadata").exists())
|
if (lock[galleryID]?.isLocked == true)
|
||||||
return@launch
|
return@launch
|
||||||
|
|
||||||
metadata.imageList?.forEach { imageName ->
|
(lock[galleryID] ?: Mutex().also { lock[galleryID] = it }).withLock {
|
||||||
imageName ?: return@forEach
|
val cacheMetadata = cacheFolder.getChild(".metadata")
|
||||||
val target = downloadFolder.getChild(imageName)
|
val downloadMetadata = downloadFolder.getChild(".metadata")
|
||||||
val source = cacheFolder.getChild(imageName)
|
|
||||||
|
|
||||||
if (!source.exists() || target.exists())
|
if (!cacheMetadata.exists())
|
||||||
return@forEach
|
return@launch
|
||||||
|
|
||||||
kotlin.runCatching {
|
if (cacheMetadata.exists()) {
|
||||||
target.createNewFile()
|
kotlin.runCatching {
|
||||||
target.outputStream()?.use { target -> source.inputStream()?.use { source ->
|
if (!downloadMetadata.exists())
|
||||||
source.copyTo(target)
|
downloadMetadata.createNewFile()
|
||||||
} }
|
|
||||||
|
downloadMetadata.writeText(Json.encodeToString(metadata))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val cacheThumbnail = cacheFolder.getChild(".thumbnail")
|
val cacheThumbnail = cacheFolder.getChild(".thumbnail")
|
||||||
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
|
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
|
||||||
|
|
||||||
if (cacheThumbnail.exists() && !downloadThumbnail.exists()) {
|
if (cacheThumbnail.exists()) {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
downloadThumbnail.createNewFile()
|
if (!downloadThumbnail.exists())
|
||||||
downloadThumbnail.outputStream()?.use { target -> cacheThumbnail.inputStream()?.use { source ->
|
downloadThumbnail.createNewFile()
|
||||||
source.copyTo(target)
|
|
||||||
} }
|
downloadThumbnail.outputStream()?.use { target -> target.channel.truncate(0L); cacheThumbnail.inputStream()?.use { source ->
|
||||||
cacheThumbnail.delete()
|
source.copyTo(target)
|
||||||
|
} }
|
||||||
|
cacheThumbnail.delete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val cacheMetadata = cacheFolder.getChild(".metadata")
|
metadata.imageList?.forEach { imageName ->
|
||||||
val downloadMetadata = downloadFolder.getChild(".metadata")
|
imageName ?: return@forEach
|
||||||
|
val target = downloadFolder.getChild(imageName)
|
||||||
|
val source = cacheFolder.getChild(imageName)
|
||||||
|
|
||||||
if (cacheMetadata.exists() && !downloadMetadata.exists()) {
|
if (!source.exists())
|
||||||
kotlin.runCatching {
|
return@forEach
|
||||||
downloadMetadata.createNewFile()
|
|
||||||
downloadMetadata.writeText(Json.encodeToString(metadata))
|
kotlin.runCatching {
|
||||||
cacheMetadata.delete()
|
if (!target.exists())
|
||||||
|
target.createNewFile()
|
||||||
|
|
||||||
|
target.outputStream()?.use { target -> target.channel.truncate(0L); source.inputStream()?.use { source ->
|
||||||
|
source.copyTo(target)
|
||||||
|
} }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
cacheFolder.delete()
|
cacheFolder.deleteRecursively()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,8 +20,9 @@ package xyz.quaver.pupil.util.downloader
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.util.Log
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -80,13 +81,13 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
|
|||||||
}.invoke()
|
}.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
return downloadFolderMapInstance!!
|
return downloadFolderMapInstance ?: mutableMapOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun isDownloading(galleryID: Int): Boolean {
|
fun isDownloading(galleryID: Int): Boolean {
|
||||||
val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
|
val isThisGallery: (Call) -> Boolean = { !it.isCanceled && (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
|
||||||
|
|
||||||
return downloadFolderMap.containsKey(galleryID)
|
return downloadFolderMap.containsKey(galleryID)
|
||||||
&& client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
|
&& client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
|
||||||
@@ -96,16 +97,13 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
|
|||||||
fun getDownloadFolder(galleryID: Int): FileX? =
|
fun getDownloadFolder(galleryID: Int): FileX? =
|
||||||
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
|
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
|
||||||
|
|
||||||
@Synchronized
|
fun addDownloadFolder(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
fun addDownloadFolder(galleryID: Int) {
|
val name = Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
|
||||||
val name = runBlocking {
|
?.formatDownloadFolder() ?: return@launch
|
||||||
Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
|
|
||||||
}?.formatDownloadFolder() ?: return
|
|
||||||
|
|
||||||
val folder = downloadFolder.getChild(name)
|
val folder = downloadFolder.getChild(name)
|
||||||
|
|
||||||
if (folder.exists())
|
if (folder.exists()) return@launch
|
||||||
return
|
|
||||||
|
|
||||||
folder.mkdir()
|
folder.mkdir()
|
||||||
|
|
||||||
|
|||||||
@@ -19,35 +19,49 @@
|
|||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.storage.StorageManager
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import androidx.core.content.ContextCompat
|
import kotlinx.coroutines.Dispatchers
|
||||||
import androidx.core.net.toUri
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import xyz.quaver.pupil.histories
|
||||||
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.lang.reflect.Array
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
val mutex = Mutex()
|
||||||
@Deprecated("Use downloader.Cache instead")
|
fun cleanCache(context: Context) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
fun getCachedGallery(context: Context, galleryID: Int) =
|
if (mutex.isLocked) return@launch
|
||||||
File(getDownloadDirectory(context), galleryID.toString()).let {
|
|
||||||
if (it.exists())
|
mutex.withLock {
|
||||||
it
|
val cacheFolder = File(context.cacheDir, "imageCache")
|
||||||
else
|
val downloadManager = DownloadManager.getInstance(context)
|
||||||
File(context.cacheDir, "imageCache/$galleryID")
|
|
||||||
|
val limit = (Preferences.get<String>("cache_limit").toLongOrNull() ?: 0L)*1024*1024*1024
|
||||||
|
|
||||||
|
if (limit == 0L) return@withLock
|
||||||
|
|
||||||
|
val cacheSize = {
|
||||||
|
var size = 0L
|
||||||
|
|
||||||
|
cacheFolder.walk().forEach {
|
||||||
|
size += it.length()
|
||||||
|
}
|
||||||
|
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheSize.invoke() > limit)
|
||||||
|
while (cacheSize.invoke() > limit/2) {
|
||||||
|
val caches = cacheFolder.list() ?: return@withLock
|
||||||
|
|
||||||
|
synchronized(histories) {
|
||||||
|
(histories.firstOrNull {
|
||||||
|
caches.contains(it.toString()) && !downloadManager.isDownloading(it)
|
||||||
|
} ?: return@withLock).let {
|
||||||
|
Cache.delete(context, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
@Deprecated("Use downloader.Cache instead")
|
|
||||||
fun getDownloadDirectory(context: Context) =
|
|
||||||
Preferences.get<String>("dl_location").let {
|
|
||||||
if (it.isNotEmpty() && !it.startsWith("content"))
|
|
||||||
File(it)
|
|
||||||
else
|
|
||||||
context.getExternalFilesDir(null)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
@Deprecated("Use FileX instead")
|
|
||||||
fun File.isParentOf(another: File) =
|
|
||||||
another.absolutePath.startsWith(this.absolutePath)
|
|
||||||
@@ -19,18 +19,14 @@
|
|||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import android.content.Intent
|
import kotlinx.serialization.json.*
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import xyz.quaver.Code
|
import xyz.quaver.pupil.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.pupil.hitomi.GalleryInfo
|
||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.pupil.hitomi.getReferer
|
||||||
import xyz.quaver.hitomi.getReferer
|
import xyz.quaver.pupil.hitomi.imageUrlFromImage
|
||||||
import xyz.quaver.hitomi.imageUrlFromImage
|
|
||||||
import xyz.quaver.hiyobi.createImgList
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
@@ -40,7 +36,7 @@ fun String.wordCapitalize() : String {
|
|||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
for (word in this.split(" "))
|
for (word in this.split(" "))
|
||||||
result.add(word.capitalize(Locale.US))
|
result.add(word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() })
|
||||||
|
|
||||||
return result.joinToString(" ")
|
return result.joinToString(" ")
|
||||||
}
|
}
|
||||||
@@ -93,39 +89,50 @@ fun GalleryBlock.formatDownloadFolder(): String =
|
|||||||
formatMap.entries.fold(it) { str, (k, v) ->
|
formatMap.entries.fold(it) { str, (k, v) ->
|
||||||
str.replace(k, v.invoke(this), true)
|
str.replace(k, v.invoke(this), true)
|
||||||
}
|
}
|
||||||
}.replace(Regex("""[*\\|"?><:/]"""), "")
|
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
|
||||||
|
|
||||||
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
|
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
|
||||||
format.let {
|
format.let {
|
||||||
formatMap.entries.fold(it) { str, (k, v) ->
|
formatMap.entries.fold(it) { str, (k, v) ->
|
||||||
str.replace(k, v.invoke(this), true)
|
str.replace(k, v.invoke(this), true)
|
||||||
}
|
}
|
||||||
}.replace(Regex("""[*\\|"?><:/]"""), "")
|
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
|
||||||
|
|
||||||
val Reader.requestBuilders: List<Request.Builder>
|
suspend fun GalleryInfo.getRequestBuilders(): List<Request.Builder> {
|
||||||
get() {
|
val galleryID = this.id ?: 0
|
||||||
val galleryID = this.galleryInfo.id ?: 0
|
val lowQuality = Preferences["low_quality", true]
|
||||||
val lowQuality = Preferences["low_quality", true]
|
|
||||||
|
|
||||||
return when(code) {
|
return this.files.map {
|
||||||
Code.HITOMI -> {
|
Request.Builder()
|
||||||
this.galleryInfo.files.map {
|
.url(
|
||||||
Request.Builder()
|
runCatching {
|
||||||
.url(imageUrlFromImage(galleryID, it, !lowQuality))
|
imageUrlFromImage(galleryID, it, !lowQuality)
|
||||||
.header("Referer", getReferer(galleryID))
|
|
||||||
}
|
}
|
||||||
}
|
.onFailure {
|
||||||
Code.HIYOBI -> {
|
FirebaseCrashlytics.getInstance().recordException(it)
|
||||||
createImgList(galleryID, this, lowQuality).map {
|
|
||||||
Request.Builder()
|
|
||||||
.url(it.path)
|
|
||||||
}
|
}
|
||||||
}
|
.getOrDefault("https://a/")
|
||||||
}
|
)
|
||||||
|
.header("Referer", "https://hitomi.la/")
|
||||||
|
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun String.ellipsize(n: Int): String =
|
fun String.ellipsize(n: Int): String =
|
||||||
if (this.length > n)
|
if (this.length > n)
|
||||||
this.slice(0 until n) + "…"
|
this.slice(0 until n) + "…"
|
||||||
else
|
else
|
||||||
this
|
this
|
||||||
|
|
||||||
|
operator fun JsonElement.get(index: Int) =
|
||||||
|
this.jsonArray[index]
|
||||||
|
|
||||||
|
operator fun JsonElement.get(tag: String) =
|
||||||
|
this.jsonObject[tag]
|
||||||
|
|
||||||
|
fun JsonElement.getOrNull(tag: String) = kotlin.runCatching {
|
||||||
|
this.jsonObject.getOrDefault(tag, null)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
val JsonElement.content
|
||||||
|
get() = this.jsonPrimitive.contentOrNull
|
||||||
68
app/src/main/java/xyz/quaver/pupil/util/translation.kt
Normal file
68
app/src/main/java/xyz/quaver/pupil/util/translation.kt
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okhttp3.Request
|
||||||
|
import xyz.quaver.pupil.client
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
private val filesURL = "https://api.github.com/repos/tom5079/Pupil/git/trees/tags"
|
||||||
|
private val contentURL = "https://raw.githubusercontent.com/tom5079/Pupil/tags/"
|
||||||
|
|
||||||
|
var translations: Map<String, String> = run {
|
||||||
|
updateTranslations()
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
fun updateTranslations() = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
translations = emptyMap()
|
||||||
|
kotlin.runCatching {
|
||||||
|
translations = Json.decodeFromString<Map<String, String>>(client.newCall(
|
||||||
|
Request.Builder()
|
||||||
|
.url(contentURL + "${Preferences["tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json")
|
||||||
|
.build()
|
||||||
|
).execute().also { if (it.code() != 200) return@launch }.body()?.use { it.string() } ?: return@launch).filterValues { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAvailableLanguages(): List<String> {
|
||||||
|
val languages = Locale.getISOLanguages()
|
||||||
|
|
||||||
|
val json = Json.parseToJsonElement(client.newCall(
|
||||||
|
Request.Builder()
|
||||||
|
.url(filesURL)
|
||||||
|
.build()
|
||||||
|
).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.string() } ?: return emptyList())
|
||||||
|
|
||||||
|
return listOf("en") + (json["tree"]?.jsonArray?.mapNotNull {
|
||||||
|
val name = it["path"]?.jsonPrimitive?.content?.takeWhile { c -> c != '.' }
|
||||||
|
|
||||||
|
languages.firstOrNull { code -> code.equals(name, ignoreCase = true) }
|
||||||
|
} ?: emptyList())
|
||||||
|
}
|
||||||
@@ -18,48 +18,26 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Base64
|
|
||||||
import android.util.Log
|
|
||||||
import android.webkit.URLUtil
|
import android.webkit.URLUtil
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import ru.noties.markwon.Markwon
|
import ru.noties.markwon.Markwon
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.Reader
|
|
||||||
import xyz.quaver.hitomi.getGalleryBlock
|
|
||||||
import xyz.quaver.hitomi.getReader
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.getChild
|
|
||||||
import xyz.quaver.io.util.*
|
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.client
|
import xyz.quaver.pupil.client
|
||||||
import xyz.quaver.pupil.favorites
|
import xyz.quaver.pupil.favorites
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.downloader.Metadata
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
@@ -160,7 +138,6 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
|||||||
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.ok) { _, _ ->
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
|
||||||
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
|
||||||
@@ -182,7 +159,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
|||||||
Preferences["update_download_id"] = it
|
Preferences["update_download_id"] = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setNegativeButton(if (force) android.R.string.cancel else R.string.ignore_update) { _, _ ->
|
setNegativeButton(if (force) android.R.string.cancel else R.string.ignore) { _, _ ->
|
||||||
if (!force)
|
if (!force)
|
||||||
preferences.edit()
|
preferences.edit()
|
||||||
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
||||||
@@ -221,116 +198,4 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((
|
|||||||
}.onFailure { onFailure?.invoke(it) }
|
}.onFailure { onFailure?.invoke(it) }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
private var job: Job? = null
|
|
||||||
private val receiver = object: BroadcastReceiver() {
|
|
||||||
val ACTION_CANCEL = "ACTION_IMPORT_CANCEL"
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
context ?: return
|
|
||||||
|
|
||||||
when (intent?.action) {
|
|
||||||
ACTION_CANCEL -> {
|
|
||||||
job?.cancel()
|
|
||||||
NotificationManagerCompat.from(context).cancel(R.id.notification_id_import)
|
|
||||||
context.unregisterReceiver(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
|
|
||||||
registerReceiver(receiver, IntentFilter().apply { addAction(receiver.ACTION_CANCEL) })
|
|
||||||
|
|
||||||
val notificationManager = NotificationManagerCompat.from(this)
|
|
||||||
val action = NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
|
||||||
PendingIntent.getBroadcast(this, R.id.notification_import_cancel_action.normalizeID(), Intent(receiver.ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
).build()
|
|
||||||
val notification = NotificationCompat.Builder(this, "import")
|
|
||||||
.setContentTitle(getText(R.string.import_old_galleries_notification))
|
|
||||||
.setProgress(0, 0, true)
|
|
||||||
.addAction(action)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setOngoing(true)
|
|
||||||
|
|
||||||
DownloadService.cancel(this)
|
|
||||||
|
|
||||||
job?.cancel()
|
|
||||||
job = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val downloadFolders = downloadFolder.listFiles { folder ->
|
|
||||||
folder.isDirectory && !downloadFolderMap.values.contains(folder.name)
|
|
||||||
}?.map {
|
|
||||||
if (it !is FileX)
|
|
||||||
FileX(this@migrate, it)
|
|
||||||
else
|
|
||||||
it
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloadFolders.isNullOrEmpty()) return@launch
|
|
||||||
|
|
||||||
downloadFolders.forEachIndexed { index, folder ->
|
|
||||||
notification
|
|
||||||
.setContentText(getString(R.string.import_old_galleries_notification_text, index, downloadFolders.size))
|
|
||||||
.setProgress(index, downloadFolders.size, false)
|
|
||||||
notificationManager.notify(R.id.notification_id_import, notification.build())
|
|
||||||
|
|
||||||
kotlin.runCatching {
|
|
||||||
val metadata = kotlin.runCatching {
|
|
||||||
folder.getChild(".metadata").readText()?.let { Json.parseToJsonElement(it).jsonObject }
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
val galleryID = folder.name.toIntOrNull() ?: return@runCatching
|
|
||||||
|
|
||||||
val galleryBlock: GalleryBlock? = kotlin.runCatching {
|
|
||||||
metadata?.get("galleryBlock")?.let { Json.decodeFromJsonElement<GalleryBlock>(it) }
|
|
||||||
}.getOrNull() ?: getGalleryBlock(galleryID)
|
|
||||||
val reader: Reader? = kotlin.runCatching {
|
|
||||||
metadata?.get("reader")?.let { Json.decodeFromJsonElement<Reader>(it) }
|
|
||||||
}.getOrNull() ?: getReader(galleryID)
|
|
||||||
|
|
||||||
metadata?.get("thumbnail")?.jsonPrimitive?.contentOrNull?.also { thumbnail ->
|
|
||||||
val file = folder.getChild(".thumbnail").also {
|
|
||||||
if (it.exists())
|
|
||||||
it.delete()
|
|
||||||
it.createNewFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
file.writeBytes(Base64.decode(thumbnail, Base64.DEFAULT))
|
|
||||||
}
|
|
||||||
|
|
||||||
val list: MutableList<String?> =
|
|
||||||
MutableList(reader!!.galleryInfo.files.size) { null }
|
|
||||||
|
|
||||||
folder.listFiles { file ->
|
|
||||||
file?.nameWithoutExtension?.let {
|
|
||||||
Regex("""\d{5}""").matches(it) && it.toIntOrNull() != null
|
|
||||||
} == true
|
|
||||||
}?.forEach {
|
|
||||||
list[it.nameWithoutExtension.toInt()] = it.name
|
|
||||||
}
|
|
||||||
|
|
||||||
folder.getChild(".metadata").also { if (it.exists()) it.delete(); it.createNewFile() }.writeText(
|
|
||||||
Json.encodeToString(Metadata(galleryBlock, reader, list))
|
|
||||||
)
|
|
||||||
|
|
||||||
synchronized(Cache) {
|
|
||||||
Cache.delete(galleryID)
|
|
||||||
}
|
|
||||||
downloadFolderMap[galleryID] = folder.name
|
|
||||||
|
|
||||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile(); it.writeText(Json.encodeToString(downloadFolderMap)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notification
|
|
||||||
.setContentText(getText(R.string.import_old_galleries_notification_done))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
.setOngoing(false)
|
|
||||||
.mActions.clear()
|
|
||||||
notificationManager.notify(R.id.notification_id_import, notification.build())
|
|
||||||
|
|
||||||
kotlin.runCatching {
|
|
||||||
unregisterReceiver(receiver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 159 B |
@@ -1,3 +1,3 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
||||||
<path android:fillColor="#FF000000" android:pathData="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
<path android:fillColor="#FF000000" android:pathData="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
||||||
</vector>
|
</vector>
|
||||||
6
app/src/main/res/drawable/navigate_prev.xml
Normal file
6
app/src/main/res/drawable/navigate_prev.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
||||||
|
<group android:pivotX="12" android:scaleX="-1">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<item android:bottom="1dp" android:left="1dp" android:right="1dp" android:top="1dp">
|
<item android:bottom="1dp" android:left="1dp" android:right="1dp" android:top="1dp">
|
||||||
<shape android:shape="rectangle">
|
<shape android:shape="rectangle">
|
||||||
<stroke android:width="1dp" android:color="#555555"/>
|
<stroke android:width="1dp" android:color="#555555"/>
|
||||||
<solid android:color="@color/transparent"/>
|
<solid android:color="@android:color/transparent"/>
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|||||||
8
app/src/main/res/drawable/sort_variant.xml
Normal file
8
app/src/main/res/drawable/sort_variant.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!-- 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="#000" android:pathData="M3,13H15V11H3M3,6V8H21V6M3,18H9V16H3V18Z" />
|
||||||
|
</vector>
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
|
||||||
~ Copyright (C) 2019 tom5079
|
|
||||||
~
|
|
||||||
~ This program is free software: you can redistribute it and/or modify
|
|
||||||
~ it under the terms of the GNU General Public License as published by
|
|
||||||
~ the Free Software Foundation, either version 3 of the License, or
|
|
||||||
~ (at your option) any later version.
|
|
||||||
~
|
|
||||||
~ This program is distributed in the hope that it will be useful,
|
|
||||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
~ GNU General Public License for more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License
|
|
||||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<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:handleDrawable="@drawable/thumb"
|
|
||||||
app:handleHasFixedSize="true"
|
|
||||||
app:handleHeight="72dp"
|
|
||||||
app:handleWidth="24dp"
|
|
||||||
app:disableTrack="true"
|
|
||||||
app:hideHandleAfter="1000"
|
|
||||||
app:trackMarginStart="64dp"
|
|
||||||
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>
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
|
||||||
~ Copyright (C) 2019 tom5079
|
|
||||||
~
|
|
||||||
~ This program is free software: you can redistribute it and/or modify
|
|
||||||
~ it under the terms of the GNU General Public License as published by
|
|
||||||
~ the Free Software Foundation, either version 3 of the License, or
|
|
||||||
~ (at your option) any later version.
|
|
||||||
~
|
|
||||||
~ This program is distributed in the hope that it will be useful,
|
|
||||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
~ GNU General Public License for more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License
|
|
||||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<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:handleDrawable="@drawable/thumb"
|
|
||||||
app:handleHasFixedSize="true"
|
|
||||||
app:handleHeight="72dp"
|
|
||||||
app:handleWidth="24dp"
|
|
||||||
app:disableTrack="true"
|
|
||||||
app:hideHandleAfter="1000"
|
|
||||||
app:trackMarginStart="64dp"
|
|
||||||
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_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>
|
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
tools:ignore="Autofill"
|
tools:ignore="Autofill"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:hint="@string/settings_default_query"
|
android:hint="@string/settings_default_query"
|
||||||
android:id="@+id/default_query_dialog_edittext"
|
android:id="@+id/edittext"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
@@ -36,10 +36,10 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/default_query_dialog_language_layout"
|
android:id="@+id/language_layout"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_edittext"
|
app:layout_constraintTop_toBottomOf="@id/edittext"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
@@ -49,14 +49,14 @@
|
|||||||
android:text="@string/default_query_dialog_language"/>
|
android:text="@string/default_query_dialog_language"/>
|
||||||
|
|
||||||
<Spinner
|
<Spinner
|
||||||
android:id="@+id/default_query_dialog_language_selector"
|
android:id="@+id/language_selector"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/default_query_dialog_BL_layout"
|
android:id="@+id/BL_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"
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
android:paddingStart="0dp"
|
android:paddingStart="0dp"
|
||||||
android:paddingEnd="8dp"
|
android:paddingEnd="8dp"
|
||||||
android:paddingRight="8dp"
|
android:paddingRight="8dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_language_layout"
|
app:layout_constraintTop_toBottomOf="@id/language_layout"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
@@ -75,14 +75,14 @@
|
|||||||
android:text="@string/default_query_dialog_filter_BL"/>
|
android:text="@string/default_query_dialog_filter_BL"/>
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/default_query_dialog_BL_checkbox"
|
android:id="@+id/BL_checkbox"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/default_query_dialog_guro_layout"
|
android:id="@+id/guro_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"
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
android:paddingStart="0dp"
|
android:paddingStart="0dp"
|
||||||
android:paddingEnd="8dp"
|
android:paddingEnd="8dp"
|
||||||
android:paddingRight="8dp"
|
android:paddingRight="8dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_BL_layout"
|
app:layout_constraintTop_toBottomOf="@id/BL_layout"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
android:text="@string/default_query_dialog_filter_guro"/>
|
android:text="@string/default_query_dialog_filter_guro"/>
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/default_query_dialog_guro_checkbox"
|
android:id="@+id/guro_checkbox"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
android:paddingStart="0dp"
|
android:paddingStart="0dp"
|
||||||
android:paddingEnd="8dp"
|
android:paddingEnd="8dp"
|
||||||
android:paddingRight="8dp"
|
android:paddingRight="8dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_guro_layout"
|
app:layout_constraintTop_toBottomOf="@id/guro_layout"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
android:text="@string/default_query_dialog_filter_loli"/>
|
android:text="@string/default_query_dialog_filter_loli"/>
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/default_query_dialog_loli_checkbox"
|
android:id="@+id/loli_checkbox"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
@@ -19,14 +19,13 @@
|
|||||||
|
|
||||||
<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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
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"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/gallery_toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
@@ -40,35 +39,35 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="8dp">
|
android:padding="8dp">
|
||||||
|
|
||||||
<ImageView
|
<com.github.piasy.biv.view.BigImageView
|
||||||
android:id="@+id/gallery_cover"
|
android:id="@+id/cover"
|
||||||
android:layout_width="150dp"
|
android:layout_width="150dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
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/title"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/gallery_title"
|
android:id="@+id/title"
|
||||||
style="@style/TextAppearance.AppCompat.Headline"
|
style="@style/TextAppearance.AppCompat.Headline"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
|
app:layout_constraintLeft_toRightOf="@id/cover"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginStart="8dp"/>
|
android:layout_marginStart="8dp"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="@style/TextAppearance.AppCompat.Medium"
|
style="@style/TextAppearance.AppCompat.Medium"
|
||||||
android:id="@+id/gallery_artist"
|
android:id="@+id/artist"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/gallery_title"
|
app:layout_constraintTop_toBottomOf="@id/title"
|
||||||
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
|
app:layout_constraintLeft_toRightOf="@id/cover"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginStart="8dp"/>
|
android:layout_marginStart="8dp"/>
|
||||||
@@ -76,15 +75,15 @@
|
|||||||
<View
|
<View
|
||||||
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/artist"
|
||||||
app:layout_constraintBottom_toTopOf="@id/gallery_type"/>
|
app:layout_constraintBottom_toTopOf="@id/type"/>
|
||||||
|
|
||||||
<com.google.android.material.chip.Chip
|
<com.google.android.material.chip.Chip
|
||||||
android:id="@+id/gallery_type"
|
android:id="@+id/type"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
|
app:layout_constraintLeft_toRightOf="@id/cover"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginStart="8dp"/>
|
android:layout_marginStart="8dp"/>
|
||||||
|
|
||||||
@@ -100,7 +99,7 @@
|
|||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/gallery_contents"
|
android:id="@+id/contents"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"/>
|
android:orientation="vertical"/>
|
||||||
@@ -112,7 +111,7 @@
|
|||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/gallery_progressbar"
|
android:id="@+id/progressbar"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
@@ -123,11 +122,11 @@
|
|||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/gallery_fab"
|
android:id="@+id/fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="16dp"
|
android:layout_margin="16dp"
|
||||||
app:layout_anchor="@id/gallery_toolbar"
|
app:layout_anchor="@id/toolbar"
|
||||||
app:layout_anchorGravity="bottom|end"/>
|
app:layout_anchorGravity="bottom|end"/>
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -25,14 +25,14 @@
|
|||||||
android:padding="8dp">
|
android:padding="8dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/gallery_details"
|
android:id="@+id/type"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
android:textColor="@color/colorAccent"/>
|
android:textColor="@color/colorAccent"/>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/gallery_details_contents"
|
android:id="@+id/contents"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"/>
|
android:orientation="vertical"/>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
android:layout_margin="8dp">
|
android:layout_margin="8dp">
|
||||||
|
|
||||||
<com.tbuonomo.viewpagerdotsindicator.DotsIndicator
|
<com.tbuonomo.viewpagerdotsindicator.DotsIndicator
|
||||||
android:id="@+id/gallery_dotindicator"
|
android:id="@+id/dotindicator"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
@@ -26,13 +26,13 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||||
android:id="@+id/gallery_details_type"
|
android:id="@+id/type"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingBottom="8dp"/>
|
android:paddingBottom="8dp"/>
|
||||||
|
|
||||||
<com.google.android.material.chip.ChipGroup
|
<com.google.android.material.chip.ChipGroup
|
||||||
android:id="@+id/gallery_details_tags"
|
android:id="@+id/tags"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:chipSpacingVertical="4dp"/>
|
app:chipSpacingVertical="4dp"/>
|
||||||
160
app/src/main/res/layout/galleryblock_item.xml
Normal file
160
app/src/main/res/layout/galleryblock_item.xml
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Pupil, Hitomi.la viewer for Android
|
||||||
|
~ Copyright (C) 2019 tom5079
|
||||||
|
~
|
||||||
|
~ This program is free software: you can redistribute it and/or modify
|
||||||
|
~ it under the terms of the GNU General Public License as published by
|
||||||
|
~ the Free Software Foundation, either version 3 of the License, or
|
||||||
|
~ (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ This program is distributed in the hope that it will be useful,
|
||||||
|
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
~ GNU General Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License
|
||||||
|
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<xyz.quaver.pupil.ui.view.ProgressCard
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/galleryblock_card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="true"
|
||||||
|
app:cardCornerRadius="4dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
app:cardUseCompatPadding="true"
|
||||||
|
tools:ignore="RtlHardcoded">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.github.piasy.biv.view.BigImageView
|
||||||
|
android:id="@+id/galleryblock_thumbnail"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:contentDescription="@string/galleryblock_thumbnail_description"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:clickable="false"
|
||||||
|
app:layout_constraintHeight_default="spread"
|
||||||
|
app:layout_constraintHeight_min="200dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/barrier"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/TextAppearance.AppCompat.Headline"
|
||||||
|
android:id="@+id/galleryblock_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/TextAppearance.AppCompat.Medium"
|
||||||
|
android:id="@+id/galleryblock_artist"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/galleryblock_title" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/galleryblock_series"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/galleryblock_type"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/galleryblock_language"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
|
||||||
|
|
||||||
|
<xyz.quaver.pupil.ui.view.TagChipGroup
|
||||||
|
android:id="@+id/galleryblock_tag_group"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:chipSpacing="4dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/galleryblock_language"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/barrier"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:constraint_referenced_ids="galleryblock_thumbnail, galleryblock_tag_group"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/divider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?android:attr/listDivider"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/barrier"
|
||||||
|
android:layout_margin="8dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/galleryblock_id"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/divider"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/galleryblock_pagecount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/divider"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/galleryblock_favorite"
|
||||||
|
android:contentDescription="@string/app_name"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:srcCompat="@drawable/ic_star_empty"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/divider"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</xyz.quaver.pupil.ui.view.ProgressCard>
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
|
||||||
~ Copyright (C) 2019 tom5079
|
|
||||||
~
|
|
||||||
~ This program is free software: you can redistribute it and/or modify
|
|
||||||
~ it under the terms of the GNU General Public License as published by
|
|
||||||
~ the Free Software Foundation, either version 3 of the License, or
|
|
||||||
~ (at your option) any later version.
|
|
||||||
~
|
|
||||||
~ This program is distributed in the hope that it will be useful,
|
|
||||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
~ GNU General Public License for more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License
|
|
||||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
app:cardCornerRadius="8dp"
|
|
||||||
android:clipChildren="true"
|
|
||||||
tools:ignore="RtlHardcoded">
|
|
||||||
|
|
||||||
<com.daimajia.swipe.SwipeLayout
|
|
||||||
android:id="@+id/galleryblock_swipe_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:drag_edge="right"
|
|
||||||
app:show_mode="pull_out">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/galleryblock_download"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:minWidth="70dp"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:background="@android:color/holo_blue_dark"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:text="@string/main_download"
|
|
||||||
android:foreground="?android:attr/selectableItemBackground"
|
|
||||||
android:focusable="true"
|
|
||||||
android:clickable="true"
|
|
||||||
tools:ignore="UnusedAttribute" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/galleryblock_delete"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:minWidth="70dp"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:background="@android:color/holo_red_dark"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:text="@string/main_delete"
|
|
||||||
android:foreground="?android:attr/selectableItemBackground"
|
|
||||||
android:focusable="true"
|
|
||||||
android:clickable="true"
|
|
||||||
tools:ignore="UnusedAttribute" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/galleryblock_primary"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?android:attr/selectableItemBackground"
|
|
||||||
android:focusable="true"
|
|
||||||
android:clickable="true">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/galleryblock_progressbar_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="4dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<androidx.core.widget.ContentLoadingProgressBar
|
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
|
||||||
android:id="@+id/galleryblock_progressbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="-4dp"
|
|
||||||
android:layout_marginTop="-4dp"
|
|
||||||
android:progress="50"
|
|
||||||
android:layout_gravity="center_vertical"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/galleryblock_progress_complete"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="4dp"
|
|
||||||
android:visibility="invisible"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:contentDescription="@string/reader_imageview_description"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/galleryblock_thumbnail"
|
|
||||||
android:layout_width="150dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/galleryblock_thumbnail_description"
|
|
||||||
android:adjustViewBounds="true"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar_layout"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/barrier"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="@style/TextAppearance.AppCompat.Headline"
|
|
||||||
android:id="@+id/galleryblock_title"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar_layout"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="@style/TextAppearance.AppCompat.Medium"
|
|
||||||
android:id="@+id/galleryblock_artist"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_title"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/galleryblock_series"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/galleryblock_type"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/galleryblock_language"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
|
||||||
android:id="@+id/padding"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:barrierDirection="bottom"
|
|
||||||
app:constraint_referenced_ids="galleryblock_language"/>
|
|
||||||
|
|
||||||
<com.google.android.material.chip.ChipGroup
|
|
||||||
android:id="@+id/galleryblock_tag_group"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
app:chipSpacing="4dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/padding"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"/>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
|
||||||
android:id="@+id/barrier"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:barrierDirection="bottom"
|
|
||||||
app:constraint_referenced_ids="galleryblock_thumbnail,galleryblock_tag_group"/>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/divider"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="@color/light_gray"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier"
|
|
||||||
android:layout_margin="8dp"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/galleryblock_id"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/divider"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/galleryblock_pagecount"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/divider"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintLeft_toRightOf="parent"
|
|
||||||
app:layout_constraintRight_toLeftOf="parent" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/galleryblock_favorite"
|
|
||||||
android:contentDescription="@string/app_name"
|
|
||||||
android:layout_width="32dp"
|
|
||||||
android:layout_height="32dp"
|
|
||||||
android:layout_marginRight="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:srcCompat="@drawable/ic_star_empty"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/divider"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</com.daimajia.swipe.SwipeLayout>
|
|
||||||
|
|
||||||
</androidx.cardview.widget.CardView>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
|
||||||
~ Copyright (C) 2019 tom5079
|
|
||||||
~
|
|
||||||
~ This program is free software: you can redistribute it and/or modify
|
|
||||||
~ it under the terms of the GNU General Public License as published by
|
|
||||||
~ the Free Software Foundation, either version 3 of the License, or
|
|
||||||
~ (at your option) any later version.
|
|
||||||
~
|
|
||||||
~ This program is distributed in the hope that it will be useful,
|
|
||||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
~ GNU General Public License for more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License
|
|
||||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<LinearLayout 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:orientation="horizontal"
|
|
||||||
android:gravity="center">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/icon_next"
|
|
||||||
android:contentDescription="@string/page_indicator_placeholder"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"
|
|
||||||
app:srcCompat="@drawable/ic_navigate_next_black_24dp"
|
|
||||||
app:tint="@color/colorAccent"
|
|
||||||
android:rotation="180"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_next"
|
|
||||||
android:layout_width="1dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:ellipsize="end" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
|
||||||
~ Copyright (C) 2019 tom5079
|
|
||||||
~
|
|
||||||
~ This program is free software: you can redistribute it and/or modify
|
|
||||||
~ it under the terms of the GNU General Public License as published by
|
|
||||||
~ the Free Software Foundation, either version 3 of the License, or
|
|
||||||
~ (at your option) any later version.
|
|
||||||
~
|
|
||||||
~ This program is distributed in the hope that it will be useful,
|
|
||||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
~ GNU General Public License for more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License
|
|
||||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<LinearLayout 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:orientation="horizontal"
|
|
||||||
android:gravity="center">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/icon_prev"
|
|
||||||
android:contentDescription="@string/page_indicator_placeholder"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"
|
|
||||||
app:srcCompat="@drawable/ic_navigate_next_black_24dp"
|
|
||||||
app:tint="@color/colorAccent"
|
|
||||||
android:rotation="180"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_prev"
|
|
||||||
android:layout_width="1dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:ellipsize="end" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
|
||||||
~ Copyright (C) 2019 tom5079
|
|
||||||
~
|
|
||||||
~ This program is free software: you can redistribute it and/or modify
|
|
||||||
~ it under the terms of the GNU General Public License as published by
|
|
||||||
~ the Free Software Foundation, either version 3 of the License, or
|
|
||||||
~ (at your option) any later version.
|
|
||||||
~
|
|
||||||
~ This program is distributed in the hope that it will be useful,
|
|
||||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
~ GNU General Public License for more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License
|
|
||||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
|
||||||
app:layout_constraintHeight_max="2000dp"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
android:paddingBottom="8dp"
|
|
||||||
android:background="@drawable/reader_item_boundary">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/progress_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/reader_item_progressbar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
style="?android:progressBarStyleHorizontal"
|
|
||||||
android:indeterminate="false"
|
|
||||||
android:progress="0"
|
|
||||||
android:max="100"
|
|
||||||
android:visibility="visible"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/reader_index"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
style="@style/TextAppearance.AppCompat.Caption"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<com.github.chrisbanes.photoview.PhotoView
|
|
||||||
android:id="@+id/image"
|
|
||||||
android:adjustViewBounds="true"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
app:layout_constraintBottom_toTopOf="@id/lock_button_layout">
|
app:layout_constraintBottom_toTopOf="@id/lock_button_layout">
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/lock_fingerprint"
|
android:id="@+id/fingerprint_btn"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:srcCompat="@drawable/fingerprint"
|
app:srcCompat="@drawable/fingerprint"
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
android:gravity="center">
|
android:gravity="center">
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/lock_pattern"
|
android:id="@+id/pattern_btn"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:tint="@null"
|
app:tint="@null"
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
app:fabSize="mini"/>
|
app:fabSize="mini"/>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/lock_pin"
|
android:id="@+id/pin_btn"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:tint="@null"
|
app:tint="@null"
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
app:fabSize="mini"/>
|
app:fabSize="mini"/>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/lock_password"
|
android:id="@+id/password_btn"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:tint="@null"
|
app:tint="@null"
|
||||||
@@ -21,17 +21,18 @@
|
|||||||
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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/main_drawer_layout"
|
android:id="@+id/drawer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:openDrawer="start">
|
tools:openDrawer="start">
|
||||||
|
|
||||||
<include layout="@layout/activity_main_content"
|
<include android:id="@+id/contents"
|
||||||
|
layout="@layout/main_activity_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
<com.google.android.material.navigation.NavigationView
|
<com.google.android.material.navigation.NavigationView
|
||||||
android:id="@+id/main_nav_view"
|
android:id="@+id/nav_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="start"
|
android:layout_gravity="start"
|
||||||
127
app/src/main/res/layout/main_activity_content.xml
Normal file
127
app/src/main/res/layout/main_activity_content.xml
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<?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.coordinatorlayout.widget.CoordinatorLayout 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">
|
||||||
|
|
||||||
|
<xyz.quaver.pupil.ui.view.MainView
|
||||||
|
android:id="@+id/view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:handleDrawable="@drawable/thumb"
|
||||||
|
app:handleHasFixedSize="true"
|
||||||
|
app:handleHeight="72dp"
|
||||||
|
app:handleWidth="24dp"
|
||||||
|
app:disableTrack="true"
|
||||||
|
app:hideHandleAfter="1000"
|
||||||
|
app:trackMarginStart="64dp"
|
||||||
|
app:addLastItemPadding="true"
|
||||||
|
app:popupDrawable="@android:color/transparent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/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>
|
||||||
|
|
||||||
|
</xyz.quaver.pupil.ui.view.MainView>
|
||||||
|
|
||||||
|
<androidx.core.widget.ContentLoadingProgressBar
|
||||||
|
style="?android:attr/progressBarStyle"
|
||||||
|
android:id="@+id/progressbar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminate="true"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/noresult"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="@string/main_no_result"
|
||||||
|
android:linksClickable="true"
|
||||||
|
android:visibility="invisible"/>
|
||||||
|
|
||||||
|
<com.github.clans.fab.FloatingActionMenu
|
||||||
|
android:id="@+id/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/cancel_fab"
|
||||||
|
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/jump_fab"
|
||||||
|
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/random_fab"
|
||||||
|
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/id_fab"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<xyz.quaver.pupil.ui.view.FloatingSearchView
|
||||||
|
android:id="@+id/searchview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:searchBarMarginLeft="6dp"
|
||||||
|
app:searchBarMarginRight="6dp"
|
||||||
|
app:searchBarMarginTop="6dp"
|
||||||
|
app:searchHint="@string/search_hint"
|
||||||
|
app:suggestionAnimDuration="250"
|
||||||
|
app:showSearchKey="true"
|
||||||
|
app:leftActionMode="showHamburger"
|
||||||
|
app:menu="@menu/main"
|
||||||
|
app:dismissOnOutsideTouch="true"
|
||||||
|
app:close_search_on_keyboard_dismiss="false" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="?android:textAppearanceLarge"
|
style="?android:textAppearanceLarge"
|
||||||
android:id="@+id/dialog_title"
|
android:id="@+id/title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/reader_go_to_page"
|
android:text="@string/reader_go_to_page"
|
||||||
@@ -33,20 +33,20 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"/>
|
app:layout_constraintStart_toStartOf="parent"/>
|
||||||
|
|
||||||
<NumberPicker
|
<NumberPicker
|
||||||
android:id="@+id/dialog_number_picker"
|
android:id="@+id/number_picker"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/dialog_title"
|
app:layout_constraintTop_toBottomOf="@id/title"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/dialog_ok"
|
android:id="@+id/ok_button"
|
||||||
style="?borderlessButtonStyle"
|
style="?borderlessButtonStyle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@android:string/ok"
|
android:text="@android:string/ok"
|
||||||
app:layout_constraintTop_toBottomOf="@id/dialog_number_picker"
|
app:layout_constraintTop_toBottomOf="@id/number_picker"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
tools:context=".ui.fragment.PatternLockFragment">
|
tools:context=".ui.fragment.PatternLockFragment">
|
||||||
|
|
||||||
<com.andrognito.patternlockview.PatternLockView
|
<com.andrognito.patternlockview.PatternLockView
|
||||||
android:id="@+id/lock_pattern_view"
|
android:id="@+id/pattern_lock_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
75
app/src/main/res/layout/progress_card_view.xml
Normal file
75
app/src/main/res/layout/progress_card_view.xml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<merge
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:parentTag="androidx.cardview.widget.CardView">
|
||||||
|
|
||||||
|
<com.daimajia.swipe.SwipeLayout
|
||||||
|
android:id="@+id/swipe_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:drag_edge="right"
|
||||||
|
app:show_mode="pull_out">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/download"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minWidth="70dp"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@android:color/holo_blue_dark"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:text="@string/main_download"
|
||||||
|
android:foreground="?android:attr/selectableItemBackground"
|
||||||
|
android:focusable="true"
|
||||||
|
android:clickable="true"
|
||||||
|
tools:ignore="UnusedAttribute" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/delete"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minWidth="70dp"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@android:color/holo_red_dark"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:text="@string/main_delete"
|
||||||
|
android:foreground="?android:attr/selectableItemBackground"
|
||||||
|
android:focusable="true"
|
||||||
|
android:clickable="true"
|
||||||
|
tools:ignore="UnusedAttribute" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.core.widget.ContentLoadingProgressBar
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:id="@+id/progressbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="4dp"
|
||||||
|
android:progress="50"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.daimajia.swipe.SwipeLayout>
|
||||||
|
|
||||||
|
</merge>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
android:padding="16dp">
|
android:padding="16dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/proxy_title"
|
android:id="@+id/title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingBottom="16dp"
|
android:paddingBottom="16dp"
|
||||||
@@ -35,46 +35,46 @@
|
|||||||
app:layout_constraintLeft_toLeftOf="parent"/>
|
app:layout_constraintLeft_toLeftOf="parent"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/proxy_type_text"
|
android:id="@+id/type_text"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/proxy_title"
|
app:layout_constraintTop_toBottomOf="@id/title"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
android:text="@string/proxy_dialog_type"
|
android:text="@string/proxy_dialog_type"
|
||||||
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
|
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
|
||||||
|
|
||||||
<Spinner
|
<Spinner
|
||||||
android:id="@+id/proxy_type_selector"
|
android:id="@+id/type_selector"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/proxy_type_text"/>
|
app:layout_constraintTop_toBottomOf="@id/type_text"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/proxy_server_text"
|
android:id="@+id/server_text"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/proxy_type_selector"
|
app:layout_constraintTop_toBottomOf="@id/type_selector"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
android:text="@string/proxy_dialog_server"
|
android:text="@string/proxy_dialog_server"
|
||||||
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
|
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/proxy_address_layout"
|
android:id="@+id/address_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/proxy_server_text">
|
app:layout_constraintTop_toBottomOf="@id/server_text">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatEditText
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
android:id="@+id/proxy_addr"
|
android:id="@+id/addr"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_weight="2"
|
android:layout_weight="2"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/proxy_dialog_addr_hint"/>
|
android:hint="@string/proxy_dialog_addr_hint"/>
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatEditText
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
android:id="@+id/proxy_port"
|
android:id="@+id/port"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -83,39 +83,39 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatEditText
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
android:id="@+id/proxy_username"
|
android:id="@+id/username"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/proxy_address_layout"
|
app:layout_constraintTop_toBottomOf="@id/address_layout"
|
||||||
android:hint="@string/proxy_dialog_username_hint"
|
android:hint="@string/proxy_dialog_username_hint"
|
||||||
android:enabled="false"/>
|
android:enabled="false"/>
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatEditText
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
android:id="@+id/proxy_password"
|
android:id="@+id/password"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/proxy_username"
|
app:layout_constraintTop_toBottomOf="@id/username"
|
||||||
android:hint="@string/proxy_dialog_password_hint"
|
android:hint="@string/proxy_dialog_password_hint"
|
||||||
android:enabled="false"/>
|
android:enabled="false"/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/proxy_cancel"
|
android:id="@+id/cancel_button"
|
||||||
style="?borderlessButtonStyle"
|
style="?borderlessButtonStyle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@android:string/cancel"
|
android:text="@android:string/cancel"
|
||||||
app:layout_constraintTop_toBottomOf="@id/proxy_password"
|
app:layout_constraintTop_toBottomOf="@id/password"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/proxy_ok"
|
app:layout_constraintEnd_toStartOf="@id/ok_button"
|
||||||
app:layout_constraintRight_toLeftOf="@id/proxy_ok"/>
|
app:layout_constraintRight_toLeftOf="@id/ok_button"/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/proxy_ok"
|
android:id="@+id/ok_button"
|
||||||
style="?borderlessButtonStyle"
|
style="?borderlessButtonStyle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@android:string/ok"
|
android:text="@android:string/ok"
|
||||||
app:layout_constraintTop_toBottomOf="@id/proxy_password"
|
app:layout_constraintTop_toBottomOf="@id/password"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"/>
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
@@ -18,12 +18,11 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<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"
|
||||||
android:id="@+id/reader_layout"
|
|
||||||
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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/dark_gray"
|
android:background="@android:color/darker_gray"
|
||||||
tools:context=".ui.ReaderActivity">
|
tools:context=".ui.ReaderActivity">
|
||||||
|
|
||||||
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
||||||
@@ -37,10 +36,10 @@
|
|||||||
app:hideHandleAfter="1000"
|
app:hideHandleAfter="1000"
|
||||||
app:handleHasFixedSize="true"
|
app:handleHasFixedSize="true"
|
||||||
app:addLastItemPadding="true"
|
app:addLastItemPadding="true"
|
||||||
app:popupDrawable="@color/transparent">
|
app:popupDrawable="@android:color/transparent">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/reader_recyclerview"
|
android:id="@+id/recyclerview"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
||||||
@@ -56,13 +55,13 @@
|
|||||||
android:layout_margin="8dp"/>
|
android:layout_margin="8dp"/>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/reader_download_progressbar"
|
android:id="@+id/download_progressbar"
|
||||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="4dp"/>
|
android:layout_height="4dp"/>
|
||||||
|
|
||||||
<com.github.clans.fab.FloatingActionMenu
|
<com.github.clans.fab.FloatingActionMenu
|
||||||
android:id="@+id/reader_fab"
|
android:id="@+id/fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
@@ -70,7 +69,7 @@
|
|||||||
app:menu_colorNormal="@color/colorAccent">
|
app:menu_colorNormal="@color/colorAccent">
|
||||||
|
|
||||||
<com.github.clans.fab.FloatingActionButton
|
<com.github.clans.fab.FloatingActionButton
|
||||||
android:id="@+id/reader_fab_download"
|
android:id="@+id/download_fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:srcCompat="@drawable/ic_download"
|
app:srcCompat="@drawable/ic_download"
|
||||||
@@ -78,7 +77,7 @@
|
|||||||
app:fab_size="mini"/>
|
app:fab_size="mini"/>
|
||||||
|
|
||||||
<com.github.clans.fab.FloatingActionButton
|
<com.github.clans.fab.FloatingActionButton
|
||||||
android:id="@+id/reader_fab_retry"
|
android:id="@+id/retry_fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:srcCompat="@drawable/refresh"
|
app:srcCompat="@drawable/refresh"
|
||||||
@@ -86,7 +85,7 @@
|
|||||||
app:fab_size="mini"/>
|
app:fab_size="mini"/>
|
||||||
|
|
||||||
<com.github.clans.fab.FloatingActionButton
|
<com.github.clans.fab.FloatingActionButton
|
||||||
android:id="@+id/reader_fab_auto"
|
android:id="@+id/auto_fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:srcCompat="@drawable/eye_white"
|
app:srcCompat="@drawable/eye_white"
|
||||||
@@ -94,7 +93,7 @@
|
|||||||
app:fab_size="mini"/>
|
app:fab_size="mini"/>
|
||||||
|
|
||||||
<com.github.clans.fab.FloatingActionButton
|
<com.github.clans.fab.FloatingActionButton
|
||||||
android:id="@+id/reader_fab_fullscreen"
|
android:id="@+id/fullscreen_fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:srcCompat="@drawable/ic_fullscreen"
|
app:srcCompat="@drawable/ic_fullscreen"
|
||||||
72
app/src/main/res/layout/reader_item.xml
Normal file
72
app/src/main/res/layout/reader_item.xml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Pupil, Hitomi.la viewer for Android
|
||||||
|
~ Copyright (C) 2019 tom5079
|
||||||
|
~
|
||||||
|
~ This program is free software: you can redistribute it and/or modify
|
||||||
|
~ it under the terms of the GNU General Public License as published by
|
||||||
|
~ the Free Software Foundation, either version 3 of the License, or
|
||||||
|
~ (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ This program is distributed in the hope that it will be useful,
|
||||||
|
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
~ GNU General Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License
|
||||||
|
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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_marginBottom="8dp"
|
||||||
|
android:background="@drawable/reader_item_boundary">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/guideline_center_vertical"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_percent="0.5"/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/reader_item_progressbar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="?android:progressBarStyleHorizontal"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:progress="0"
|
||||||
|
android:max="100"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/guideline_center_vertical"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/reader_index"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/guideline_center_vertical"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/reader_item_progressbar"
|
||||||
|
app:layout_constraintRight_toRightOf="@id/reader_item_progressbar"
|
||||||
|
style="@style/TextAppearance.AppCompat.Caption"/>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/progress_group"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:constraint_referenced_ids="reader_item_progressbar, reader_index"/>
|
||||||
|
|
||||||
|
<com.github.piasy.biv.view.BigImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:initScaleType="fitCenter"
|
||||||
|
app:optimizeDisplay="true"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
30
app/src/main/res/layout/swipe_pageturn_view.xml
Normal file
30
app/src/main/res/layout/swipe_pageturn_view.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:parentTag="android.widget.FrameLayout">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/prev"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|center"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
app:drawableStartCompat="@drawable/navigate_prev"
|
||||||
|
app:drawableLeftCompat="@drawable/navigate_prev"
|
||||||
|
app:drawableTint="@color/colorAccent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/next"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|center"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
app:drawableEndCompat="@drawable/navigate_next"
|
||||||
|
app:drawableRightCompat="@drawable/navigate_next"
|
||||||
|
app:drawableTint="@color/colorAccent" />
|
||||||
|
|
||||||
|
</merge>
|
||||||
@@ -20,10 +20,10 @@
|
|||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu 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">
|
||||||
|
|
||||||
<item android:id="@+id/main_menu_thin"
|
<item android:id="@+id/sort"
|
||||||
android:title="@string/main_menu_thin"/>
|
android:title="@string/main_menu_sort"
|
||||||
|
android:icon="@drawable/sort_variant"
|
||||||
<item android:title="@string/main_menu_sort">
|
app:showAsAction="ifRoom">
|
||||||
<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"
|
||||||
@@ -41,4 +41,9 @@
|
|||||||
android:title="@string/main_settings"
|
android:title="@string/main_settings"
|
||||||
app:showAsAction="always"/>
|
app:showAsAction="always"/>
|
||||||
|
|
||||||
|
<item android:id="@+id/main_menu_thin"
|
||||||
|
android:title="@string/main_menu_thin"
|
||||||
|
app:showAsAction="never"
|
||||||
|
android:checkable="true"/>
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
<string name="main_jump_title">ページ移動</string>
|
<string name="main_jump_title">ページ移動</string>
|
||||||
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string>
|
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string>
|
||||||
<string name="unable_to_connect">hitomi.laに接続できません</string>
|
<string name="unable_to_connect">hitomi.laに接続できません</string>
|
||||||
<string name="main_move">%1$dページへ移動</string>
|
<string name="main_move_to_page">%1$dページへ移動</string>
|
||||||
<string name="settings_clear_downloads">ダウンロード削除</string>
|
<string name="settings_clear_downloads">ダウンロード削除</string>
|
||||||
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか?</string>
|
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか?</string>
|
||||||
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</string>
|
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</string>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<string name="main_menu_sort">ソート</string>
|
<string name="main_menu_sort">ソート</string>
|
||||||
<string name="main_menu_sort_newest">投稿日時順</string>
|
<string name="main_menu_sort_newest">投稿日時順</string>
|
||||||
<string name="main_menu_sort_popular">人気順</string>
|
<string name="main_menu_sort_popular">人気順</string>
|
||||||
<string name="ignore_update">無視</string>
|
<string name="ignore">無視</string>
|
||||||
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string>
|
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string>
|
||||||
<string name="settings_dark_mode_title">ダークモード</string>
|
<string name="settings_dark_mode_title">ダークモード</string>
|
||||||
<string name="settings_dark_mode_summary">夜にシコりたい方々へ</string>
|
<string name="settings_dark_mode_summary">夜にシコりたい方々へ</string>
|
||||||
@@ -127,10 +127,8 @@
|
|||||||
<string name="settings_lock_fingerprint_prompt">Pupil指紋ロック™</string>
|
<string name="settings_lock_fingerprint_prompt">Pupil指紋ロック™</string>
|
||||||
<string name="settings_lock_fingerprint_prompt_subtitle">こうかはばつぐんだ!</string>
|
<string name="settings_lock_fingerprint_prompt_subtitle">こうかはばつぐんだ!</string>
|
||||||
<string name="default_query_dialog_filter_loli">登場人物を全て18歳以上にする</string>
|
<string name="default_query_dialog_filter_loli">登場人物を全て18歳以上にする</string>
|
||||||
<string name="settings_cache_disable">キャッシュを使用しない</string>
|
|
||||||
<string name="settings_download_when_cache_disable_warning">キャッシュを使用しないため、ダウンロードできません</string>
|
|
||||||
<string name="settings_user_id">ユーザーID</string>
|
<string name="settings_user_id">ユーザーID</string>
|
||||||
<string name="settings_user_id_toast">ユーザーIDをクリップボードにコピーしました</string>
|
<string name="copied_to_clipboard">クリップボードにコピーしました</string>
|
||||||
<string name="reader_fab_retry">リトライ</string>
|
<string name="reader_fab_retry">リトライ</string>
|
||||||
<string name="reader_fab_auto">まばたき検知スクロール</string>
|
<string name="reader_fab_auto">まばたき検知スクロール</string>
|
||||||
<string name="search_all">全てのギャラリーを対象に検索</string>
|
<string name="search_all">全てのギャラリーを対象に検索</string>
|
||||||
@@ -152,4 +150,11 @@
|
|||||||
<string name="camera_denied">カメラ権限が拒否されているため、まばたき検知使用できません</string>
|
<string name="camera_denied">カメラ権限が拒否されているため、まばたき検知使用できません</string>
|
||||||
<string name="no_camera">この機器には前面カメラが装着されていません</string>
|
<string name="no_camera">この機器には前面カメラが装着されていません</string>
|
||||||
<string name="error">エラー</string>
|
<string name="error">エラー</string>
|
||||||
|
<string name="settings_cache_limit">キャッシュサイズ制限</string>
|
||||||
|
<string name="unlimited">制限なし</string>
|
||||||
|
<string name="settings_tag_translation">タグ言語</string>
|
||||||
|
<string name="settings_tag_translation_message">Githubにて翻訳に参加できます</string>
|
||||||
|
<string name="settings_max_concurrent_download">並列ダウンロード</string>
|
||||||
|
<string name="unaccessible_download_folder">アンドロイド11以上では外部からのアプリ内部空間接近が不可能です。ダウンロードフォルダを変更しますか?</string>
|
||||||
|
<string name="settings_networking">ネットワーク</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -12,10 +12,10 @@
|
|||||||
<string name="settings_galleries_per_page">한 번에 로드할 갤러리 수</string>
|
<string name="settings_galleries_per_page">한 번에 로드할 갤러리 수</string>
|
||||||
<string name="settings_search_title">검색 설정</string>
|
<string name="settings_search_title">검색 설정</string>
|
||||||
<string name="settings_title">설정</string>
|
<string name="settings_title">설정</string>
|
||||||
<string name="update_notification_description">apk 다운로드중…</string>
|
<string name="update_notification_description">업데이트 다운로드중…</string>
|
||||||
<string name="update_title">업데이트가 있습니다!</string>
|
<string name="update_title">업데이트가 있습니다!</string>
|
||||||
<string name="warning">경고</string>
|
<string name="warning">경고</string>
|
||||||
<string name="main_no_result">결과 없음</string>
|
<string name="main_no_result">결과 없음\n해결법</string>
|
||||||
<string name="settings_miscellaneous_title">기타</string>
|
<string name="settings_miscellaneous_title">기타</string>
|
||||||
<string name="settings_clear_history">기록 삭제</string>
|
<string name="settings_clear_history">기록 삭제</string>
|
||||||
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<string name="main_jump_title">페이지 이동</string>
|
<string name="main_jump_title">페이지 이동</string>
|
||||||
<string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string>
|
<string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string>
|
||||||
<string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string>
|
<string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string>
|
||||||
<string name="main_move">%1$d 페이지로 이동</string>
|
<string name="main_move_to_page">%1$d 페이지로 이동</string>
|
||||||
<string name="settings_clear_downloads">다운로드 삭제</string>
|
<string name="settings_clear_downloads">다운로드 삭제</string>
|
||||||
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
|
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
|
||||||
<string name="main_drawer_favorite">즐겨찾기</string>
|
<string name="main_drawer_favorite">즐겨찾기</string>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
<string name="main_menu_sort">정렬</string>
|
<string name="main_menu_sort">정렬</string>
|
||||||
<string name="main_menu_sort_popular">인기순</string>
|
<string name="main_menu_sort_popular">인기순</string>
|
||||||
<string name="main_menu_sort_newest">시간순</string>
|
<string name="main_menu_sort_newest">시간순</string>
|
||||||
<string name="ignore_update">무시</string>
|
<string name="ignore">무시</string>
|
||||||
<string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string>
|
<string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string>
|
||||||
<string name="settings_dark_mode_title">다크 모드</string>
|
<string name="settings_dark_mode_title">다크 모드</string>
|
||||||
<string name="settings_dark_mode_summary">딥 다크한 모오드</string>
|
<string name="settings_dark_mode_summary">딥 다크한 모오드</string>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<string name="proxy_dialog_error">잘못된 값</string>
|
<string name="proxy_dialog_error">잘못된 값</string>
|
||||||
<string name="proxy_dialog_addr_hint">서버 주소</string>
|
<string name="proxy_dialog_addr_hint">서버 주소</string>
|
||||||
<string name="proxy_dialog_server">서버</string>
|
<string name="proxy_dialog_server">서버</string>
|
||||||
<string name="main_menu_thin">간단히 보기 모드</string>
|
<string name="main_menu_thin">간단히 보기</string>
|
||||||
<string name="main_fab_cancel">다운로드 모두 취소</string>
|
<string name="main_fab_cancel">다운로드 모두 취소</string>
|
||||||
<string name="channel_update">업데이트</string>
|
<string name="channel_update">업데이트</string>
|
||||||
<string name="channel_update_description">업데이트 진행상황 표시</string>
|
<string name="channel_update_description">업데이트 진행상황 표시</string>
|
||||||
@@ -127,10 +127,8 @@
|
|||||||
<string name="settings_lock_fingerprint_prompt">Pupil 지문 인식™</string>
|
<string name="settings_lock_fingerprint_prompt">Pupil 지문 인식™</string>
|
||||||
<string name="settings_lock_fingerprint_prompt_subtitle">힘세고 강한 지문 인식</string>
|
<string name="settings_lock_fingerprint_prompt_subtitle">힘세고 강한 지문 인식</string>
|
||||||
<string name="default_query_dialog_filter_loli">판사님 저는 페도가 아닙니다</string>
|
<string name="default_query_dialog_filter_loli">판사님 저는 페도가 아닙니다</string>
|
||||||
<string name="settings_cache_disable">캐시 비활성화</string>
|
|
||||||
<string name="settings_download_when_cache_disable_warning">캐시를 활성화 해야 다운로드를 진행할 수 있습니다</string>
|
|
||||||
<string name="settings_user_id">유저 ID</string>
|
<string name="settings_user_id">유저 ID</string>
|
||||||
<string name="settings_user_id_toast">유저 ID를 클립보드에 복사했습니다</string>
|
<string name="copied_to_clipboard">클립보드에 복사됨</string>
|
||||||
<string name="reader_fab_retry">재시도</string>
|
<string name="reader_fab_retry">재시도</string>
|
||||||
<string name="reader_fab_auto">눈 깜빡임 감지 스크롤</string>
|
<string name="reader_fab_auto">눈 깜빡임 감지 스크롤</string>
|
||||||
<string name="search_all">모든 갤러리 검색</string>
|
<string name="search_all">모든 갤러리 검색</string>
|
||||||
@@ -152,4 +150,11 @@
|
|||||||
<string name="camera_denied">카메라 권한이 거부되었기 때문에 눈 깜빡임 감지가 불가능합니다</string>
|
<string name="camera_denied">카메라 권한이 거부되었기 때문에 눈 깜빡임 감지가 불가능합니다</string>
|
||||||
<string name="no_camera">이 장치에는 전면 카메라가 없습니다</string>
|
<string name="no_camera">이 장치에는 전면 카메라가 없습니다</string>
|
||||||
<string name="error">오류</string>
|
<string name="error">오류</string>
|
||||||
|
<string name="settings_cache_limit">캐시 크기 제한</string>
|
||||||
|
<string name="unlimited">무제한</string>
|
||||||
|
<string name="settings_tag_translation">태그 언어</string>
|
||||||
|
<string name="settings_tag_translation_message">Github에서 번역에 참여하세요</string>
|
||||||
|
<string name="settings_max_concurrent_download">병렬 다운로드</string>
|
||||||
|
<string name="unaccessible_download_folder">안드로이드 11 이상에서는 외부에서 현재 다운로드 폴더에 접근할 수 없습니다. 변경하시겠습니까?</string>
|
||||||
|
<string name="settings_networking">네트워크</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<string-array name="settings_galleries_per_page">
|
<string-array name="settings_galleries_per_page">
|
||||||
|
<item>5</item>
|
||||||
<item>10</item>
|
<item>10</item>
|
||||||
<item>25</item>
|
<item>25</item>
|
||||||
<item>50</item>
|
<item>50</item>
|
||||||
@@ -47,15 +48,50 @@
|
|||||||
<item>japanese|日本語</item>
|
<item>japanese|日本語</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="mirrors">
|
|
||||||
<item>HITOMI|hitomi.la</item>
|
|
||||||
<item>HIYOBI|hiyobi.me</item>
|
|
||||||
</string-array>
|
|
||||||
|
|
||||||
<string-array name="proxy_type">
|
<string-array name="proxy_type">
|
||||||
<item>Direct</item>
|
<item>Direct</item>
|
||||||
<item>HTTP</item>
|
<item>HTTP</item>
|
||||||
<item>SOCKS</item>
|
<item>SOCKS</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="cache_size">
|
||||||
|
<item>0</item>
|
||||||
|
<item>1</item>
|
||||||
|
<item>2</item>
|
||||||
|
<item>4</item>
|
||||||
|
<item>8</item>
|
||||||
|
<item>16</item>
|
||||||
|
<item>32</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="cache_size_text">
|
||||||
|
<item>@string/unlimited</item>
|
||||||
|
<item>1G</item>
|
||||||
|
<item>2G</item>
|
||||||
|
<item>4G</item>
|
||||||
|
<item>8G</item>
|
||||||
|
<item>16G</item>
|
||||||
|
<item>32G</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="concurrent_download">
|
||||||
|
<item>0</item>
|
||||||
|
<item>1</item>
|
||||||
|
<item>2</item>
|
||||||
|
<item>4</item>
|
||||||
|
<item>8</item>
|
||||||
|
<item>16</item>
|
||||||
|
<item>32</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="concurrent_download_text">
|
||||||
|
<item>@string/unlimited</item>
|
||||||
|
<item>1</item>
|
||||||
|
<item>2</item>
|
||||||
|
<item>4</item>
|
||||||
|
<item>8</item>
|
||||||
|
<item>16</item>
|
||||||
|
<item>32</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
31
app/src/main/res/values/attrs.xml
Normal file
31
app/src/main/res/values/attrs.xml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Pupil, Hitomi.la viewer for Android
|
||||||
|
~ Copyright (C) 2020 tom5079
|
||||||
|
~
|
||||||
|
~ This program is free software: you can redistribute it and/or modify
|
||||||
|
~ it under the terms of the GNU General Public License as published by
|
||||||
|
~ the Free Software Foundation, either version 3 of the License, or
|
||||||
|
~ (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ This program is distributed in the hope that it will be useful,
|
||||||
|
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
~ GNU General Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License
|
||||||
|
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
<declare-styleable name="TagChipGroup">
|
||||||
|
<attr name="maxTag" format="integer"/>
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
|
<declare-styleable name="RippleCircleStatus">
|
||||||
|
<attr name="half" format="enum">
|
||||||
|
<enum name="top" value="1"/>
|
||||||
|
<enum name="bottom" value="-1"/>
|
||||||
|
</attr>
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
<color name="colorPrimaryDark">#0093c4</color>
|
<color name="colorPrimaryDark">#0093c4</color>
|
||||||
<color name="colorAccent">#D81B60</color>
|
<color name="colorAccent">#D81B60</color>
|
||||||
|
|
||||||
|
<color name="material_light_blue_300">#4fc3f7</color>
|
||||||
|
<color name="material_light_blue_700">#0288d1</color>
|
||||||
<color name="material_pink_600">#d81b60</color>
|
<color name="material_pink_600">#d81b60</color>
|
||||||
<color name="material_blue_700">#1976d2</color>
|
<color name="material_blue_700">#1976d2</color>
|
||||||
<color name="material_green_a700">#00c853</color>
|
<color name="material_green_a700">#00c853</color>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||||
|
|
||||||
<dimen name="galleryblock_thumbnail_thin">100dp</dimen>
|
<dimen name="galleryblock_thumbnail_thin">100dp</dimen>
|
||||||
|
|
||||||
<dimen name="reader_max_height">2000dp</dimen>
|
<dimen name="reader_max_height" tools:ignore="PxUsage">2000px</dimen>
|
||||||
|
|
||||||
<dimen name="thumb_width">24dp</dimen>
|
<dimen name="thumb_width">24dp</dimen>
|
||||||
<dimen name="thumb_height">72dp</dimen>
|
<dimen name="thumb_height">72dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="thumbnail_page_height">300dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user