Compare commits
129 Commits
5.1.6
...
5.2.8-BETA
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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"
|
||||
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="1.8" />
|
||||
<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>
|
||||
17
.idea/deploymentTargetDropDown.xml
generated
Normal file
17
.idea/deploymentTargetDropDown.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<targetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="$USER_HOME$/.android/avd/Pixel_2_API_30.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2022-01-04T14:31:24.587053Z" />
|
||||
</component>
|
||||
</project>
|
||||
5
.idea/gradle.xml
generated
5
.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$" />
|
||||
@@ -14,7 +16,6 @@
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="useQualifiedModuleNames" value="true" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
15
.idea/jarRepositories.xml
generated
15
.idea/jarRepositories.xml
generated
@@ -71,5 +71,20 @@
|
||||
<option name="name" value="maven3" />
|
||||
<option name="url" value="http://dl.bintray.com/piasy/maven" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven2" />
|
||||
<option name="name" value="maven2" />
|
||||
<option name="url" value="https://guardian.github.io/maven/repo-releases/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenLocal" />
|
||||
<option name="name" value="MavenLocal" />
|
||||
<option name="url" value="file:$USER_HOME$/.m2/repository/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven2" />
|
||||
<option name="name" value="maven2" />
|
||||
<option name="url" value="https://oss.sonatype.org/content/repositories/snapshots" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/misc.xml
generated
9
.idea/misc.xml
generated
@@ -1,6 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="app/src/main/res/layout/reader_activity.xml" value="0.14351851851851852" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</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>
|
||||
@@ -2,8 +2,8 @@
|
||||
*Pupil, Hitomi.la viewer for Android*
|
||||
|
||||

