Compare commits
239 Commits
5.1.1-hotf
...
5.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
117d6dcd2b | ||
|
|
2608796929 | ||
|
|
792f5b5a7f | ||
|
|
a77b1db749 | ||
|
|
9d984d92af | ||
|
|
e303f25991 | ||
|
|
85973d2305 | ||
|
|
13f8d7b747 | ||
|
|
e198860edb | ||
|
|
fc8355467b | ||
|
|
67abc15442 | ||
|
|
e94cddb86a | ||
|
|
700f7a33a5 | ||
|
|
41e952144d | ||
|
|
910ed65937 | ||
|
|
e06701a2fb | ||
|
|
62dce26c73 | ||
|
|
ac0cff62d4 | ||
|
|
655c060814 | ||
|
|
36d27895e7 | ||
|
|
803481f74c | ||
|
|
b3ca1686e3 | ||
|
|
8f220eb0cb | ||
|
|
51d5f42e8b | ||
|
|
8d8c5ace61 | ||
|
|
4bb6b8ccc9 | ||
|
|
6bebd36e83 | ||
|
|
edc7053e50 | ||
|
|
55e6ef5f78 | ||
|
|
9781d7a5dc | ||
|
|
b83cf87cd8 | ||
|
|
430864512d | ||
|
|
16eeef1878 | ||
|
|
994d4b589b | ||
|
|
43adba6f13 | ||
|
|
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 |
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">
|
||||
<settings>
|
||||
<module2copyright>
|
||||
<element module="Pupil" copyright="GPL" />
|
||||
<element module="Project Files" copyright="GPL" />
|
||||
</module2copyright>
|
||||
</settings>
|
||||
</component>
|
||||
28
.idea/deploymentTargetDropDown.xml
generated
Normal file
28
.idea/deploymentTargetDropDown.xml
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<runningDeviceTargetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="RUNNING_DEVICE_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="$USER_HOME$/.android/avd/Pixel_2_API_31.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</runningDeviceTargetSelectedWithDropDown>
|
||||
<targetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="$USER_HOME$/.android/avd/Pixel_2_API_31.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2022-02-01T02:15:22.286886Z" />
|
||||
</component>
|
||||
</project>
|
||||
4
.idea/gradle.xml
generated
4
.idea/gradle.xml
generated
@@ -4,9 +4,11 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="/usr/share/java/gradle" />
|
||||
<option name="gradleJvm" value="11" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<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
@@ -71,5 +71,30 @@
|
||||
<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>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven3" />
|
||||
<option name="name" value="maven3" />
|
||||
<option name="url" value="https://maven.mozilla.org/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven3" />
|
||||
<option name="name" value="maven3" />
|
||||
<option name="url" value="https://oss.sonatype.org/content/repositories/snapshots/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
13
.idea/misc.xml
generated
13
.idea/misc.xml
generated
@@ -1,6 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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="../../../../layout/custom_preview.xml" value="0.2564814814814815" />
|
||||
<entry key="app/src/main/res/layout/reader_activity.xml" value="0.14351851851851852" />
|
||||
<entry key="app/src/main/res/xml/lock_preferences.xml" value="0.5119791666666667" />
|
||||
<entry key="app/src/main/res/xml/manage_storage_preferences.xml" value="0.2604166666666667" />
|
||||
<entry key="app/src/main/res/xml/root_preferences.xml" value="0.5119791666666667" />
|
||||
</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" />
|
||||
</component>
|
||||
<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*
|
||||
|
||||
[](https://discord.gg/Stj4b5v)
|
||||

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

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

|
||||
*Reader Screen*
|
||||
|
||||
Images are censored to be SFW
|
||||
# Features
|
||||

|
||||
|
||||
# Installation
|
||||
|
||||
@@ -26,4 +20,7 @@ or Build app yourself
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "kotlin-android"
|
||||
apply plugin: "kotlin-kapt"
|
||||
apply plugin: "kotlin-android-extensions"
|
||||
apply plugin: "kotlin-parcelize"
|
||||
apply plugin: "kotlinx-serialization"
|
||||
apply plugin: "com.google.android.gms.oss-licenses-plugin"
|
||||
|
||||
if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
|
||||
if (file("google-services.json").exists()) {
|
||||
logger.lifecycle("Firebase Enabled")
|
||||
apply plugin: "com.google.gms.google-services"
|
||||
apply plugin: "com.google.firebase.crashlytics"
|
||||
@@ -32,28 +32,27 @@ configurations {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 31
|
||||
defaultConfig {
|
||||
applicationId "xyz.quaver.pupil"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
versionCode 61
|
||||
versionName "5.1.1-hotfix1"
|
||||
targetSdkVersion 31
|
||||
versionCode 69
|
||||
versionName "5.3.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
defaultConfig.minSdkVersion 21
|
||||
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-DEBUG"
|
||||
|
||||
buildConfigField("Boolean", "CENSOR", "false")
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
|
||||
ext.enableCrashlytics = false
|
||||
ext.alwaysUpdateBuildId = false
|
||||
}
|
||||
@@ -61,55 +60,58 @@ android {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
buildConfigField("Boolean", "CENSOR", "false")
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
buildToolsVersion = "29.0.3"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0-RC2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
||||
implementation "androidx.activity:activity-ktx:1.2.0-alpha08"
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.0-alpha08"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.0.1"
|
||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||
implementation "androidx.activity:activity-ktx:1.4.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.0"
|
||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
|
||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||
|
||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||
|
||||
implementation "com.google.android.material:material:1.3.0-alpha02"
|
||||
implementation "com.google.android.material:material:1.5.0"
|
||||
|
||||
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 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.1.1"
|
||||
implementation "com.google.android.gms:play-services-mlkit-face-detection:17.0.0"
|
||||
|
||||
implementation "com.github.clans:fab:1.6.4"
|
||||
|
||||
//implementation "com.quiph.ui:recyclerviewfastscroller:0.2.1"
|
||||
|
||||
implementation 'com.github.piasy:BigImageViewer:1.6.7'
|
||||
implementation 'com.github.piasy:FrescoImageLoader:1.6.7'
|
||||
implementation 'com.github.piasy:FrescoImageViewFactory:1.6.7'
|
||||
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"
|
||||
@@ -124,17 +126,19 @@ dependencies {
|
||||
|
||||
implementation "ru.noties.markwon:core:3.1.0"
|
||||
|
||||
implementation "xyz.quaver:libpupil:1.7.2"
|
||||
implementation "xyz.quaver:documentfilex:0.2.15"
|
||||
implementation "xyz.quaver:floatingsearchview:1.0.5"
|
||||
implementation "org.jsoup:jsoup:1.14.3"
|
||||
|
||||
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"
|
||||
}
|
||||
implementation ("app.cash.zipline:zipline:1.0.0-SNAPSHOT") {
|
||||
exclude group: "com.squareup.okio", module: "okio"
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
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"
|
||||
}
|
||||
17
app/proguard-rules.pro
vendored
17
app/proguard-rules.pro
vendored
@@ -22,21 +22,6 @@
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep class * extends com.bumptech.glide.module.AppGlideModule {
|
||||
<init>(...);
|
||||
}
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
|
||||
*** rewind();
|
||||
}
|
||||
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
|
||||
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.SerializationKt
|
||||
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; } # <-- change package name to your app's
|
||||
@@ -48,4 +33,4 @@
|
||||
}
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
|
||||
-keep class xyz.quaver.pupil.util.Preferences
|
||||
-keep class xyz.quaver.pupil.** { *; }
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
@@ -10,11 +10,11 @@
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"properties": [],
|
||||
"versionCode": 61,
|
||||
"versionName": "5.1.1-hotfix1",
|
||||
"enabled": true,
|
||||
"attributes": [],
|
||||
"versionCode": 69,
|
||||
"versionName": "5.3.0",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
]
|
||||
],
|
||||
"elementType": "File"
|
||||
}
|
||||
@@ -20,10 +20,18 @@
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import xyz.quaver.pupil.hitomi.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
@@ -32,10 +40,142 @@ import org.junit.runner.RunWith
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
// @Before
|
||||
// fun init() {
|
||||
// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
// }
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
clientBuilder = OkHttpClient.Builder()
|
||||
.readTimeout(0, TimeUnit.SECONDS)
|
||||
.writeTimeout(0, TimeUnit.SECONDS)
|
||||
.callTimeout(0, TimeUnit.SECONDS)
|
||||
.connectTimeout(0, TimeUnit.SECONDS)
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request().newBuilder()
|
||||
.header("Referer", "https://hitomi.la/")
|
||||
.build()
|
||||
|
||||
chain.proceed(request)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
fun test_empty() {
|
||||
print(
|
||||
"".trim()
|
||||
.replace(Regex("""^\?"""), "")
|
||||
.lowercase(Locale.getDefault())
|
||||
.split(Regex("\\s+"))
|
||||
.map {
|
||||
it.replace('_', ' ')
|
||||
})
|
||||
}
|
||||
@Test
|
||||
fun test_nozomi() {
|
||||
val nozomi = getGalleryIDsFromNozomi(null, "index", "all")
|
||||
|
||||
Log.d("PUPILD", nozomi.size.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_search() {
|
||||
val ids = getGalleryIDsForQuery("language:korean").reversed()
|
||||
|
||||
print(ids.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_suggestions() {
|
||||
val suggestions = getSuggestionsForQuery("language:g")
|
||||
|
||||
print(suggestions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_doSearch() {
|
||||
val r = runBlocking {
|
||||
doSearch("language:korean")
|
||||
}
|
||||
|
||||
Log.d("PUPILD", r.take(10).toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getBlock() {
|
||||
val galleryBlock = getGalleryBlock(2097576)
|
||||
|
||||
print(galleryBlock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getGallery() {
|
||||
val gallery = getGallery(2097751)
|
||||
|
||||
print(gallery)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getGalleryInfo() {
|
||||
val info = getGalleryInfo(1469394)
|
||||
|
||||
print(info)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getReader() {
|
||||
val reader = getGalleryInfo(1722144)
|
||||
|
||||
print(reader)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getImages() {
|
||||
val galleryID = 2099306
|
||||
|
||||
val images = getGalleryInfo(galleryID).files.map {
|
||||
imageUrlFromImage(galleryID, it,false)
|
||||
}
|
||||
|
||||
images.forEachIndexed { index, image ->
|
||||
println("Testing $index/${images.size}: $image")
|
||||
val response = client.newCall(
|
||||
Request.Builder()
|
||||
.url(image)
|
||||
.header("Referer", "https://hitomi.la/")
|
||||
.build()
|
||||
).execute()
|
||||
|
||||
assertEquals(200, response.code())
|
||||
|
||||
println("$index/${images.size} Passed")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_urlFromUrlFromHash() {
|
||||
val url = urlFromUrlFromHash(1531795, GalleryFiles(
|
||||
212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300
|
||||
), "webp")
|
||||
|
||||
print(url)
|
||||
}
|
||||
|
||||
// @Test
|
||||
// suspend fun test_doSearch_extreme() {
|
||||
// val query = "language:korean -tag:sample -female:humiliation -female:diaper -female:strap-on -female:squirting -female:lizard_girl -female:voyeurism -type:artistcg -female:blood -female:ryona -male:blood -male:ryona -female:crotch_tattoo -male:urethra_insertion -female:living_clothes -male:tentacles -female:slave -female:gag -male:gag -female:wooden_horse -male:exhibitionism -male:miniguy -female:mind_break -male:mind_break -male:unbirth -tag:scanmark -tag:no_penetration -tag:nudity_only -female:enema -female:brain_fuck -female:navel_fuck -tag:novel -tag:mosaic_censorship -tag:webtoon -male:rape -female:rape -female:yuri -male:anal -female:anal -female:futanari -female:huge_breasts -female:big_areolae -male:torture -male:stuck_in_wall -female:stuck_in_wall -female:torture -female:birth -female:pregnant -female:drugs -female:bdsm -female:body_writing -female:cbt -male:dark_skin -male:insect -female:insect -male:vore -female:vore -female:vomit -female:urination -female:urethra_insertion -tag:mmf_threesome -female:sex_toys -female:double_penetration -female:eggs -female:prolapse -male:smell -male:bestiality -female:bestiality -female:big_ass -female:milf -female:mother -male:dilf -male:netorare -female:netorare -female:cosplaying -female:filming -female:armpit_sex -female:armpit_licking -female:tickling -female:lactation -male:skinsuit -female:skinsuit -male:bbm -female:prostitution -female:double_penetration -female:females_only -male:males_only -female:tentacles -female:tentacles -female:stomach_deformation -female:hairy_armpits -female:large_insertions -female:mind_control -male:orc -female:dark_skin -male:yandere -female:yandere -female:scat -female:toddlercon -female:bbw -female:hairy -male:cuntboy -male:lactation -male:drugs -female:body_modification -female:monoeye -female:chikan -female:long_tongue -female:harness -female:fisting -female:glory_hole -female:latex -male:latex -female:unbirth -female:giantess -female:sole_dickgirl -female:robot -female:doll_joints -female:machine -tag:artbook -male:cbt -female:farting -male:farting -male:midget -female:midget -female:exhibitionism -male:monster -female:big_nipples -female:big_clit -female:gyaru -female:piercing -female:necrophilia -female:snuff -female:smell -male:cheating -female:cheating -male:snuff -female:harem -male:harem"
|
||||
// print(doSearch(query).size)
|
||||
// }
|
||||
|
||||
// @Test
|
||||
// suspend fun test_parse() {
|
||||
// print(doSearch("-male:yaoi -female:yaoi -female:loli").size)
|
||||
// }
|
||||
|
||||
@Test
|
||||
fun test_subdomainFromUrl() {
|
||||
val galleryInfo = getGalleryInfo(1929109).files[2]
|
||||
print(urlFromUrlFromHash(1929109, galleryInfo, "webp", null, "a"))
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||
<uses-permission android:name="android.permission.WRITE_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="23"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<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.autofocus" android:required="false" />
|
||||
@@ -58,7 +59,8 @@
|
||||
<activity
|
||||
android:name=".ui.ReaderActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:parentActivityName=".ui.MainActivity">
|
||||
android:parentActivityName=".ui.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -222,7 +224,8 @@
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:theme="@style/NoActionBarAppTheme">
|
||||
android:theme="@style/NoActionBarAppTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -18,31 +18,41 @@
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.app.*
|
||||
import android.app.Application
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import app.cash.zipline.QuickJs
|
||||
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.GooglePlayServicesRepairableException
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.FirebaseApp
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.Dispatcher
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.hitomi.evaluationContext
|
||||
import xyz.quaver.pupil.hitomi.readText
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.*
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.setClient
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@@ -65,12 +75,50 @@ var clientHolder: OkHttpClient? = null
|
||||
val client: OkHttpClient
|
||||
get() = clientHolder ?: clientBuilder.build().also {
|
||||
clientHolder = it
|
||||
setClient(it)
|
||||
}
|
||||
|
||||
private var version = ""
|
||||
var runtimeReady = false
|
||||
private set
|
||||
lateinit var runtime: QuickJs
|
||||
private set
|
||||
|
||||
class Pupil : Application() {
|
||||
|
||||
companion object {
|
||||
lateinit var instance: Pupil
|
||||
private set
|
||||
}
|
||||
|
||||
init {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
withContext(evaluationContext) {
|
||||
runtime = QuickJs.create()
|
||||
}
|
||||
while (true) {
|
||||
kotlin.runCatching {
|
||||
val newVersion = URL("https://tom5079.github.io/PupilSources/hitomi.html.ver").readText()
|
||||
|
||||
if (version != newVersion) {
|
||||
runtimeReady = false
|
||||
version = newVersion
|
||||
evaluationContext.cancelChildren()
|
||||
withContext(evaluationContext) {
|
||||
Log.d("PUPILD", "UPDATE!")
|
||||
runtime.evaluate(URL("https://tom5079.github.io/PupilSources/assets/js/gg.js").readText())
|
||||
runtimeReady = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delay(10000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
instance = this
|
||||
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
@@ -80,6 +128,7 @@ class Pupil : Application() {
|
||||
else userID
|
||||
}
|
||||
|
||||
FirebaseApp.initializeApp(this)
|
||||
FirebaseCrashlytics.getInstance().setUserId(userID)
|
||||
|
||||
val proxyInfo = getProxyInfo()
|
||||
@@ -89,15 +138,24 @@ class Pupil : Application() {
|
||||
.readTimeout(0, TimeUnit.SECONDS)
|
||||
.proxyInfo(proxyInfo)
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
val request = chain.request().newBuilder()
|
||||
.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36")
|
||||
.header("Referer", "https://hitomi.la/")
|
||||
.build()
|
||||
|
||||
val tag = request.tag() ?: return@addInterceptor 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 {
|
||||
Preferences.get<String>("download_folder").also {
|
||||
if (it.startsWith("content") && Build.VERSION.SDK_INT > 19)
|
||||
if (it.startsWith("content://"))
|
||||
contentResolver.takePersistableUriPermission(
|
||||
Uri.parse(it),
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
@@ -105,20 +163,28 @@ class Pupil : Application() {
|
||||
|
||||
if (!FileX(this, it).canWrite())
|
||||
throw Exception()
|
||||
|
||||
DownloadManager.getInstance(this).migrate()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
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)
|
||||
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0)
|
||||
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
|
||||
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
|
||||
|
||||
favoriteTags.filter { it.tag.contains('_') }.forEach {
|
||||
favoriteTags.remove(it)
|
||||
}
|
||||
|
||||
/*
|
||||
if (BuildConfig.DEBUG)
|
||||
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
||||
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)*/
|
||||
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(this)
|
||||
@@ -128,7 +194,14 @@ class Pupil : Application() {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
BigImageViewer.initialize(FrescoImageLoader.with(this))
|
||||
BigImageViewer.initialize(
|
||||
FrescoImageLoader.with(
|
||||
this,
|
||||
OkHttpImagePipelineConfigFactory
|
||||
.newBuilder(this, client)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
|
||||
package xyz.quaver.pupil.adapters
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.SparseBooleanArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.cardview.widget.CardView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
@@ -34,16 +34,16 @@ import com.daimajia.swipe.SwipeLayout
|
||||
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
||||
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
|
||||
import com.github.piasy.biv.loader.ImageLoader
|
||||
import kotlinx.android.synthetic.main.item_galleryblock.view.*
|
||||
import kotlinx.android.synthetic.main.item_reader.view.*
|
||||
import kotlinx.coroutines.*
|
||||
import xyz.quaver.hitomi.getReader
|
||||
import xyz.quaver.io.util.getChild
|
||||
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.hitomi.getGallery
|
||||
import xyz.quaver.pupil.hitomi.getGalleryInfo
|
||||
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.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
@@ -52,300 +52,260 @@ import java.io.File
|
||||
|
||||
class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
||||
|
||||
enum class ViewType {
|
||||
NEXT,
|
||||
GALLERY,
|
||||
PREV
|
||||
}
|
||||
|
||||
var update = true
|
||||
var updateAll = true
|
||||
var thin: Boolean = Preferences["thin"]
|
||||
|
||||
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||
var updateJob: Job? = null
|
||||
|
||||
private fun updateProgress(context: Context, galleryID: Int) {
|
||||
val cache = Cache.getInstance(context, galleryID)
|
||||
inner class GalleryViewHolder(val binding: GalleryblockItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
private var galleryID: Int = 0
|
||||
|
||||
init {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (cache.metadata.reader == null) {
|
||||
view.galleryblock_progressbar_layout.visibility = View.GONE
|
||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||
return@launch
|
||||
while (updateAll) {
|
||||
updateProgress(itemView.context)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
val imageList = cache.metadata.imageList!!
|
||||
progress = imageList.count { it != null }
|
||||
max = imageList.size
|
||||
|
||||
progress = imageList.count { it != null }
|
||||
max = imageList.size
|
||||
|
||||
with(view.galleryblock_progressbar_layout) {
|
||||
if (visibility == View.GONE)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (!imageList.contains(null)) {
|
||||
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
|
||||
this@GalleryViewHolder.binding.galleryblockId.setOnClickListener {
|
||||
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("gallery_id", galleryID.toString())
|
||||
)
|
||||
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
val resources = context.resources
|
||||
val languages = resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val galleryBlock = cache.getGalleryBlock() ?: return@launch
|
||||
|
||||
val artists = galleryBlock.artists
|
||||
val series = galleryBlock.series
|
||||
|
||||
galleryblock_thumbnail.apply {
|
||||
setOnClickListener {
|
||||
view.performClick()
|
||||
}
|
||||
setOnLongClickListener {
|
||||
view.performLongClick()
|
||||
}
|
||||
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
|
||||
setImageLoaderCallback(object: ImageLoader.Callback {
|
||||
override fun onFail(error: Exception?) {
|
||||
Cache.getInstance(context, galleryID).let { cache ->
|
||||
cache.cacheFolder.getChild(".thumbnail").let { if (it.exists()) it.delete() }
|
||||
cache.downloadFolder?.getChild(".thumbnail")?.let { if (it.exists()) it.delete() }
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
val resources = itemView.context.resources
|
||||
val languages = resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
override fun onCacheHit(imageType: Int, image: File?) {}
|
||||
override fun onCacheMiss(imageType: Int, image: File?) {}
|
||||
override fun onFinish() {}
|
||||
override fun onProgress(progress: Int) {}
|
||||
override fun onStart() {}
|
||||
override fun onSuccess(image: File?) {}
|
||||
})
|
||||
ssiv?.recycle()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
showImage(cache.getThumbnail())
|
||||
}
|
||||
}
|
||||
val artists = galleryBlock.artists
|
||||
val series = galleryBlock.series
|
||||
|
||||
if (updateJob == null)
|
||||
updateJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
while (update) {
|
||||
updateProgress(context, galleryID)
|
||||
delay(1000)
|
||||
binding.galleryblockThumbnail.apply {
|
||||
setOnClickListener {
|
||||
itemView.performClick()
|
||||
}
|
||||
setOnLongClickListener {
|
||||
itemView.performLongClick()
|
||||
}
|
||||
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
|
||||
setImageLoaderCallback(object: ImageLoader.Callback {
|
||||
override fun onFail(error: Exception?) {
|
||||
Cache.delete(context, galleryID)
|
||||
}
|
||||
|
||||
override fun onCacheHit(imageType: Int, image: File?) {}
|
||||
override fun onCacheMiss(imageType: Int, image: File?) {}
|
||||
override fun onFinish() {}
|
||||
override fun onProgress(progress: Int) {}
|
||||
override fun onStart() {}
|
||||
override fun onSuccess(image: File?) {}
|
||||
})
|
||||
ssiv?.recycle()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
cache.getThumbnail().let { launch(Dispatchers.Main) {
|
||||
showImage(it)
|
||||
} }
|
||||
}
|
||||
}
|
||||
|
||||
galleryblock_title.text = galleryBlock.title
|
||||
with(galleryblock_artist) {
|
||||
text = artists.joinToString(", ") { it.wordCapitalize() }
|
||||
visibility = when {
|
||||
artists.isNotEmpty() -> View.VISIBLE
|
||||
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
|
||||
}
|
||||
}
|
||||
binding.galleryblockTitle.text = galleryBlock.title
|
||||
with(binding.galleryblockArtist) {
|
||||
text = artists.joinToString { it.wordCapitalize() }
|
||||
visibility = when {
|
||||
artists.isNotEmpty() -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
|
||||
galleryblock_tag_group.removeAllViews()
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
galleryBlock.relatedTags.sortedBy {
|
||||
val tag = Tag.parse(it)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val gallery = runCatching {
|
||||
getGallery(galleryID)
|
||||
}.getOrNull()
|
||||
|
||||
if (favoriteTags.contains(tag))
|
||||
-1
|
||||
else
|
||||
when(Tag.parse(it).area) {
|
||||
"female" -> 0
|
||||
"male" -> 1
|
||||
else -> 2
|
||||
}
|
||||
}.map {
|
||||
TagChip(context, Tag.parse(it)).apply {
|
||||
setOnClickListener { view ->
|
||||
for (callback in onChipClickedHandler)
|
||||
callback.invoke((view as TagChip).tag)
|
||||
if (gallery?.groups?.isNotEmpty() != true)
|
||||
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.isNullOrEmpty() -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
with(galleryblock_favorite) {
|
||||
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)
|
||||
with(binding.galleryblockTagGroup) {
|
||||
onClickListener = {
|
||||
onChipClickedHandler.forEach { callback ->
|
||||
callback.invoke(it)
|
||||
}
|
||||
else -> {
|
||||
favorites.add(galleryBlock.id)
|
||||
}
|
||||
|
||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
|
||||
this ?: return@apply
|
||||
tags.clear()
|
||||
|
||||
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
|
||||
override fun onAnimationEnd(drawable: Drawable?) {
|
||||
setImageResource(R.drawable.ic_star_filled)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
tags.addAll(
|
||||
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
|
||||
}
|
||||
})
|
||||
start()
|
||||
})
|
||||
}.map {
|
||||
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 (thin) {
|
||||
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)>()
|
||||
var onDownloadClickedHandler: ((Int) -> Unit)? = null
|
||||
var onDeleteClickedHandler: ((Int) -> Unit)? = null
|
||||
|
||||
var showNext = false
|
||||
var showPrev = false
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
return GalleryViewHolder(GalleryblockItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is GalleryViewHolder) {
|
||||
val galleryID = galleries[position-(if (showPrev) 1 else 0)]
|
||||
val galleryID = galleries[position]
|
||||
|
||||
holder.bind(galleryID)
|
||||
|
||||
with(holder.view.galleryblock_primary) {
|
||||
setOnClickListener {
|
||||
holder.view.performClick()
|
||||
}
|
||||
setOnLongClickListener {
|
||||
holder.view.performLongClick()
|
||||
}
|
||||
}
|
||||
|
||||
holder.view.galleryblock_download.setOnClickListener {
|
||||
holder.binding.galleryblockCard.binding.download.setOnClickListener {
|
||||
onDownloadClickedHandler?.invoke(position)
|
||||
}
|
||||
|
||||
holder.view.galleryblock_delete.setOnClickListener {
|
||||
holder.binding.galleryblockCard.binding.delete.setOnClickListener {
|
||||
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?) {
|
||||
mItemManger.closeAllExcept(layout)
|
||||
|
||||
holder.view.galleryblock_download.text =
|
||||
if (DownloadManager.getInstance(holder.view.context).isDownloading(galleryID))
|
||||
holder.view.context.getString(android.R.string.cancel)
|
||||
holder.binding.galleryblockCard.binding.download.text =
|
||||
if (DownloadManager.getInstance(holder.binding.root.context).isDownloading(galleryID))
|
||||
holder.binding.root.context.getString(android.R.string.cancel)
|
||||
else
|
||||
holder.view.context.getString(R.string.main_download)
|
||||
holder.binding.root.context.getString(R.string.main_download)
|
||||
}
|
||||
|
||||
override fun onClose(layout: SwipeLayout?) {}
|
||||
@@ -357,27 +317,7 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
||||
super.onViewDetachedFromWindow(holder)
|
||||
override fun getItemCount() = galleries.size
|
||||
|
||||
if (holder is GalleryViewHolder) {
|
||||
holder.updateJob?.cancel()
|
||||
holder.updateJob = 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
|
||||
override fun getSwipeLayoutResourceId(position: Int) = R.id.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
|
||||
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
package xyz.quaver.pupil.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.DiscretePathEffect
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
@@ -40,10 +39,10 @@ import com.facebook.imagepipeline.image.ImageInfo
|
||||
import com.github.piasy.biv.view.BigImageView
|
||||
import com.github.piasy.biv.view.ImageShownCallback
|
||||
import com.github.piasy.biv.view.ImageViewFactory
|
||||
import kotlinx.android.synthetic.main.item_reader.view.*
|
||||
import kotlinx.coroutines.*
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.ReaderItemBinding
|
||||
import xyz.quaver.pupil.hitomi.GalleryInfo
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import java.io.File
|
||||
@@ -53,105 +52,113 @@ class ReaderAdapter(
|
||||
private val activity: ReaderActivity,
|
||||
private val galleryID: Int
|
||||
) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||
|
||||
var reader: Reader? = null
|
||||
var galleryInfo: GalleryInfo? = null
|
||||
|
||||
var isFullScreen = false
|
||||
|
||||
var onItemClickListener : (() -> (Unit))? = null
|
||||
|
||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||
fun clear() {
|
||||
view.image.ssiv?.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.item_reader, parent, false
|
||||
).let {
|
||||
with(it) {
|
||||
image.setImageViewFactory(FrescoImageViewFactory().apply {
|
||||
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
with (binding.image) {
|
||||
setImageViewFactory(FrescoImageViewFactory().apply {
|
||||
updateView = { imageInfo ->
|
||||
it.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
|
||||
}
|
||||
}
|
||||
})
|
||||
image.setImageShownCallback(object : ImageShownCallback {
|
||||
setImageShownCallback(object : ImageShownCallback {
|
||||
override fun onMainImageShown() {
|
||||
it.image.mainView.let { v ->
|
||||
binding.image.mainView.let { v ->
|
||||
when (v) {
|
||||
is SubsamplingScaleImageView ->
|
||||
if (!isFullScreen) it.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
if (!isFullScreen) binding.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onThumbnailShown() {}
|
||||
})
|
||||
image.setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
|
||||
image.setOnClickListener {
|
||||
this.performClick()
|
||||
}
|
||||
|
||||
setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
|
||||
setOnClickListener {
|
||||
onItemClickListener?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
ViewHolder(it)
|
||||
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 progress = activity.downloader?.progress?.get(galleryID)?.get(position)
|
||||
|
||||
if (progress?.isInfinite() == true && image != null) {
|
||||
binding.progressGroup.visibility = View.INVISIBLE
|
||||
binding.image.showImage(image.uri)
|
||||
} else {
|
||||
binding.progressGroup.visibility = View.VISIBLE
|
||||
binding.readerItemProgressbar.progress =
|
||||
if (progress?.isInfinite() == true)
|
||||
100
|
||||
else
|
||||
progress?.roundToInt() ?: 0
|
||||
|
||||
clear()
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(1000)
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
binding.image.mainView.let {
|
||||
when (it) {
|
||||
is SubsamplingScaleImageView ->
|
||||
it.recycle()
|
||||
is SimpleDraweeView ->
|
||||
it.controller = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.view as ConstraintLayout
|
||||
|
||||
if (cache == null)
|
||||
cache = Cache.getInstance(holder.view.context, galleryID)
|
||||
|
||||
if (!isFullScreen) {
|
||||
holder.view.setBackgroundResource(R.drawable.reader_item_boundary)
|
||||
holder.view.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
height = 0
|
||||
dimensionRatio =
|
||||
"${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
|
||||
}
|
||||
} else {
|
||||
holder.view.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
holder.view.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
||||
dimensionRatio = null
|
||||
}
|
||||
holder.view.background = null
|
||||
}
|
||||
|
||||
holder.view.reader_index.text = (position+1).toString()
|
||||
|
||||
val image = cache!!.getImage(position)
|
||||
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
|
||||
|
||||
if (progress?.isInfinite() == true && image != null) {
|
||||
holder.view.progress_group.visibility = View.INVISIBLE
|
||||
holder.view.image.showImage(image.uri)
|
||||
} else {
|
||||
holder.view.progress_group.visibility = View.VISIBLE
|
||||
holder.view.reader_item_progressbar.progress =
|
||||
if (progress?.isInfinite() == true)
|
||||
100
|
||||
else
|
||||
progress?.roundToInt() ?: 0
|
||||
|
||||
holder.clear()
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(1000)
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
holder.bind(position)
|
||||
}
|
||||
|
||||
override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0
|
||||
override fun getItemCount() = galleryInfo?.files?.size ?: 0
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
holder.clear()
|
||||
|
||||
210
app/src/main/java/xyz/quaver/pupil/hitomi/common.kt
Normal file
210
app/src/main/java/xyz/quaver/pupil/hitomi/common.kt
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* 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.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Request
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.runtime
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
const val protocol = "https:"
|
||||
|
||||
@Serializable
|
||||
data class Artist(
|
||||
val artist: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Group(
|
||||
val group: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Parody(
|
||||
val parody: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Character(
|
||||
val character: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Tag(
|
||||
val tag: String,
|
||||
val url: String,
|
||||
val female: String? = null,
|
||||
val male: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Language(
|
||||
val galleryid: String,
|
||||
val url: String,
|
||||
val language_localname: String,
|
||||
val name: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GalleryInfo(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val japanese_title: String? = null,
|
||||
val language: String? = null,
|
||||
val type: String,
|
||||
val date: String,
|
||||
val artists: List<Artist>? = null,
|
||||
val groups: List<Group>? = null,
|
||||
val parodys: List<Parody>? = null,
|
||||
val tags: List<Tag>? = null,
|
||||
val related: List<Int>,
|
||||
val languages: List<Language>,
|
||||
val characters: List<Character>? = null,
|
||||
val scene_indexes: List<Int>,
|
||||
val files: List<GalleryFiles>
|
||||
)
|
||||
|
||||
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("CODE ${it.code()}") }.body()?.use { it.string() } ?: throw IOException()
|
||||
}
|
||||
|
||||
fun URL.readBytes(settings: HeaderSetter? = null): ByteArray {
|
||||
val request = Request.Builder()
|
||||
.url(this).let {
|
||||
settings?.invoke(it) ?: it
|
||||
}.build()
|
||||
|
||||
return client.newCall(request).execute().also { if (it.code() != 200) throw IOException("CODE ${it.code()}") }.body()?.use { it.bytes() } ?: throw IOException()
|
||||
}
|
||||
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
fun getGalleryInfo(galleryID: Int) =
|
||||
json.decodeFromString<GalleryInfo>(
|
||||
URL("$protocol//$domain/galleries/$galleryID.js").readText()
|
||||
.replace("var galleryinfo = ", "")
|
||||
)
|
||||
|
||||
//common.js
|
||||
const val domain = "ltn.hitomi.la"
|
||||
const val galleryblockextension = ".html"
|
||||
const val galleryblockdir = "galleryblock"
|
||||
const val nozomiextension = ".nozomi"
|
||||
|
||||
val evaluationContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + Job()
|
||||
|
||||
object gg {
|
||||
|
||||
suspend fun m(g: Int): Int = withContext(evaluationContext) {
|
||||
runtime.evaluate("gg.m($g)").toString().toInt()
|
||||
}
|
||||
suspend fun b(): String = withContext(evaluationContext) {
|
||||
runtime.evaluate("gg.b").toString()
|
||||
}
|
||||
|
||||
suspend fun s(h: String): String = withContext(evaluationContext) {
|
||||
runtime.evaluate("gg.s('$h')").toString()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun subdomainFromURL(url: String, base: String? = null) : String {
|
||||
var retval = "b"
|
||||
|
||||
if (!base.isNullOrBlank())
|
||||
retval = base
|
||||
|
||||
val b = 16
|
||||
|
||||
val r = Regex("""/[0-9a-f]{61}([0-9a-f]{2})([0-9a-f])""")
|
||||
val m = r.find(url) ?: return "a"
|
||||
|
||||
val g = m.groupValues.let { it[2]+it[1] }.toIntOrNull(b)
|
||||
|
||||
if (g != null) {
|
||||
retval = (97+ gg.m(g)).toChar().toString() + retval
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
suspend fun urlFromUrl(url: String, base: String? = null) : String {
|
||||
return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/")
|
||||
}
|
||||
|
||||
suspend fun fullPathFromHash(hash: String) : String =
|
||||
"${gg.b()}${gg.s(hash)}/$hash"
|
||||
|
||||
fun realFullPathFromHash(hash: String): String =
|
||||
hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1/$hash")
|
||||
|
||||
suspend fun urlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null) : String {
|
||||
val ext = ext ?: dir ?: image.name.takeLastWhile { it != '.' }
|
||||
val dir = dir ?: "images"
|
||||
return "https://a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext"
|
||||
}
|
||||
|
||||
suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null) =
|
||||
if (base == "tn")
|
||||
urlFromUrl("https://a.hitomi.la/$dir/${realFullPathFromHash(image.hash)}.$ext", base)
|
||||
else
|
||||
urlFromUrl(urlFromHash(galleryID, image, dir, ext), base)
|
||||
|
||||
suspend fun rewriteTnPaths(html: String) {
|
||||
html.replace(Regex("""//tn\.hitomi\.la/[^/]+/[0-9a-f]/[0-9a-f]{2}/[0-9a-f]{64}""")) { url ->
|
||||
runBlocking {
|
||||
urlFromUrl(url.value, "tn")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
54
app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt
Normal file
54
app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
@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 info = getGalleryInfo(galleryID)
|
||||
|
||||
return Gallery(
|
||||
info.related,
|
||||
info.languages.map { it.name to it.galleryid },
|
||||
urlFromUrlFromHash(galleryID, info.files.first(), "webpbigtn", "webp", "tn"),
|
||||
info.title,
|
||||
info.artists?.map { it.artist }.orEmpty(),
|
||||
info.groups?.map { it.group }.orEmpty(),
|
||||
info.type,
|
||||
info.language.orEmpty(),
|
||||
info.parodys?.map { it.parody }.orEmpty(),
|
||||
info.characters?.map { it.character }.orEmpty(),
|
||||
info.tags?.map { "${if (it.female.isNullOrEmpty()) "" else "female:"}${if (it.male.isNullOrEmpty()) "" else "male:"}${it.tag}" }.orEmpty(),
|
||||
info.files.map { urlFromUrlFromHash(galleryID, it, "webpsmalltn", "webp", "tn") }
|
||||
)
|
||||
}
|
||||
92
app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt
Normal file
92
app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 java.net.URL
|
||||
import java.net.URLDecoder
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import kotlin.io.readText
|
||||
|
||||
//galleryblock.js
|
||||
fun fetchNozomi(area: String? = null, tag: String = "index", language: String = "all", start: Int = -1, count: Int = -1) : Pair<List<Int>, Int> {
|
||||
val url = when(area) {
|
||||
null -> "$protocol//$domain/$tag-$language$nozomiextension"
|
||||
else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
|
||||
}
|
||||
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
requestMethod = "GET"
|
||||
|
||||
if (start != -1 && count != -1) {
|
||||
val startByte = start*4
|
||||
val endByte = (start+count)*4-1
|
||||
|
||||
setRequestProperty("Range", "bytes=$startByte-$endByte")
|
||||
}
|
||||
|
||||
connect()
|
||||
|
||||
val totalItems = getHeaderField("Content-Range")
|
||||
.replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
|
||||
|
||||
val nozomi = ArrayList<Int>()
|
||||
|
||||
val arrayBuffer = ByteBuffer
|
||||
.wrap(inputStream.readBytes())
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
while (arrayBuffer.hasRemaining())
|
||||
nozomi.add(arrayBuffer.int)
|
||||
|
||||
return Pair(nozomi, totalItems)
|
||||
}
|
||||
}
|
||||
|
||||
@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>,
|
||||
val groups: List<String>
|
||||
)
|
||||
|
||||
suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock {
|
||||
val info = getGalleryInfo(galleryID)
|
||||
|
||||
return GalleryBlock(
|
||||
galleryID,
|
||||
"",
|
||||
listOf(urlFromUrlFromHash(galleryID, info.files.first(), "webpbigtn", "webp", "tn")),
|
||||
info.title,
|
||||
info.artists?.map { it.artist }.orEmpty(),
|
||||
info.parodys?.map { it.parody }.orEmpty(),
|
||||
info.type,
|
||||
info.language.orEmpty(),
|
||||
info.tags?.map { "${if (it.female.isNullOrEmpty()) "" else "female:"}${if (it.male.isNullOrEmpty()) "" else "male:"}${it.tag}" }.orEmpty(),
|
||||
info.groups?.map { it.group }.orEmpty()
|
||||
)
|
||||
}
|
||||
38
app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt
Normal file
38
app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 xyz.quaver.pupil.hitomi.GalleryInfo
|
||||
import xyz.quaver.pupil.hitomi.getGalleryInfo
|
||||
|
||||
@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"))
|
||||
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
|
||||
}
|
||||
328
app/src/main/java/xyz/quaver/pupil/hitomi/search.kt
Normal file
328
app/src/main/java/xyz/quaver/pupil/hitomi/search.kt
Normal file
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* 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 okhttp3.Request
|
||||
import xyz.quaver.pupil.client
|
||||
import java.net.URL
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.security.MessageDigest
|
||||
import kotlin.math.min
|
||||
|
||||
//searchlib.js
|
||||
const val separator = "-"
|
||||
const val extension = ".html"
|
||||
const val index_dir = "tagindex"
|
||||
const val galleries_index_dir = "galleriesindex"
|
||||
const val max_node_size = 464
|
||||
const val B = 16
|
||||
const val compressed_nozomi_prefix = "n"
|
||||
|
||||
val tag_index_version: String by lazy { getIndexVersion("tagindex") }
|
||||
val galleries_index_version: String by lazy { getIndexVersion("galleriesindex") }
|
||||
|
||||
fun sha256(data: ByteArray) : ByteArray {
|
||||
return MessageDigest.getInstance("SHA-256").digest(data)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
fun hashTerm(term: String) : UByteArray {
|
||||
return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4)
|
||||
}
|
||||
|
||||
fun sanitize(input: String) : String {
|
||||
return input.replace(Regex("[/#]"), "")
|
||||
}
|
||||
|
||||
fun getIndexVersion(name: String) =
|
||||
URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText()
|
||||
|
||||
//search.js
|
||||
fun getGalleryIDsForQuery(query: String) : Set<Int> {
|
||||
query.replace("_", " ").let {
|
||||
if (it.indexOf(':') > -1) {
|
||||
val sides = it.split(":")
|
||||
val ns = sides[0]
|
||||
var tag = sides[1]
|
||||
|
||||
var area : String? = ns
|
||||
var language = "all"
|
||||
when (ns) {
|
||||
"female", "male" -> {
|
||||
area = "tag"
|
||||
tag = it
|
||||
}
|
||||
"language" -> {
|
||||
area = null
|
||||
language = tag
|
||||
tag = "index"
|
||||
}
|
||||
}
|
||||
|
||||
return getGalleryIDsFromNozomi(area, tag, language)
|
||||
}
|
||||
|
||||
val key = hashTerm(it)
|
||||
val field = "galleries"
|
||||
|
||||
val node = getNodeAtAddress(field, 0) ?: return emptySet()
|
||||
|
||||
val data = bSearch(field, key, node)
|
||||
|
||||
if (data != null)
|
||||
return getGalleryIDsFromData(data)
|
||||
|
||||
return emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
fun getSuggestionsForQuery(query: String) : List<Suggestion> {
|
||||
query.replace('_', ' ').let {
|
||||
var field = "global"
|
||||
var term = it
|
||||
|
||||
if (term.indexOf(':') > -1) {
|
||||
val sides = it.split(':')
|
||||
field = sides[0]
|
||||
term = sides[1]
|
||||
}
|
||||
|
||||
val key = hashTerm(term)
|
||||
val node = getNodeAtAddress(field, 0) ?: return emptyList()
|
||||
val data = bSearch(field, key, node)
|
||||
|
||||
if (data != null)
|
||||
return getSuggestionsFromData(field, data)
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
|
||||
fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggestion> {
|
||||
val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data"
|
||||
val (offset, length) = data
|
||||
if (length > 10000 || length <= 0)
|
||||
throw Exception("length $length is too long")
|
||||
|
||||
val inbuf = getURLAtRange(url, offset.until(offset+length))
|
||||
|
||||
val suggestions = ArrayList<Suggestion>()
|
||||
|
||||
val buffer = ByteBuffer
|
||||
.wrap(inbuf)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
val numberOfSuggestions = buffer.int
|
||||
|
||||
if (numberOfSuggestions > 100 || numberOfSuggestions <= 0)
|
||||
throw Exception("number of suggestions $numberOfSuggestions is too long")
|
||||
|
||||
for (i in 0.until(numberOfSuggestions)) {
|
||||
var top = buffer.int
|
||||
|
||||
val ns = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
|
||||
buffer.position(buffer.position()+top)
|
||||
|
||||
top = buffer.int
|
||||
|
||||
val tag = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
|
||||
buffer.position(buffer.position()+top)
|
||||
|
||||
val count = buffer.int
|
||||
|
||||
val tagname = sanitize(tag)
|
||||
val u =
|
||||
when(ns) {
|
||||
"female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
|
||||
"language" -> "/index-$tagname${separator}1$extension"
|
||||
else -> "/$ns/$tagname${separator}all${separator}1$extension"
|
||||
}
|
||||
|
||||
suggestions.add(Suggestion(tag, count, u, ns))
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> {
|
||||
val nozomiAddress =
|
||||
when(area) {
|
||||
null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension"
|
||||
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
||||
}
|
||||
|
||||
val bytes = try {
|
||||
URL(nozomiAddress).readBytes()
|
||||
} catch (e: Exception) {
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
val nozomi = mutableSetOf<Int>()
|
||||
|
||||
val arrayBuffer = ByteBuffer
|
||||
.wrap(bytes)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
while (arrayBuffer.hasRemaining())
|
||||
nozomi.add(arrayBuffer.int)
|
||||
|
||||
return nozomi
|
||||
}
|
||||
|
||||
fun getGalleryIDsFromData(data: Pair<Long, Int>) : Set<Int> {
|
||||
val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data"
|
||||
val (offset, length) = data
|
||||
if (length > 100000000 || length <= 0)
|
||||
throw Exception("length $length is too long")
|
||||
|
||||
val inbuf = getURLAtRange(url, offset.until(offset+length))
|
||||
|
||||
val galleryIDs = mutableSetOf<Int>()
|
||||
|
||||
val buffer = ByteBuffer
|
||||
.wrap(inbuf)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
val numberOfGalleryIDs = buffer.int
|
||||
|
||||
val expectedLength = numberOfGalleryIDs*4+4
|
||||
|
||||
if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0)
|
||||
throw Exception("number_of_galleryids $numberOfGalleryIDs is too long")
|
||||
else if (inbuf.size != expectedLength)
|
||||
throw Exception("inbuf.byteLength ${inbuf.size} != expected_length $expectedLength")
|
||||
|
||||
for (i in 0.until(numberOfGalleryIDs))
|
||||
galleryIDs.add(buffer.int)
|
||||
|
||||
return galleryIDs
|
||||
}
|
||||
|
||||
fun getNodeAtAddress(field: String, address: Long) : Node? {
|
||||
val url =
|
||||
when(field) {
|
||||
"galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index"
|
||||
"languages" -> "$protocol//$domain/$galleries_index_dir/languages.$galleries_index_version.index"
|
||||
"nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.$galleries_index_version.index"
|
||||
else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index"
|
||||
}
|
||||
|
||||
val nodedata = getURLAtRange(url, address.until(address+ max_node_size))
|
||||
|
||||
return decodeNode(nodedata)
|
||||
}
|
||||
|
||||
fun getURLAtRange(url: String, range: LongRange) : ByteArray {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.header("Range", "bytes=${range.first}-${range.last}")
|
||||
.build()
|
||||
|
||||
return client.newCall(request).execute().body()?.use { it.bytes() } ?: byteArrayOf()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
data class Node(val keys: List<UByteArray>, val datas: List<Pair<Long, Int>>, val subNodeAddresses: List<Long>)
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
fun decodeNode(data: ByteArray) : Node {
|
||||
val buffer = ByteBuffer
|
||||
.wrap(data)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
val uData = data.toUByteArray()
|
||||
|
||||
val numberOfKeys = buffer.int
|
||||
val keys = ArrayList<UByteArray>()
|
||||
|
||||
for (i in 0.until(numberOfKeys)) {
|
||||
val keySize = buffer.int
|
||||
|
||||
if (keySize == 0 || keySize > 32)
|
||||
throw Exception("fatal: !keySize || keySize > 32")
|
||||
|
||||
keys.add(uData.sliceArray(buffer.position().until(buffer.position()+keySize)))
|
||||
buffer.position(buffer.position()+keySize)
|
||||
}
|
||||
|
||||
val numberOfDatas = buffer.int
|
||||
val datas = ArrayList<Pair<Long, Int>>()
|
||||
|
||||
for (i in 0.until(numberOfDatas)) {
|
||||
val offset = buffer.long
|
||||
val length = buffer.int
|
||||
|
||||
datas.add(Pair(offset, length))
|
||||
}
|
||||
|
||||
val numberOfSubNodeAddresses = B +1
|
||||
val subNodeAddresses = ArrayList<Long>()
|
||||
|
||||
for (i in 0.until(numberOfSubNodeAddresses)) {
|
||||
val subNodeAddress = buffer.long
|
||||
subNodeAddresses.add(subNodeAddress)
|
||||
}
|
||||
|
||||
return Node(keys, datas, subNodeAddresses)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
fun bSearch(field: String, key: UByteArray, node: Node) : Pair<Long, Int>? {
|
||||
fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray) : Int {
|
||||
val top = min(dv1.size, dv2.size)
|
||||
|
||||
for (i in 0.until(top)) {
|
||||
if (dv1[i] < dv2[i])
|
||||
return -1
|
||||
else if (dv1[i] > dv2[i])
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
fun locateKey(key: UByteArray, node: Node) : Pair<Boolean, Int> {
|
||||
for (i in node.keys.indices) {
|
||||
val cmpResult = compareArrayBuffers(key, node.keys[i])
|
||||
|
||||
if (cmpResult <= 0)
|
||||
return Pair(cmpResult==0, i)
|
||||
}
|
||||
|
||||
return Pair(false, node.keys.size)
|
||||
}
|
||||
|
||||
fun isLeaf(node: Node) : Boolean {
|
||||
for (subnode in node.subNodeAddresses)
|
||||
if (subnode != 0L)
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (node.keys.isEmpty())
|
||||
return null
|
||||
|
||||
val (there, where) = locateKey(key, node)
|
||||
if (there)
|
||||
return node.datas[where]
|
||||
else if (isLeaf(node))
|
||||
return null
|
||||
|
||||
val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where]) ?: return null
|
||||
return bSearch(field, key, nextNode)
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@@ -54,13 +55,15 @@ class UpdateBroadcastReceiver : BroadcastReceiver() {
|
||||
|
||||
val uri = downloadManager.query(query).use { cursor ->
|
||||
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)
|
||||
|
||||
when (uri.scheme) {
|
||||
"file" ->
|
||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!))
|
||||
else
|
||||
uri
|
||||
"content" -> uri
|
||||
else -> null
|
||||
}
|
||||
@@ -73,10 +76,10 @@ class UpdateBroadcastReceiver : BroadcastReceiver() {
|
||||
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW).apply {
|
||||
val pendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||
}, 0)
|
||||
}, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else 0)
|
||||
|
||||
val notification = NotificationCompat.Builder(context, "update")
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
|
||||
@@ -23,6 +23,8 @@ import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
@@ -37,16 +39,11 @@ import okhttp3.Callback
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okio.*
|
||||
import xyz.quaver.pupil.PupilInterceptor
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.interceptors
|
||||
import xyz.quaver.pupil.*
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.util.*
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.ellipsize
|
||||
import xyz.quaver.pupil.util.normalizeID
|
||||
import xyz.quaver.pupil.util.requestBuilders
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.math.ceil
|
||||
@@ -77,7 +74,7 @@ class DownloadService : Service() {
|
||||
|
||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||
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 =
|
||||
NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
||||
@@ -87,10 +84,10 @@ class DownloadService : Service() {
|
||||
Intent(this, DownloadService::class.java)
|
||||
.putExtra(KEY_COMMAND, COMMAND_CANCEL)
|
||||
.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()
|
||||
|
||||
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
|
||||
notification[galleryID] = NotificationCompat.Builder(this, "download").apply {
|
||||
setContentTitle(getString(R.string.reader_loading))
|
||||
setContentText(getString(R.string.reader_notification_text))
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
@@ -98,7 +95,7 @@ class DownloadService : Service() {
|
||||
addAction(action)
|
||||
setProgress(0, 0, true)
|
||||
setOngoing(true)
|
||||
})
|
||||
}
|
||||
|
||||
notify(galleryID)
|
||||
}
|
||||
@@ -123,7 +120,7 @@ class DownloadService : Service() {
|
||||
.setProgress(max, progress, false)
|
||||
.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()) }
|
||||
else
|
||||
notificationManager.cancel(galleryID)
|
||||
@@ -170,12 +167,17 @@ class DownloadService : Service() {
|
||||
|
||||
private val interceptor: PupilInterceptor = { chain ->
|
||||
val request = chain.request()
|
||||
var response = chain.proceed(request)
|
||||
|
||||
var retry = 5
|
||||
while (!response.isSuccessful && retry > 0) {
|
||||
var response = chain.proceed(request)
|
||||
var limit = 5
|
||||
|
||||
while (!response.isSuccessful) {
|
||||
if (response.code() == 503) {
|
||||
Thread.sleep(200)
|
||||
} else if (--limit > 0)
|
||||
break
|
||||
|
||||
response = chain.proceed(request)
|
||||
retry--
|
||||
}
|
||||
|
||||
response.newBuilder()
|
||||
@@ -198,18 +200,17 @@ class DownloadService : Service() {
|
||||
* Float.POSITIVE_INFINITY -> Download completed
|
||||
*/
|
||||
val progress = ConcurrentHashMap<Int, MutableList<Float>>()
|
||||
var priority = 0
|
||||
|
||||
fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
|
||||
|
||||
private val callback = object: Callback {
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
|
||||
if (e.message?.contains("cancel", true) == false) {
|
||||
val galleryID = (call.request().tag() as Tag).galleryID
|
||||
|
||||
// Retry
|
||||
cancel(galleryID)
|
||||
download(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,28 +218,32 @@ class DownloadService : Service() {
|
||||
val (galleryID, index, startId) = call.request().tag() as Tag
|
||||
val ext = call.request().url().encodedPath().split('.').last()
|
||||
|
||||
kotlin.runCatching {
|
||||
val image = response.also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } ?: throw Exception()
|
||||
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
runCatching {
|
||||
response.also {
|
||||
if (it.code() != 200) throw IOException(
|
||||
"$galleryID $index ${response.request().url()} CODE ${it.code()}"
|
||||
)
|
||||
}.body()?.use {
|
||||
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
|
||||
|
||||
Cache.getInstance(this@DownloadService, galleryID)
|
||||
.putImage(index, "${index.toString().padStart(padding, '0')}.$ext", it.byteStream())
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
kotlin.runCatching {
|
||||
Cache.getInstance(this@DownloadService, galleryID).putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image)
|
||||
}.onSuccess {
|
||||
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
|
||||
notify(galleryID)
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
if (DownloadManager.getInstance(this@DownloadService)
|
||||
.getDownloadFolder(galleryID) != null)
|
||||
.getDownloadFolder(galleryID) != null
|
||||
)
|
||||
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
|
||||
|
||||
startId?.let { stopSelf(it) }
|
||||
}
|
||||
}.onFailure {
|
||||
cancel(galleryID)
|
||||
download(galleryID)
|
||||
}
|
||||
} ?: throw Exception("Response null")
|
||||
}.onFailure {
|
||||
FirebaseCrashlytics.getInstance().recordException(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,29 +294,33 @@ class DownloadService : Service() {
|
||||
fun delete(galleryID: Int, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
|
||||
cancel(galleryID)
|
||||
DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
|
||||
Cache.delete(galleryID)
|
||||
Cache.delete(this@DownloadService, galleryID)
|
||||
|
||||
startId?.let { stopSelf(it) }
|
||||
}
|
||||
|
||||
fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
|
||||
if (progress.containsKey(galleryID))
|
||||
cancel(galleryID)
|
||||
if (DownloadManager.getInstance(this@DownloadService).isDownloading(galleryID))
|
||||
return@launch
|
||||
|
||||
cleanCache(this@DownloadService)
|
||||
|
||||
val cache = Cache.getInstance(this@DownloadService, galleryID)
|
||||
|
||||
initNotification(galleryID)
|
||||
|
||||
val reader = cache.getReader()
|
||||
val galleryInfo = cache.getGalleryInfo()
|
||||
|
||||
// Gallery doesn't exist
|
||||
if (reader == null) {
|
||||
if (galleryInfo == null) {
|
||||
delete(galleryID)
|
||||
progress[galleryID] = mutableListOf()
|
||||
return@launch
|
||||
}
|
||||
|
||||
progress[galleryID] = MutableList(reader.galleryInfo.files.size) { 0F }
|
||||
histories.add(galleryID)
|
||||
|
||||
progress[galleryID] = MutableList(galleryInfo.files.size) { 0F }
|
||||
|
||||
cache.metadata.imageList?.let {
|
||||
it.forEachIndexed { index, image ->
|
||||
@@ -320,16 +329,14 @@ class DownloadService : Service() {
|
||||
}
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
if (DownloadManager.getInstance(this@DownloadService)
|
||||
.getDownloadFolder(galleryID) != null )
|
||||
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
|
||||
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
|
||||
|
||||
notificationManager.cancel(galleryID)
|
||||
startId?.let { stopSelf(it) }
|
||||
return@launch
|
||||
}
|
||||
|
||||
notification[galleryID]?.setContentTitle(reader.galleryInfo.title?.ellipsize(30))
|
||||
notification[galleryID]?.setContentTitle(galleryInfo.title?.ellipsize(30))
|
||||
notify(galleryID)
|
||||
|
||||
val queued = mutableSetOf<Int>()
|
||||
@@ -343,7 +350,7 @@ class DownloadService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
reader.requestBuilders.forEachIndexed { index, it ->
|
||||
galleryInfo.getRequestBuilders().forEachIndexed { index, it ->
|
||||
if (progress[galleryID]?.get(index)?.isInfinite() == false) {
|
||||
val request = it.tag(Tag(galleryID, index, startId)).build()
|
||||
client.newCall(request).enqueue(callback)
|
||||
|
||||
22
app/src/main/java/xyz/quaver/pupil/types/SendLogException.kt
Normal file
22
app/src/main/java/xyz/quaver/pupil/types/SendLogException.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2022 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.types
|
||||
|
||||
class SendLogException : Exception()
|
||||
class JavascriptException(message: String?) : Exception(message)
|
||||
@@ -18,17 +18,22 @@
|
||||
|
||||
package xyz.quaver.pupil.types
|
||||
|
||||
import kotlinx.android.parcel.IgnoredOnParcel
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.hitomi.Suggestion
|
||||
import xyz.quaver.pupil.hitomi.Suggestion
|
||||
import xyz.quaver.pupil.util.translations
|
||||
|
||||
@Parcelize
|
||||
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)
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val body = s
|
||||
override val body =
|
||||
if (translations[s] != null)
|
||||
"${translations[s]} ($s)"
|
||||
else
|
||||
s
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
|
||||
@@ -29,10 +29,8 @@ import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.andrognito.patternlockview.PatternLockView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_lock.*
|
||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
|
||||
import kotlinx.android.synthetic.main.fragment_pin_lock.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.LockActivityBinding
|
||||
import xyz.quaver.pupil.ui.fragment.PINLockFragment
|
||||
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
@@ -45,6 +43,8 @@ class LockActivity : AppCompatActivity() {
|
||||
private lateinit var lockManager: LockManager
|
||||
private var mode: String? = null
|
||||
|
||||
private lateinit var binding: LockActivityBinding
|
||||
|
||||
private val patternLockFragment = PatternLockFragment().apply {
|
||||
var lastPass = ""
|
||||
onPatternDrawn = {
|
||||
@@ -57,7 +57,7 @@ class LockActivity : AppCompatActivity() {
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} else
|
||||
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
}
|
||||
"add_lock" -> {
|
||||
if (lastPass.isEmpty()) {
|
||||
@@ -69,7 +69,7 @@ class LockActivity : AppCompatActivity() {
|
||||
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
|
||||
finish()
|
||||
} else {
|
||||
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
lastPass = ""
|
||||
|
||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
||||
@@ -92,15 +92,15 @@ class LockActivity : AppCompatActivity() {
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} 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 {
|
||||
override fun onAnimationEnd(animation: Animation?) {
|
||||
pin_lock_view.resetPinLockView()
|
||||
pin_lock_view.isEnabled = true
|
||||
binding.pinLockView.resetPinLockView()
|
||||
binding.pinLockView.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animation?) {
|
||||
pin_lock_view.isEnabled = false
|
||||
binding.pinLockView.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation?) {
|
||||
@@ -114,22 +114,22 @@ class LockActivity : AppCompatActivity() {
|
||||
if (lastPass.isEmpty()) {
|
||||
lastPass = it
|
||||
|
||||
pin_lock_view.resetPinLockView()
|
||||
binding.pinLockView.resetPinLockView()
|
||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
if (lastPass == it) {
|
||||
LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it))
|
||||
finish()
|
||||
} else {
|
||||
indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
||||
binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
||||
setAnimationListener(object: Animation.AnimationListener {
|
||||
override fun onAnimationEnd(animation: Animation?) {
|
||||
pin_lock_view.resetPinLockView()
|
||||
pin_lock_view.isEnabled = true
|
||||
binding.pinLockView.resetPinLockView()
|
||||
binding.pinLockView.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animation?) {
|
||||
pin_lock_view.isEnabled = false
|
||||
binding.pinLockView.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation?) {
|
||||
@@ -173,7 +173,8 @@ class LockActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_lock)
|
||||
binding = LockActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
lockManager = try {
|
||||
LockManager(this)
|
||||
@@ -210,7 +211,7 @@ class LockActivity : AppCompatActivity() {
|
||||
Preferences["lock_fingerprint"]
|
||||
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
||||
) {
|
||||
lock_fingerprint.apply {
|
||||
binding.fingerprintBtn.apply {
|
||||
isEnabled = true
|
||||
setOnClickListener {
|
||||
showBiometricPrompt()
|
||||
@@ -219,7 +220,7 @@ class LockActivity : AppCompatActivity() {
|
||||
showBiometricPrompt()
|
||||
}
|
||||
|
||||
lock_pattern.apply {
|
||||
binding.patternBtn.apply {
|
||||
isEnabled = lockManager.contains(Lock.Type.PATTERN)
|
||||
setOnClickListener {
|
||||
supportFragmentManager.beginTransaction().replace(
|
||||
@@ -227,7 +228,7 @@ class LockActivity : AppCompatActivity() {
|
||||
).commit()
|
||||
}
|
||||
}
|
||||
lock_pin.apply {
|
||||
binding.pinBtn.apply {
|
||||
isEnabled = lockManager.contains(Lock.Type.PIN)
|
||||
setOnClickListener {
|
||||
supportFragmentManager.beginTransaction().replace(
|
||||
@@ -235,7 +236,7 @@ class LockActivity : AppCompatActivity() {
|
||||
).commit()
|
||||
}
|
||||
}
|
||||
lock_password.isEnabled = false
|
||||
binding.passwordBtn.isEnabled = false
|
||||
|
||||
when (lockManager.locks!!.first().type) {
|
||||
Lock.Type.PIN -> {
|
||||
@@ -253,20 +254,20 @@ class LockActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
"add_lock" -> {
|
||||
lock_pattern.isEnabled = false
|
||||
lock_pin.isEnabled = false
|
||||
lock_fingerprint.isEnabled = false
|
||||
lock_password.isEnabled = false
|
||||
binding.patternBtn.isEnabled = false
|
||||
binding.pinBtn.isEnabled = false
|
||||
binding.fingerprintBtn.isEnabled = false
|
||||
binding.passwordBtn.isEnabled = false
|
||||
|
||||
when(intent.getStringExtra("type")!!) {
|
||||
"pattern" -> {
|
||||
lock_pattern.isEnabled = true
|
||||
binding.patternBtn.isEnabled = true
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content, patternLockFragment
|
||||
).commit()
|
||||
}
|
||||
"pin" -> {
|
||||
lock_pin.isEnabled = true
|
||||
binding.pinBtn.isEnabled = true
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content, pinLockFragment
|
||||
).commit()
|
||||
|
||||
@@ -21,45 +21,50 @@ package xyz.quaver.pupil.ui
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.text.util.Linkify
|
||||
import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.activity_main_content.*
|
||||
import kotlinx.coroutines.*
|
||||
import xyz.quaver.floatingsearchview.FloatingSearchView
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.floatingsearchview.util.view.MenuView
|
||||
import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
||||
import xyz.quaver.hitomi.doSearch
|
||||
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
||||
import xyz.quaver.hitomi.getSuggestionsForQuery
|
||||
import xyz.quaver.pupil.*
|
||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||
import xyz.quaver.pupil.databinding.MainActivityBinding
|
||||
import xyz.quaver.pupil.hitomi.doSearch
|
||||
import xyz.quaver.pupil.hitomi.getGalleryIDsFromNozomi
|
||||
import xyz.quaver.pupil.hitomi.getSuggestionsForQuery
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.types.*
|
||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
|
||||
import xyz.quaver.pupil.ui.dialog.GalleryDialog
|
||||
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.DownloadManager
|
||||
import xyz.quaver.pupil.util.restore
|
||||
import kotlin.math.abs
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -100,18 +105,20 @@ class MainActivity :
|
||||
private var loadingJob: Job? = null
|
||||
private var currentPage = 0
|
||||
|
||||
private lateinit var binding: MainActivityBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
binding = MainActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
intent.dataString?.let { url ->
|
||||
restore(url,
|
||||
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 = {
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -120,15 +127,33 @@ class MainActivity :
|
||||
if (Preferences["download_folder", ""].isEmpty())
|
||||
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()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
checkUpdate(this)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override fun onBackPressed() {
|
||||
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 {
|
||||
query = queryStack.last()
|
||||
|
||||
@@ -144,7 +169,7 @@ class MainActivity :
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.update = false
|
||||
(binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
@@ -185,34 +210,36 @@ class MainActivity :
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
var prevP1 = 0
|
||||
main_appbar_layout.addOnOffsetChangedListener(
|
||||
AppBarLayout.OnOffsetChangedListener { _, p1 ->
|
||||
main_searchview.translationY = p1.toFloat()
|
||||
main_recyclerview.scrollBy(0, prevP1 - p1)
|
||||
binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
// -height of the search view < translationY < 0
|
||||
binding.contents.searchview.translationY =
|
||||
min(
|
||||
max(
|
||||
binding.contents.searchview.translationY - dy,
|
||||
-binding.contents.searchview.binding.querySection.root.height.toFloat()
|
||||
), 0F)
|
||||
|
||||
with(main_fab) {
|
||||
if (prevP1 > p1)
|
||||
hideMenuButton(true)
|
||||
else if (prevP1 < p1)
|
||||
showMenuButton(true)
|
||||
}
|
||||
|
||||
prevP1 = p1
|
||||
if (dy > 0)
|
||||
binding.contents.fab.hideMenuButton(true)
|
||||
else if (dy < 0)
|
||||
binding.contents.fab.showMenuButton(true)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
Linkify.addLinks(binding.contents.noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) })
|
||||
|
||||
//NavigationView
|
||||
main_nav_view.setNavigationItemSelectedListener(this)
|
||||
binding.navView.setNavigationItemSelectedListener(this)
|
||||
|
||||
with(main_fab_cancel) {
|
||||
with(binding.contents.cancelFab) {
|
||||
setImageResource(R.drawable.cancel)
|
||||
setOnClickListener {
|
||||
DownloadService.cancel(this@MainActivity)
|
||||
}
|
||||
}
|
||||
|
||||
with(main_fab_jump) {
|
||||
with(binding.contents.jumpFab) {
|
||||
setImageResource(R.drawable.ic_jump)
|
||||
setOnClickListener {
|
||||
val perPage = Preferences["per_page", "25"].toInt()
|
||||
@@ -240,7 +267,7 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
|
||||
with(main_fab_random) {
|
||||
with(binding.contents.randomFab) {
|
||||
setImageResource(R.drawable.shuffle_variant)
|
||||
setOnClickListener {
|
||||
runBlocking {
|
||||
@@ -270,7 +297,7 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
|
||||
with(main_fab_id) {
|
||||
with(binding.contents.idFab) {
|
||||
setImageResource(R.drawable.numeric)
|
||||
setOnClickListener {
|
||||
val editText = EditText(context).apply {
|
||||
@@ -282,17 +309,65 @@ class MainActivity :
|
||||
setTitle(R.string.main_open_gallery_by_id)
|
||||
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton
|
||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryID", galleryID)
|
||||
}
|
||||
val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton
|
||||
|
||||
startActivity(intent)
|
||||
GalleryDialog(this@MainActivity, galleryID).apply {
|
||||
onChipClickedHandler.add {
|
||||
runOnUiThread {
|
||||
query = it.toQuery()
|
||||
currentPage = 0
|
||||
|
||||
cancelFetch()
|
||||
clearGalleries()
|
||||
fetchGalleries(query, sortMode)
|
||||
loadBlocks()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}.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()
|
||||
setupRecyclerView()
|
||||
fetchGalleries(query, sortMode)
|
||||
@@ -301,7 +376,7 @@ class MainActivity :
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun setupRecyclerView() {
|
||||
with(main_recyclerview) {
|
||||
with(binding.contents.recyclerview) {
|
||||
adapter = GalleryBlockAdapter(galleries).apply {
|
||||
onChipClickedHandler.add {
|
||||
runOnUiThread {
|
||||
@@ -342,14 +417,12 @@ class MainActivity :
|
||||
loadBlocks()
|
||||
}
|
||||
|
||||
completeFlag.put(galleryID, false)
|
||||
|
||||
closeAllItems()
|
||||
}
|
||||
}
|
||||
ItemClickSupport.addTo(this).apply {
|
||||
onItemClickListener = listener@{ _, position, v ->
|
||||
if (v !is CardView)
|
||||
if (v !is ProgressCard)
|
||||
return@listener
|
||||
|
||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
||||
@@ -360,10 +433,10 @@ class MainActivity :
|
||||
}
|
||||
|
||||
onItemLongClickListener = listener@{ _, position, v ->
|
||||
if (v !is CardView)
|
||||
if (v !is ProgressCard)
|
||||
return@listener false
|
||||
|
||||
val galleryID = galleries[position]
|
||||
val galleryID = galleries.getOrNull(position) ?: return@listener true
|
||||
|
||||
GalleryDialog(this@MainActivity, galleryID).apply {
|
||||
onChipClickedHandler.add {
|
||||
@@ -383,207 +456,6 @@ class MainActivity :
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -604,10 +476,10 @@ class MainActivity :
|
||||
|
||||
private var suggestionJob : Job? = null
|
||||
private fun setupSearchBar() {
|
||||
with(main_searchview as xyz.quaver.pupil.ui.view.FloatingSearchView) {
|
||||
with(binding.contents.searchview) {
|
||||
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
|
||||
override fun onMenuOpened() {
|
||||
(this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
|
||||
(this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
|
||||
}
|
||||
|
||||
override fun onMenuClosed() {
|
||||
@@ -691,7 +563,7 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
|
||||
attachNavigationDrawerToMenuButton(main_drawer_layout)
|
||||
attachNavigationDrawerToMenuButton(this@MainActivity.binding.drawer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,7 +574,7 @@ class MainActivity :
|
||||
val thin = !item.isChecked
|
||||
|
||||
item.isChecked = thin
|
||||
main_recyclerview.apply {
|
||||
binding.contents.recyclerview.apply {
|
||||
(adapter as GalleryBlockAdapter).apply {
|
||||
this.thin = thin
|
||||
|
||||
@@ -743,7 +615,7 @@ class MainActivity :
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||
runOnUiThread {
|
||||
main_drawer_layout.closeDrawers()
|
||||
binding.drawer.closeDrawers()
|
||||
|
||||
when(item.itemId) {
|
||||
R.id.main_drawer_home -> {
|
||||
@@ -812,19 +684,17 @@ class MainActivity :
|
||||
loadingJob?.cancel()
|
||||
}
|
||||
|
||||
private fun clearGalleries() {
|
||||
private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch {
|
||||
galleries.clear()
|
||||
|
||||
with(main_recyclerview.adapter as GalleryBlockAdapter?) {
|
||||
with(binding.contents.recyclerview.adapter as GalleryBlockAdapter?) {
|
||||
this ?: return@with
|
||||
|
||||
this.completeFlag.clear()
|
||||
this.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
main_appbar_layout.setExpanded(true)
|
||||
main_noresult.visibility = View.INVISIBLE
|
||||
main_progressbar.show()
|
||||
binding.contents.noresult.visibility = View.INVISIBLE
|
||||
binding.contents.progressbar.show()
|
||||
}
|
||||
|
||||
private fun fetchGalleries(query: String, sortMode: SortMode) {
|
||||
@@ -839,7 +709,7 @@ class MainActivity :
|
||||
}
|
||||
|
||||
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) {
|
||||
cancelFetch()
|
||||
clearGalleries()
|
||||
@@ -917,7 +787,7 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -931,18 +801,21 @@ class MainActivity :
|
||||
throw Exception("No result")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
if (e.message != "No result")
|
||||
if (e !is CancellationException)
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
main_noresult.visibility = View.VISIBLE
|
||||
main_progressbar.hide()
|
||||
binding.contents.noresult.visibility = View.VISIBLE
|
||||
binding.contents.progressbar.hide()
|
||||
}
|
||||
|
||||
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 ->
|
||||
for (chunk in chunks)
|
||||
chunk.map { galleryID ->
|
||||
@@ -954,10 +827,10 @@ class MainActivity :
|
||||
}.forEach {
|
||||
it.await()?.also {
|
||||
withContext(Dispatchers.Main) {
|
||||
main_progressbar.hide()
|
||||
binding.contents.progressbar.hide()
|
||||
|
||||
galleries.add(it)
|
||||
main_recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
|
||||
binding.contents.recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,9 @@ import android.view.animation.Animation
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.view.animation.TranslateAnimation
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -47,19 +45,15 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.mlkit.vision.face.Face
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.pupil.R
|
||||
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.histories
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.camera
|
||||
@@ -67,8 +61,6 @@ import xyz.quaver.pupil.util.closeCamera
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.startCamera
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class ReaderActivity : BaseActivity() {
|
||||
|
||||
@@ -80,7 +72,7 @@ class ReaderActivity : BaseActivity() {
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
(reader_recyclerview.adapter as ReaderAdapter).isFullScreen = value
|
||||
(binding.recyclerview.adapter as ReaderAdapter).isFullScreen = value
|
||||
}
|
||||
|
||||
private lateinit var cache: Cache
|
||||
@@ -88,6 +80,8 @@ class ReaderActivity : BaseActivity() {
|
||||
private val conn = object: ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
downloader = (service as DownloadService.Binder).service.also {
|
||||
it.priority = 0
|
||||
|
||||
if (!it.progress.containsKey(galleryID))
|
||||
DownloadService.download(this@ReaderActivity, galleryID, true)
|
||||
}
|
||||
@@ -121,9 +115,12 @@ class ReaderActivity : BaseActivity() {
|
||||
private var eyeType: Eye? = null
|
||||
private var eyeTime: Long = 0L
|
||||
|
||||
private lateinit var binding: ReaderActivityBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_reader)
|
||||
binding = ReaderActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
title = getString(R.string.reader_loading)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||
@@ -164,12 +161,10 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.reader, menu)
|
||||
|
||||
with(menu?.findItem(R.id.reader_menu_favorite)) {
|
||||
this ?: return@with
|
||||
|
||||
with(menu.findItem(R.id.reader_menu_favorite)) {
|
||||
if (favorites.contains(galleryID))
|
||||
(icon as Animatable).start()
|
||||
}
|
||||
@@ -181,17 +176,19 @@ class ReaderActivity : BaseActivity() {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when(item.itemId) {
|
||||
R.id.reader_menu_page_indicator -> {
|
||||
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
|
||||
with(view.dialog_number_picker) {
|
||||
// TODO: Switch to DialogFragment
|
||||
val binding = NumberpickerDialogBinding.inflate(layoutInflater, binding.root, false)
|
||||
|
||||
with(binding.numberPicker) {
|
||||
minValue = 1
|
||||
maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0
|
||||
maxValue = cache.metadata.galleryInfo?.files?.size ?: 0
|
||||
value = currentPage
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this).apply {
|
||||
setView(view)
|
||||
setView(binding.root)
|
||||
}.create()
|
||||
view.dialog_ok.setOnClickListener {
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(view.dialog_number_picker.value-1, 0)
|
||||
binding.okButton.setOnClickListener {
|
||||
(this@ReaderActivity.binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(binding.numberPicker.value-1, 0)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
@@ -230,14 +227,16 @@ class ReaderActivity : BaseActivity() {
|
||||
if (downloader != null)
|
||||
unbindService(conn)
|
||||
|
||||
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
|
||||
DownloadService.cancel(this, galleryID)
|
||||
downloader?.priority = galleryID
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
update = false
|
||||
|
||||
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
|
||||
DownloadService.cancel(this, galleryID)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
@@ -259,12 +258,12 @@ class ReaderActivity : BaseActivity() {
|
||||
//currentPage is 1-based
|
||||
return when(keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage-2, 0)
|
||||
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-2, 0)
|
||||
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
|
||||
(binding.recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
|
||||
|
||||
true
|
||||
}
|
||||
@@ -286,44 +285,35 @@ class ReaderActivity : BaseActivity() {
|
||||
if (downloader.progress[galleryID]?.isEmpty() == true) { //Gallery not found
|
||||
update = false
|
||||
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()
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
||||
histories.add(galleryID)
|
||||
|
||||
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||
reader_download_progressbar.progress =
|
||||
binding.downloadProgressbar.max = binding.recyclerview.adapter?.itemCount ?: 0
|
||||
binding.downloadProgressbar.progress =
|
||||
downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
|
||||
|
||||
if (title == getString(R.string.reader_loading)) {
|
||||
val reader = cache.metadata.reader
|
||||
val galleryInfo = cache.metadata.galleryInfo
|
||||
|
||||
if (reader != null) {
|
||||
with(reader_recyclerview.adapter as ReaderAdapter) {
|
||||
this.reader = reader
|
||||
if (galleryInfo != null) {
|
||||
with(binding.recyclerview.adapter as ReaderAdapter) {
|
||||
this.galleryInfo = galleryInfo
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
title = reader.galleryInfo.title
|
||||
title = galleryInfo.title
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title =
|
||||
"$currentPage/${reader.galleryInfo.files.size}"
|
||||
"$currentPage/${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
|
||||
}
|
||||
)
|
||||
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.hitomi)
|
||||
}
|
||||
}
|
||||
|
||||
if (downloader.isCompleted(galleryID)) { //Download finished
|
||||
reader_download_progressbar.visibility = View.GONE
|
||||
binding.downloadProgressbar.visibility = View.GONE
|
||||
|
||||
animateDownloadFAB(false)
|
||||
}
|
||||
@@ -332,7 +322,7 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
with(reader_recyclerview) {
|
||||
with(binding.recyclerview) {
|
||||
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
|
||||
onItemClickListener = {
|
||||
if (isScroll) {
|
||||
@@ -342,7 +332,7 @@ class ReaderActivity : BaseActivity() {
|
||||
scrollMode(false)
|
||||
fullscreen(true)
|
||||
} 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,9 +342,9 @@ class ReaderActivity : BaseActivity() {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
if (dy < 0)
|
||||
this@ReaderActivity.reader_fab.showMenuButton(true)
|
||||
binding.fab.showMenuButton(true)
|
||||
else if (dy > 0)
|
||||
this@ReaderActivity.reader_fab.hideMenuButton(true)
|
||||
binding.fab.hideMenuButton(true)
|
||||
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
|
||||
@@ -362,12 +352,11 @@ class ReaderActivity : BaseActivity() {
|
||||
return
|
||||
currentPage = layoutManager.findFirstVisibleItemPosition()+1
|
||||
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
|
||||
|
||||
setOnClickListener {
|
||||
@@ -384,14 +373,14 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
with(reader_fab_retry) {
|
||||
with(binding.retryFab) {
|
||||
setImageResource(R.drawable.refresh)
|
||||
setOnClickListener {
|
||||
DownloadService.download(context, galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
with(reader_fab_auto) {
|
||||
with(binding.autoFab) {
|
||||
setImageResource(R.drawable.eye_white)
|
||||
setOnClickListener {
|
||||
when {
|
||||
@@ -411,13 +400,13 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
with(reader_fab_fullscreen) {
|
||||
with(binding.fullscreenFab) {
|
||||
setImageResource(R.drawable.ic_fullscreen)
|
||||
setOnClickListener {
|
||||
isFullscreen = true
|
||||
fullscreen(isFullscreen)
|
||||
|
||||
this@ReaderActivity.reader_fab.close(true)
|
||||
binding.fab.close(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,8 +416,8 @@ class ReaderActivity : BaseActivity() {
|
||||
if (isFullscreen) {
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
supportActionBar?.hide()
|
||||
this@ReaderActivity.reader_fab.visibility = View.INVISIBLE
|
||||
this@ReaderActivity.scroller.let {
|
||||
binding.fab.visibility = View.INVISIBLE
|
||||
binding.scroller.let {
|
||||
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
||||
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
||||
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb_horizontal)
|
||||
@@ -437,8 +426,8 @@ class ReaderActivity : BaseActivity() {
|
||||
} else {
|
||||
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
||||
supportActionBar?.show()
|
||||
this@ReaderActivity.reader_fab.visibility = View.VISIBLE
|
||||
this@ReaderActivity.scroller.let {
|
||||
binding.fab.visibility = View.VISIBLE
|
||||
binding.scroller.let {
|
||||
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
||||
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
||||
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb)
|
||||
@@ -449,23 +438,27 @@ class ReaderActivity : BaseActivity() {
|
||||
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) {
|
||||
if (isScroll) {
|
||||
snapHelper.attachToRecyclerView(null)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerview.layoutManager = LinearLayoutManager(this)
|
||||
} else {
|
||||
snapHelper.attachToRecyclerView(reader_recyclerview)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, Preferences["rtl", false])
|
||||
snapHelper.attachToRecyclerView(binding.recyclerview)
|
||||
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) {
|
||||
with(reader_fab_download) {
|
||||
with(binding.downloadFab) {
|
||||
if (animate) {
|
||||
val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
|
||||
|
||||
@@ -494,7 +487,7 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
val cameraCallback: (List<Face>) -> Unit = callback@{ faces ->
|
||||
eye_card.dot.let {
|
||||
binding.eyeCard.dot.let {
|
||||
it.visibility = View.VISIBLE
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(50)
|
||||
@@ -504,9 +497,9 @@ class ReaderActivity : BaseActivity() {
|
||||
|
||||
if (faces.size != 1)
|
||||
ContextCompat.getDrawable(this, R.drawable.eye_off).let {
|
||||
with(eye_card) {
|
||||
left_eye.setImageDrawable(it)
|
||||
right_eye.setImageDrawable(it)
|
||||
with(binding.eyeCard) {
|
||||
leftEye.setImageDrawable(it)
|
||||
rightEye.setImageDrawable(it)
|
||||
}
|
||||
|
||||
return@callback
|
||||
@@ -517,16 +510,16 @@ class ReaderActivity : BaseActivity() {
|
||||
faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true
|
||||
)
|
||||
|
||||
with(eye_card) {
|
||||
left_eye.setImageDrawable(
|
||||
with(binding.eyeCard) {
|
||||
leftEye.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
context,
|
||||
leftEye.context,
|
||||
if (left) R.drawable.eye else R.drawable.eye_closed
|
||||
)
|
||||
)
|
||||
right_eye.setImageDrawable(
|
||||
rightEye.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
context,
|
||||
rightEye.context,
|
||||
if (right) R.drawable.eye else R.drawable.eye_closed
|
||||
)
|
||||
)
|
||||
@@ -553,7 +546,7 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
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!!) {
|
||||
Eye.RIGHT -> {
|
||||
if (it.reverseLayout) currentPage - 2 else currentPage
|
||||
@@ -569,11 +562,11 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun toggleCamera() {
|
||||
val eyes = this@ReaderActivity.eye_card
|
||||
val eyes = binding.eyeCard.root
|
||||
when (camera) {
|
||||
null -> {
|
||||
reader_fab_auto.labelText = getString(R.string.reader_fab_auto_cancel)
|
||||
reader_fab_auto.setImageResource(R.drawable.eye_off_white)
|
||||
binding.autoFab.labelText = getString(R.string.reader_fab_auto_cancel)
|
||||
binding.autoFab.setImageResource(R.drawable.eye_off_white)
|
||||
eyes.apply {
|
||||
visibility = View.VISIBLE
|
||||
TranslateAnimation(0F, 0F, -100F, 0F).apply {
|
||||
@@ -586,8 +579,8 @@ class ReaderActivity : BaseActivity() {
|
||||
cameraEnabled = true
|
||||
}
|
||||
else -> {
|
||||
reader_fab_auto.labelText = getString(R.string.reader_fab_auto)
|
||||
reader_fab_auto.setImageResource(R.drawable.eye_white)
|
||||
binding.autoFab.labelText = getString(R.string.reader_fab_auto)
|
||||
binding.autoFab.setImageResource(R.drawable.eye_white)
|
||||
eyes.apply {
|
||||
TranslateAnimation(0F, 0F, 0F, -100F).apply {
|
||||
duration = 500
|
||||
|
||||
@@ -18,30 +18,30 @@
|
||||
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_default_query.*
|
||||
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding
|
||||
import xyz.quaver.pupil.types.Tags
|
||||
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 {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
private val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
|
||||
private val languages: Map<String, String> by lazy {
|
||||
requireContext().resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
private val reverseLanguages: Map<String, String> by lazy {
|
||||
languages.entries.associate { (k, v) -> v to k }
|
||||
}
|
||||
|
||||
private val excludeBL = "-male:yaoi"
|
||||
private val excludeGuro = listOf("-female:guro", "-male:guro")
|
||||
@@ -49,46 +49,15 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
|
||||
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTitle(R.string.default_query_dialog_title)
|
||||
setView(build())
|
||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
|
||||
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
|
||||
private var _binding: DefaultQueryDialogBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
with(default_query_dialog_language_selector) {
|
||||
if (selectedItemPosition != 0)
|
||||
newTags.add("language:${reverseLanguages[selectedItem]}")
|
||||
}
|
||||
|
||||
if (default_query_dialog_BL_checkbox.isChecked)
|
||||
newTags.add(excludeBL)
|
||||
|
||||
if (default_query_dialog_guro_checkbox.isChecked)
|
||||
excludeGuro.forEach { tag ->
|
||||
newTags.add(tag)
|
||||
}
|
||||
|
||||
if (default_query_dialog_loli_checkbox.isChecked)
|
||||
excludeLoli.forEach { tag ->
|
||||
newTags.add(tag)
|
||||
}
|
||||
|
||||
onPositiveButtonClickListener?.invoke(newTags)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun build() : View {
|
||||
private fun initView() {
|
||||
val tags = Tags.parse(
|
||||
Preferences["default_query"]
|
||||
)
|
||||
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
||||
|
||||
with(view.default_query_dialog_language_selector) {
|
||||
with(binding.languageSelector) {
|
||||
adapter =
|
||||
ArrayAdapter(
|
||||
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)
|
||||
if (tags.contains(excludeBL))
|
||||
tags.remove(excludeBL)
|
||||
}
|
||||
|
||||
with(view.default_query_dialog_guro_checkbox) {
|
||||
with(binding.guroCheckbox) {
|
||||
isChecked = excludeGuro.all { tags.contains(it) }
|
||||
if (excludeGuro.all { tags.contains(it) })
|
||||
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) }
|
||||
if (excludeLoli.all { tags.contains(it) })
|
||||
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)
|
||||
addTextChangedListener(object : TextWatcher {
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.DownloadFolderNameDialogBinding
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.formatDownloadFolder
|
||||
@@ -37,38 +37,49 @@ import xyz.quaver.pupil.util.formatMap
|
||||
|
||||
class DownloadFolderNameDialogFragment : DialogFragment() {
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun build(): View {
|
||||
val galleryID = Cache.instances.let { if (it.size == 0) 1199708 else it.keys.elementAt((0 until it.size).random()) }
|
||||
val galleryBlock = runBlocking {
|
||||
Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
|
||||
}
|
||||
private var _binding: DownloadFolderNameDialogBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
return layoutInflater.inflate(R.layout.dialog_download_folder_name, null).apply {
|
||||
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
|
||||
edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
|
||||
edittext.addTextChangedListener {
|
||||
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
|
||||
}
|
||||
ok_button.setOnClickListener {
|
||||
val newValue = edittext.text.toString()
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
_binding = DownloadFolderNameDialogBinding.inflate(layoutInflater)
|
||||
|
||||
if ((newValue as? String)?.contains("/") != false) {
|
||||
Snackbar.make(this, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
initView()
|
||||
|
||||
Preferences["download_folder_name"] = edittext.text.toString()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
return Dialog(requireContext()).apply {
|
||||
setContentView(binding.root)
|
||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||
Dialog(requireContext()).apply {
|
||||
setContentView(build())
|
||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
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,52 +18,49 @@
|
||||
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
|
||||
import kotlinx.android.synthetic.main.item_download_folder.view.*
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.toFile
|
||||
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.byteToString
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.migrate
|
||||
import java.io.File
|
||||
|
||||
class DownloadLocationDialogFragment : DialogFragment() {
|
||||
private val entries = mutableMapOf<File?, View>()
|
||||
|
||||
private var _binding: DownloadLocationDialogBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val entries = mutableMapOf<File?, DownloadLocationItemBinding>()
|
||||
|
||||
private val requestDownloadFolderLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
val activity = activity ?: return@registerForActivityResult
|
||||
val context = context ?: return@registerForActivityResult
|
||||
val dialog = dialog ?: return@registerForActivityResult
|
||||
|
||||
it.data?.data?.also { uri ->
|
||||
val takeFlags: Int =
|
||||
activity.intent.flags and
|
||||
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
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]?.location_available?.text = uri.toFile(context)?.canonicalPath
|
||||
entries[null]?.locationAvailable?.text = uri.toFile(context)?.canonicalPath
|
||||
Preferences["download_folder"] = uri.toString()
|
||||
} else {
|
||||
Snackbar.make(
|
||||
@@ -75,9 +72,18 @@ class DownloadLocationDialogFragment : DialogFragment() {
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,51 +104,46 @@ class DownloadLocationDialogFragment : DialogFragment() {
|
||||
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
|
||||
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
||||
}
|
||||
else {
|
||||
entries[null]?.location_available?.text = directory
|
||||
entries[null]?.locationAvailable?.text = directory
|
||||
Preferences["download_folder"] = File(directory).toURI().toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun build() : View? {
|
||||
val context = context ?: return null
|
||||
|
||||
val view = layoutInflater.inflate(R.layout.dialog_download_folder, null) as LinearLayout
|
||||
|
||||
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
||||
private fun initView() {
|
||||
val externalFilesDirs = ContextCompat.getExternalFilesDirs(requireContext(), null)
|
||||
|
||||
externalFilesDirs.forEachIndexed { index, dir ->
|
||||
dir ?: return@forEachIndexed
|
||||
|
||||
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
|
||||
location_type.text = context.getString(when (index) {
|
||||
DownloadLocationItemBinding.inflate(layoutInflater, binding.root, true).apply {
|
||||
locationType.text = requireContext().getString(when (index) {
|
||||
0 -> R.string.settings_download_folder_internal
|
||||
else -> R.string.settings_download_folder_removable
|
||||
})
|
||||
location_available.text = context.getString(
|
||||
locationAvailable.text = requireContext().getString(
|
||||
R.string.settings_download_folder_available,
|
||||
byteToString(dir.freeSpace)
|
||||
)
|
||||
setOnClickListener {
|
||||
entries.values.forEach {
|
||||
it.button.isChecked = false
|
||||
root.setOnClickListener {
|
||||
entries.values.forEach { entry ->
|
||||
entry.button.isChecked = false
|
||||
}
|
||||
button.performClick()
|
||||
Preferences["download_folder"] = dir.toUri().toString()
|
||||
}
|
||||
entries[dir] = this
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
|
||||
location_type.text = context.getString(R.string.settings_download_folder_custom)
|
||||
setOnClickListener {
|
||||
entries.values.forEach {
|
||||
it.button.isChecked = false
|
||||
DownloadLocationItemBinding.inflate(layoutInflater, binding.root, true).apply {
|
||||
locationType.text = requireContext().getString(R.string.settings_download_folder_custom)
|
||||
root.setOnClickListener {
|
||||
entries.values.forEach { entry ->
|
||||
entry.button.isChecked = false
|
||||
}
|
||||
button.performClick()
|
||||
|
||||
@@ -166,31 +167,33 @@ class DownloadLocationDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
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 }
|
||||
entries[key]!!.button.isChecked = true
|
||||
if (key == null) entries[key]!!.location_available.text = downloadFolder
|
||||
|
||||
return view
|
||||
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
_binding = DownloadLocationDialogBinding.inflate(layoutInflater)
|
||||
|
||||
builder
|
||||
.setTitle(R.string.settings_download_folder)
|
||||
.setView(build())
|
||||
.setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
|
||||
initView()
|
||||
|
||||
return AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.settings_download_folder)
|
||||
setView(binding.root)
|
||||
setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
|
||||
if (Preferences["download_folder", ""].isEmpty())
|
||||
Preferences["download_folder"] = context?.getExternalFilesDir(null)?.toUri()?.toString() ?: ""
|
||||
|
||||
DownloadManager.getInstance(requireContext()).migrate()
|
||||
Preferences["download_folder"] = context.getExternalFilesDir(null)?.toUri()?.toString() ?: ""
|
||||
}
|
||||
|
||||
isCancelable = false
|
||||
isCancelable = false
|
||||
}.create()
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout.LayoutParams
|
||||
@@ -32,36 +31,36 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import xyz.quaver.hitomi.Gallery
|
||||
import xyz.quaver.hitomi.getGallery
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.hitomi.Gallery
|
||||
import xyz.quaver.pupil.hitomi.getGallery
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
||||
import xyz.quaver.pupil.databinding.*
|
||||
import xyz.quaver.pupil.favoriteTags
|
||||
import xyz.quaver.pupil.histories
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.ui.view.TagChip
|
||||
import xyz.quaver.pupil.util.ItemClickSupport
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(context) {
|
||||
|
||||
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
||||
|
||||
private lateinit var binding: GalleryDialogBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.dialog_gallery)
|
||||
binding = GalleryDialogBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
window?.attributes.apply {
|
||||
this ?: return@apply
|
||||
@@ -70,13 +69,12 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
|
||||
height = LayoutParams.MATCH_PARENT
|
||||
}
|
||||
|
||||
with(gallery_fab) {
|
||||
with(binding.fab) {
|
||||
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right))
|
||||
setOnClickListener {
|
||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryID", galleryID)
|
||||
})
|
||||
histories.add(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,12 +82,12 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
|
||||
try {
|
||||
val gallery = getGallery(galleryID)
|
||||
|
||||
gallery_cover.post {
|
||||
gallery_progressbar.visibility = View.GONE
|
||||
gallery_title.text = gallery.title
|
||||
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
|
||||
launch (Dispatchers.Main) {
|
||||
binding.progressbar.visibility = View.GONE
|
||||
binding.title.text = gallery.title
|
||||
binding.artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
|
||||
|
||||
with(gallery_type) {
|
||||
with(binding.type) {
|
||||
text = gallery.type.wordCapitalize()
|
||||
setOnClickListener {
|
||||
gallery.type.let {
|
||||
@@ -106,23 +104,26 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
|
||||
}
|
||||
}
|
||||
|
||||
gallery_cover.showImage(Uri.parse(gallery.cover))
|
||||
binding.cover.showImage(Uri.parse(gallery.cover))
|
||||
|
||||
addDetails(gallery)
|
||||
addThumbnails(gallery)
|
||||
addRelated(gallery)
|
||||
}
|
||||
} 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) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
||||
gallery_details.setText(R.string.gallery_details)
|
||||
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
||||
type.setText(R.string.gallery_details)
|
||||
|
||||
listOf(
|
||||
R.string.gallery_artists,
|
||||
@@ -159,13 +160,13 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
|
||||
}
|
||||
)
|
||||
).filter {
|
||||
(_, content) -> content.isNotEmpty()
|
||||
(_, content) -> content.isNotEmpty()
|
||||
}.forEach { (title, content) ->
|
||||
inflater.inflate(R.layout.item_gallery_details, gallery_details_contents, false).apply {
|
||||
gallery_details_type.setText(title)
|
||||
GalleryDialogTagsBinding.inflate(layoutInflater, contents, true).apply {
|
||||
type.setText(title)
|
||||
|
||||
content.forEach { tag ->
|
||||
gallery_details_tags.addView(
|
||||
tags.addView(
|
||||
TagChip(context, tag).apply {
|
||||
setOnClickListener {
|
||||
onChipClickedHandler.forEach { handler ->
|
||||
@@ -175,42 +176,34 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
|
||||
}
|
||||
)
|
||||
}
|
||||
}.let {
|
||||
gallery_details_contents.addView(it)
|
||||
}
|
||||
}
|
||||
}.let {
|
||||
gallery_contents.addView(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addThumbnails(gallery: Gallery) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
||||
gallery_details.setText(R.string.gallery_thumbnails)
|
||||
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
||||
type.setText(R.string.gallery_thumbnails)
|
||||
|
||||
val pager = ViewPager2(context).apply {
|
||||
adapter = ThumbnailPageAdapter(gallery.thumbnails)
|
||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
gallery_details_contents.addView(
|
||||
contents.addView(
|
||||
pager,
|
||||
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
)
|
||||
|
||||
LayoutInflater.from(context).inflate(R.layout.dialog_gallery_dotindicator, gallery_details_contents)
|
||||
|
||||
gallery_dotindicator.setViewPager2(pager)
|
||||
}.let {
|
||||
gallery_contents.addView(it)
|
||||
// TODO: Change to direct allocation
|
||||
GalleryDialogDotindicatorBinding.inflate(layoutInflater, contents, true).apply {
|
||||
dotindicator.setViewPager2(pager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addRelated(gallery: Gallery) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val galleries = ArrayList<Int>()
|
||||
val galleries = mutableListOf<Int>()
|
||||
|
||||
val adapter = GalleryBlockAdapter(galleries).apply {
|
||||
onChipClickedHandler.add { tag ->
|
||||
@@ -220,10 +213,10 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
|
||||
}
|
||||
}
|
||||
|
||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
||||
gallery_details.setText(R.string.gallery_related)
|
||||
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
||||
type.setText(R.string.gallery_related)
|
||||
|
||||
RecyclerView(context).apply {
|
||||
contents.addView(RecyclerView(context).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
this.adapter = adapter
|
||||
|
||||
@@ -232,7 +225,6 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
|
||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryID", galleries[position])
|
||||
})
|
||||
histories.add(galleries[position])
|
||||
}
|
||||
onItemLongClickListener = { _, position, _ ->
|
||||
GalleryDialog(context, galleries[position]).apply {
|
||||
@@ -244,22 +236,18 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
|
||||
true
|
||||
}
|
||||
}
|
||||
}.let {
|
||||
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
||||
}
|
||||
}.let {
|
||||
gallery_contents.addView(it)
|
||||
}
|
||||
})
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
gallery.related.forEach { galleryID ->
|
||||
Cache.getInstance(context, galleryID).getGalleryBlock()?.let {
|
||||
galleries.add(galleryID)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
gallery.related.forEach { galleryID ->
|
||||
Cache.getInstance(context, galleryID).getGalleryBlock()?.let {
|
||||
galleries.add(galleryID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main) {
|
||||
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,58 +18,60 @@
|
||||
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_proxy.view.*
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.clientBuilder
|
||||
import xyz.quaver.pupil.clientHolder
|
||||
import xyz.quaver.pupil.databinding.ProxyDialogBinding
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.ProxyInfo
|
||||
import xyz.quaver.pupil.util.getProxyInfo
|
||||
import xyz.quaver.pupil.util.proxyInfo
|
||||
import java.net.Proxy
|
||||
|
||||
class ProxyDialog(context: Context) : AlertDialog(context) {
|
||||
class ProxyDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setView(build())
|
||||
private var _binding: ProxyDialogBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
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 build() : View {
|
||||
private fun initView() {
|
||||
val proxyInfo = getProxyInfo()
|
||||
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null)
|
||||
|
||||
val enabler = { enable: Boolean ->
|
||||
view?.proxy_addr?.isEnabled = enable
|
||||
view?.proxy_port?.isEnabled = enable
|
||||
view?.proxy_username?.isEnabled = enable
|
||||
view?.proxy_password?.isEnabled = enable
|
||||
binding.addr.isEnabled = enable
|
||||
binding.port.isEnabled = enable
|
||||
binding.username.isEnabled = enable
|
||||
binding.password.isEnabled = enable
|
||||
|
||||
if (!enable) {
|
||||
view?.proxy_addr?.text = null
|
||||
view?.proxy_port?.text = null
|
||||
view?.proxy_username?.text = null
|
||||
view?.proxy_password?.text = null
|
||||
binding.addr.text = null
|
||||
binding.port.text = null
|
||||
binding.username.text = null
|
||||
binding.password.text = null
|
||||
}
|
||||
}
|
||||
|
||||
with(view.proxy_type_selector) {
|
||||
with(binding.typeSelector) {
|
||||
adapter = ArrayAdapter(
|
||||
context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
@@ -87,29 +89,29 @@ class ProxyDialog(context: Context) : AlertDialog(context) {
|
||||
}
|
||||
}
|
||||
|
||||
view.proxy_addr.setText(proxyInfo.host)
|
||||
view.proxy_port.setText(proxyInfo.port?.toString())
|
||||
view.proxy_username.setText(proxyInfo.username)
|
||||
view.proxy_password.setText(proxyInfo.password)
|
||||
binding.addr.setText(proxyInfo.host)
|
||||
binding.port.setText(proxyInfo.port?.toString())
|
||||
binding.username.setText(proxyInfo.username)
|
||||
binding.password.setText(proxyInfo.password)
|
||||
|
||||
enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT)
|
||||
|
||||
view.proxy_cancel.setOnClickListener {
|
||||
binding.cancelButton.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
view.proxy_ok.setOnClickListener {
|
||||
val type = Proxy.Type.values()[view.proxy_type_selector.selectedItemPosition]
|
||||
val addr = view.proxy_addr.text?.toString()
|
||||
val port = view.proxy_port.text?.toString()?.toIntOrNull()
|
||||
val username = view.proxy_username.text?.toString()
|
||||
val password = view.proxy_password.text?.toString()
|
||||
binding.okButton.setOnClickListener {
|
||||
val type = Proxy.Type.values()[binding.typeSelector.selectedItemPosition]
|
||||
val addr = binding.addr.text?.toString()
|
||||
val port = binding.port.text?.toString()?.toIntOrNull()
|
||||
val username = binding.username.text?.toString()
|
||||
val password = binding.password.text?.toString()
|
||||
|
||||
if (type != Proxy.Type.DIRECT) {
|
||||
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)
|
||||
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)
|
||||
return@setOnClickListener
|
||||
@@ -126,8 +128,6 @@ class ProxyDialog(context: Context) : AlertDialog(context) {
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,21 +18,37 @@
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.SAFileX
|
||||
import xyz.quaver.io.util.deleteRecursively
|
||||
import xyz.quaver.io.util.getChild
|
||||
import xyz.quaver.io.util.readText
|
||||
import xyz.quaver.io.util.writeText
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.histories
|
||||
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.Metadata
|
||||
import java.io.File
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
|
||||
|
||||
@@ -61,6 +77,8 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
||||
if (dir.exists())
|
||||
dir.deleteRecursively()
|
||||
|
||||
Cache.instances.clear()
|
||||
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
var size = 0L
|
||||
@@ -77,6 +95,46 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"recover_downloads" -> {
|
||||
val density = context.resources.displayMetrics.density
|
||||
this.icon = object: CircularProgressDrawable(context) {
|
||||
override fun getIntrinsicHeight() = (24*density).roundToInt()
|
||||
override fun getIntrinsicWidth() = (24*density).roundToInt()
|
||||
}.apply {
|
||||
setStyle(CircularProgressDrawable.DEFAULT)
|
||||
colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN)
|
||||
start()
|
||||
}
|
||||
|
||||
val downloadManager = DownloadManager.getInstance(context)
|
||||
|
||||
val downloadFolderMap = downloadManager.downloadFolderMap
|
||||
|
||||
downloadFolderMap.clear()
|
||||
|
||||
downloadManager.downloadFolder.listFiles { file -> file.isDirectory }?.forEach { folder ->
|
||||
val metadataFile = FileX(context, folder, ".metadata")
|
||||
|
||||
if (!metadataFile.exists()) return@forEach
|
||||
|
||||
val metadata = metadataFile.readText()?.let {
|
||||
Json.decodeFromString<Metadata>(it)
|
||||
} ?: return@forEach
|
||||
|
||||
val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach
|
||||
|
||||
downloadFolderMap[galleryID] = folder.name
|
||||
}
|
||||
|
||||
downloadManager.downloadFolderMap.putAll(downloadFolderMap)
|
||||
val downloads = FileX(context, downloadManager.downloadFolder, ".download")
|
||||
|
||||
if (!downloads.exists()) downloads.createNewFile()
|
||||
downloads.writeText(Json.encodeToString(downloadFolderMap))
|
||||
|
||||
this.icon = null
|
||||
Toast.makeText(context, android.R.string.ok, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
"delete_downloads" -> {
|
||||
val dir = DownloadManager.getInstance(context).downloadFolder
|
||||
|
||||
@@ -91,7 +149,12 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
||||
}
|
||||
|
||||
if (dir.exists())
|
||||
dir.listFiles()?.forEach { (it as? FileX)?.deleteRecursively() }
|
||||
dir.listFiles()?.forEach {
|
||||
when (it) {
|
||||
is FileX -> it.deleteRecursively()
|
||||
else -> it.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
job = launch {
|
||||
var size = 0L
|
||||
@@ -183,6 +246,12 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
||||
|
||||
onPreferenceClickListener = this@ManageStorageFragment
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("recover_downloads")) {
|
||||
this ?: return@with
|
||||
|
||||
onPreferenceClickListener = this@ManageStorageFragment
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
@@ -24,30 +24,34 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.andrognito.pinlockview.PinLockListener
|
||||
import kotlinx.android.synthetic.main.fragment_pin_lock.view.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.PinLockFragmentBinding
|
||||
|
||||
class PINLockFragment : Fragment(), PinLockListener {
|
||||
class PINLockFragment : Fragment() {
|
||||
|
||||
private var _binding: PinLockFragmentBinding? = null
|
||||
val binding get() = _binding!!
|
||||
|
||||
var onPINEntered: ((String) -> Unit)? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_pin_lock, container, false).apply {
|
||||
pin_lock_view.attachIndicatorDots(indicator_dots)
|
||||
pin_lock_view.setPinLockListener(this@PINLockFragment)
|
||||
}
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = PinLockFragmentBinding.inflate(inflater, container, false)
|
||||
|
||||
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?) {
|
||||
onPINEntered?.invoke(pin!!)
|
||||
}
|
||||
|
||||
override fun onEmpty() {
|
||||
|
||||
}
|
||||
|
||||
override fun onPinChange(pinLength: Int, intermediatePin: String?) {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,38 +26,36 @@ import androidx.fragment.app.Fragment
|
||||
import com.andrognito.patternlockview.PatternLockView
|
||||
import com.andrognito.patternlockview.listener.PatternLockViewListener
|
||||
import com.andrognito.patternlockview.utils.PatternLockUtils
|
||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
|
||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.view.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.PatternLockFragmentBinding
|
||||
|
||||
class PatternLockFragment : Fragment(), PatternLockViewListener {
|
||||
class PatternLockFragment : Fragment() {
|
||||
|
||||
private var _binding: PatternLockFragmentBinding? = null
|
||||
val binding get() = _binding!!
|
||||
|
||||
var onPatternDrawn: ((String) -> Unit)? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_pattern_lock, container, false).apply {
|
||||
lock_pattern_view.addPatternLockListener(this@PatternLockFragment)
|
||||
}
|
||||
): View {
|
||||
_binding = PatternLockFragmentBinding.inflate(inflater, container, false)
|
||||
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 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() {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,19 +24,27 @@ import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import androidx.preference.*
|
||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Dispatcher
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.getChild
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.clientBuilder
|
||||
import xyz.quaver.pupil.clientHolder
|
||||
import xyz.quaver.pupil.types.SendLogException
|
||||
import xyz.quaver.pupil.ui.LockActivity
|
||||
import xyz.quaver.pupil.ui.SettingsActivity
|
||||
import xyz.quaver.pupil.ui.dialog.*
|
||||
import xyz.quaver.pupil.util.*
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class SettingsFragment :
|
||||
PreferenceFragmentCompat(),
|
||||
@@ -84,12 +92,12 @@ class SettingsFragment :
|
||||
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
|
||||
}
|
||||
"default_query" -> {
|
||||
DefaultQueryDialog(requireContext()).apply {
|
||||
DefaultQueryDialog().apply {
|
||||
onPositiveButtonClickListener = { newTags ->
|
||||
Preferences["default_query"] = newTags.toString()
|
||||
summary = newTags.toString()
|
||||
}
|
||||
}.show()
|
||||
}.show(parentFragmentManager, "Default Query Dialog")
|
||||
}
|
||||
"app_lock" -> {
|
||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||
@@ -97,19 +105,15 @@ class SettingsFragment :
|
||||
}
|
||||
lockLauncher.launch(intent)
|
||||
}
|
||||
"mirrors" -> {
|
||||
MirrorDialog(requireContext())
|
||||
.show()
|
||||
}
|
||||
"proxy" -> {
|
||||
ProxyDialog(requireContext())
|
||||
.show()
|
||||
ProxyDialogFragment().show(parentFragmentManager, "Proxy Dialog")
|
||||
}
|
||||
"user_id" -> {
|
||||
FirebaseCrashlytics.getInstance().recordException(SendLogException())
|
||||
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||
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
|
||||
}
|
||||
@@ -123,6 +127,9 @@ class SettingsFragment :
|
||||
this ?: return false
|
||||
|
||||
when (key) {
|
||||
"tag_translation" -> {
|
||||
updateTranslations()
|
||||
}
|
||||
"nomedia" -> {
|
||||
val create = (newValue as? Boolean) ?: return false
|
||||
|
||||
@@ -164,6 +171,18 @@ class SettingsFragment :
|
||||
"download_folder_name" -> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,14 +262,32 @@ class SettingsFragment :
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"mirrors" -> {
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"proxy" -> {
|
||||
summary = getProxyInfo().type.name
|
||||
|
||||
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" -> {
|
||||
onPreferenceChangeListener = this@SettingsFragment
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@ import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.os.Parcelable
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
@@ -38,8 +38,6 @@ import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import xyz.quaver.floatingsearchview.FloatingSearchView
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.floatingsearchview.util.MenuPopupHelper
|
||||
import xyz.quaver.floatingsearchview.util.view.MenuView
|
||||
import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.favoriteTags
|
||||
@@ -57,12 +55,12 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
|
||||
var onFavoriteHistorySwitchClickListener: (() -> Unit)? = null
|
||||
|
||||
init {
|
||||
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
|
||||
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or searchInputView.imeOptions
|
||||
|
||||
searchInputView.addTextChangedListener(this)
|
||||
onSearchListener = this
|
||||
onBindSuggestionCallback = { a, b, c, d, e ->
|
||||
onBindSuggestion(a, b, c, d, e)
|
||||
onBindSuggestionCallback = { binding, item, itemPosition ->
|
||||
onBindSuggestion(binding.root, binding.leftIcon, binding.body, item, itemPosition)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +111,7 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
|
||||
) {
|
||||
when(item) {
|
||||
is TagSuggestion -> {
|
||||
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
|
||||
val tag = "${item.n}:${item.s}"
|
||||
|
||||
leftIcon?.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
@@ -164,9 +162,7 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
|
||||
}
|
||||
}
|
||||
|
||||
if (item.t == -1) {
|
||||
textView?.text = item.s
|
||||
} else {
|
||||
if (item.t > 0) {
|
||||
(suggestionView as? LinearLayout)?.let {
|
||||
val count = it.findViewById<TextView>(R.id.count)
|
||||
if (count == null)
|
||||
@@ -217,4 +213,4 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import com.google.android.material.chip.Chip
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.favoriteTags
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.translations
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
@@ -90,7 +91,7 @@ class TagChip(context: Context, _tag: Tag) : Chip(context) {
|
||||
|
||||
text = when (tag.area) {
|
||||
"language" -> languages[tag.tag]
|
||||
else -> tag.tag.wordCapitalize()
|
||||
else -> (translations[tag.tag] ?: tag.tag).wordCapitalize()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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.json.Json
|
||||
import kotlinx.serialization.serializer
|
||||
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 {
|
||||
@@ -38,27 +41,26 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
|
||||
load()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun load() {
|
||||
synchronized(this) {
|
||||
set.clear()
|
||||
kotlin.runCatching {
|
||||
Json.decodeFromString(serializer, file.readText())
|
||||
}.onSuccess {
|
||||
set.addAll(it)
|
||||
}
|
||||
set.clear()
|
||||
kotlin.runCatching {
|
||||
Json.decodeFromString(serializer, file.readText())
|
||||
}.onSuccess {
|
||||
set.addAll(it)
|
||||
}.onFailure {
|
||||
FirebaseCrashlytics.getInstance().recordException(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
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 {
|
||||
load()
|
||||
|
||||
set.remove(element)
|
||||
|
||||
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 {
|
||||
load()
|
||||
|
||||
set.removeAll(elements)
|
||||
|
||||
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 {
|
||||
load()
|
||||
|
||||
return set.remove(element).also {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun clear() {
|
||||
set.clear()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,29 +24,47 @@ import android.net.Uri
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Request
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.*
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.hitomi.GalleryBlock
|
||||
import xyz.quaver.pupil.hitomi.GalleryInfo
|
||||
import xyz.quaver.pupil.hitomi.getGalleryBlock
|
||||
import xyz.quaver.pupil.hitomi.getGalleryInfo
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
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 Metadata(
|
||||
var galleryBlock: GalleryBlock? = null,
|
||||
var reader: Reader? = null,
|
||||
var galleryInfo: GalleryInfo? = null,
|
||||
var imageList: MutableList<String?>? = null
|
||||
) {
|
||||
fun copy(): Metadata = Metadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
||||
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) {
|
||||
@@ -60,8 +78,8 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun delete(galleryID: Int) {
|
||||
instances[galleryID]?.cacheFolder?.deleteRecursively()
|
||||
fun delete(context: Context, galleryID: Int) {
|
||||
File(context.cacheDir, "imageCache/$galleryID").deleteRecursively()
|
||||
instances.remove(galleryID)
|
||||
}
|
||||
}
|
||||
@@ -71,8 +89,8 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
|
||||
}
|
||||
|
||||
var metadata = kotlin.runCatching {
|
||||
findFile(".metadata")?.readText()?.let {
|
||||
Json.decodeFromString<Metadata>(it)
|
||||
findFile(".metadata")?.readText()?.let { metadata ->
|
||||
Json.decodeFromString<Metadata>(metadata)
|
||||
}
|
||||
}.getOrNull() ?: Metadata()
|
||||
|
||||
@@ -107,146 +125,129 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
|
||||
}
|
||||
|
||||
suspend fun getGalleryBlock(): GalleryBlock? {
|
||||
val sources = listOf(
|
||||
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||
)
|
||||
|
||||
return metadata.galleryBlock
|
||||
?: withContext(Dispatchers.IO) {
|
||||
var galleryBlock: GalleryBlock? = null
|
||||
|
||||
for (source in sources) {
|
||||
galleryBlock = try {
|
||||
source.invoke()
|
||||
} catch (e: Exception) { null }
|
||||
|
||||
if (galleryBlock != null)
|
||||
break
|
||||
}
|
||||
|
||||
galleryBlock?.also {
|
||||
setMetadata { metadata -> metadata.galleryBlock = it }
|
||||
}
|
||||
try {
|
||||
getGalleryBlock(galleryID).also {
|
||||
setMetadata { metadata -> metadata.galleryBlock = it }
|
||||
}
|
||||
} catch (e: Exception) { return@withContext null }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun getThumbnail(): Uri? =
|
||||
suspend fun getThumbnail(): Uri =
|
||||
findFile(".thumbnail")?.uri
|
||||
?: getGalleryBlock()?.thumbnails?.firstOrNull()?.let { withContext(Dispatchers.IO) {
|
||||
kotlin.runCatching {
|
||||
val request = Request.Builder()
|
||||
.url(it)
|
||||
.header("Referer", "https://hitomi.la/")
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() }
|
||||
}.getOrNull()?.let { thumbnail -> kotlin.runCatching {
|
||||
cacheFolder.getChild(".thumbnail").also { it.writeBytes(thumbnail) }
|
||||
cacheFolder.getChild(".thumbnail").also {
|
||||
if (!it.exists())
|
||||
it.createNewFile()
|
||||
|
||||
it.writeBytes(thumbnail)
|
||||
}
|
||||
}.getOrNull()?.uri }
|
||||
} }
|
||||
} } ?: Uri.EMPTY
|
||||
|
||||
suspend fun getReader(): Reader? {
|
||||
val mirrors = Preferences.get<String>("mirrors").let { if (it.isEmpty()) emptyList() else it.split('>') }
|
||||
suspend fun getGalleryInfo(): GalleryInfo? {
|
||||
|
||||
val sources = mapOf(
|
||||
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
|
||||
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
|
||||
).let {
|
||||
if (mirrors.isNotEmpty())
|
||||
it.toSortedMap{ o1, o2 -> mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name) }
|
||||
else
|
||||
it
|
||||
}
|
||||
|
||||
return metadata.reader
|
||||
return metadata.galleryInfo
|
||||
?: withContext(Dispatchers.IO) {
|
||||
var reader: Reader? = null
|
||||
try {
|
||||
getGalleryInfo(galleryID).also {
|
||||
setMetadata { metadata ->
|
||||
metadata.galleryInfo = it
|
||||
|
||||
for (source in sources) {
|
||||
reader = try {
|
||||
source.value.invoke()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
if (reader != null)
|
||||
break
|
||||
}
|
||||
|
||||
reader?.also {
|
||||
setMetadata { metadata ->
|
||||
metadata.reader = it
|
||||
|
||||
if (metadata.imageList == null)
|
||||
metadata.imageList = MutableList(reader.galleryInfo.files.size) { null }
|
||||
if (metadata.imageList == null)
|
||||
metadata.imageList = MutableList(it.files.size) { null }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getImage(index: Int): FileX? =
|
||||
metadata.imageList?.get(index)?.let { findFile(it) }
|
||||
metadata.imageList?.getOrNull(index)?.let { findFile(it) }
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
fun putImage(index: Int, fileName: String, data: ByteArray) {
|
||||
fun putImage(index: Int, fileName: String, data: InputStream) {
|
||||
val file = cacheFolder.getChild(fileName)
|
||||
|
||||
file.createNewFile()
|
||||
file.writeBytes(data)
|
||||
if (!file.exists())
|
||||
file.createNewFile()
|
||||
file.outputStream()?.use {
|
||||
data.copyTo(it)
|
||||
}
|
||||
setMetadata { metadata -> metadata.imageList!![index] = fileName }
|
||||
}
|
||||
|
||||
private val lock = ConcurrentHashMap<Int, Mutex>()
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
|
||||
val downloadFolder = downloadFolder ?: return@launch
|
||||
|
||||
val cacheMetadata = cacheFolder.getChild(".metadata")
|
||||
val downloadMetadata = downloadFolder.getChild(".metadata")
|
||||
|
||||
if (downloadMetadata.exists() || !cacheMetadata.exists())
|
||||
if (lock[galleryID]?.isLocked == true)
|
||||
return@launch
|
||||
|
||||
if (cacheMetadata.exists()) {
|
||||
kotlin.runCatching {
|
||||
downloadMetadata.createNewFile()
|
||||
downloadMetadata.writeText(Json.encodeToString(metadata))
|
||||
(lock[galleryID] ?: Mutex().also { lock[galleryID] = it }).withLock {
|
||||
val cacheMetadata = cacheFolder.getChild(".metadata")
|
||||
val downloadMetadata = downloadFolder.getChild(".metadata")
|
||||
|
||||
cacheMetadata.delete()
|
||||
if (!cacheMetadata.exists())
|
||||
return@launch
|
||||
|
||||
if (cacheMetadata.exists()) {
|
||||
kotlin.runCatching {
|
||||
if (!downloadMetadata.exists())
|
||||
downloadMetadata.createNewFile()
|
||||
|
||||
downloadMetadata.writeText(Json.encodeToString(metadata))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val cacheThumbnail = cacheFolder.getChild(".thumbnail")
|
||||
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
|
||||
val cacheThumbnail = cacheFolder.getChild(".thumbnail")
|
||||
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
|
||||
|
||||
if (cacheThumbnail.exists() && !downloadThumbnail.exists()) {
|
||||
kotlin.runCatching {
|
||||
if (!downloadThumbnail.exists())
|
||||
downloadThumbnail.createNewFile()
|
||||
if (cacheThumbnail.exists()) {
|
||||
kotlin.runCatching {
|
||||
if (!downloadThumbnail.exists())
|
||||
downloadThumbnail.createNewFile()
|
||||
|
||||
downloadThumbnail.outputStream()?.use { target -> cacheThumbnail.inputStream()?.use { source ->
|
||||
source.copyTo(target)
|
||||
} }
|
||||
cacheThumbnail.delete()
|
||||
downloadThumbnail.outputStream()?.use { target -> target.channel.truncate(0L); cacheThumbnail.inputStream()?.use { source ->
|
||||
source.copyTo(target)
|
||||
} }
|
||||
cacheThumbnail.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metadata.imageList?.forEach { imageName ->
|
||||
imageName ?: return@forEach
|
||||
val target = downloadFolder.getChild(imageName)
|
||||
val source = cacheFolder.getChild(imageName)
|
||||
metadata.imageList?.forEach { imageName ->
|
||||
imageName ?: return@forEach
|
||||
val target = downloadFolder.getChild(imageName)
|
||||
val source = cacheFolder.getChild(imageName)
|
||||
|
||||
if (!source.exists() || target.exists())
|
||||
return@forEach
|
||||
if (!source.exists())
|
||||
return@forEach
|
||||
|
||||
kotlin.runCatching {
|
||||
if (!target.exists())
|
||||
target.createNewFile()
|
||||
kotlin.runCatching {
|
||||
if (!target.exists())
|
||||
target.createNewFile()
|
||||
|
||||
target.outputStream()?.use { target -> source.inputStream()?.use { source ->
|
||||
source.copyTo(target)
|
||||
} }
|
||||
target.outputStream()?.use { target -> target.channel.truncate(0L); source.inputStream()?.use { source ->
|
||||
source.copyTo(target)
|
||||
} }
|
||||
}
|
||||
}
|
||||
|
||||
cacheFolder.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,9 @@ package xyz.quaver.pupil.util.downloader
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -47,14 +48,12 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
|
||||
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
|
||||
|
||||
val downloadFolder: FileX
|
||||
get() = {
|
||||
kotlin.runCatching {
|
||||
FileX(this, Preferences.get<String>("download_folder"))
|
||||
}.getOrElse {
|
||||
Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
|
||||
defaultDownloadFolder
|
||||
}
|
||||
}.invoke()
|
||||
get() = kotlin.runCatching {
|
||||
FileX(this, Preferences.get<String>("download_folder"))
|
||||
}.getOrElse {
|
||||
Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
|
||||
defaultDownloadFolder
|
||||
}
|
||||
|
||||
private var prevDownloadFolder: FileX? = null
|
||||
private var downloadFolderMapInstance: MutableMap<Int, String>? = null
|
||||
@@ -63,30 +62,28 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
|
||||
get() {
|
||||
if (prevDownloadFolder != downloadFolder) {
|
||||
prevDownloadFolder = downloadFolder
|
||||
downloadFolderMapInstance = {
|
||||
downloadFolderMapInstance = run {
|
||||
val file = downloadFolder.getChild(".download")
|
||||
|
||||
val data = if (file.exists())
|
||||
kotlin.runCatching {
|
||||
file.readText()?.let { Json.decodeFromString<MutableMap<Int, String>>(it) }
|
||||
file.readText()?.let{ Json.decodeFromString<MutableMap<Int, String>>(it) }
|
||||
}.onFailure { file.delete() }.getOrNull()
|
||||
else
|
||||
null
|
||||
|
||||
data ?: {
|
||||
data ?: run {
|
||||
file.createNewFile()
|
||||
mutableMapOf<Int, String>()
|
||||
}.invoke()
|
||||
}.invoke()
|
||||
mutableMapOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return downloadFolderMapInstance!!
|
||||
return downloadFolderMapInstance ?: mutableMapOf()
|
||||
}
|
||||
|
||||
|
||||
@Synchronized
|
||||
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)
|
||||
&& client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
|
||||
@@ -96,23 +93,19 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
|
||||
fun getDownloadFolder(galleryID: Int): FileX? =
|
||||
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
|
||||
|
||||
@Synchronized
|
||||
fun addDownloadFolder(galleryID: Int) {
|
||||
val name = runBlocking {
|
||||
Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
|
||||
}?.formatDownloadFolder() ?: return
|
||||
fun addDownloadFolder(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
||||
val name = Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
|
||||
?.formatDownloadFolder() ?: return@launch
|
||||
|
||||
val folder = downloadFolder.getChild(name)
|
||||
|
||||
if (folder.exists())
|
||||
return
|
||||
|
||||
folder.mkdir()
|
||||
|
||||
downloadFolderMap[galleryID] = folder.name
|
||||
downloadFolderMap[galleryID] = name
|
||||
|
||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
|
||||
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
|
||||
|
||||
if (folder.exists()) return@launch
|
||||
folder.mkdir()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
||||
@@ -19,35 +19,49 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.storage.StorageManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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.FileOutputStream
|
||||
import java.lang.reflect.Array
|
||||
import java.net.URL
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use downloader.Cache instead")
|
||||
fun getCachedGallery(context: Context, galleryID: Int) =
|
||||
File(getDownloadDirectory(context), galleryID.toString()).let {
|
||||
if (it.exists())
|
||||
it
|
||||
else
|
||||
File(context.cacheDir, "imageCache/$galleryID")
|
||||
val mutex = Mutex()
|
||||
fun cleanCache(context: Context) = CoroutineScope(Dispatchers.IO).launch {
|
||||
if (mutex.isLocked) return@launch
|
||||
|
||||
mutex.withLock {
|
||||
val cacheFolder = File(context.cacheDir, "imageCache")
|
||||
val downloadManager = DownloadManager.getInstance(context)
|
||||
|
||||
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,13 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.serialization.json.*
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.getReferer
|
||||
import xyz.quaver.hitomi.imageUrlFromImage
|
||||
import xyz.quaver.hiyobi.createImgList
|
||||
import xyz.quaver.pupil.hitomi.GalleryBlock
|
||||
import xyz.quaver.pupil.hitomi.GalleryInfo
|
||||
import xyz.quaver.pupil.hitomi.imageUrlFromImage
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@@ -40,7 +35,7 @@ fun String.wordCapitalize() : String {
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
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(" ")
|
||||
}
|
||||
@@ -82,7 +77,8 @@ fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
|
||||
val formatMap = mapOf<String, GalleryBlock.() -> (String)>(
|
||||
"-id-" to { id.toString() },
|
||||
"-title-" to { title },
|
||||
"-artist-" to { artists.joinToString() }
|
||||
"-artist-" to { artists.joinToString() },
|
||||
"-group-" to { groups.joinToString() }
|
||||
// TODO
|
||||
)
|
||||
/**
|
||||
@@ -93,39 +89,49 @@ fun GalleryBlock.formatDownloadFolder(): String =
|
||||
formatMap.entries.fold(it) { str, (k, v) ->
|
||||
str.replace(k, v.invoke(this), true)
|
||||
}
|
||||
}.replace(Regex("""[*\\|"?><:/]"""), "")
|
||||
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
|
||||
|
||||
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
|
||||
format.let {
|
||||
formatMap.entries.fold(it) { str, (k, v) ->
|
||||
str.replace(k, v.invoke(this), true)
|
||||
}
|
||||
}.replace(Regex("""[*\\|"?><:/]"""), "")
|
||||
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
|
||||
|
||||
val Reader.requestBuilders: List<Request.Builder>
|
||||
get() {
|
||||
val galleryID = this.galleryInfo.id ?: 0
|
||||
val lowQuality = Preferences["low_quality", true]
|
||||
suspend fun GalleryInfo.getRequestBuilders(): List<Request.Builder> {
|
||||
val galleryID = this.id.toIntOrNull() ?: 0
|
||||
val lowQuality = Preferences["low_quality", true]
|
||||
|
||||
return when(code) {
|
||||
Code.HITOMI -> {
|
||||
this.galleryInfo.files.map {
|
||||
Request.Builder()
|
||||
.url(imageUrlFromImage(galleryID, it, !lowQuality))
|
||||
.header("Referer", getReferer(galleryID))
|
||||
return this.files.map {
|
||||
Request.Builder()
|
||||
.url(
|
||||
runCatching {
|
||||
imageUrlFromImage(galleryID, it, !lowQuality)
|
||||
}
|
||||
}
|
||||
Code.HIYOBI -> {
|
||||
createImgList(galleryID, this, lowQuality).map {
|
||||
Request.Builder()
|
||||
.url(it.path)
|
||||
.onFailure {
|
||||
FirebaseCrashlytics.getInstance().recordException(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
.getOrDefault("https://a/")
|
||||
)
|
||||
.header("Referer", "https://hitomi.la/")
|
||||
}
|
||||
}
|
||||
|
||||
fun String.ellipsize(n: Int): String =
|
||||
if (this.length > n)
|
||||
this.slice(0 until n) + "…"
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.DownloadManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import android.webkit.URLUtil
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import ru.noties.markwon.Markwon
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.getGalleryBlock
|
||||
import xyz.quaver.hitomi.getReader
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.getChild
|
||||
import xyz.quaver.io.util.readText
|
||||
import xyz.quaver.io.util.writeBytes
|
||||
import xyz.quaver.io.util.writeText
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.Metadata
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
@@ -181,10 +159,10 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
||||
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)
|
||||
preferences.edit()
|
||||
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
||||
.putLong("ignore_update_until", System.currentTimeMillis() + 86400000)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
@@ -220,116 +198,4 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((
|
||||
}.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"/>
|
||||
</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>
|
||||
@@ -1,145 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Pupil, Hitomi.la viewer for Android
|
||||
~ Copyright (C) 2020 tom5079
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.MainActivity">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/main_appbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android: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="@android: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="@android: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>
|
||||
|
||||
<xyz.quaver.pupil.ui.view.FloatingSearchView
|
||||
android:id="@+id/main_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"
|
||||
tools:ignore="NewApi" />
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -28,7 +28,7 @@
|
||||
tools:ignore="Autofill"
|
||||
android:inputType="text"
|
||||
android:hint="@string/settings_default_query"
|
||||
android:id="@+id/default_query_dialog_edittext"
|
||||
android:id="@+id/edittext"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
@@ -36,10 +36,10 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/default_query_dialog_language_layout"
|
||||
android:id="@+id/language_layout"
|
||||
android:layout_width="0dp"
|
||||
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_constraintEnd_toEndOf="parent">
|
||||
|
||||
@@ -49,14 +49,14 @@
|
||||
android:text="@string/default_query_dialog_language"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/default_query_dialog_language_selector"
|
||||
android:id="@+id/language_selector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/default_query_dialog_BL_layout"
|
||||
android:id="@+id/BL_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
@@ -64,7 +64,7 @@
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="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_constraintEnd_toEndOf="parent">
|
||||
|
||||
@@ -75,14 +75,14 @@
|
||||
android:text="@string/default_query_dialog_filter_BL"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/default_query_dialog_BL_checkbox"
|
||||
android:id="@+id/BL_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/default_query_dialog_guro_layout"
|
||||
android:id="@+id/guro_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
@@ -90,7 +90,7 @@
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="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_constraintEnd_toEndOf="parent">
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
android:text="@string/default_query_dialog_filter_guro"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/default_query_dialog_guro_checkbox"
|
||||
android:id="@+id/guro_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="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_constraintEnd_toEndOf="parent">
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
android:text="@string/default_query_dialog_filter_loli"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/default_query_dialog_loli_checkbox"
|
||||
android:id="@+id/loli_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
@@ -19,14 +19,13 @@
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/gallery_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/gallery_toolbar"
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
@@ -41,34 +40,34 @@
|
||||
android:padding="8dp">
|
||||
|
||||
<com.github.piasy.biv.view.BigImageView
|
||||
android:id="@+id/gallery_cover"
|
||||
android:id="@+id/cover"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/gallery_title"
|
||||
app:layout_constraintRight_toLeftOf="@id/title"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gallery_title"
|
||||
android:id="@+id/title"
|
||||
style="@style/TextAppearance.AppCompat.Headline"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
|
||||
app:layout_constraintLeft_toRightOf="@id/cover"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"/>
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.AppCompat.Medium"
|
||||
android:id="@+id/gallery_artist"
|
||||
android:id="@+id/artist"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/gallery_title"
|
||||
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
app:layout_constraintLeft_toRightOf="@id/cover"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"/>
|
||||
@@ -76,15 +75,15 @@
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/gallery_artist"
|
||||
app:layout_constraintBottom_toTopOf="@id/gallery_type"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/artist"
|
||||
app:layout_constraintBottom_toTopOf="@id/type"/>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/gallery_type"
|
||||
android:id="@+id/type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
|
||||
app:layout_constraintLeft_toRightOf="@id/cover"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"/>
|
||||
|
||||
@@ -100,7 +99,7 @@
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/gallery_contents"
|
||||
android:id="@+id/contents"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"/>
|
||||
@@ -112,7 +111,7 @@
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/gallery_progressbar"
|
||||
android:id="@+id/progressbar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
@@ -123,11 +122,11 @@
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/gallery_fab"
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_anchor="@id/gallery_toolbar"
|
||||
app:layout_anchor="@id/toolbar"
|
||||
app:layout_anchorGravity="bottom|end"/>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -25,14 +25,14 @@
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gallery_details"
|
||||
android:id="@+id/type"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/colorAccent"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/gallery_details_contents"
|
||||
android:id="@+id/contents"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"/>
|
||||
@@ -25,7 +25,7 @@
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<com.tbuonomo.viewpagerdotsindicator.DotsIndicator
|
||||
android:id="@+id/gallery_dotindicator"
|
||||
android:id="@+id/dotindicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
@@ -26,13 +26,13 @@
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:id="@+id/gallery_details_type"
|
||||
android:id="@+id/type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="8dp"/>
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/gallery_details_tags"
|
||||
android:id="@+id/tags"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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>
|
||||
|
||||
<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"
|
||||
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_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_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>
|
||||
@@ -41,7 +41,7 @@
|
||||
app:layout_constraintBottom_toTopOf="@id/lock_button_layout">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/lock_fingerprint"
|
||||
android:id="@+id/fingerprint_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/fingerprint"
|
||||
@@ -64,7 +64,7 @@
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/lock_pattern"
|
||||
android:id="@+id/pattern_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:tint="@null"
|
||||
@@ -73,7 +73,7 @@
|
||||
app:fabSize="mini"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/lock_pin"
|
||||
android:id="@+id/pin_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:tint="@null"
|
||||
@@ -84,7 +84,7 @@
|
||||
app:fabSize="mini"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/lock_password"
|
||||
android:id="@+id/password_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:tint="@null"
|
||||
@@ -21,17 +21,18 @@
|
||||
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/main_drawer_layout"
|
||||
android:id="@+id/drawer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
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_height="match_parent"/>
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/main_nav_view"
|
||||
android:id="@+id/nav_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
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
|
||||
style="?android:textAppearanceLarge"
|
||||
android:id="@+id/dialog_title"
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/reader_go_to_page"
|
||||
@@ -33,20 +33,20 @@
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<NumberPicker
|
||||
android:id="@+id/dialog_number_picker"
|
||||
android:id="@+id/number_picker"
|
||||
android:layout_width="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_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/dialog_ok"
|
||||
android:id="@+id/ok_button"
|
||||
style="?borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@android:string/ok"
|
||||
app:layout_constraintTop_toBottomOf="@id/dialog_number_picker"
|
||||
app:layout_constraintTop_toBottomOf="@id/number_picker"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
tools:context=".ui.fragment.PatternLockFragment">
|
||||
|
||||
<com.andrognito.patternlockview.PatternLockView
|
||||
android:id="@+id/lock_pattern_view"
|
||||
android:id="@+id/pattern_lock_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/proxy_title"
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="16dp"
|
||||
@@ -35,46 +35,46 @@
|
||||
app:layout_constraintLeft_toLeftOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/proxy_type_text"
|
||||
android:id="@+id/type_text"
|
||||
android:layout_width="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_constraintLeft_toLeftOf="parent"
|
||||
android:text="@string/proxy_dialog_type"
|
||||
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/proxy_type_selector"
|
||||
android:id="@+id/type_selector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/proxy_type_text"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/type_text"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/proxy_server_text"
|
||||
android:id="@+id/server_text"
|
||||
android:layout_width="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_constraintLeft_toLeftOf="parent"
|
||||
android:text="@string/proxy_dialog_server"
|
||||
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/proxy_address_layout"
|
||||
android:id="@+id/address_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/proxy_server_text">
|
||||
app:layout_constraintTop_toBottomOf="@id/server_text">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/proxy_addr"
|
||||
android:id="@+id/addr"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="2"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/proxy_dialog_addr_hint"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/proxy_port"
|
||||
android:id="@+id/port"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -83,39 +83,39 @@
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/proxy_username"
|
||||
android:id="@+id/username"
|
||||
android:layout_width="match_parent"
|
||||
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:enabled="false"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/proxy_password"
|
||||
android:id="@+id/password"
|
||||
android:layout_width="match_parent"
|
||||
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:enabled="false"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/proxy_cancel"
|
||||
android:id="@+id/cancel_button"
|
||||
style="?borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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_constraintEnd_toStartOf="@id/proxy_ok"
|
||||
app:layout_constraintRight_toLeftOf="@id/proxy_ok"/>
|
||||
app:layout_constraintEnd_toStartOf="@id/ok_button"
|
||||
app:layout_constraintRight_toLeftOf="@id/ok_button"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/proxy_ok"
|
||||
android:id="@+id/ok_button"
|
||||
style="?borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@android:string/ok"
|
||||
app:layout_constraintTop_toBottomOf="@id/proxy_password"
|
||||
app:layout_constraintTop_toBottomOf="@id/password"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"/>
|
||||
@@ -18,7 +18,6 @@
|
||||
-->
|
||||
|
||||
<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:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
@@ -40,7 +39,7 @@
|
||||
app:popupDrawable="@android:color/transparent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/reader_recyclerview"
|
||||
android:id="@+id/recyclerview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
||||
@@ -56,13 +55,13 @@
|
||||
android:layout_margin="8dp"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/reader_download_progressbar"
|
||||
android:id="@+id/download_progressbar"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"/>
|
||||
|
||||
<com.github.clans.fab.FloatingActionMenu
|
||||
android:id="@+id/reader_fab"
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
@@ -70,7 +69,7 @@
|
||||
app:menu_colorNormal="@color/colorAccent">
|
||||
|
||||
<com.github.clans.fab.FloatingActionButton
|
||||
android:id="@+id/reader_fab_download"
|
||||
android:id="@+id/download_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_download"
|
||||
@@ -78,7 +77,7 @@
|
||||
app:fab_size="mini"/>
|
||||
|
||||
<com.github.clans.fab.FloatingActionButton
|
||||
android:id="@+id/reader_fab_retry"
|
||||
android:id="@+id/retry_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/refresh"
|
||||
@@ -86,7 +85,7 @@
|
||||
app:fab_size="mini"/>
|
||||
|
||||
<com.github.clans.fab.FloatingActionButton
|
||||
android:id="@+id/reader_fab_auto"
|
||||
android:id="@+id/auto_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/eye_white"
|
||||
@@ -94,7 +93,7 @@
|
||||
app:fab_size="mini"/>
|
||||
|
||||
<com.github.clans.fab.FloatingActionButton
|
||||
android:id="@+id/reader_fab_fullscreen"
|
||||
android:id="@+id/fullscreen_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_fullscreen"
|
||||
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>
|
||||
@@ -48,7 +48,7 @@
|
||||
<string name="main_jump_title">ページ移動</string>
|
||||
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</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_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか?</string>
|
||||
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</string>
|
||||
@@ -73,7 +73,7 @@
|
||||
<string name="main_menu_sort">ソート</string>
|
||||
<string name="main_menu_sort_newest">投稿日時順</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="settings_dark_mode_title">ダークモード</string>
|
||||
<string name="settings_dark_mode_summary">夜にシコりたい方々へ</string>
|
||||
@@ -128,7 +128,7 @@
|
||||
<string name="settings_lock_fingerprint_prompt_subtitle">こうかはばつぐんだ!</string>
|
||||
<string name="default_query_dialog_filter_loli">登場人物を全て18歳以上にする</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_auto">まばたき検知スクロール</string>
|
||||
<string name="search_all">全てのギャラリーを対象に検索</string>
|
||||
@@ -150,4 +150,12 @@
|
||||
<string name="camera_denied">カメラ権限が拒否されているため、まばたき検知使用できません</string>
|
||||
<string name="no_camera">この機器には前面カメラが装着されていません</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>
|
||||
<string name="settings_recover_downloads">ダウンロードデータベースを再構築</string>
|
||||
</resources>
|
||||
@@ -12,10 +12,10 @@
|
||||
<string name="settings_galleries_per_page">한 번에 로드할 갤러리 수</string>
|
||||
<string name="settings_search_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="warning">경고</string>
|
||||
<string name="main_no_result">결과 없음</string>
|
||||
<string name="main_no_result">결과 없음\n해결법</string>
|
||||
<string name="settings_miscellaneous_title">기타</string>
|
||||
<string name="settings_clear_history">기록 삭제</string>
|
||||
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
||||
@@ -47,7 +47,7 @@
|
||||
<string name="main_jump_title">페이지 이동</string>
|
||||
<string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</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_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
|
||||
<string name="main_drawer_favorite">즐겨찾기</string>
|
||||
@@ -71,7 +71,7 @@
|
||||
<string name="main_menu_sort">정렬</string>
|
||||
<string name="main_menu_sort_popular">인기순</string>
|
||||
<string name="main_menu_sort_newest">시간순</string>
|
||||
<string name="ignore_update">무시</string>
|
||||
<string name="ignore">무시</string>
|
||||
<string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string>
|
||||
<string name="settings_dark_mode_title">다크 모드</string>
|
||||
<string name="settings_dark_mode_summary">딥 다크한 모오드</string>
|
||||
@@ -128,7 +128,7 @@
|
||||
<string name="settings_lock_fingerprint_prompt_subtitle">힘세고 강한 지문 인식</string>
|
||||
<string name="default_query_dialog_filter_loli">판사님 저는 페도가 아닙니다</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_auto">눈 깜빡임 감지 스크롤</string>
|
||||
<string name="search_all">모든 갤러리 검색</string>
|
||||
@@ -150,4 +150,12 @@
|
||||
<string name="camera_denied">카메라 권한이 거부되었기 때문에 눈 깜빡임 감지가 불가능합니다</string>
|
||||
<string name="no_camera">이 장치에는 전면 카메라가 없습니다</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>
|
||||
<string name="settings_recover_downloads">다운로드 데이터베이스 복구</string>
|
||||
</resources>
|
||||
@@ -2,6 +2,7 @@
|
||||
<resources>
|
||||
|
||||
<string-array name="settings_galleries_per_page">
|
||||
<item>5</item>
|
||||
<item>10</item>
|
||||
<item>25</item>
|
||||
<item>50</item>
|
||||
@@ -47,15 +48,50 @@
|
||||
<item>japanese|日本語</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="mirrors">
|
||||
<item>HITOMI|hitomi.la</item>
|
||||
<item>HIYOBI|hiyobi.me</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="proxy_type">
|
||||
<item>Direct</item>
|
||||
<item>HTTP</item>
|
||||
<item>SOCKS</item>
|
||||
</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>
|
||||
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="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_blue_700">#1976d2</color>
|
||||
<color name="material_green_a700">#00c853</color>
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
<string name="email" translatable="false">mailto:pupil.hentai@gmail.com</string>
|
||||
<string name="discord" translatable="false">https://discord.gg/Stj4b5v</string>
|
||||
|
||||
<!-- Korean only -->
|
||||
<string name="https_text" translatable="false">해결법</string>
|
||||
<string name="https" translatable="false">https://bit.ly/34dUBwy</string>
|
||||
|
||||
<string name="backup_url" translatable="false">http://ix.io/</string>
|
||||
|
||||
<string name="main_settings" translatable="false">Settings</string>
|
||||
@@ -17,12 +21,18 @@
|
||||
<string name="reader_imageview_description" translatable="false">Content ImageView</string>
|
||||
<string name="page_indicator_placeholder" translatable="false">-/-</string>
|
||||
|
||||
<string name="galleryblock_artist_with_group" translatable="false">%s (%s)</string>
|
||||
|
||||
<!-- Translate needed down here -->
|
||||
|
||||
<string name="warning">Warning</string>
|
||||
<string name="error">Error</string>
|
||||
|
||||
<string name="ignore_update">Ignore</string>
|
||||
<string name="ignore">Ignore</string>
|
||||
|
||||
<string name="unlimited">Unlimited</string>
|
||||
|
||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
||||
|
||||
<string name="channel_download">Download</string>
|
||||
<string name="channel_download_description">Shows download status</string>
|
||||
@@ -39,6 +49,8 @@
|
||||
|
||||
<string name="main_no_result">No result</string>
|
||||
|
||||
<string name="unaccessible_download_folder">From Android 11 and above, current Download folder cannot be accessed by outside apps. Would you like to change the download folder?</string>
|
||||
|
||||
<string name="main_drawer_home">Home</string>
|
||||
<string name="main_drawer_history">History</string>
|
||||
<string name="main_drawer_downloads">Downloads</string>
|
||||
@@ -63,7 +75,7 @@
|
||||
<string name="main_fab_random">Open a random gallery</string>
|
||||
<string name="main_fab_cancel">Cancel all downloads</string>
|
||||
|
||||
<string name="main_move">Move to page %1$d</string>
|
||||
<string name="main_move_to_page">Move to page %1$d</string>
|
||||
|
||||
<string name="main_download">DOWNLOAD</string>
|
||||
<string name="main_delete">DELETE</string>
|
||||
@@ -138,6 +150,7 @@
|
||||
<string name="settings_storage_usage_loading">Calculating storage usage…</string>
|
||||
<string name="settings_clear_cache">Clear cache</string>
|
||||
<string name="settings_clear_cache_alert_message">Deleting cache can affect image loading speed. Do you want to continue?</string>
|
||||
<string name="settings_recover_downloads">Reconstruct download database</string>
|
||||
<string name="settings_clear_downloads">Clear downloads</string>
|
||||
<string name="settings_clear_downloads_alert_message">Delete all downloaded galleries.\nDo you want to continue?</string>
|
||||
<string name="settings_clear_history">Clear history</string>
|
||||
@@ -155,6 +168,8 @@
|
||||
<string name="settings_download_folder_available">%s available</string>
|
||||
<string name="settings_download_folder_custom">Custom Location</string>
|
||||
<string name="settings_download_folder_not_writable">This folder is not writable. Please select another folder.</string>
|
||||
<string name="settings_cache_limit">Cache Limit</string>
|
||||
<string name="settings_nomedia_title">Hide image from gallery</string>
|
||||
<string name="settings_low_quality">Low quality images</string>
|
||||
<string name="settings_low_quality_summary">Load low quality images to improve load speed and data usage</string>
|
||||
|
||||
@@ -163,20 +178,24 @@
|
||||
<string name="settings_app_lock">App lock</string>
|
||||
<string name="settings_app_lock_type">App lock type</string>
|
||||
|
||||
<!-- SETTINGS/NETWORKING -->
|
||||
<string name="settings_networking">Networking</string>
|
||||
<string name="settings_mirror_summary">Load images from mirrors</string>
|
||||
<string name="settings_proxy_title">Proxy</string>
|
||||
<string name="settings_max_concurrent_download">Concurrent Download</string>
|
||||
|
||||
<!-- SETTINGS/MISCELLANEOUS -->
|
||||
|
||||
<string name="settings_miscellaneous_title">Miscellaneous</string>
|
||||
<string name="settings_mirror_summary">Load images from mirrors</string>
|
||||
<string name="settings_proxy_title">Proxy</string>
|
||||
<string name="settings_tag_translation">Tag Language</string>
|
||||
<string name="settings_tag_translation_message">Participate in translation on Github</string>
|
||||
<string name="settings_rtl">Turn pages Right-to-Left</string>
|
||||
<string name="settings_security_mode_title">Enable security mode</string>
|
||||
<string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string>
|
||||
<string name="settings_dark_mode_title">Dark mode</string>
|
||||
<string name="settings_dark_mode_summary">Protect yourself against light attacks!</string>
|
||||
<string name="settings_nomedia_title">Hide image from gallery</string>
|
||||
<string name="settings_import_old_galleries">Import old galleries</string>
|
||||
<string name="settings_user_id">User ID</string>
|
||||
<string name="settings_user_id_toast">User ID is copied to clipboard</string>
|
||||
<string name="settings_oss">Open Source Notice</string>
|
||||
|
||||
<!-- MANAGE FAVORITES -->
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
app:key="delete_downloads"
|
||||
app:title="@string/settings_clear_downloads"/>
|
||||
|
||||
<Preference
|
||||
app:key="recover_downloads"
|
||||
app:title="@string/settings_recover_downloads"
|
||||
app:iconSpaceReserved="true"/>
|
||||
|
||||
<Preference
|
||||
app:key="clear_history"
|
||||
app:title="@string/settings_clear_history"/>
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
-->
|
||||
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
</domain-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">ix.io</domain>
|
||||
</domain-config>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<Preference
|
||||
app:key="app_version"
|
||||
@@ -44,6 +44,14 @@
|
||||
app:key="download_folder"
|
||||
app:title="@string/settings_download_folder"/>
|
||||
|
||||
<ListPreference
|
||||
app:key="cache_limit"
|
||||
app:title="@string/settings_cache_limit"
|
||||
app:entries="@array/cache_size_text"
|
||||
app:entryValues="@array/cache_size"
|
||||
app:defaultValue="8"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="nomedia"
|
||||
app:title="@string/settings_nomedia_title"/>
|
||||
@@ -66,17 +74,30 @@
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:title="@string/settings_miscellaneous_title">
|
||||
|
||||
<Preference
|
||||
app:key="mirrors"
|
||||
app:title="@string/settings_mirror_title"
|
||||
app:summary="@string/settings_mirror_summary"/>
|
||||
app:title="@string/settings_networking">
|
||||
|
||||
<Preference
|
||||
app:key="proxy"
|
||||
app:title="@string/settings_proxy_title"/>
|
||||
|
||||
<ListPreference
|
||||
app:key="max_concurrent_download"
|
||||
android:title="@string/settings_max_concurrent_download"
|
||||
app:entries="@array/concurrent_download_text"
|
||||
app:entryValues="@array/concurrent_download"
|
||||
android:defaultValue="0"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:title="@string/settings_miscellaneous_title">
|
||||
|
||||
<ListPreference
|
||||
app:key="tag_translation"
|
||||
app:title="@string/settings_tag_translation"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="rtl"
|
||||
app:title="@string/settings_rtl"
|
||||
|
||||
@@ -26,21 +26,14 @@ package xyz.quaver.pupil
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.Test
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
class ExampleUnitTest {
|
||||
|
||||
@Test
|
||||
fun test() {
|
||||
val a = mutableSetOf<Int>()
|
||||
|
||||
print(a::class.java.methods.firstOrNull { it.name == "add" }?.genericParameterTypes?.firstOrNull() as? ParameterizedType)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
17
build.gradle
17
build.gradle
@@ -3,29 +3,30 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:4.0.1"
|
||||
classpath 'com.android.tools.build:gradle:7.1.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath "com.google.gms:google-services:4.3.3"
|
||||
classpath "com.google.gms:google-services:4.3.10"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
classpath "com.google.firebase:firebase-crashlytics-gradle:2.3.0"
|
||||
classpath "com.google.firebase:perf-plugin:1.3.1"
|
||||
classpath "com.google.android.gms:oss-licenses-plugin:0.10.2"
|
||||
classpath "com.google.firebase:firebase-crashlytics-gradle:2.8.1"
|
||||
classpath "com.google.firebase:perf-plugin:1.4.1"
|
||||
classpath "com.google.android.gms:oss-licenses-plugin:0.10.4"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven { url "http://dl.bintray.com/piasy/maven" }
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven { url "https://jitpack.io" }
|
||||
maven { url "https://guardian.github.com/maven/repo-releases" }
|
||||
maven { url "https://guardian.github.io/maven/repo-releases/" }
|
||||
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||
org.gradle.parallel=true
|
||||
org.gradle.daemon=true
|
||||
org.gradle.configureondemand=true
|
||||
org.gradle.caching=true
|
||||
kotlin.code.style=official
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
|
||||
kotlin_version=1.4.10
|
||||
kotlin_version=1.6.10
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Thu Jun 18 15:48:09 KST 2020
|
||||
#Tue Oct 13 22:37:11 KST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||
|
||||
51
gradlew
vendored
Normal file → Executable file
51
gradlew
vendored
Normal file → Executable file
@@ -1,5 +1,21 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
@@ -109,8 +125,8 @@ if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
@@ -138,19 +154,19 @@ if $cygwin ; then
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -159,14 +175,9 @@ save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
18
gradlew.bat
vendored
18
gradlew.bat
vendored
@@ -1,3 +1,19 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
Reference in New Issue
Block a user