|
||||
[](https://github.com/tom5079/Pupil/releases/download/5.1.5-hotfix2/Pupil-v5.1.5-hotfix2.apk)
|
||||
[](https://discord.gg/Stj4b5v)
|
||||
[](https://github.com/tom5079/Pupil/releases/download/5.2.7/Pupil-v5.2.7.apk)
|
||||
[](https://discord.gg/Stj4b5v)
|
||||
|
||||
# Features
|
||||

|
||||
@@ -20,7 +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,27 +32,27 @@ configurations {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 31
|
||||
defaultConfig {
|
||||
applicationId "xyz.quaver.pupil"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
versionCode 63
|
||||
versionName "5.1.5-hotfix2"
|
||||
targetSdkVersion 31
|
||||
versionCode 69
|
||||
versionName "5.2.8-BETA01"
|
||||
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"
|
||||
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
|
||||
ext.enableCrashlytics = false
|
||||
ext.alwaysUpdateBuildId = false
|
||||
}
|
||||
@@ -63,6 +63,9 @@ android {
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
|
||||
@@ -71,43 +74,43 @@ android {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
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-android:1.4.0-M1"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0"
|
||||
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-beta01"
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.0-beta01"
|
||||
implementation "androidx.appcompat:appcompat:1.4.0"
|
||||
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.constraintlayout:constraintlayout:2.0.2"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.2"
|
||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
implementation "androidx.work:work-runtime-ktx:2.4.0"
|
||||
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-alpha03"
|
||||
implementation "com.google.android.material:material:1.4.0"
|
||||
|
||||
implementation "com.google.firebase:firebase-core:17.5.1"
|
||||
implementation "com.google.firebase:firebase-analytics:17.6.0"
|
||||
implementation "com.google.firebase:firebase-crashlytics:17.2.2"
|
||||
implementation "com.google.firebase:firebase-perf:19.0.9"
|
||||
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:16.2.1"
|
||||
|
||||
implementation "com.github.clans:fab:1.6.4"
|
||||
|
||||
//implementation "com.quiph.ui:recyclerviewfastscroller:0.2.1"
|
||||
|
||||
implementation 'com.github.piasy:BigImageViewer:1.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'
|
||||
|
||||
//noinspection GradleDependency
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
@@ -122,17 +125,17 @@ dependencies {
|
||||
|
||||
implementation "ru.noties.markwon:core:3.1.0"
|
||||
|
||||
implementation "xyz.quaver:libpupil:1.7.2"
|
||||
implementation "xyz.quaver:documentfilex:0.4-alpha02"
|
||||
implementation "xyz.quaver:floatingsearchview:1.0.7"
|
||||
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 "com.google.guava:guava:31.0.1-android"
|
||||
|
||||
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"
|
||||
}
|
||||
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@@ -32,4 +32,5 @@
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
|
||||
-keep class com.hippo.quickjs.** { *; }
|
||||
@@ -1,18 +1,20 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "xyz.quaver.pupil",
|
||||
"variantName": "processReleaseResources",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"versionCode": 63,
|
||||
"versionName": "5.1.5-hotfix2",
|
||||
"attributes": [],
|
||||
"versionCode": 69,
|
||||
"versionName": "5.2.8-BETA01",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
]
|
||||
],
|
||||
"elementType": "File"
|
||||
}
|
||||
@@ -20,10 +20,15 @@
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.util.Log
|
||||
import android.webkit.*
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import xyz.quaver.pupil.hitomi.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
@@ -32,10 +37,95 @@ import org.junit.runner.RunWith
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Before
|
||||
fun init() {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
runBlocking {
|
||||
withContext(Dispatchers.Main) {
|
||||
webView = WebView(appContext).apply {
|
||||
settings.javaScriptEnabled = true
|
||||
|
||||
addJavascriptInterface(object {
|
||||
@JavascriptInterface
|
||||
fun onResult(uid: String, result: String) {
|
||||
_webViewFlow.tryEmit(uid to result)
|
||||
}
|
||||
}, "Callback")
|
||||
|
||||
loadDataWithBaseURL(
|
||||
"https://hitomi.la/",
|
||||
"""
|
||||
<script src="https://ltn.hitomi.la/jquery.min.js"></script>
|
||||
<script src="https://ltn.hitomi.la/common.js"></script>
|
||||
<script src="https://ltn.hitomi.la/search.js"></script>
|
||||
<script src="https://ltn.hitomi.la/searchlib.js"></script>
|
||||
<script src="https://ltn.hitomi.la/results.js></script>
|
||||
""".trimIndent(),
|
||||
"text/html",
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
fun test_getGalleryIDsFromNozomi() {
|
||||
runBlocking {
|
||||
val result = getGalleryIDsFromNozomi(null, "index", "all")
|
||||
|
||||
Log.d("PUPILD", "getGalleryIDsFromNozomi: ${result.size}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getGalleryIDsForQuery() {
|
||||
runBlocking {
|
||||
val result = getGalleryIDsForQuery("female:crotch tattoo")
|
||||
|
||||
Log.d("PUPILD", "getGalleryIDsForQuery: ${result.size}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getSuggestionsForQuery() {
|
||||
runBlocking {
|
||||
val result = getSuggestionsForQuery("fem")
|
||||
|
||||
Log.d("PUPILD", "getSuggestionsForQuery: ${result.size}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_urlFromUrlFromHash() {
|
||||
runBlocking {
|
||||
val galleryInfo = getGalleryInfo(2102416)
|
||||
|
||||
val result = galleryInfo.files.map {
|
||||
imageUrlFromImage(2102416, it, false)
|
||||
}
|
||||
|
||||
Log.d("PUPILD", result.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getGalleryInfo() {
|
||||
runBlocking {
|
||||
val galleryInfo = getGalleryInfo(2102416)
|
||||
|
||||
Log.d("PUPILD", galleryInfo.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getGalleryBlock() {
|
||||
runBlocking {
|
||||
val block = getGalleryBlock(2102731)
|
||||
|
||||
Log.d("PUPILD", block.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.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-sdk-23 android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission-sdk-23 android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<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,14 +18,19 @@
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.webkit.*
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
@@ -34,18 +39,22 @@ 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.android.material.snackbar.Snackbar
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import okhttp3.*
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.hitomi.evaluations
|
||||
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.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@@ -68,12 +77,152 @@ var clientHolder: OkHttpClient? = null
|
||||
val client: OkHttpClient
|
||||
get() = clientHolder ?: clientBuilder.build().also {
|
||||
clientHolder = it
|
||||
setClient(it)
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var webView: WebView
|
||||
val _webViewFlow = MutableSharedFlow<Pair<String, String?>>(
|
||||
extraBufferCapacity = 2,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val webViewFlow = _webViewFlow.asSharedFlow()
|
||||
var webViewReady = false
|
||||
private set
|
||||
var webViewFailed = false
|
||||
private set
|
||||
private var reloadJob: Job? = null
|
||||
|
||||
fun reloadWebView() {
|
||||
if (reloadJob?.isActive == true) return
|
||||
|
||||
reloadJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
if (evaluations.isEmpty()) {
|
||||
webViewReady = false
|
||||
webViewFailed = false
|
||||
|
||||
while (evaluations.isNotEmpty()) yield()
|
||||
|
||||
runCatching {
|
||||
URL(
|
||||
if (isDebugBuild)
|
||||
"https://tom5079.github.io/Pupil/hitomi-dev.html"
|
||||
else
|
||||
"https://tom5079.github.io/Pupil/hitomi.html"
|
||||
).readText()
|
||||
}.onFailure {
|
||||
webViewFailed = true
|
||||
}.getOrNull()?.let { html ->
|
||||
launch(Dispatchers.Main) {
|
||||
webView.loadDataWithBaseURL(
|
||||
"https://hitomi.la/",
|
||||
html,
|
||||
"text/html",
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var htmlVersion: String = ""
|
||||
fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch {
|
||||
while (true) {
|
||||
if (
|
||||
webViewFailed ||
|
||||
runCatching {
|
||||
URL(
|
||||
if (isDebugBuild)
|
||||
"https://tom5079.github.io/Pupil/hitomi-dev.html.ver"
|
||||
else
|
||||
"https://tom5079.github.io/Pupil/hitomi.html.ver"
|
||||
).readText()
|
||||
}.getOrNull().let { version ->
|
||||
(!version.isNullOrEmpty() && version != htmlVersion).also {
|
||||
if (it) htmlVersion = version!!
|
||||
}
|
||||
}
|
||||
) {
|
||||
reloadWebView()
|
||||
}
|
||||
|
||||
delay(if (webViewReady && !webViewFailed) 10000 else 1000)
|
||||
}
|
||||
}
|
||||
|
||||
var isDebugBuild: Boolean = false
|
||||
private lateinit var userAgent: String
|
||||
|
||||
class Pupil : Application() {
|
||||
|
||||
companion object {
|
||||
lateinit var instance: Pupil
|
||||
private set
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate() {
|
||||
instance = this
|
||||
isDebugBuild = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
|
||||
|
||||
if (isDebugBuild) WebView.setWebContentsDebuggingEnabled(true)
|
||||
|
||||
webView = WebView(this).apply {
|
||||
with (settings) {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
}
|
||||
|
||||
userAgent = settings.userAgentString
|
||||
|
||||
webViewClient = object: WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
webViewReady = true
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
error: WebResourceError?
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
FirebaseCrashlytics.getInstance().log(
|
||||
"onReceivedError: ${error?.description}"
|
||||
)
|
||||
}
|
||||
webViewFailed = true
|
||||
}
|
||||
}
|
||||
|
||||
webChromeClient = object: WebChromeClient() {
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||
FirebaseCrashlytics.getInstance().log(
|
||||
"onConsoleMessage: ${consoleMessage?.message()} (${consoleMessage?.sourceId()}:${consoleMessage?.lineNumber()})"
|
||||
)
|
||||
|
||||
return super.onConsoleMessage(consoleMessage)
|
||||
}
|
||||
}
|
||||
|
||||
addJavascriptInterface(object {
|
||||
@JavascriptInterface
|
||||
fun onResult(uid: String, result: String) {
|
||||
_webViewFlow.tryEmit(uid to result)
|
||||
}
|
||||
@JavascriptInterface
|
||||
fun onError(uid: String, message: String) {
|
||||
_webViewFlow.tryEmit(uid to null)
|
||||
Toast.makeText(this@Pupil, message, Toast.LENGTH_LONG).show()
|
||||
FirebaseCrashlytics.getInstance().recordException(
|
||||
Exception(message)
|
||||
)
|
||||
}
|
||||
}, "Callback")
|
||||
}
|
||||
|
||||
reloadWhenFailedOrUpdate()
|
||||
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
@@ -92,24 +241,29 @@ class Pupil : Application() {
|
||||
.readTimeout(0, TimeUnit.SECONDS)
|
||||
.proxyInfo(proxyInfo)
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
val request = chain.request().newBuilder()
|
||||
.header("User-Agent", userAgent)
|
||||
.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)
|
||||
contentResolver.takePersistableUriPermission(
|
||||
Uri.parse(it),
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
contentResolver.takePersistableUriPermission(
|
||||
Uri.parse(it),
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
|
||||
if (!FileX(this, it).canWrite())
|
||||
throw Exception()
|
||||
|
||||
DownloadManager.getInstance(this).migrate()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Preferences.remove("download_folder")
|
||||
@@ -125,8 +279,13 @@ class Pupil : Application() {
|
||||
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)
|
||||
|
||||
@@ -18,15 +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.Log
|
||||
import android.util.SparseBooleanArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.cardview.widget.CardView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
@@ -35,15 +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.coroutines.*
|
||||
import xyz.quaver.hitomi.getGallery
|
||||
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.ProgressCard
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
@@ -52,333 +52,260 @@ import java.io.File
|
||||
|
||||
class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
||||
|
||||
enum class ViewType {
|
||||
NEXT,
|
||||
GALLERY,
|
||||
PREV
|
||||
}
|
||||
|
||||
var updateAll = true
|
||||
var thin: Boolean = Preferences["thin"]
|
||||
|
||||
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||
inner class GalleryViewHolder(val binding: GalleryblockItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
private var galleryID: Int = 0
|
||||
|
||||
init {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
while (updateAll) {
|
||||
updateProgress(view.context)
|
||||
updateProgress(itemView.context)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProgress(context: Context) {
|
||||
val cache = Cache.getInstance(context, galleryID)
|
||||
private fun updateProgress(context: Context) = CoroutineScope(Dispatchers.Main).launch {
|
||||
with(binding.galleryblockCard) {
|
||||
val imageList = Cache.getInstance(context, galleryID).metadata.imageList
|
||||
|
||||
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
|
||||
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) {
|
||||
this.galleryID = galleryID
|
||||
updateProgress(view.context)
|
||||
updateProgress(itemView.context)
|
||||
|
||||
val cache = Cache.getInstance(view.context, galleryID)
|
||||
val cache = Cache.getInstance(itemView.context, galleryID)
|
||||
|
||||
val galleryBlock = runBlocking {
|
||||
cache.getGalleryBlock()
|
||||
} ?: return
|
||||
|
||||
with(view) {
|
||||
val resources = context.resources
|
||||
val languages = resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
} }
|
||||
}
|
||||
val resources = itemView.context.resources
|
||||
val languages = resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
galleryblock_title.text = galleryBlock.title
|
||||
with(galleryblock_artist) {
|
||||
text = artists.joinToString { it.wordCapitalize() }
|
||||
visibility = when {
|
||||
artists.isNotEmpty() -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
val artists = galleryBlock.artists
|
||||
val series = galleryBlock.series
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val gallery = runCatching {
|
||||
getGallery(galleryID)
|
||||
}.getOrNull()
|
||||
|
||||
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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.galleryblockThumbnail.apply {
|
||||
setOnClickListener {
|
||||
itemView.performClick()
|
||||
}
|
||||
with(galleryblock_series) {
|
||||
text =
|
||||
resources.getString(
|
||||
R.string.galleryblock_series,
|
||||
series.joinToString(", ") { it.wordCapitalize() })
|
||||
visibility = when {
|
||||
series.isNotEmpty() -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
setOnLongClickListener {
|
||||
itemView.performLongClick()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
with(galleryblock_tag_group) {
|
||||
onClickListener = {
|
||||
onChipClickedHandler.forEach { callback ->
|
||||
callback.invoke(it)
|
||||
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() }
|
||||
}
|
||||
}
|
||||
|
||||
tags.clear()
|
||||
|
||||
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
|
||||
}
|
||||
}.map {
|
||||
Tag.parse(it)
|
||||
}
|
||||
)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
galleryblock_id.text = galleryBlock.id.toString()
|
||||
galleryblock_pagecount.text = "-"
|
||||
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 {
|
||||
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)
|
||||
cache.getThumbnail().let { launch(Dispatchers.Main) {
|
||||
showImage(it)
|
||||
} }
|
||||
}
|
||||
}
|
||||
|
||||
binding.galleryblockTitle.text = galleryBlock.title
|
||||
with(binding.galleryblockArtist) {
|
||||
text = artists.joinToString { it.wordCapitalize() }
|
||||
visibility = when {
|
||||
artists.isNotEmpty() -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val gallery = runCatching {
|
||||
getGallery(galleryID)
|
||||
}.getOrNull()
|
||||
|
||||
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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
with(binding.galleryblockSeries) {
|
||||
text =
|
||||
resources.getString(
|
||||
R.string.galleryblock_series,
|
||||
series.joinToString(", ") { it.wordCapitalize() })
|
||||
visibility = when {
|
||||
series.isNotEmpty() -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
}
|
||||
binding.galleryblockType.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
|
||||
with(binding.galleryblockLanguage) {
|
||||
text =
|
||||
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
|
||||
visibility = when {
|
||||
galleryBlock.language.isNotEmpty() -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
with(binding.galleryblockTagGroup) {
|
||||
onClickListener = {
|
||||
onChipClickedHandler.forEach { callback ->
|
||||
callback.invoke(it)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
tags.clear()
|
||||
|
||||
setImageResource(R.drawable.ic_star_empty)
|
||||
}
|
||||
else -> {
|
||||
favorites.add(galleryBlock.id)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
tags.addAll(
|
||||
galleryBlock.relatedTags.sortedBy {
|
||||
val tag = Tag.parse(it)
|
||||
|
||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
|
||||
this ?: return@apply
|
||||
if (favoriteTags.contains(tag))
|
||||
-1
|
||||
else
|
||||
when(Tag.parse(it).area) {
|
||||
"female" -> 0
|
||||
"male" -> 1
|
||||
else -> 2
|
||||
}
|
||||
}.map {
|
||||
Tag.parse(it)
|
||||
}
|
||||
)
|
||||
|
||||
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
|
||||
override fun onAnimationEnd(drawable: Drawable?) {
|
||||
setImageResource(R.drawable.ic_star_filled)
|
||||
}
|
||||
})
|
||||
start()
|
||||
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_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?) {}
|
||||
@@ -390,18 +317,7 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() =
|
||||
galleries.size +
|
||||
(if (showNext) 1 else 0) +
|
||||
(if (showPrev) 1 else 0)
|
||||
override fun getItemCount() = galleries.size
|
||||
|
||||
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,16 +52,93 @@ 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) {
|
||||
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
with (binding.image) {
|
||||
setImageViewFactory(FrescoImageViewFactory().apply {
|
||||
updateView = { imageInfo ->
|
||||
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
|
||||
}
|
||||
}
|
||||
})
|
||||
setImageShownCallback(object : ImageShownCallback {
|
||||
override fun onMainImageShown() {
|
||||
binding.image.mainView.let { v ->
|
||||
when (v) {
|
||||
is SubsamplingScaleImageView ->
|
||||
if (!isFullScreen) binding.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onThumbnailShown() {}
|
||||
})
|
||||
|
||||
setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
|
||||
setOnClickListener {
|
||||
onItemClickListener?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
view.image.mainView.let {
|
||||
binding.image.mainView.let {
|
||||
when (it) {
|
||||
is SubsamplingScaleImageView ->
|
||||
it.recycle()
|
||||
@@ -74,91 +150,15 @@ class ReaderAdapter(
|
||||
}
|
||||
|
||||
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 {
|
||||
updateView = { imageInfo ->
|
||||
it.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
|
||||
}
|
||||
}
|
||||
})
|
||||
image.setImageShownCallback(object : ImageShownCallback {
|
||||
override fun onMainImageShown() {
|
||||
it.image.mainView.let { v ->
|
||||
when (v) {
|
||||
is SubsamplingScaleImageView ->
|
||||
if (!isFullScreen) it.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onThumbnailShown() {}
|
||||
})
|
||||
image.setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
|
||||
image.setOnClickListener {
|
||||
this.performClick()
|
||||
}
|
||||
setOnClickListener {
|
||||
onItemClickListener?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
ViewHolder(it)
|
||||
}
|
||||
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()
|
||||
|
||||
56
app/src/main/java/xyz/quaver/pupil/hitomi/Utils.kt
Normal file
56
app/src/main/java/xyz/quaver/pupil/hitomi/Utils.kt
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2019 tom5079
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xyz.quaver
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.plus
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import xyz.quaver.pupil.client
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* kotlinx.serialization.json.Json object for global use
|
||||
* properties should not be changed
|
||||
*
|
||||
* @see [https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.json/-json/index.html]
|
||||
*/
|
||||
val json = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
allowSpecialFloatingPointValues = true
|
||||
useArrayPolymorphism = true
|
||||
}
|
||||
|
||||
typealias HeaderSetter = (Request.Builder) -> Request.Builder
|
||||
fun URL.readText(settings: HeaderSetter? = null): String {
|
||||
val request = Request.Builder()
|
||||
.url(this).let {
|
||||
settings?.invoke(it) ?: it
|
||||
}.build()
|
||||
|
||||
return client.newCall(request).execute().also{ if (it.code() != 200) throw IOException() }.body()?.use { it.string() } ?: throw IOException()
|
||||
}
|
||||
135
app/src/main/java/xyz/quaver/pupil/hitomi/common.kt
Normal file
135
app/src/main/java/xyz/quaver/pupil/hitomi/common.kt
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2019 tom5079
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.hitomi
|
||||
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import com.google.common.collect.ConcurrentHashMultiset
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.quaver.json
|
||||
import xyz.quaver.pupil.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
const val protocol = "https:"
|
||||
|
||||
val evaluations = Collections.newSetFromMap<String>(ConcurrentHashMap())
|
||||
|
||||
suspend fun WebView.evaluate(script: String): String = withContext(Dispatchers.Main) {
|
||||
val result: String = withTimeout(10000) {
|
||||
while (webViewFailed || !webViewReady) yield()
|
||||
|
||||
val uid = UUID.randomUUID().toString()
|
||||
|
||||
evaluations.add(uid)
|
||||
|
||||
suspendCoroutine { continuation ->
|
||||
evaluateJavascript(script) {
|
||||
evaluations.remove(uid)
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun WebView.evaluatePromise(script: String, then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result))).catch(err => Callback.onError(%uid, JSON.stringify(error)))"): String? = withContext(Dispatchers.Main) {
|
||||
val flow: Flow<Pair<String, String?>> = withTimeout(10000) {
|
||||
while (webViewFailed || !webViewReady) yield()
|
||||
|
||||
val uid = UUID.randomUUID().toString()
|
||||
|
||||
evaluations.add(uid)
|
||||
|
||||
evaluateJavascript((script+then).replace("%uid", "'$uid'"), null)
|
||||
|
||||
webViewFlow.transformWhile { (currentUid, result) ->
|
||||
if (currentUid == uid) {
|
||||
evaluations.remove(uid)
|
||||
emit(currentUid to result)
|
||||
}
|
||||
currentUid != uid
|
||||
}
|
||||
}
|
||||
|
||||
flow.first().second
|
||||
}
|
||||
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
suspend fun getGalleryInfo(galleryID: Int): GalleryInfo {
|
||||
val result = webView.evaluatePromise("get_gallery_info($galleryID)")
|
||||
|
||||
return json.decodeFromString(result!!)
|
||||
}
|
||||
|
||||
//common.js
|
||||
const val domain = "ltn.hitomi.la"
|
||||
const val galleryblockdir = "galleryblock"
|
||||
const val nozomiextension = ".nozomi"
|
||||
|
||||
val String?.js: String
|
||||
get() = if (this == null) "null" else "'$this'"
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null): String {
|
||||
val result = webView.evaluate(
|
||||
"""
|
||||
url_from_url_from_hash(
|
||||
${galleryID.toString().js},
|
||||
${Json.encodeToString(image)},
|
||||
${dir.js}, ${ext.js}, ${base.js}
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
|
||||
FirebaseCrashlytics.getInstance().log(
|
||||
"""
|
||||
url_from_url_from_hash(
|
||||
${galleryID.toString().js},
|
||||
${Json.encodeToString(image)},
|
||||
${dir.js}, ${ext.js}, ${base.js}
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
return Json.decodeFromString(result)
|
||||
}
|
||||
|
||||
suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
|
||||
return when {
|
||||
noWebp ->
|
||||
urlFromUrlFromHash(galleryID, image)
|
||||
// image.hasavif != 0 ->
|
||||
// urlFromUrlFromHash(galleryID, image, "avif", null, "a")
|
||||
image.haswebp != 0 ->
|
||||
urlFromUrlFromHash(galleryID, image, "webp", null, "a")
|
||||
else ->
|
||||
urlFromUrlFromHash(galleryID, image)
|
||||
}
|
||||
}
|
||||
80
app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt
Normal file
80
app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2019 tom5079
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.hitomi
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import xyz.quaver.readText
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
|
||||
@Serializable
|
||||
data class Gallery(
|
||||
val related: List<Int>,
|
||||
val langList: List<Pair<String, String>>,
|
||||
val cover: String,
|
||||
val title: String,
|
||||
val artists: List<String>,
|
||||
val groups: List<String>,
|
||||
val type: String,
|
||||
val language: String,
|
||||
val series: List<String>,
|
||||
val characters: List<String>,
|
||||
val tags: List<String>,
|
||||
val thumbnails: List<String>
|
||||
)
|
||||
suspend fun getGallery(galleryID: Int) : Gallery {
|
||||
val url = Jsoup.parse(URL("https://hitomi.la/galleries/$galleryID.html").readText())
|
||||
.select("link").attr("href")
|
||||
|
||||
val doc = Jsoup.parse(URL(url).readText())
|
||||
|
||||
val related = Regex("\\d+")
|
||||
.findAll(doc.select("script").first()!!.html())
|
||||
.map {
|
||||
it.value.toInt()
|
||||
}.toList()
|
||||
|
||||
val langList = doc.select("#lang-list a").map {
|
||||
Pair(it.text(), "$protocol//hitomi.la${it.attr("href")}")
|
||||
}
|
||||
|
||||
val cover = protocol + doc.selectFirst(".cover img")!!.attr("src")
|
||||
val title = doc.selectFirst(".gallery h1 a")!!.text()
|
||||
val artists = doc.select(".gallery h2 a").map { it.text() }
|
||||
val groups = doc.select(".gallery-info a[href~=^/group/]").map { it.text() }
|
||||
val type = doc.selectFirst(".gallery-info a[href~=^/type/]")!!.text()
|
||||
|
||||
val language = run {
|
||||
val href = doc.select(".gallery-info a[href~=^/index.+\\.html\$]").attr("href")
|
||||
Regex("""index-([^-]+)(-.+)?\.html""").find(href)?.groupValues?.getOrNull(1) ?: ""
|
||||
}
|
||||
|
||||
val series = doc.select(".gallery-info a[href~=^/series/]").map { it.text() }
|
||||
val characters = doc.select(".gallery-info a[href~=^/character/]").map { it.text() }
|
||||
|
||||
val tags = doc.select(".gallery-info a[href~=^/tag/]").map {
|
||||
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
||||
href.slice(5 until href.indexOf('-'))
|
||||
}
|
||||
|
||||
val thumbnails = getGalleryInfo(galleryID).files.map { galleryInfo ->
|
||||
urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn")
|
||||
}
|
||||
|
||||
return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails)
|
||||
}
|
||||
79
app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt
Normal file
79
app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2019 tom5079
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.hitomi
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import xyz.quaver.pupil.webView
|
||||
import xyz.quaver.readText
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.*
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
@Serializable
|
||||
data class GalleryBlock(
|
||||
val id: Int,
|
||||
val galleryUrl: String,
|
||||
val thumbnails: List<String>,
|
||||
val title: String,
|
||||
val artists: List<String>,
|
||||
val series: List<String>,
|
||||
val type: String,
|
||||
val language: String,
|
||||
val relatedTags: List<String>
|
||||
)
|
||||
|
||||
suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock {
|
||||
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
|
||||
|
||||
val html: String = webView.evaluatePromise(
|
||||
"""
|
||||
$.get('$url').always(function(data, status) {
|
||||
if (status === 'success') {
|
||||
Callback.onResult(%uid, data);
|
||||
}
|
||||
});
|
||||
""".trimIndent(),
|
||||
then = ""
|
||||
)!!
|
||||
|
||||
val doc = Jsoup.parse(html)
|
||||
|
||||
val galleryUrl = doc.selectFirst("h1 > a")!!.attr("href")
|
||||
|
||||
val thumbnails = doc.select(".dj-img-cont img").map { protocol + it.attr("src") }
|
||||
|
||||
val title = doc.selectFirst("h1 > a")!!.text()
|
||||
val artists = doc.select(".artist-list a").map{ it.text() }
|
||||
val series = doc.select(".dj-content a[href~=^/series/]").map { it.text() }
|
||||
val type = doc.selectFirst("a[href~=^/type/]")!!.text()
|
||||
|
||||
val language = run {
|
||||
val href = doc.select("a[href~=^/index.+\\.html\$]").attr("href")
|
||||
Regex("""index-([^-]+)(-.+)?\.html""").find(href)?.groupValues?.getOrNull(1) ?: ""
|
||||
}
|
||||
|
||||
val relatedTags = doc.select(".relatedtags a").map {
|
||||
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
||||
href.slice(5 until href.indexOf("-all"))
|
||||
}
|
||||
|
||||
return GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||
}
|
||||
49
app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt
Normal file
49
app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019 tom5079
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.hitomi
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
|
||||
|
||||
@Serializable
|
||||
data class GalleryInfo(
|
||||
val language_localname: String? = null,
|
||||
val language: String? = null,
|
||||
val date: String? = null,
|
||||
val files: List<GalleryFiles>,
|
||||
val id: Int? = null,
|
||||
val type: String? = null,
|
||||
val title: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GalleryFiles(
|
||||
val width: Int,
|
||||
val hash: String,
|
||||
val haswebp: Int = 0,
|
||||
val name: String,
|
||||
val height: Int,
|
||||
val hasavif: Int = 0,
|
||||
val hasavifsmalltn: Int? = 0
|
||||
)
|
||||
|
||||
//Set header `Referer` to reader url to avoid 403 error
|
||||
@Deprecated("", replaceWith = ReplaceWith("getGalleryInfo"))
|
||||
suspend fun getReader(galleryID: Int) : GalleryInfo {
|
||||
return getGalleryInfo(galleryID)
|
||||
}
|
||||
86
app/src/main/java/xyz/quaver/pupil/hitomi/results.kt
Normal file
86
app/src/main/java/xyz/quaver/pupil/hitomi/results.kt
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.*
|
||||
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.map {
|
||||
async {
|
||||
runCatching {
|
||||
getGalleryIDsForQuery(it)
|
||||
}.getOrElse { emptySet() }
|
||||
}
|
||||
}
|
||||
|
||||
var results = when {
|
||||
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all")
|
||||
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all")
|
||||
else -> emptySet()
|
||||
}
|
||||
|
||||
fun filterPositive(newResults: Set<Int>) {
|
||||
results = when {
|
||||
results.isEmpty() -> newResults
|
||||
else -> results intersect newResults
|
||||
}
|
||||
}
|
||||
|
||||
fun filterNegative(newResults: Set<Int>) {
|
||||
results = results subtract newResults
|
||||
}
|
||||
|
||||
//positive results
|
||||
positiveResults.forEach {
|
||||
filterPositive(it.await())
|
||||
}
|
||||
|
||||
//negative results
|
||||
negativeResults.forEach {
|
||||
filterNegative(it.await())
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
50
app/src/main/java/xyz/quaver/pupil/hitomi/search.kt
Normal file
50
app/src/main/java/xyz/quaver/pupil/hitomi/search.kt
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.quaver.pupil.webView
|
||||
|
||||
//searchlib.js
|
||||
const val extension = ".html"
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun getGalleryIDsForQuery(query: String) : Set<Int> {
|
||||
val result = webView.evaluatePromise("get_galleryids_for_query('$query')") ?: return emptySet()
|
||||
|
||||
return Json.decodeFromString(result)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun getSuggestionsForQuery(query: String) : List<Suggestion> {
|
||||
val result = webView.evaluatePromise("get_suggestions_for_query('$query')") ?: return emptyList()
|
||||
|
||||
return Json.decodeFromString<List<List<Suggestion>?>>(result)[0] ?: return emptyList()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> {
|
||||
val jsArea = if (area == null) "null" else "'$area'"
|
||||
|
||||
return Json.decodeFromString(webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""") ?: return emptySet())
|
||||
}
|
||||
@@ -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,7 +55,7 @@ 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) {
|
||||
@@ -76,7 +77,7 @@ class UpdateBroadcastReceiver : BroadcastReceiver() {
|
||||
val pendingIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||
}, 0)
|
||||
}, 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,10 +23,14 @@ 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
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.common.util.concurrent.RateLimiter
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -38,14 +42,10 @@ import okhttp3.ResponseBody
|
||||
import okio.*
|
||||
import xyz.quaver.pupil.*
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.util.cleanCache
|
||||
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.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.log10
|
||||
@@ -75,7 +75,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),
|
||||
@@ -85,7 +85,7 @@ 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[galleryID] = NotificationCompat.Builder(this, "download").apply {
|
||||
@@ -166,14 +166,24 @@ class DownloadService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private val rateLimiter = RateLimiter.create(2.0)
|
||||
private val rateLimitHost = Regex("..?\\.hitomi.la")
|
||||
|
||||
private val interceptor: PupilInterceptor = { chain ->
|
||||
val request = chain.request()
|
||||
var response = chain.proceed(request)
|
||||
|
||||
var retry = 5
|
||||
while (!response.isSuccessful && retry > 0) {
|
||||
if (rateLimitHost.matches(request.url().host()))
|
||||
rateLimiter.acquire()
|
||||
|
||||
var response = chain.proceed(request)
|
||||
var limit = 5
|
||||
|
||||
if (!response.isSuccessful && limit > 0) {
|
||||
Thread.sleep(10000)
|
||||
if (rateLimitHost.matches(request.url().host()))
|
||||
rateLimiter.acquire()
|
||||
response = chain.proceed(request)
|
||||
retry--
|
||||
limit -= 1
|
||||
}
|
||||
|
||||
response.newBuilder()
|
||||
@@ -203,12 +213,10 @@ class DownloadService : Service() {
|
||||
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,7 +225,7 @@ class DownloadService : Service() {
|
||||
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 image = response.also { if (it.code() != 200) throw IOException("$galleryID $index ${response.request().url()} CODE ${it.code()}") }.body()?.use { it.bytes() } ?: throw Exception("Response null")
|
||||
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
@@ -235,10 +243,11 @@ class DownloadService : Service() {
|
||||
startId?.let { stopSelf(it) }
|
||||
}
|
||||
}.onFailure {
|
||||
cancel(galleryID)
|
||||
download(galleryID)
|
||||
FirebaseCrashlytics.getInstance().recordException(it)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
FirebaseCrashlytics.getInstance().recordException(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,10 +312,10 @@ class DownloadService : Service() {
|
||||
|
||||
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
|
||||
@@ -314,7 +323,7 @@ class DownloadService : Service() {
|
||||
|
||||
histories.add(galleryID)
|
||||
|
||||
progress[galleryID] = MutableList(reader.galleryInfo.files.size) { 0F }
|
||||
progress[galleryID] = MutableList(galleryInfo.files.size) { 0F }
|
||||
|
||||
cache.metadata.imageList?.let {
|
||||
it.forEachIndexed { index, image ->
|
||||
@@ -332,7 +341,7 @@ class DownloadService : Service() {
|
||||
return@launch
|
||||
}
|
||||
|
||||
notification[galleryID]?.setContentTitle(reader.galleryInfo.title?.ellipsize(30))
|
||||
notification[galleryID]?.setContentTitle(galleryInfo.title?.ellipsize(30))
|
||||
notify(galleryID)
|
||||
|
||||
val queued = mutableSetOf<Int>()
|
||||
@@ -346,7 +355,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)
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -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,41 +21,42 @@ 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.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
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.hitomi.doSearch
|
||||
import xyz.quaver.pupil.hitomi.getGalleryIDsFromNozomi
|
||||
import xyz.quaver.pupil.hitomi.getSuggestionsForQuery
|
||||
import xyz.quaver.pupil.*
|
||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||
import xyz.quaver.pupil.databinding.MainActivityBinding
|
||||
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
|
||||
@@ -63,10 +64,7 @@ import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.restore
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.*
|
||||
|
||||
class MainActivity :
|
||||
BaseActivity(),
|
||||
@@ -105,18 +103,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()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -125,15 +125,35 @@ 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()
|
||||
|
||||
@@ -149,7 +169,7 @@ class MainActivity :
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.updateAll = false
|
||||
(binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
@@ -190,36 +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(main_noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) })
|
||||
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()
|
||||
@@ -247,7 +267,7 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
|
||||
with(main_fab_random) {
|
||||
with(binding.contents.randomFab) {
|
||||
setImageResource(R.drawable.shuffle_variant)
|
||||
setOnClickListener {
|
||||
runBlocking {
|
||||
@@ -277,7 +297,7 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
|
||||
with(main_fab_id) {
|
||||
with(binding.contents.idFab) {
|
||||
setImageResource(R.drawable.numeric)
|
||||
setOnClickListener {
|
||||
val editText = EditText(context).apply {
|
||||
@@ -310,6 +330,44 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -318,7 +376,7 @@ class MainActivity :
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun setupRecyclerView() {
|
||||
with(main_recyclerview) {
|
||||
with(binding.contents.recyclerview) {
|
||||
adapter = GalleryBlockAdapter(galleries).apply {
|
||||
onChipClickedHandler.add {
|
||||
runOnUiThread {
|
||||
@@ -359,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)
|
||||
@@ -377,7 +433,7 @@ class MainActivity :
|
||||
}
|
||||
|
||||
onItemLongClickListener = listener@{ _, position, v ->
|
||||
if (v !is CardView)
|
||||
if (v !is ProgressCard)
|
||||
return@listener false
|
||||
|
||||
val galleryID = galleries.getOrNull(position) ?: return@listener true
|
||||
@@ -400,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,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() {
|
||||
@@ -708,7 +563,7 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
|
||||
attachNavigationDrawerToMenuButton(main_drawer_layout)
|
||||
attachNavigationDrawerToMenuButton(this@MainActivity.binding.drawer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,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
|
||||
|
||||
@@ -760,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 -> {
|
||||
@@ -829,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) {
|
||||
@@ -856,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()
|
||||
@@ -934,7 +787,7 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,17 +802,21 @@ class MainActivity :
|
||||
}
|
||||
} 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 ->
|
||||
@@ -971,10 +828,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,17 +45,14 @@ 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.services.DownloadService
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
@@ -75,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
|
||||
@@ -118,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)
|
||||
@@ -161,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()
|
||||
}
|
||||
@@ -178,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()
|
||||
}
|
||||
|
||||
@@ -258,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
|
||||
}
|
||||
@@ -285,42 +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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -329,7 +322,7 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
with(reader_recyclerview) {
|
||||
with(binding.recyclerview) {
|
||||
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
|
||||
onItemClickListener = {
|
||||
if (isScroll) {
|
||||
@@ -339,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,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
|
||||
|
||||
@@ -359,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 {
|
||||
@@ -381,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 {
|
||||
@@ -408,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,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)
|
||||
@@ -434,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)
|
||||
@@ -446,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)
|
||||
|
||||
@@ -491,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)
|
||||
@@ -501,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
|
||||
@@ -514,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
|
||||
)
|
||||
)
|
||||
@@ -550,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
|
||||
@@ -566,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 {
|
||||
@@ -583,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,15 @@
|
||||
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.DownloadFolderNameDialogBinding
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.formatDownloadFolder
|
||||
@@ -37,38 +35,48 @@ import xyz.quaver.pupil.util.formatMap
|
||||
|
||||
class DownloadFolderNameDialogFragment : DialogFragment() {
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun build(): View {
|
||||
private var _binding: DownloadFolderNameDialogBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
_binding = DownloadFolderNameDialogBinding.inflate(layoutInflater)
|
||||
|
||||
initView()
|
||||
|
||||
return Dialog(requireContext()).apply {
|
||||
setContentView(binding.root)
|
||||
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()) }
|
||||
val galleryBlock = runBlocking {
|
||||
Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
|
||||
}
|
||||
|
||||
return layoutInflater.inflate(R.layout.dialog_download_folder_name, null).apply {
|
||||
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
|
||||
edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
|
||||
edittext.addTextChangedListener {
|
||||
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
|
||||
binding.message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
|
||||
binding.edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
|
||||
binding.edittext.addTextChangedListener {
|
||||
binding.message.text = requireContext().getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
|
||||
}
|
||||
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
|
||||
}
|
||||
ok_button.setOnClickListener {
|
||||
val newValue = edittext.text.toString()
|
||||
|
||||
if ((newValue as? String)?.contains("/") != false) {
|
||||
Snackbar.make(this, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
Preferences["download_folder_name"] = binding.edittext.text.toString()
|
||||
|
||||
Preferences["download_folder_name"] = edittext.text.toString()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||
Dialog(requireContext()).apply {
|
||||
setContentView(build())
|
||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,21 +31,17 @@ 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.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
|
||||
@@ -60,9 +55,12 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
|
||||
|
||||
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
|
||||
@@ -71,7 +69,7 @@ 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 {
|
||||
@@ -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,14 +104,14 @@ 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).apply {
|
||||
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))))
|
||||
@@ -124,10 +122,8 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -164,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 ->
|
||||
@@ -180,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 ->
|
||||
@@ -225,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
|
||||
|
||||
@@ -248,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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -94,7 +94,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,25 +21,28 @@ package xyz.quaver.pupil.ui.fragment
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.os.Bundle
|
||||
import android.os.LocaleList
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.preference.*
|
||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
||||
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.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(),
|
||||
@@ -87,12 +90,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 {
|
||||
@@ -100,19 +103,14 @@ class SettingsFragment :
|
||||
}
|
||||
lockLauncher.launch(intent)
|
||||
}
|
||||
"mirrors" -> {
|
||||
MirrorDialog(requireContext())
|
||||
.show()
|
||||
}
|
||||
"proxy" -> {
|
||||
ProxyDialog(requireContext())
|
||||
.show()
|
||||
ProxyDialogFragment().show(parentFragmentManager, "Proxy Dialog")
|
||||
}
|
||||
"user_id" -> {
|
||||
(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
|
||||
}
|
||||
@@ -126,7 +124,7 @@ class SettingsFragment :
|
||||
this ?: return false
|
||||
|
||||
when (key) {
|
||||
"tag_language" -> {
|
||||
"tag_translation" -> {
|
||||
updateTranslations()
|
||||
}
|
||||
"nomedia" -> {
|
||||
@@ -170,6 +168,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,7 +259,12 @@ class SettingsFragment :
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"tag_language" -> {
|
||||
"proxy" -> {
|
||||
summary = getProxyInfo().type.name
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"tag_translation" -> {
|
||||
this as ListPreference
|
||||
|
||||
isEnabled = false
|
||||
@@ -270,14 +285,6 @@ class SettingsFragment :
|
||||
onPreferenceChangeListener = this@SettingsFragment
|
||||
|
||||
}
|
||||
"mirrors" -> {
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"proxy" -> {
|
||||
summary = getProxyInfo().type.name
|
||||
|
||||
onPreferenceClickListener = 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
|
||||
@@ -61,8 +59,8 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
|
||||
|
||||
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(
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,7 +32,7 @@ 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 {
|
||||
val maxChipSize = 10
|
||||
const val maxChipSize = 10
|
||||
}
|
||||
|
||||
var maxChipSize: Int = Defaults.maxChipSize
|
||||
@@ -86,7 +86,7 @@ class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSe
|
||||
addView(it.await())
|
||||
}
|
||||
|
||||
if (maxChipSize > 0 && tags.size > maxChipSize && parent == null)
|
||||
if (maxChipSize > 0 && tags.size > maxChipSize)
|
||||
addView(moreView)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,13 @@
|
||||
|
||||
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
|
||||
import java.util.*
|
||||
|
||||
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
|
||||
|
||||
@@ -46,6 +48,8 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
|
||||
Json.decodeFromString(serializer, file.readText())
|
||||
}.onSuccess {
|
||||
set.addAll(it)
|
||||
}.onFailure {
|
||||
FirebaseCrashlytics.getInstance().recordException(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +61,6 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
|
||||
|
||||
@Synchronized
|
||||
override fun add(element: T): Boolean {
|
||||
load()
|
||||
|
||||
set.remove(element)
|
||||
|
||||
return set.add(element).also {
|
||||
@@ -68,8 +70,6 @@ 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 {
|
||||
@@ -79,8 +79,6 @@ 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()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ package xyz.quaver.pupil.util.downloader
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -32,24 +33,64 @@ 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.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
var galleryBlock: GalleryBlock? = null,
|
||||
var reader: Reader? = null,
|
||||
data class OldGalleryBlock(
|
||||
val code: String,
|
||||
val id: Int,
|
||||
val galleryUrl: String,
|
||||
val thumbnails: List<String>,
|
||||
val title: String,
|
||||
val artists: List<String>,
|
||||
val series: List<String>,
|
||||
val type: String,
|
||||
val language: String,
|
||||
val relatedTags: List<String>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OldReader(val code: String, val galleryInfo: GalleryInfo)
|
||||
|
||||
@Serializable
|
||||
data class OldMetadata(
|
||||
var galleryBlock: OldGalleryBlock? = null,
|
||||
var reader: OldReader? = null,
|
||||
var imageList: MutableList<String?>? = null
|
||||
) {
|
||||
fun copy(): Metadata = Metadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
||||
fun copy(): OldMetadata = OldMetadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
var galleryBlock: GalleryBlock? = null,
|
||||
var galleryInfo: GalleryInfo? = null,
|
||||
var imageList: MutableList<String?>? = null
|
||||
) {
|
||||
constructor(old: OldMetadata) : this(old.galleryBlock?.let { galleryBlock -> GalleryBlock(
|
||||
galleryBlock.id,
|
||||
galleryBlock.galleryUrl,
|
||||
galleryBlock.thumbnails,
|
||||
galleryBlock.title,
|
||||
galleryBlock.artists,
|
||||
galleryBlock.series,
|
||||
galleryBlock.type,
|
||||
galleryBlock.language,
|
||||
galleryBlock.relatedTags) },
|
||||
old.reader?.galleryInfo,
|
||||
old.imageList
|
||||
)
|
||||
|
||||
fun copy(): Metadata = Metadata(galleryBlock, galleryInfo, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
||||
}
|
||||
|
||||
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
|
||||
@@ -74,8 +115,12 @@ 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 ->
|
||||
kotlin.runCatching {
|
||||
Json.decodeFromString<Metadata>(metadata)
|
||||
}.getOrElse {
|
||||
Metadata(Json.decodeFromString<OldMetadata>(metadata))
|
||||
}
|
||||
}
|
||||
}.getOrNull() ?: Metadata()
|
||||
|
||||
@@ -110,27 +155,13 @@ 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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,41 +185,21 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
|
||||
}.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,21 +19,14 @@
|
||||
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 kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
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.getReferer
|
||||
import xyz.quaver.pupil.hitomi.imageUrlFromImage
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@@ -43,7 +36,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(" ")
|
||||
}
|
||||
@@ -105,27 +98,25 @@ fun GalleryBlock.formatDownloadFolderTest(format: String): String =
|
||||
}
|
||||
}.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 ?: 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/")
|
||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36")
|
||||
}
|
||||
}
|
||||
|
||||
fun String.ellipsize(n: Int): String =
|
||||
if (this.length > n)
|
||||
@@ -137,4 +128,11 @@ operator fun JsonElement.get(index: Int) =
|
||||
this.jsonArray[index]
|
||||
|
||||
operator fun JsonElement.get(tag: String) =
|
||||
this.jsonObject[tag]
|
||||
this.jsonObject[tag]
|
||||
|
||||
fun JsonElement.getOrNull(tag: String) = kotlin.runCatching {
|
||||
this.jsonObject.getOrDefault(tag, null)
|
||||
}.getOrNull()
|
||||
|
||||
val JsonElement.content
|
||||
get() = this.jsonPrimitive.contentOrNull
|
||||
@@ -45,7 +45,7 @@ fun updateTranslations() = CoroutineScope(Dispatchers.IO).launch {
|
||||
kotlin.runCatching {
|
||||
translations = Json.decodeFromString<Map<String, String>>(client.newCall(
|
||||
Request.Builder()
|
||||
.url(contentURL + "${Preferences["tag_language", ""]}.json")
|
||||
.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() }
|
||||
}
|
||||
|
||||
@@ -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,7 +159,7 @@ 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)
|
||||
@@ -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(this@migrate, 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,147 +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>
|
||||
|
||||
<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>
|
||||
|
||||
<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:gravity="center"
|
||||
android:text="@string/main_no_result"
|
||||
android:linksClickable="true"
|
||||
android:visibility="invisible"/>
|
||||
|
||||
<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,235 +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_constraintHeight_default="spread"
|
||||
app:layout_constraintHeight_min="200dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar_layout"
|
||||
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_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" />
|
||||
|
||||
<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_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_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>
|
||||
|
||||
</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>
|
||||
@@ -151,8 +151,10 @@
|
||||
<string name="no_camera">この機器には前面カメラが装着されていません</string>
|
||||
<string name="error">エラー</string>
|
||||
<string name="settings_cache_limit">キャッシュサイズ制限</string>
|
||||
<string name="settings_cache_unlimited">制限なし</string>
|
||||
<string name="settings_tag_language">タグ言語</string>
|
||||
<string name="settings_tag_language_message">Githubにて翻訳に参加できます</string>
|
||||
<string name="settings_concurrent_download">並列ダウンロード</string>
|
||||
<string name="unlimited">制限なし</string>
|
||||
<string name="settings_tag_translation">タグ言語</string>
|
||||
<string name="settings_tag_translation_message">Githubにて翻訳に参加できます</string>
|
||||
<string name="settings_max_concurrent_download">並列ダウンロード</string>
|
||||
<string name="unaccessible_download_folder">アンドロイド11以上では外部からのアプリ内部空間接近が不可能です。ダウンロードフォルダを変更しますか?</string>
|
||||
<string name="settings_networking">ネットワーク</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -151,8 +151,10 @@
|
||||
<string name="no_camera">이 장치에는 전면 카메라가 없습니다</string>
|
||||
<string name="error">오류</string>
|
||||
<string name="settings_cache_limit">캐시 크기 제한</string>
|
||||
<string name="settings_cache_unlimited">무제한</string>
|
||||
<string name="settings_tag_language">태그 언어</string>
|
||||
<string name="settings_tag_language_message">Github에서 번역에 참여하세요</string>
|
||||
<string name="settings_concurrent_download">병렬 다운로드</string>
|
||||
<string name="unlimited">무제한</string>
|
||||
<string name="settings_tag_translation">태그 언어</string>
|
||||
<string name="settings_tag_translation_message">Github에서 번역에 참여하세요</string>
|
||||
<string name="settings_max_concurrent_download">병렬 다운로드</string>
|
||||
<string name="unaccessible_download_folder">안드로이드 11 이상에서는 외부에서 현재 다운로드 폴더에 접근할 수 없습니다. 변경하시겠습니까?</string>
|
||||
<string name="settings_networking">네트워크</string>
|
||||
</resources>
|
||||
@@ -48,11 +48,6 @@
|
||||
<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>
|
||||
@@ -70,7 +65,7 @@
|
||||
</string-array>
|
||||
|
||||
<string-array name="cache_size_text">
|
||||
<item>@string/settings_cache_unlimited</item>
|
||||
<item>@string/unlimited</item>
|
||||
<item>1G</item>
|
||||
<item>2G</item>
|
||||
<item>4G</item>
|
||||
@@ -79,4 +74,24 @@
|
||||
<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>
|
||||
@@ -21,4 +21,11 @@
|
||||
<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>
|
||||
|
||||
@@ -28,7 +28,11 @@
|
||||
<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>
|
||||
@@ -45,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>
|
||||
@@ -69,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>
|
||||
@@ -162,7 +168,6 @@
|
||||
<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_cache_unlimited">Unlimited</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>
|
||||
@@ -172,14 +177,17 @@
|
||||
<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_tag_language">Tag Language</string>
|
||||
<string name="settings_concurrent_download">Concurrent Download</string>
|
||||
<string name="settings_tag_language_message">Participate in translation on Github</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>
|
||||
@@ -187,7 +195,6 @@
|
||||
<string name="settings_dark_mode_summary">Protect yourself against light attacks!</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 -->
|
||||
|
||||
@@ -1,5 +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"
|
||||
@@ -73,23 +74,30 @@
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:title="@string/settings_miscellaneous_title">
|
||||
|
||||
<ListPreference
|
||||
app:key="tag_language"
|
||||
app:title="@string/settings_tag_language"
|
||||
app:defaultValue="en"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
|
||||
<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,21 @@ package xyz.quaver.pupil
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import xyz.quaver.pupil.hitomi.getGalleryInfo
|
||||
import xyz.quaver.pupil.hitomi.imageUrlFromImage
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ExampleUnitTest {
|
||||
|
||||
@Test
|
||||
fun test() {
|
||||
val a = mutableSetOf<Int>()
|
||||
|
||||
print(a::class.java.methods.firstOrNull { it.name == "add" }?.genericParameterTypes?.firstOrNull() as? ParameterizedType)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
16
build.gradle
16
build.gradle
@@ -3,29 +3,29 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||
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.2"
|
||||
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.0"
|
||||
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/" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,4 +20,4 @@ kotlin.code.style=official
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
|
||||
kotlin_version=1.4.10
|
||||
kotlin_version=1.6.10
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
||||
|
||||
Reference in New Issue
Block a user