Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d5834821a | ||
|
|
ca077c4fee | ||
|
|
85d01f60f1 | ||
|
|
066d73b217 | ||
|
|
ba069d8f8e | ||
|
|
275684c9ce | ||
|
|
49d87a08d2 | ||
|
|
04c500f3d8 | ||
|
|
d05c1e4d08 | ||
|
|
bb63959678 | ||
|
|
842148647f | ||
|
|
19308d840a | ||
|
|
46bd1318cd | ||
|
|
9d1998fe52 | ||
|
|
a714a8230b | ||
|
|
b5432cd0b4 | ||
|
|
5634e94f3e | ||
|
|
c1a71b0db3 | ||
|
|
d93e7f8834 | ||
|
|
3175b2c45c | ||
|
|
547b6e8e3b | ||
|
|
d88ac27e72 | ||
|
|
e551a40d08 | ||
|
|
e810abe33a | ||
|
|
6172a73719 | ||
|
|
7455e68a45 | ||
|
|
748495ca64 | ||
|
|
f6d9c7f550 | ||
|
|
384e6c61b0 | ||
|
|
d49c9cec20 | ||
|
|
4b27f1aba1 | ||
|
|
a0a989c785 | ||
|
|
ecaecc1b91 | ||
|
|
938156aa71 | ||
|
|
d30c51bb3a | ||
|
|
874606bff9 | ||
|
|
07643e4b4c | ||
|
|
20bc5423cf | ||
|
|
b84cddffdc | ||
|
|
e46d1123df | ||
|
|
48f90faf4e | ||
|
|
615b52c4fa | ||
|
|
2c9c8e223c | ||
|
|
01a653835e | ||
|
|
9d80857a38 | ||
|
|
8a9ab6b36c | ||
|
|
4edc87c197 | ||
|
|
10712e6e62 | ||
|
|
d73dc19d3d | ||
|
|
c204353220 | ||
|
|
37123a2cd5 | ||
|
|
a39484b6ea | ||
|
|
e81b5a4e3a | ||
|
|
0b87c57fbf | ||
|
|
5fd985ba39 | ||
|
|
8c64548513 | ||
|
|
a6de64ceb9 | ||
|
|
16ebb437a3 | ||
|
|
683118a3f4 | ||
|
|
08e38ed45c | ||
|
|
7abf08f1fb | ||
|
|
f3019e9b84 | ||
|
|
9ea55664b6 | ||
|
|
c468764234 | ||
|
|
31c3178430 | ||
|
|
e81c189afc | ||
|
|
e0ccac13c1 | ||
|
|
93228459d7 | ||
|
|
63e07f56e0 | ||
|
|
ee87122bb2 | ||
|
|
290dda9018 | ||
|
|
1d3d78b936 | ||
|
|
a947bc6415 | ||
|
|
9ca891b2f5 | ||
|
|
48e0ebc8ae | ||
|
|
b323353006 | ||
|
|
c85d3ebe81 | ||
|
|
ce843abec8 | ||
|
|
6b43faa70e | ||
|
|
2d0c997b2e | ||
|
|
1db5118377 | ||
|
|
26b53ed7ac | ||
|
|
2c85ea6443 | ||
|
|
cbc2b30f47 | ||
|
|
0b58deb92c | ||
|
|
ed1cf23c91 | ||
|
|
6fbb644e4b | ||
|
|
774867502d | ||
|
|
c8b1439aeb | ||
|
|
38c16adffe | ||
|
|
18aede2701 | ||
|
|
c59d08a0a1 | ||
|
|
66ae29eb5b | ||
|
|
7d9cb3e150 | ||
|
|
9922a9f82a | ||
|
|
445b9b4673 | ||
|
|
0ef7b358e0 | ||
|
|
2d3fb75576 | ||
|
|
d55ff6d68e | ||
|
|
079654a9c7 | ||
|
|
30263c6260 | ||
|
|
3159c343c1 | ||
|
|
ceaa930623 | ||
|
|
6a8539106b | ||
|
|
7a24c3c08e | ||
|
|
251abeb090 | ||
|
|
a61fe9f98c | ||
|
|
d29c7bf91a | ||
|
|
ed4911c441 | ||
|
|
d40b4f3748 | ||
|
|
f3c4fe1914 | ||
|
|
55ee841bd0 | ||
|
|
657fb488ee | ||
|
|
4eef0b93fb | ||
|
|
f2be56435c | ||
|
|
fa6b3ad7ba | ||
|
|
52c05e6888 | ||
|
|
865bf0ba83 | ||
|
|
3f827d1bad | ||
|
|
0561d5f55c | ||
|
|
1bf2e1dacc | ||
|
|
db5a221b56 | ||
|
|
295285f132 | ||
|
|
5052b6c074 | ||
|
|
f98f45dc54 | ||
|
|
8d16950f46 | ||
|
|
74033b9f4a | ||
|
|
e497d47374 | ||
|
|
a97af59260 | ||
|
|
2197de98ea | ||
|
|
c004c7f71a | ||
|
|
69fc3ad4e8 | ||
|
|
678a8f0914 | ||
|
|
08c4c0bf1f | ||
|
|
f2a2656837 | ||
|
|
2011572270 | ||
|
|
3b682667e1 | ||
|
|
6da8de6463 | ||
|
|
039d415871 | ||
|
|
776f53bde0 | ||
|
|
58e535595e | ||
|
|
96ad5f6a6c | ||
|
|
043f7bedd8 | ||
|
|
8a58564812 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -16,4 +16,4 @@
|
||||
/gh-pages
|
||||
|
||||
#Private files
|
||||
/app/google-services.json
|
||||
**/google-services.json
|
||||
13
.idea/codeStyles/Project.xml
generated
13
.idea/codeStyles/Project.xml
generated
@@ -1,5 +1,9 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="RIGHT_MARGIN" value="120" />
|
||||
<AndroidXmlCodeStyleSettings>
|
||||
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
@@ -14,6 +18,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -24,6 +29,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -35,6 +41,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -45,6 +52,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -55,6 +63,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -65,6 +74,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -75,6 +85,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -86,6 +97,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -97,6 +109,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
|
||||
7
.idea/kotlinCodeInsightSettings.xml
generated
Normal file
7
.idea/kotlinCodeInsightSettings.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinCodeInsightWorkspaceSettings">
|
||||
<option name="addUnambiguousImportsOnTheFly" value="true" />
|
||||
<option name="optimizeImportsOnTheFly" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/kotlinc.xml
generated
Normal file
6
.idea/kotlinc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Kotlin2JvmCompilerArguments">
|
||||
<option name="jvmTarget" value="1.8" />
|
||||
</component>
|
||||
</project>
|
||||
27
README.md
27
README.md
@@ -1,2 +1,27 @@
|
||||
# Pupil
|
||||
Hitomi.la viewer for Android
|
||||
|
||||

|
||||
*Pupil, Hitomi.la viewer for Android*
|
||||
|
||||
# Screenshot
|
||||

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

|
||||
*Reader Screen*
|
||||
|
||||
Images are censored to be SFW
|
||||
|
||||
# Installation
|
||||
|
||||
Go [Releases page](https://github.com/tom5079/Pupil/releases) and get latest version or
|
||||
Visit [github page](https://tom5079.github.io/Pupil/) (only available in Korean)
|
||||
or Build app yourself
|
||||
|
||||
# Manual
|
||||
|
||||
[Manual](https://tom5079.github.io/Pupil/2019/06/06/manual-kr.html) is only available in Korean. Consider using translator.
|
||||
|
||||
# Contribution
|
||||
|
||||
Any kind of contribution is appriciated. Feel free to leave PR!
|
||||
|
||||
@@ -3,9 +3,15 @@ apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'io.fabric'
|
||||
apply plugin: 'com.google.firebase.firebase-perf'
|
||||
|
||||
if (file("src/google-services.json").exists() && file("src/debug/google-services.json").exists()) {
|
||||
logger.lifecycle("Firebase Enabled")
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'io.fabric'
|
||||
apply plugin: 'com.google.firebase.firebase-perf'
|
||||
} else {
|
||||
logger.lifecycle("Firebase Disabled")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
@@ -13,20 +19,27 @@ android {
|
||||
applicationId "xyz.quaver.pupil"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
versionCode 27
|
||||
versionName "3.2-beta2"
|
||||
versionCode 42
|
||||
versionName "4.6-beta1"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled true
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
debug {
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-DEBUG"
|
||||
|
||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
buildTypes.each {
|
||||
it.buildConfigField('boolean', 'PRERELEASE', 'true')
|
||||
it.buildConfigField('boolean', 'CENSOR', 'false')
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
@@ -36,6 +49,7 @@ android {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
buildToolsVersion = '29.0.2'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -44,33 +58,35 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0"
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.preference:preference:1.1.0-rc01'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha09'
|
||||
implementation 'com.google.firebase:firebase-core:17.1.0'
|
||||
implementation 'com.google.firebase:firebase-perf:19.0.0'
|
||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha04'
|
||||
implementation 'com.google.firebase:firebase-core:17.2.2'
|
||||
implementation 'com.google.firebase:firebase-perf:19.0.5'
|
||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
||||
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
||||
implementation 'com.github.clans:fab:1.6.4'
|
||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
||||
implementation ("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
|
||||
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
||||
implementation 'com.jsibbold:zoomage:1.3.0'
|
||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||
kapt 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
|
||||
11
app/proguard-rules.pro
vendored
11
app/proguard-rules.pro
vendored
@@ -18,4 +18,13 @@
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":25,"versionName":"3.1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":42,"versionName":"4.6-beta1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||
@@ -20,20 +20,21 @@
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import xyz.quaver.hitomi.fetchNozomi
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hiyobi.createImgList
|
||||
import xyz.quaver.hiyobi.getReader
|
||||
import xyz.quaver.hiyobi.user_agent
|
||||
import xyz.quaver.pupil.ui.LockActivity
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import java.net.URL
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
@@ -49,9 +50,6 @@ class ExampleInstrumentedTest {
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("xyz.quaver.pupil", appContext.packageName)
|
||||
|
||||
Log.d("Pupil", fetchNozomi().first.size.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -59,18 +57,18 @@ class ExampleInstrumentedTest {
|
||||
val activityTestRule = ActivityTestRule(LockActivity::class.java)
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
activityTestRule.launchActivity(Intent())
|
||||
|
||||
while(true);
|
||||
ContextCompat.getExternalFilesDirs(appContext, null).forEachIndexed { index, file ->
|
||||
Log.i("PUPILD", "$index: ${file?.absolutePath}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_doSearch() {
|
||||
val reader = getReader(1426382)
|
||||
val reader = getReader( 1426382)
|
||||
|
||||
val data: ByteArray
|
||||
|
||||
with(URL(reader.readerItems[0].url).openConnection() as HttpsURLConnection) {
|
||||
with(URL(createImgList(1426382, reader)[0].path).openConnection() as HttpsURLConnection) {
|
||||
setRequestProperty("User-Agent", user_agent)
|
||||
setRequestProperty("Cookie", cookie)
|
||||
|
||||
@@ -79,4 +77,37 @@ class ExampleInstrumentedTest {
|
||||
|
||||
Log.d("Pupil", data.size.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_downloadWorker() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
val galleryID = 515515
|
||||
|
||||
val worker = DownloadWorker.getInstance(context)
|
||||
|
||||
worker.queue.add(galleryID)
|
||||
|
||||
while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) {
|
||||
Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null")
|
||||
|
||||
if (worker.progress[galleryID]?.all { !it.isFinite() } == true)
|
||||
break
|
||||
}
|
||||
|
||||
Log.i("PUPILD", "DONE!!")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getReaderOrNull() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
val galleryID = 1561552
|
||||
|
||||
runBlocking {
|
||||
Log.i("PUPILD", Cache(context).getReader(galleryID)?.title ?: "null")
|
||||
}
|
||||
|
||||
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.title ?: "null")
|
||||
}
|
||||
}
|
||||
22
app/src/debug/res/values/strings.xml
Normal file
22
app/src/debug/res/values/strings.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Pupil, Hitomi.la viewer for Android
|
||||
~ Copyright (C) 2020 tom5079
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name" translatable="false" tools:override="true">Pupil-Debug</string>
|
||||
</resources>
|
||||
@@ -1,11 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="xyz.quaver.pupil">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".Pupil"
|
||||
@@ -15,9 +17,22 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
tools:replace="android:theme">
|
||||
|
||||
<activity android:name=".ui.LockActivity"/>
|
||||
<provider
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
|
||||
</provider>
|
||||
|
||||
<activity android:name=".ui.LockActivity" />
|
||||
<activity
|
||||
android:name=".ui.ReaderActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
@@ -40,18 +55,7 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="히요비.asia"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="xn--9w3b15m8vo.asia"
|
||||
android:host="hiyobi.me"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
@@ -84,20 +88,9 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="히요비.asia"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="xn--9w3b15m8vo.asia"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="http" />
|
||||
android:host="hiyobi.me"
|
||||
android:scheme="http"
|
||||
android:pathPrefix="/reader" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -125,6 +118,7 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -30,13 +30,13 @@ import androidx.preference.PreferenceManager
|
||||
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 xyz.quaver.pupil.util.Histories
|
||||
import java.io.File
|
||||
|
||||
class Pupil : MultiDexApplication() {
|
||||
|
||||
lateinit var histories: Histories
|
||||
lateinit var downloads: Histories
|
||||
lateinit var favorites: Histories
|
||||
|
||||
init {
|
||||
@@ -46,10 +46,23 @@ class Pupil : MultiDexApplication() {
|
||||
override fun onCreate() {
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
try {
|
||||
PreferenceManager.getDefaultSharedPreferences(this).getInt("dl_location", 0)
|
||||
} catch (e: Exception) {
|
||||
preference.edit().remove("dl_location").apply()
|
||||
}
|
||||
|
||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||
downloads = Histories(File(ContextCompat.getDataDir(this), "downloads.json"))
|
||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||
|
||||
if (BuildConfig.DEBUG)
|
||||
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
||||
|
||||
val file = preference.getString("dl_location", null)
|
||||
|
||||
if (file?.startsWith("content") == true)
|
||||
preference.edit().remove("dl_location").apply()
|
||||
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(this)
|
||||
} catch (e: GooglePlayServicesRepairableException) {
|
||||
@@ -58,21 +71,18 @@ class Pupil : MultiDexApplication() {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
if (!preference.getBoolean("channel_created", false)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = getString(R.string.channel_download_description)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_MIN).apply {
|
||||
description = getString(R.string.channel_download_description)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
}
|
||||
|
||||
preference.edit().putBoolean("channel_created", true).apply()
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
|
||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
|
||||
package xyz.quaver.pupil.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Base64
|
||||
import android.util.SparseBooleanArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -27,34 +29,33 @@ import android.widget.LinearLayout
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.daimajia.swipe.SwipeLayout
|
||||
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
||||
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.android.synthetic.main.item_galleryblock.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import xyz.quaver.pupil.util.getCachedGallery
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
||||
|
||||
enum class ViewType {
|
||||
NEXT,
|
||||
@@ -62,10 +63,58 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
PREV
|
||||
}
|
||||
|
||||
private val glide = Glide.with(context)
|
||||
private lateinit var favorites: Histories
|
||||
|
||||
inner class GalleryViewHolder(val view: CardView) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: Pair<GalleryBlock, Deferred<String>>) {
|
||||
val timer = Timer()
|
||||
|
||||
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||
var timerTask: TimerTask? = null
|
||||
|
||||
private fun updateProgress(context: Context, galleryID: Int) {
|
||||
val cache = Cache(context).getCachedGallery(galleryID)
|
||||
val reader = Cache(context).getReaderOrNull(galleryID)
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (reader == null) {
|
||||
view.galleryblock_progressbar.visibility = View.GONE
|
||||
view.galleryblock_progress_complete.visibility = View.GONE
|
||||
return@launch
|
||||
}
|
||||
|
||||
with(view.galleryblock_progressbar) {
|
||||
|
||||
progress = cache.listFiles()?.count { file ->
|
||||
Regex("^[0-9]+.+\$").matches(file.name)
|
||||
} ?: 0
|
||||
|
||||
if (visibility == View.GONE) {
|
||||
visibility = View.VISIBLE
|
||||
max = reader.galleryInfo.size
|
||||
}
|
||||
|
||||
if (progress == max) {
|
||||
if (completeFlag.get(galleryID, false)) {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageResource(R.drawable.ic_progressbar)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
|
||||
this?.start()
|
||||
})
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
completeFlag.put(galleryID, true)
|
||||
}
|
||||
} else
|
||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(galleryBlock: GalleryBlock) {
|
||||
with(view) {
|
||||
val resources = context.resources
|
||||
val languages = resources.getStringArray(R.array.languages).map {
|
||||
@@ -74,16 +123,23 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
val (galleryBlock: GalleryBlock, thumbnail: Deferred<String>) = item
|
||||
|
||||
val artists = galleryBlock.artists
|
||||
val series = galleryBlock.series
|
||||
|
||||
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
|
||||
it.start()
|
||||
})
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val cache = thumbnail.await()
|
||||
val thumbnail = Cache(context).getThumbnail(galleryBlock.id).let {
|
||||
if (it != null)
|
||||
Base64.decode(it, Base64.DEFAULT)
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
glide
|
||||
.load(cache)
|
||||
.load(thumbnail)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
@@ -95,75 +151,28 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
}
|
||||
|
||||
//Check cache
|
||||
val readerCache = { File(getCachedGallery(context, galleryBlock.id), "reader.json") }
|
||||
val imageCache = { File(getCachedGallery(context, galleryBlock.id), "images") }
|
||||
val cache = Cache(context).getCachedGallery(galleryBlock.id)
|
||||
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
|
||||
|
||||
try {
|
||||
Json(JsonConfiguration.Stable)
|
||||
.parse(Reader.serializer(), readerCache.invoke().readText())
|
||||
} catch(e: Exception) {
|
||||
readerCache.invoke().delete()
|
||||
}
|
||||
|
||||
if (readerCache.invoke().exists()) {
|
||||
val reader = Json(JsonConfiguration.Stable)
|
||||
.parse(Reader.serializer(), readerCache.invoke().readText())
|
||||
if (reader != null) {
|
||||
val count = cache.listFiles()?.count {
|
||||
Regex("^[0-9]+.+\$").matches(it.name)
|
||||
} ?: 0
|
||||
|
||||
with(galleryblock_progressbar) {
|
||||
max = reader.readerItems.size
|
||||
progress = imageCache.invoke().list()?.size ?: 0
|
||||
max = reader.galleryInfo.size
|
||||
progress = count
|
||||
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
} else
|
||||
galleryblock_progressbar.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (refreshTasks[this@GalleryViewHolder] == null) {
|
||||
val refresh = Timer(false).schedule(0, 1000) {
|
||||
post {
|
||||
with(view.galleryblock_progressbar) {
|
||||
progress = imageCache.invoke().list()?.size ?: 0
|
||||
|
||||
if (!readerCache.invoke().exists()) {
|
||||
visibility = View.GONE
|
||||
max = 0
|
||||
progress = 0
|
||||
|
||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||
} else {
|
||||
if (visibility == View.GONE) {
|
||||
val reader = Json(JsonConfiguration.Stable)
|
||||
.parse(Reader.serializer(), readerCache.invoke().readText())
|
||||
max = reader.readerItems.size
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (progress == max) {
|
||||
if (completeFlag.get(galleryBlock.id, false)) {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageResource(R.drawable.ic_progressbar)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
|
||||
this?.start()
|
||||
})
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
completeFlag.put(galleryBlock.id, true)
|
||||
}
|
||||
} else
|
||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
if (timerTask == null)
|
||||
timerTask = timer.schedule(0, 1000) {
|
||||
updateProgress(context, galleryBlock.id)
|
||||
}
|
||||
|
||||
refreshTasks[this@GalleryViewHolder] = refresh
|
||||
}
|
||||
|
||||
galleryblock_title.text = galleryBlock.title
|
||||
with(galleryblock_artist) {
|
||||
text = artists.joinToString(", ") { it.wordCapitalize() }
|
||||
@@ -216,6 +225,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
else -> null
|
||||
}
|
||||
text = tag.tag.wordCapitalize()
|
||||
setEnsureMinTouchTargetSize(false)
|
||||
setOnClickListener {
|
||||
for (callback in onChipClickedHandler)
|
||||
callback.invoke(tag)
|
||||
@@ -272,10 +282,11 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
}
|
||||
}
|
||||
|
||||
private val refreshTasks = HashMap<GalleryViewHolder, TimerTask>()
|
||||
val completeFlag = SparseBooleanArray()
|
||||
|
||||
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
||||
var onDownloadClickedHandler: ((Int) -> Unit)? = null
|
||||
var onDeleteClickedHandler: ((Int) -> Unit)? = null
|
||||
|
||||
var showNext = false
|
||||
var showPrev = false
|
||||
@@ -301,18 +312,56 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is GalleryViewHolder)
|
||||
holder.bind(galleries[position-(if (showPrev) 1 else 0)])
|
||||
if (holder is GalleryViewHolder) {
|
||||
val gallery = galleries[position-(if (showPrev) 1 else 0)]
|
||||
|
||||
holder.bind(gallery)
|
||||
|
||||
with(holder.view.galleryblock_primary) {
|
||||
setOnClickListener {
|
||||
holder.view.performClick()
|
||||
}
|
||||
setOnLongClickListener {
|
||||
holder.view.performLongClick()
|
||||
}
|
||||
}
|
||||
|
||||
holder.view.galleryblock_download.setOnClickListener {
|
||||
onDownloadClickedHandler?.invoke(position)
|
||||
}
|
||||
|
||||
holder.view.galleryblock_delete.setOnClickListener {
|
||||
onDeleteClickedHandler?.invoke(position)
|
||||
}
|
||||
|
||||
mItemManger.bindView(holder.view, position)
|
||||
|
||||
holder.view.galleryblock_swipe_layout.addSwipeListener(object: SwipeLayout.SwipeListener {
|
||||
override fun onStartOpen(layout: SwipeLayout?) {
|
||||
mItemManger.closeAllExcept(layout)
|
||||
|
||||
holder.view.galleryblock_download.text =
|
||||
if (DownloadWorker.getInstance(holder.view.context).progress.indexOfKey(gallery.id) < 0)
|
||||
holder.view.context.getString(R.string.main_download)
|
||||
else
|
||||
holder.view.context.getString(android.R.string.cancel)
|
||||
}
|
||||
|
||||
override fun onClose(layout: SwipeLayout?) {}
|
||||
override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {}
|
||||
override fun onOpen(layout: SwipeLayout?) {}
|
||||
override fun onStartClose(layout: SwipeLayout?) {}
|
||||
override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
||||
super.onViewDetachedFromWindow(holder)
|
||||
|
||||
if (holder is GalleryViewHolder) {
|
||||
val task = refreshTasks[holder] ?: return
|
||||
|
||||
task.cancel()
|
||||
refreshTasks.remove(holder)
|
||||
holder.timerTask?.cancel()
|
||||
holder.timerTask = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,4 +377,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
else -> ViewType.GALLERY
|
||||
}.ordinal
|
||||
}
|
||||
|
||||
override fun getSwipeLayoutResourceId(position: Int) = R.id.galleryblock_swipe_layout
|
||||
}
|
||||
85
app/src/main/java/xyz/quaver/pupil/adapters/MirrorAdapter.kt
Normal file
85
app/src/main/java/xyz/quaver/pupil/adapters/MirrorAdapter.kt
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.item_mirrors.view.*
|
||||
import xyz.quaver.pupil.R
|
||||
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 {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString("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
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
with(holder.view) {
|
||||
mirror_name.text = mirrors[list.elementAt(position)]
|
||||
mirror_button.setOnTouchListener { _, event ->
|
||||
if (event.action == MotionEvent.ACTION_DOWN)
|
||||
onStartDrag?.invoke(holder)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.item_mirrors, parent, false
|
||||
).let {
|
||||
ViewHolder(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = mirrors.size
|
||||
|
||||
}
|
||||
@@ -18,29 +18,79 @@
|
||||
|
||||
package xyz.quaver.pupil.adapters
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.ListPreloader
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import io.fabric.sdk.android.Fabric
|
||||
import kotlinx.android.synthetic.main.item_reader.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.getCachedGallery
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ReaderAdapter(private val glide: RequestManager,
|
||||
private val galleryID: Int,
|
||||
private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||
class ReaderAdapter(private val context: Context,
|
||||
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||
|
||||
val glide = Glide.with(context)
|
||||
|
||||
//region Glide.RecyclerView
|
||||
val sizeProvider = ListPreloader.PreloadSizeProvider<File> { _, _, position ->
|
||||
Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.getOrNull(position)?.let {
|
||||
arrayOf(it.width, it.height).toIntArray()
|
||||
}
|
||||
}
|
||||
val modelProvider = object: ListPreloader.PreloadModelProvider<File> {
|
||||
override fun getPreloadItems(position: Int): MutableList<File> {
|
||||
return listOf(Cache(context).getImages(galleryID)?.get(position)).filterNotNullTo(mutableListOf())
|
||||
}
|
||||
|
||||
override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? {
|
||||
return glide
|
||||
.load(item)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
|
||||
//endregion
|
||||
|
||||
var reader: Reader? = null
|
||||
val timer = Timer()
|
||||
|
||||
var isFullScreen = false
|
||||
private var prev : Drawable? = null
|
||||
|
||||
var onItemClickListener : ((Int) -> (Unit))? = null
|
||||
|
||||
init {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
reader = Cache(context).getReader(galleryID)
|
||||
launch(Dispatchers.Main) {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
||||
|
||||
@@ -53,43 +103,74 @@ class ReaderAdapter(private val glide: RequestManager,
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.view as ImageView
|
||||
holder.view as ConstraintLayout
|
||||
|
||||
glide
|
||||
.load(File(getCachedGallery(holder.view.context, galleryID), images[position]))
|
||||
.dontTransform()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
if (isFullScreen)
|
||||
placeholder(prev)
|
||||
}
|
||||
.listener(object: RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
) = false
|
||||
if (isFullScreen) {
|
||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
||||
holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
||||
} else {
|
||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
|
||||
holder.view.container.layoutParams.height = 0
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
prev = resource?.constantState?.newDrawable()?.mutate()
|
||||
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
||||
onItemClickListener?.invoke(position)
|
||||
}
|
||||
|
||||
return false
|
||||
holder.view.container.setOnClickListener {
|
||||
onItemClickListener?.invoke(position)
|
||||
}
|
||||
|
||||
if (!isFullScreen)
|
||||
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
||||
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
|
||||
|
||||
holder.view.reader_index.text = (position+1).toString()
|
||||
|
||||
val images = Cache(context).getImages(galleryID)
|
||||
|
||||
if (images?.get(position) != null) {
|
||||
glide
|
||||
.load(images[position])
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.dontTransform()
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
}
|
||||
})
|
||||
.into(holder.view)
|
||||
.into(holder.view.image)
|
||||
} else {
|
||||
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
||||
|
||||
if (progress?.isNaN() == true) {
|
||||
if (Fabric.isInitialized())
|
||||
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
|
||||
|
||||
glide
|
||||
.load(R.drawable.image_broken_variant)
|
||||
.into(holder.view.image)
|
||||
|
||||
return
|
||||
} else {
|
||||
holder.view.reader_item_progressbar.progress =
|
||||
if (progress?.isInfinite() == true)
|
||||
100
|
||||
else
|
||||
progress?.roundToInt() ?: 0
|
||||
|
||||
holder.view.image.setImageDrawable(null)
|
||||
}
|
||||
|
||||
timer.schedule(1000) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = images.size
|
||||
override fun getItemCount() = reader?.galleryInfo?.size ?: 0
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_lock.*
|
||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
|
||||
|
||||
@@ -18,23 +18,19 @@
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.*
|
||||
import android.text.style.AlignmentSpan
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
@@ -42,7 +38,6 @@ import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
@@ -51,33 +46,29 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.arlib.floatingsearchview.FloatingSearchView
|
||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import com.arlib.floatingsearchview.util.view.SearchInputView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.fabric.sdk.android.Fabric
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.activity_main_content.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.content
|
||||
import kotlinx.serialization.list
|
||||
import kotlinx.serialization.stringify
|
||||
import ru.noties.markwon.Markwon
|
||||
import xyz.quaver.hitomi.*
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.doSearch
|
||||
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
||||
import xyz.quaver.hitomi.getSuggestionsForQuery
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.types.TagSuggestion
|
||||
import xyz.quaver.pupil.types.Tags
|
||||
import xyz.quaver.pupil.ui.dialog.GalleryDialog
|
||||
import xyz.quaver.pupil.util.*
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
@@ -98,7 +89,7 @@ class MainActivity : AppCompatActivity() {
|
||||
POPULAR
|
||||
}
|
||||
|
||||
private val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>()
|
||||
private val galleries = ArrayList<GalleryBlock>()
|
||||
|
||||
private var query = ""
|
||||
set(value) {
|
||||
@@ -121,7 +112,6 @@ class MainActivity : AppCompatActivity() {
|
||||
private var currentPage = 0
|
||||
|
||||
private lateinit var histories: Histories
|
||||
private lateinit var downloads: Histories
|
||||
private lateinit var favorites: Histories
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -130,6 +120,7 @@ class MainActivity : AppCompatActivity() {
|
||||
val lockManager = try {
|
||||
LockManager(this)
|
||||
} catch (e: Exception) {
|
||||
Log.i("PUPILD", e.toString())
|
||||
android.app.AlertDialog.Builder(this).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.lock_corrupted)
|
||||
@@ -137,14 +128,13 @@ class MainActivity : AppCompatActivity() {
|
||||
finish()
|
||||
}
|
||||
}.show()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (lockManager.isNotEmpty())
|
||||
startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK)
|
||||
|
||||
checkPermissions()
|
||||
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
if (Locale.getDefault().language == "ko") {
|
||||
@@ -161,13 +151,12 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
with(application as Pupil) {
|
||||
this@MainActivity.histories = histories
|
||||
this@MainActivity.downloads = downloads
|
||||
this@MainActivity.favorites = favorites
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
checkUpdate()
|
||||
checkUpdate(this)
|
||||
|
||||
initView()
|
||||
}
|
||||
@@ -187,6 +176,12 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
@@ -256,125 +251,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkUpdate() {
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
|
||||
|
||||
if (ignoreUpdateUntil > System.currentTimeMillis())
|
||||
return
|
||||
|
||||
fun extractReleaseNote(update: JsonObject, locale: String) : String {
|
||||
val markdown = update["body"]!!.content
|
||||
|
||||
val target = when(locale) {
|
||||
"ko" -> "한국어"
|
||||
"ja" -> "日本語"
|
||||
else -> "English"
|
||||
}
|
||||
|
||||
val releaseNote = Regex("^# Release Note.+$")
|
||||
val language = Regex("^## $target$")
|
||||
val end = Regex("^#.+$")
|
||||
|
||||
var releaseNoteFlag = false
|
||||
var languageFlag = false
|
||||
|
||||
val result = StringBuilder()
|
||||
|
||||
for(line in markdown.lines()) {
|
||||
if (releaseNote.matches(line)) {
|
||||
releaseNoteFlag = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (releaseNoteFlag) {
|
||||
if (language.matches(line)) {
|
||||
languageFlag = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (languageFlag) {
|
||||
if (end.matches(line))
|
||||
break
|
||||
|
||||
result.append(line+"\n")
|
||||
}
|
||||
}
|
||||
|
||||
return getString(R.string.update_release_note, update["tag_name"]?.content, result.toString())
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val update =
|
||||
checkUpdate(getString(R.string.release_url)) ?: return@launch
|
||||
|
||||
val (url, fileName) = getApkUrl(update) ?: return@launch
|
||||
fileName ?: return@launch
|
||||
|
||||
val dialog = AlertDialog.Builder(this@MainActivity).apply {
|
||||
setTitle(R.string.update_title)
|
||||
val msg = extractReleaseNote(update, Locale.getDefault().language)
|
||||
setMessage(Markwon.create(context).toMarkdown(msg))
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
if (!this@MainActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
AlertDialog.Builder(this@MainActivity).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.update_no_permission)
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
||||
val request = DownloadManager.Request(Uri.parse(url)).apply {
|
||||
setDescription(getString(R.string.update_notification_description))
|
||||
setTitle(getString(R.string.app_name))
|
||||
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
|
||||
}
|
||||
|
||||
val manager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
val id = manager.enqueue(request)
|
||||
|
||||
registerReceiver(object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
try {
|
||||
val install = Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
setDataAndType(manager.getUriForDownloadedFile(id), manager.getMimeTypeForDownloadedFile(id))
|
||||
}
|
||||
|
||||
startActivity(install)
|
||||
unregisterReceiver(this)
|
||||
} catch (e: Exception) {
|
||||
AlertDialog.Builder(this@MainActivity).apply {
|
||||
setTitle(R.string.update_failed)
|
||||
setMessage(R.string.update_failed_message)
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
|
||||
}
|
||||
setNegativeButton(R.string.ignore_update) { _, _ ->
|
||||
preferences.edit()
|
||||
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermissions() {
|
||||
if (this.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 13489)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
var prevP1 = 0
|
||||
main_appbar_layout.addOnOffsetChangedListener(
|
||||
@@ -448,7 +324,7 @@ class MainActivity : AppCompatActivity() {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
|
||||
}
|
||||
R.id.main_drawer_kakaotalk -> {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.kakaotalk))))
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -495,21 +371,14 @@ class MainActivity : AppCompatActivity() {
|
||||
setTitle(R.string.main_open_gallery_by_id)
|
||||
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
try {
|
||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
||||
val gallery =
|
||||
getGalleryBlock(editText.text.toString().toInt()) ?: throw Exception()
|
||||
intent.putExtra("galleryID", gallery.id)
|
||||
|
||||
startActivity(intent)
|
||||
|
||||
histories.add(gallery.id)
|
||||
} catch (e: Exception) {
|
||||
Snackbar.make(main_layout,
|
||||
R.string.main_open_gallery_by_id_error, Snackbar.LENGTH_LONG).show()
|
||||
val galleryID = editText.text.toString().toInt()
|
||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryID", galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
|
||||
histories.add(galleryID)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
@@ -523,7 +392,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
with(main_recyclerview) {
|
||||
adapter = GalleryBlockAdapter(Glide.with(this@MainActivity), galleries).apply {
|
||||
adapter = GalleryBlockAdapter(this@MainActivity, galleries).apply {
|
||||
onChipClickedHandler.add {
|
||||
runOnUiThread {
|
||||
query = it.toQuery()
|
||||
@@ -535,6 +404,48 @@ class MainActivity : AppCompatActivity() {
|
||||
loadBlocks()
|
||||
}
|
||||
}
|
||||
onDownloadClickedHandler = { position ->
|
||||
val galleryID = galleries[position].id
|
||||
|
||||
if (!completeFlag.get(galleryID, false)) {
|
||||
val worker = DownloadWorker.getInstance(context)
|
||||
|
||||
if (worker.progress.indexOfKey(galleryID) >= 0) //download in progress
|
||||
worker.cancel(galleryID)
|
||||
else {
|
||||
Cache(context).setDownloading(galleryID, true)
|
||||
|
||||
if (!worker.queue.contains(galleryID))
|
||||
worker.queue.add(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
closeAllItems()
|
||||
}
|
||||
|
||||
onDeleteClickedHandler = { position ->
|
||||
val galleryID = galleries[position].id
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
DownloadWorker.getInstance(context).cancel(galleryID)
|
||||
|
||||
Cache(context).getCachedGallery(galleryID).deleteRecursively()
|
||||
|
||||
histories.remove(galleryID)
|
||||
|
||||
if (this@MainActivity.mode != Mode.SEARCH)
|
||||
runOnUiThread {
|
||||
cancelFetch()
|
||||
clearGalleries()
|
||||
fetchGalleries(query, sortMode)
|
||||
loadBlocks()
|
||||
}
|
||||
|
||||
completeFlag.put(galleryID, false)
|
||||
}
|
||||
|
||||
closeAllItems()
|
||||
}
|
||||
}
|
||||
ItemClickSupport.addTo(this)
|
||||
.setOnItemClickListener { _, position, v ->
|
||||
@@ -542,7 +453,7 @@ class MainActivity : AppCompatActivity() {
|
||||
return@setOnItemClickListener
|
||||
|
||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
||||
val gallery = galleries[position].first
|
||||
val gallery = galleries[position]
|
||||
intent.putExtra("galleryID", gallery.id)
|
||||
|
||||
//TODO: Maybe sprinkling some transitions will be nice :D
|
||||
@@ -554,9 +465,12 @@ class MainActivity : AppCompatActivity() {
|
||||
if (v !is CardView)
|
||||
return@setOnItemLongClickListener true
|
||||
|
||||
val galleryID = galleries[position].first.id
|
||||
val galleryID = galleries[position].id
|
||||
|
||||
GalleryDialog(this@MainActivity, galleryID).apply {
|
||||
GalleryDialog(
|
||||
this@MainActivity,
|
||||
galleryID
|
||||
).apply {
|
||||
onChipClickedHandler.add {
|
||||
runOnUiThread {
|
||||
query = it.toQuery()
|
||||
@@ -779,7 +693,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private var suggestionJob : Job? = null
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
private fun setupSearchBar() {
|
||||
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
||||
//Change upper case letters to lower case
|
||||
@@ -799,15 +712,15 @@ class MainActivity : AppCompatActivity() {
|
||||
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
|
||||
}
|
||||
})
|
||||
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
|
||||
|
||||
with(main_searchview as FloatingSearchView) {
|
||||
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
|
||||
val json = Json(JsonConfiguration.Stable)
|
||||
val serializer = Tag.serializer().list
|
||||
|
||||
if (!favoritesFile.exists()) {
|
||||
favoritesFile.createNewFile()
|
||||
favoritesFile.writeText(json.stringify(Tags(listOf())))
|
||||
favoritesFile.writeText(json.stringify(serializer, Tags(listOf())))
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener {
|
||||
@@ -925,7 +838,7 @@ class MainActivity : AppCompatActivity() {
|
||||
favorites.add(tag)
|
||||
}
|
||||
|
||||
favoritesFile.writeText(json.stringify(favorites))
|
||||
favoritesFile.writeText(json.stringify(serializer, favorites))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1049,8 +962,14 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
Mode.DOWNLOAD -> {
|
||||
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
|
||||
file.isDirectory && (file.name.toIntOrNull() != null) && File(file, ".metadata").exists()
|
||||
}?.map {
|
||||
it.name.toInt()
|
||||
} ?: emptyList()
|
||||
|
||||
when {
|
||||
query.isEmpty() -> downloads.toList().apply {
|
||||
query.isEmpty() -> downloads.apply {
|
||||
totalItems = size
|
||||
}
|
||||
else -> {
|
||||
@@ -1083,9 +1002,13 @@ class MainActivity : AppCompatActivity() {
|
||||
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
|
||||
|
||||
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
val galleryIDs = galleryIDs?.await()
|
||||
val galleryIDs = try {
|
||||
galleryIDs!!.await()
|
||||
} catch (e: Exception) {
|
||||
|
||||
if (Fabric.isInitialized())
|
||||
Crashlytics.logException(e)
|
||||
|
||||
if (galleryIDs.isNullOrEmpty()) { //No result
|
||||
withContext(Dispatchers.Main) {
|
||||
main_noresult.visibility = View.VISIBLE
|
||||
main_progressbar.hide()
|
||||
@@ -1098,48 +1021,7 @@ class MainActivity : AppCompatActivity() {
|
||||
for (chunk in chunks)
|
||||
chunk.map { galleryID ->
|
||||
async {
|
||||
try {
|
||||
val json = Json(JsonConfiguration.Stable)
|
||||
val serializer = GalleryBlock.serializer()
|
||||
|
||||
val galleryBlock =
|
||||
File(getCachedGallery(this@MainActivity, galleryID), "galleryBlock.json").let { cache ->
|
||||
when {
|
||||
cache.exists() -> json.parse(serializer, cache.readText())
|
||||
else -> {
|
||||
getGalleryBlock(galleryID).apply {
|
||||
this ?: return@apply
|
||||
|
||||
if (cache.parentFile?.exists() == false)
|
||||
cache.parentFile!!.mkdirs()
|
||||
|
||||
cache.writeText(json.stringify(serializer, this))
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: return@async null
|
||||
|
||||
val thumbnail = async {
|
||||
val ext = galleryBlock.thumbnails[0].split('.').last()
|
||||
File(getCachedGallery(this@MainActivity, galleryBlock.id), "thumbnail.$ext").apply {
|
||||
if (!exists())
|
||||
try {
|
||||
with(URL(galleryBlock.thumbnails[0]).openConnection() as HttpsURLConnection) {
|
||||
if (this@apply.parentFile?.exists() == false)
|
||||
this@apply.parentFile!!.mkdirs()
|
||||
|
||||
inputStream.copyTo(FileOutputStream(this@apply))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
delete()
|
||||
}
|
||||
}.absolutePath
|
||||
}
|
||||
|
||||
Pair(galleryBlock, thumbnail)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
Cache(this@MainActivity).getGalleryBlock(galleryID)
|
||||
}
|
||||
}.forEach {
|
||||
val galleryBlock = it.await()
|
||||
|
||||
@@ -18,44 +18,39 @@
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.bumptech.glide.Glide
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.fabric.sdk.android.Fabric
|
||||
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.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
||||
import xyz.quaver.pupil.util.GalleryDownloader
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import xyz.quaver.pupil.util.ItemClickSupport
|
||||
import xyz.quaver.pupil.util.hasPermission
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
private var galleryID = 0
|
||||
private val images = ArrayList<String>()
|
||||
private var gallerySize = 0
|
||||
private var currentPage = 0
|
||||
|
||||
private var isScroll = true
|
||||
@@ -71,7 +66,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var downloader: GalleryDownloader
|
||||
private val timer = Timer()
|
||||
|
||||
private val snapHelper = PagerSnapHelper()
|
||||
|
||||
@@ -95,19 +90,16 @@ class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
handleIntent(intent)
|
||||
|
||||
Crashlytics.setInt("GalleryID", galleryID)
|
||||
if (Fabric.isInitialized())
|
||||
Crashlytics.setInt("GalleryID", galleryID)
|
||||
|
||||
if (galleryID == 0) {
|
||||
onBackPressed()
|
||||
return
|
||||
}
|
||||
|
||||
initDownloader()
|
||||
|
||||
initView()
|
||||
|
||||
if (!downloader.download)
|
||||
downloader.start()
|
||||
initDownloader()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
@@ -148,7 +140,6 @@ class ReaderActivity : AppCompatActivity() {
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.reader, menu)
|
||||
|
||||
@@ -169,7 +160,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
|
||||
with(view.dialog_number_picker) {
|
||||
minValue=1
|
||||
maxValue=gallerySize
|
||||
maxValue=reader_recyclerview?.adapter?.itemCount ?: 0
|
||||
value=currentPage
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this).apply {
|
||||
@@ -202,8 +193,11 @@ class ReaderActivity : AppCompatActivity() {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (::downloader.isInitialized && !downloader.download)
|
||||
downloader.cancel()
|
||||
timer.cancel()
|
||||
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
|
||||
|
||||
if (!Cache(this).isDownloading(galleryID))
|
||||
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
@@ -239,102 +233,68 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun initDownloader() {
|
||||
var d: GalleryDownloader? = GalleryDownloader.get(galleryID)
|
||||
|
||||
if (d == null)
|
||||
d = GalleryDownloader(this, galleryID)
|
||||
|
||||
downloader = d.apply {
|
||||
onReaderLoadedHandler = {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
title = it.title
|
||||
with(reader_download_progressbar) {
|
||||
max = it.readerItems.size
|
||||
progress = 0
|
||||
}
|
||||
with(reader_progressbar) {
|
||||
max = it.readerItems.size
|
||||
progress = 0
|
||||
}
|
||||
|
||||
gallerySize = it.readerItems.size
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.readerItems.size}"
|
||||
}
|
||||
}
|
||||
onProgressHandler = {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
reader_download_progressbar.progress = it
|
||||
menu?.findItem(R.id.reader_menu_use_hiyobi)?.isVisible = downloader.useHiyobi
|
||||
}
|
||||
}
|
||||
onDownloadedHandler = {
|
||||
val item = it.toList()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (images.isEmpty()) {
|
||||
images.addAll(item)
|
||||
reader_recyclerview.adapter?.notifyDataSetChanged()
|
||||
} else {
|
||||
images.add(item.last())
|
||||
reader_recyclerview.adapter?.notifyItemInserted(images.size-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
onErrorHandler = {
|
||||
Snackbar
|
||||
.make(reader_layout, it.message ?: it.javaClass.name, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.reader_help) { _ ->
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.error_help))))
|
||||
}
|
||||
.show()
|
||||
downloader.download = false
|
||||
}
|
||||
onCompleteHandler = {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
reader_download_progressbar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
onNotifyChangedHandler = { notify ->
|
||||
val fab = reader_fab_download
|
||||
|
||||
runOnUiThread {
|
||||
if (notify) {
|
||||
val icon = AnimatedVectorDrawableCompat.create(this, R.drawable.ic_downloading)
|
||||
icon?.registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
|
||||
override fun onAnimationEnd(drawable: Drawable?) {
|
||||
if (downloader.download)
|
||||
fab.post {
|
||||
icon.start()
|
||||
fab.labelText = getString(R.string.reader_fab_download_cancel)
|
||||
}
|
||||
else
|
||||
fab.post {
|
||||
fab.setImageResource(R.drawable.ic_download)
|
||||
fab.labelText = getString(R.string.reader_fab_download)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fab.setImageDrawable(icon)
|
||||
icon?.start()
|
||||
} else {
|
||||
runOnUiThread {
|
||||
fab.setImageResource(R.drawable.ic_download)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val worker = DownloadWorker.getInstance(this).apply {
|
||||
queue.add(galleryID)
|
||||
}
|
||||
|
||||
if (downloader.download) {
|
||||
downloader.invokeOnReaderLoaded()
|
||||
downloader.invokeOnNotifyChanged()
|
||||
timer.schedule(1000, 1000) {
|
||||
if (worker.progress.indexOfKey(galleryID) < 0) //loading
|
||||
return@schedule
|
||||
|
||||
if (worker.progress[galleryID] == null) { //Gallery not found
|
||||
timer.cancel()
|
||||
Snackbar
|
||||
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
||||
.show()
|
||||
}
|
||||
|
||||
runOnUiThread {
|
||||
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||
reader_download_progressbar.progress = worker.progress[galleryID]?.count { !it.isFinite() } ?: 0
|
||||
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||
|
||||
if (title == getString(R.string.reader_loading)) {
|
||||
val reader = (reader_recyclerview.adapter as ReaderAdapter).reader
|
||||
|
||||
if (reader != null) {
|
||||
title = reader.title
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) { //Download finished
|
||||
reader_download_progressbar.visibility = View.GONE
|
||||
|
||||
animateDownloadFAB(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
with(reader_recyclerview) {
|
||||
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, images)
|
||||
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
|
||||
onItemClickListener = {
|
||||
if (isScroll) {
|
||||
isScroll = false
|
||||
isFullscreen = true
|
||||
|
||||
scrollMode(false)
|
||||
fullscreen(true)
|
||||
} else {
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//addOnScrollListener((adapter as ReaderAdapter).preloader)
|
||||
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
@@ -349,43 +309,24 @@ class ReaderActivity : AppCompatActivity() {
|
||||
if (layoutManager.findFirstVisibleItemPosition() == -1)
|
||||
return
|
||||
currentPage = layoutManager.findFirstVisibleItemPosition()+1
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/$gallerySize"
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
|
||||
this@ReaderActivity.reader_progressbar.progress = currentPage
|
||||
}
|
||||
})
|
||||
|
||||
ItemClickSupport.addTo(this)
|
||||
.setOnItemClickListener { _, _, _ ->
|
||||
if (isScroll) {
|
||||
isScroll = false
|
||||
isFullscreen = true
|
||||
|
||||
scrollMode(false)
|
||||
fullscreen(true)
|
||||
} else {
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with(reader_fab_download) {
|
||||
setImageResource(R.drawable.ic_download)
|
||||
animateDownloadFAB(Cache(context).isDownloading(galleryID)) //If download in progress, animate button
|
||||
|
||||
setOnClickListener {
|
||||
if (Cache(context).isDownloading(galleryID)) {
|
||||
Cache(context).setDownloading(galleryID, false)
|
||||
|
||||
if (!this@ReaderActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
AlertDialog.Builder(this@ReaderActivity).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.update_no_permission)
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
return@setOnClickListener
|
||||
animateDownloadFAB(false)
|
||||
} else {
|
||||
Cache(context).setDownloading(galleryID, true)
|
||||
animateDownloadFAB(true)
|
||||
}
|
||||
|
||||
downloader.download = !downloader.download
|
||||
|
||||
if (!downloader.download)
|
||||
downloader.clearNotification()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,4 +368,34 @@ class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
||||
}
|
||||
|
||||
private fun animateDownloadFAB(animate: Boolean) {
|
||||
with(reader_fab_download) {
|
||||
if (animate) {
|
||||
val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
|
||||
|
||||
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
|
||||
override fun onAnimationEnd(drawable: Drawable?) {
|
||||
val worker = DownloadWorker.getInstance(context)
|
||||
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
|
||||
post {
|
||||
setImageResource(R.drawable.ic_download)
|
||||
labelText = getString(R.string.reader_fab_download)
|
||||
}
|
||||
else // Or continue animate
|
||||
post {
|
||||
icon.start()
|
||||
labelText = getString(R.string.reader_fab_download_cancel)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setImageDrawable(icon)
|
||||
icon?.start()
|
||||
} else {
|
||||
setImageResource(R.drawable.ic_download)
|
||||
labelText = getString(R.string.reader_fab_download)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,36 +18,31 @@
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.WindowManager
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.settings_activity.*
|
||||
import kotlinx.serialization.list
|
||||
import kotlinx.serialization.serializer
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.types.Tags
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||
import xyz.quaver.pupil.ui.fragment.LockFragment
|
||||
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.io.File
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
val REQUEST_LOCK = 38238
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -75,338 +70,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val suffix = listOf(
|
||||
"B",
|
||||
"kB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB" //really?
|
||||
)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val lockManager = LockManager(context!!)
|
||||
|
||||
findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) {
|
||||
getString(R.string.settings_lock_none)
|
||||
} else {
|
||||
lockManager.locks?.joinToString(", ") {
|
||||
when(it.type) {
|
||||
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
|
||||
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
|
||||
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDirSize(dir: File) : String {
|
||||
var size = dir.walk().map { it.length() }.sum()
|
||||
var suffixIndex = 0
|
||||
|
||||
while (size >= 1024) {
|
||||
size /= 1024
|
||||
suffixIndex++
|
||||
}
|
||||
|
||||
return getString(R.string.settings_clear_summary, size, suffix[suffixIndex])
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
||||
|
||||
with(findPreference<Preference>("app_version")) {
|
||||
this!!
|
||||
|
||||
val manager = context.packageManager
|
||||
val info = manager.getPackageInfo(context.packageName, 0)
|
||||
|
||||
summary = info.versionName
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("delete_cache")) {
|
||||
this!!
|
||||
|
||||
val dir = File(context.cacheDir, "imageCache")
|
||||
|
||||
summary = getDirSize(dir)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_cache_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
if (dir.exists())
|
||||
dir.deleteRecursively()
|
||||
|
||||
summary = getDirSize(dir)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("delete_downloads")) {
|
||||
this!!
|
||||
|
||||
val dir = getDownloadDirectory(context)!!
|
||||
|
||||
summary = getDirSize(dir)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_downloads_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
if (dir.exists())
|
||||
dir.deleteRecursively()
|
||||
|
||||
val downloads = (activity!!.application as Pupil).downloads
|
||||
|
||||
downloads.clear()
|
||||
|
||||
summary = getDirSize(dir)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
with(findPreference<Preference>("clear_history")) {
|
||||
this!!
|
||||
|
||||
val histories = (activity!!.application as Pupil).histories
|
||||
|
||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_history_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
histories.clear()
|
||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("default_query")) {
|
||||
this!!
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
summary = preferences.getString("default_query", "") ?: ""
|
||||
|
||||
val languages = resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
|
||||
|
||||
val excludeBL = "-male:yaoi"
|
||||
val excludeGuro = listOf("-female:guro", "-male:guro")
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val dialogView = LayoutInflater.from(context).inflate(
|
||||
R.layout.dialog_default_query,
|
||||
LinearLayout(context),
|
||||
false
|
||||
)
|
||||
|
||||
val tags = Tags.parse(
|
||||
preferences.getString("default_query", "") ?: ""
|
||||
)
|
||||
|
||||
summary = tags.toString()
|
||||
|
||||
with(dialogView.default_query_dialog_language_selector) {
|
||||
adapter =
|
||||
ArrayAdapter(
|
||||
context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
arrayListOf(
|
||||
getString(R.string.default_query_dialog_language_selector_none)
|
||||
).apply {
|
||||
addAll(languages.values)
|
||||
}
|
||||
)
|
||||
if (tags.any { it.area == "language" && !it.isNegative }) {
|
||||
val tag = languages[tags.first { it.area == "language" }.tag]
|
||||
if (tag != null) {
|
||||
setSelection(
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(adapter as ArrayAdapter<String>).getPosition(tag)
|
||||
)
|
||||
tags.removeByArea("language", false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_BL_checkbox) {
|
||||
isChecked = tags.contains(excludeBL)
|
||||
if (tags.contains(excludeBL))
|
||||
tags.remove(excludeBL)
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_guro_checkbox) {
|
||||
isChecked = excludeGuro.all { tags.contains(it) }
|
||||
if (excludeGuro.all { tags.contains(it) })
|
||||
excludeGuro.forEach {
|
||||
tags.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_edittext) {
|
||||
setText(tags.toString(), TextView.BufferType.EDITABLE)
|
||||
addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
s ?: return
|
||||
|
||||
if (s.any { it.isUpperCase() })
|
||||
s.replace(0, s.length, s.toString().toLowerCase())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context!!).apply {
|
||||
setView(dialogView)
|
||||
}.create()
|
||||
|
||||
dialogView.default_query_dialog_ok.setOnClickListener {
|
||||
val newTags = Tags.parse(dialogView.default_query_dialog_edittext.text.toString())
|
||||
|
||||
with(dialogView.default_query_dialog_language_selector) {
|
||||
if (selectedItemPosition != 0)
|
||||
newTags.add("language:${reverseLanguages[selectedItem]}")
|
||||
}
|
||||
|
||||
if (dialogView.default_query_dialog_BL_checkbox.isChecked)
|
||||
newTags.add(excludeBL)
|
||||
|
||||
if (dialogView.default_query_dialog_guro_checkbox.isChecked)
|
||||
excludeGuro.forEach { tag ->
|
||||
newTags.add(tag)
|
||||
}
|
||||
|
||||
preferenceManager.sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
|
||||
summary = preferences.getString("default_query", "") ?: ""
|
||||
tags.clear()
|
||||
tags.addAll(newTags)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
with(findPreference<Preference>("app_lock")) {
|
||||
this!!
|
||||
|
||||
val lockManager = LockManager(context)
|
||||
|
||||
summary = if (lockManager.locks.isNullOrEmpty()) {
|
||||
getString(R.string.settings_lock_none)
|
||||
} else {
|
||||
lockManager.locks?.joinToString(", ") {
|
||||
when(it.type) {
|
||||
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
|
||||
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
|
||||
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val intent = Intent(context, LockActivity::class.java)
|
||||
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_LOCK)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("dark_mode")) {
|
||||
this!!
|
||||
|
||||
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
|
||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
})
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LockFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val lockManager = LockManager(context!!)
|
||||
|
||||
findPreference<Preference>("lock_pattern")?.summary =
|
||||
if (lockManager.contains(Lock.Type.PATTERN))
|
||||
getString(R.string.settings_lock_enabled)
|
||||
else
|
||||
""
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
|
||||
|
||||
with(findPreference<Preference>("lock_pattern")) {
|
||||
this!!
|
||||
|
||||
if (LockManager(context!!).contains(Lock.Type.PATTERN))
|
||||
summary = getString(R.string.settings_lock_enabled)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val lockManager = LockManager(context!!)
|
||||
|
||||
if (lockManager.contains(Lock.Type.PATTERN)) {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_lock_remove_message)
|
||||
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
lockManager.remove(Lock.Type.PATTERN)
|
||||
onResume()
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
} else {
|
||||
val intent = Intent(context, LockActivity::class.java).apply {
|
||||
putExtra("mode", "add_lock")
|
||||
putExtra("type", "pattern")
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
when (item?.itemId) {
|
||||
android.R.id.home -> onBackPressed()
|
||||
@@ -426,7 +89,89 @@ class SettingsActivity : AppCompatActivity() {
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
REQUEST_RESTORE -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val uri = data?.data ?: return
|
||||
|
||||
try {
|
||||
val str = contentResolver.openInputStream(uri).use { inputStream ->
|
||||
inputStream!!
|
||||
|
||||
inputStream.readBytes().toString(Charset.defaultCharset())
|
||||
}
|
||||
|
||||
(application as Pupil).favorites.addAll(json.parse(Int.serializer().list, str).also {
|
||||
Snackbar.make(
|
||||
window.decorView,
|
||||
getString(R.string.settings_restore_successful, it.size),
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Snackbar.make(
|
||||
window.decorView,
|
||||
R.string.settings_restore_failed,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
REQUEST_DOWNLOAD_FOLDER -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
data?.data?.also { uri ->
|
||||
val takeFlags: Int =
|
||||
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
|
||||
val file = uri.toFile(this)
|
||||
|
||||
if (file?.canWrite() != true)
|
||||
Snackbar.make(
|
||||
settings,
|
||||
R.string.settings_dl_location_not_writable,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
else
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString("dl_location", file.canonicalPath)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
REQUEST_DOWNLOAD_FOLDER_OLD -> {
|
||||
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
||||
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
||||
|
||||
if (!File(directory).canWrite())
|
||||
Snackbar.make(
|
||||
settings,
|
||||
R.string.settings_dl_location_not_writable,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
else
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString("dl_location", File(directory).canonicalPath)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
when (requestCode) {
|
||||
REQUEST_WRITE_PERMISSION_AND_SAF -> {
|
||||
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||
}
|
||||
|
||||
startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.android.synthetic.main.dialog_default_query.*
|
||||
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.types.Tags
|
||||
|
||||
class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
|
||||
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 excludeBL = "-male:yaoi"
|
||||
private val excludeGuro = listOf("-female:guro", "-male:guro")
|
||||
|
||||
private lateinit var dialogView : View
|
||||
|
||||
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
initDialog()
|
||||
|
||||
setTitle(R.string.default_query_dialog_title)
|
||||
setView(dialogView)
|
||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
|
||||
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
|
||||
|
||||
with(default_query_dialog_language_selector) {
|
||||
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)
|
||||
}
|
||||
|
||||
onPositiveButtonClickListener?.invoke(newTags)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun initDialog() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val tags = Tags.parse(
|
||||
preferences.getString("default_query", "") ?: ""
|
||||
)
|
||||
|
||||
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
||||
|
||||
with(dialogView.default_query_dialog_language_selector) {
|
||||
adapter =
|
||||
ArrayAdapter(
|
||||
context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
arrayListOf(
|
||||
context.getString(R.string.default_query_dialog_language_selector_none)
|
||||
).apply {
|
||||
addAll(languages.values)
|
||||
}
|
||||
)
|
||||
if (tags.any { it.area == "language" && !it.isNegative }) {
|
||||
val tag = languages[tags.first { it.area == "language" }.tag]
|
||||
if (tag != null) {
|
||||
setSelection(
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(adapter as ArrayAdapter<String>).getPosition(tag)
|
||||
)
|
||||
tags.removeByArea("language", false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_BL_checkbox) {
|
||||
isChecked = tags.contains(excludeBL)
|
||||
if (tags.contains(excludeBL))
|
||||
tags.remove(excludeBL)
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_guro_checkbox) {
|
||||
isChecked = excludeGuro.all { tags.contains(it) }
|
||||
if (excludeGuro.all { tags.contains(it) })
|
||||
excludeGuro.forEach {
|
||||
tags.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_edittext) {
|
||||
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
|
||||
addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
s ?: return
|
||||
|
||||
if (s.any { it.isUpperCase() })
|
||||
s.replace(
|
||||
0,
|
||||
s.length,
|
||||
s.toString().toLowerCase(java.util.Locale.getDefault())
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RadioButton
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.android.synthetic.main.item_dl_location.view.*
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.io.File
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
||||
|
||||
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val buttons = mutableListOf<Pair<RadioButton, File?>>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
|
||||
|
||||
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
||||
|
||||
externalFilesDirs.forEachIndexed { index, dir ->
|
||||
|
||||
dir ?: return@forEachIndexed
|
||||
|
||||
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
|
||||
location_type.text = context.getString(when (index) {
|
||||
0 -> R.string.settings_dl_location_internal
|
||||
else -> R.string.settings_dl_location_removable
|
||||
})
|
||||
location_available.text = context.getString(
|
||||
R.string.settings_dl_location_available,
|
||||
byteToString(dir.freeSpace)
|
||||
)
|
||||
setOnClickListener {
|
||||
buttons.forEach { pair ->
|
||||
pair.first.isChecked = false
|
||||
}
|
||||
button.performClick()
|
||||
preference.edit().putString("dl_location", dir.canonicalPath).apply()
|
||||
}
|
||||
buttons.add(button to dir)
|
||||
})
|
||||
}
|
||||
|
||||
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
|
||||
location_type.text = context.getString(R.string.settings_dl_location_custom)
|
||||
setOnClickListener {
|
||||
buttons.forEach { pair ->
|
||||
pair.first.isChecked = false
|
||||
}
|
||||
button.performClick()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
|
||||
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_PERMISSION_AND_SAF)
|
||||
else {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||
}
|
||||
|
||||
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
|
||||
}
|
||||
|
||||
dismiss()
|
||||
} else { // Can't use SAF on old Androids!
|
||||
val config = DirectoryChooserConfig.builder()
|
||||
.newDirectoryName("Pupil")
|
||||
.allowNewDirectoryNameModification(true)
|
||||
.build()
|
||||
|
||||
val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
|
||||
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
||||
}
|
||||
|
||||
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER_OLD)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
buttons.add(button to null)
|
||||
})
|
||||
|
||||
externalFilesDirs.indexOfFirst {
|
||||
it.canonicalPath == getDownloadDirectory(context).canonicalPath
|
||||
}.let { index ->
|
||||
buttons[index].first.isChecked = true
|
||||
}
|
||||
|
||||
setTitle(R.string.settings_dl_location)
|
||||
|
||||
setView(view)
|
||||
|
||||
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2019 tom5079
|
||||
* 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
|
||||
@@ -16,41 +16,41 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.LinearLayout.LayoutParams
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.gridlayout.widget.GridLayout
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.dialog_galleryblock.*
|
||||
import kotlinx.android.synthetic.main.dialog_gallery.*
|
||||
import kotlinx.android.synthetic.main.gallery_details.view.*
|
||||
import kotlinx.android.synthetic.main.item_gallery_details.view.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.hitomi.Gallery
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.getGallery
|
||||
import xyz.quaver.hitomi.getGalleryBlock
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||
import xyz.quaver.pupil.adapters.ThumbnailAdapter
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.util.ItemClickSupport
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
|
||||
class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) {
|
||||
@@ -67,7 +67,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.dialog_galleryblock)
|
||||
setContentView(R.layout.dialog_gallery)
|
||||
|
||||
window?.attributes.apply {
|
||||
this ?: return@apply
|
||||
@@ -113,8 +113,11 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
}
|
||||
|
||||
Glide.with(context)
|
||||
.load(gallery.thumbnails.firstOrNull())
|
||||
.into(gallery_thumbnail)
|
||||
.load(gallery.cover)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
}.into(gallery_cover)
|
||||
|
||||
addDetails(gallery)
|
||||
addThumbnails(gallery)
|
||||
@@ -183,6 +186,8 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
else -> tag.tag.wordCapitalize()
|
||||
}
|
||||
|
||||
setEnsureMinTouchTargetSize(false)
|
||||
|
||||
setOnClickListener {
|
||||
onChipClickedHandler.forEach { handler ->
|
||||
handler.invoke(tag)
|
||||
@@ -219,9 +224,9 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
|
||||
private fun addRelated(gallery: Gallery) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>()
|
||||
val galleries = ArrayList<GalleryBlock>()
|
||||
|
||||
val adapter = GalleryBlockAdapter(glide, galleries).apply {
|
||||
val adapter = GalleryBlockAdapter(context, galleries).apply {
|
||||
onChipClickedHandler.add { tag ->
|
||||
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
|
||||
handler.invoke(tag)
|
||||
@@ -232,11 +237,11 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
gallery.related.forEachIndexed { i, galleryID ->
|
||||
async(Dispatchers.IO) {
|
||||
getGalleryBlock(galleryID)
|
||||
Cache(context).getGalleryBlock(galleryID)
|
||||
}.let {
|
||||
val galleryBlock = it.await() ?: return@let
|
||||
|
||||
galleries.add(Pair(galleryBlock, GlobalScope.async { galleryBlock.thumbnails.first() }))
|
||||
galleries.add(galleryBlock)
|
||||
adapter.notifyItemInserted(i)
|
||||
}
|
||||
}
|
||||
@@ -252,12 +257,15 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
||||
ItemClickSupport.addTo(this)
|
||||
.setOnItemClickListener { _, position, _ ->
|
||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryID", galleries[position].first.id)
|
||||
putExtra("galleryID", galleries[position].id)
|
||||
})
|
||||
(context.applicationContext as Pupil).histories.add(galleries[position].first.id)
|
||||
(context.applicationContext as Pupil).histories.add(galleries[position].id)
|
||||
}
|
||||
.setOnItemLongClickListener { _, position, _ ->
|
||||
GalleryDialog(context, galleries[position].first.id).apply {
|
||||
GalleryDialog(
|
||||
context,
|
||||
galleries[position].id
|
||||
).apply {
|
||||
onChipClickedHandler.add { tag ->
|
||||
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
||||
}
|
||||
97
app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt
Normal file
97
app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.PreferenceManager
|
||||
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
|
||||
|
||||
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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
initDialog()
|
||||
|
||||
setTitle(R.string.settings_mirror_title)
|
||||
setView(recyclerView)
|
||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun initDialog() {
|
||||
recyclerView = 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 = {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putString("mirrors", it.joinToString(">"))
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.LockActivity
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
|
||||
class LockFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val lockManager = LockManager(context!!)
|
||||
|
||||
findPreference<Preference>("lock_pattern")?.summary =
|
||||
if (lockManager.contains(Lock.Type.PATTERN))
|
||||
getString(R.string.settings_lock_enabled)
|
||||
else
|
||||
""
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
|
||||
|
||||
with(findPreference<Preference>("lock_pattern")) {
|
||||
this!!
|
||||
|
||||
if (LockManager(context!!).contains(Lock.Type.PATTERN))
|
||||
summary = getString(R.string.settings_lock_enabled)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val lockManager = LockManager(context!!)
|
||||
|
||||
if (lockManager.contains(Lock.Type.PATTERN)) {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_lock_remove_message)
|
||||
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
lockManager.remove(Lock.Type.PATTERN)
|
||||
onResume()
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
} else {
|
||||
val intent = Intent(context, LockActivity::class.java).apply {
|
||||
putExtra("mode", "add_lock")
|
||||
putExtra("type", "pattern")
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@@ -29,8 +29,6 @@ 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.util.hash
|
||||
import xyz.quaver.pupil.util.hashWithSalt
|
||||
|
||||
class PatternLockFragment : Fragment(), PatternLockViewListener {
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.LockActivity
|
||||
import xyz.quaver.pupil.ui.SettingsActivity
|
||||
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
|
||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
|
||||
import xyz.quaver.pupil.ui.dialog.MirrorDialog
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.io.File
|
||||
|
||||
|
||||
class SettingsFragment :
|
||||
PreferenceFragmentCompat(),
|
||||
Preference.OnPreferenceClickListener,
|
||||
Preference.OnPreferenceChangeListener,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val lockManager = LockManager(context!!)
|
||||
|
||||
findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) {
|
||||
getString(R.string.settings_lock_none)
|
||||
} else {
|
||||
lockManager.locks?.joinToString(", ") {
|
||||
when(it.type) {
|
||||
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
|
||||
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
|
||||
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDirSize(dir: File) : String {
|
||||
val size = dir.walk().map { it.length() }.sum()
|
||||
|
||||
return getString(R.string.settings_clear_summary, byteToString(size))
|
||||
}
|
||||
|
||||
override fun onPreferenceClick(preference: Preference?): Boolean {
|
||||
with (preference) {
|
||||
this ?: return false
|
||||
|
||||
when (key) {
|
||||
"app_version" -> {
|
||||
checkUpdate(activity as SettingsActivity, true)
|
||||
}
|
||||
"delete_cache" -> {
|
||||
val dir = File(context.cacheDir, "imageCache")
|
||||
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_cache_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
if (dir.exists())
|
||||
dir.deleteRecursively()
|
||||
|
||||
summary = getDirSize(dir)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"delete_downloads" -> {
|
||||
val dir = getDownloadDirectory(context)
|
||||
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_downloads_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
if (dir.exists())
|
||||
dir.deleteRecursively()
|
||||
|
||||
summary = getDirSize(dir)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"clear_history" -> {
|
||||
val histories = (context.applicationContext as Pupil).histories
|
||||
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_history_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
histories.clear()
|
||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"dl_location" -> {
|
||||
DownloadLocationDialog(activity!!).show()
|
||||
}
|
||||
"default_query" -> {
|
||||
DefaultQueryDialog(context).apply {
|
||||
onPositiveButtonClickListener = { newTags ->
|
||||
sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
|
||||
summary = newTags.toString()
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
"app_lock" -> {
|
||||
val intent = Intent(context, LockActivity::class.java)
|
||||
activity?.startActivityForResult(intent, REQUEST_LOCK)
|
||||
}
|
||||
"mirrors" -> {
|
||||
MirrorDialog(context)
|
||||
.show()
|
||||
}
|
||||
"backup" -> {
|
||||
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
|
||||
File(getDownloadDirectory(context), "favorites.json"),
|
||||
true
|
||||
)
|
||||
|
||||
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
"restore" -> {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
|
||||
activity?.startActivityForResult(intent, REQUEST_RESTORE)
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean {
|
||||
with (preference) {
|
||||
this ?: return false
|
||||
|
||||
when (key) {
|
||||
"dark_mode" -> {
|
||||
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
|
||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
})
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
"dl_location" -> {
|
||||
findPreference<Preference>(key)?.summary = getDownloadDirectory(context!!).canonicalPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
||||
|
||||
initPreferences()
|
||||
}
|
||||
|
||||
private fun initPreferences() {
|
||||
for (i in 0 until preferenceScreen.preferenceCount) {
|
||||
|
||||
preferenceScreen.getPreference(i).run {
|
||||
if (this is PreferenceCategory)
|
||||
(0 until preferenceCount).map { getPreference(it) }
|
||||
else
|
||||
listOf(this)
|
||||
}.forEach { preference ->
|
||||
with (preference) {
|
||||
|
||||
when (key) {
|
||||
"app_version" -> {
|
||||
val manager = context.packageManager
|
||||
val info = manager.getPackageInfo(context.packageName, 0)
|
||||
summary = context.getString(R.string.settings_app_version_description, info.versionName)
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"delete_cache" -> {
|
||||
val dir = File(context.cacheDir, "imageCache")
|
||||
summary = getDirSize(dir)
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"delete_downloads" -> {
|
||||
val dir = getDownloadDirectory(context)
|
||||
summary = getDirSize(dir)
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"clear_history" -> {
|
||||
val histories = (activity!!.application as Pupil).histories
|
||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"dl_location" -> {
|
||||
summary = getDownloadDirectory(context).canonicalPath
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"default_query" -> {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
summary = preferences.getString("default_query", "") ?: ""
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"app_lock" -> {
|
||||
val lockManager = LockManager(context)
|
||||
summary =
|
||||
if (lockManager.locks.isNullOrEmpty()) {
|
||||
getString(R.string.settings_lock_none)
|
||||
} else {
|
||||
lockManager.locks?.joinToString(", ") {
|
||||
when (it.type) {
|
||||
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
|
||||
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
|
||||
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"mirrors" -> {
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"dark_mode" -> {
|
||||
onPreferenceChangeListener = this@SettingsFragment
|
||||
}
|
||||
"backup" -> {
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"restore" -> {
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt
Normal file
30
app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
|
||||
const val REQUEST_LOCK = 38238
|
||||
const val REQUEST_RESTORE = 16546
|
||||
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
||||
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
||||
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
|
||||
|
||||
val json = Json(JsonConfiguration.Stable)
|
||||
@@ -1,325 +0,0 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2019 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.util.SparseArray
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.io.IOException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.getReader
|
||||
import xyz.quaver.hitomi.getReferer
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hiyobi.user_agent
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class GalleryDownloader(
|
||||
base: Context,
|
||||
private val galleryID: Int,
|
||||
_notify: Boolean = false
|
||||
) : ContextWrapper(base) {
|
||||
|
||||
private val downloads = (applicationContext as Pupil).downloads
|
||||
var useHiyobi = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("use_hiyobi", false)
|
||||
|
||||
var download: Boolean = false
|
||||
set(value) {
|
||||
if (value) {
|
||||
field = true
|
||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
||||
|
||||
if (reader?.isActive == false && downloadJob?.isActive != true) {
|
||||
val data = File(getDownloadDirectory(this), galleryID.toString())
|
||||
val cache = File(cacheDir, "imageCache/$galleryID")
|
||||
|
||||
if (File(cache, "images").exists() && !data.exists()) {
|
||||
cache.copyRecursively(data, true)
|
||||
cache.deleteRecursively()
|
||||
}
|
||||
|
||||
field = false
|
||||
}
|
||||
|
||||
downloads.add(galleryID)
|
||||
} else {
|
||||
field = false
|
||||
}
|
||||
|
||||
onNotifyChangedHandler?.invoke(value)
|
||||
}
|
||||
|
||||
private val reader: Deferred<Reader>?
|
||||
private var downloadJob: Job? = null
|
||||
|
||||
private lateinit var notificationBuilder: NotificationCompat.Builder
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
|
||||
var onReaderLoadedHandler: ((Reader) -> Unit)? = null
|
||||
var onProgressHandler: ((Int) -> Unit)? = null
|
||||
var onDownloadedHandler: ((List<String>) -> Unit)? = null
|
||||
var onErrorHandler: ((Exception) -> Unit)? = null
|
||||
var onCompleteHandler: (() -> Unit)? = null
|
||||
var onNotifyChangedHandler: ((Boolean) -> Unit)? = null
|
||||
|
||||
companion object : SparseArray<GalleryDownloader>()
|
||||
|
||||
init {
|
||||
put(galleryID, this)
|
||||
|
||||
initNotification()
|
||||
|
||||
reader = CoroutineScope(Dispatchers.IO).async {
|
||||
try {
|
||||
download = _notify
|
||||
val json = Json(JsonConfiguration.Stable)
|
||||
val serializer = Reader.serializer()
|
||||
|
||||
//Check cache
|
||||
val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "reader.json")
|
||||
|
||||
try {
|
||||
json.parse(serializer, cache.readText())
|
||||
} catch(e: Exception) {
|
||||
cache.delete()
|
||||
}
|
||||
|
||||
if (cache.exists()) {
|
||||
val cached = json.parse(serializer, cache.readText())
|
||||
|
||||
if (cached.readerItems.isNotEmpty()) {
|
||||
useHiyobi = when {
|
||||
cached.readerItems[0].url.contains("hitomi.la") -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
onReaderLoadedHandler?.invoke(cached)
|
||||
|
||||
return@async cached
|
||||
}
|
||||
}
|
||||
|
||||
//Cache doesn't exist. Load from internet
|
||||
val reader = when {
|
||||
useHiyobi -> {
|
||||
try {
|
||||
xyz.quaver.hiyobi.getReader(galleryID)
|
||||
} catch(e: Exception) {
|
||||
useHiyobi = false
|
||||
getReader(galleryID)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
getReader(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
if (reader.readerItems.isNotEmpty()) {
|
||||
//Save cache
|
||||
if (cache.parentFile?.exists() == false)
|
||||
cache.parentFile!!.mkdirs()
|
||||
|
||||
cache.writeText(json.stringify(serializer, reader))
|
||||
}
|
||||
|
||||
reader
|
||||
} catch (e: Exception) {
|
||||
Crashlytics.logException(e)
|
||||
Reader("", listOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
|
||||
|
||||
fun start() {
|
||||
downloadJob = CoroutineScope(Dispatchers.Default).launch {
|
||||
val reader = reader!!.await()
|
||||
|
||||
notificationBuilder.setContentTitle(reader.title)
|
||||
|
||||
if (reader.readerItems.isEmpty()) {
|
||||
onErrorHandler?.invoke(IOException(getString(R.string.unable_to_connect)))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val list = ArrayList<String>()
|
||||
|
||||
onReaderLoadedHandler?.invoke(reader)
|
||||
|
||||
notificationBuilder
|
||||
.setProgress(reader.readerItems.size, 0, false)
|
||||
.setContentText("0/${reader.readerItems.size}")
|
||||
|
||||
reader.readerItems.chunked(4).forEachIndexed { chunkIndex, chunked ->
|
||||
chunked.mapIndexed { i, it ->
|
||||
val index = chunkIndex*4+i
|
||||
|
||||
async(Dispatchers.IO) {
|
||||
val url = if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url
|
||||
|
||||
val name = "$index".padStart(4, '0')
|
||||
val ext = url.split('.').last()
|
||||
|
||||
val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "images/$name.$ext")
|
||||
|
||||
if (!cache.exists())
|
||||
try {
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
if (useHiyobi) {
|
||||
setRequestProperty("User-Agent", user_agent)
|
||||
setRequestProperty("Cookie", cookie)
|
||||
} else
|
||||
setRequestProperty("Referer", getReferer(galleryID))
|
||||
|
||||
if (cache.parentFile?.exists() == false)
|
||||
cache.parentFile!!.mkdirs()
|
||||
|
||||
inputStream.copyTo(FileOutputStream(cache))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
cache.delete()
|
||||
|
||||
onErrorHandler?.invoke(e)
|
||||
|
||||
notificationBuilder
|
||||
.setContentTitle(reader.title)
|
||||
.setContentText(getString(R.string.reader_notification_error))
|
||||
.setProgress(0, 0, false)
|
||||
|
||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
||||
}
|
||||
|
||||
"images/$name.$ext"
|
||||
}
|
||||
}.forEach {
|
||||
list.add(it.await())
|
||||
|
||||
val index = list.size
|
||||
|
||||
onProgressHandler?.invoke(index)
|
||||
|
||||
notificationBuilder
|
||||
.setProgress(reader.readerItems.size, index, false)
|
||||
.setContentText("$index/${reader.readerItems.size}")
|
||||
|
||||
if (download)
|
||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
||||
|
||||
onDownloadedHandler?.invoke(list)
|
||||
}
|
||||
}
|
||||
|
||||
Timer(false).schedule(1000) {
|
||||
notificationBuilder
|
||||
.setContentTitle(reader.title)
|
||||
.setContentText(getString(R.string.reader_notification_complete))
|
||||
.setProgress(0, 0, false)
|
||||
|
||||
if (download) {
|
||||
File(cacheDir, "imageCache/${galleryID}").let {
|
||||
if (it.exists()) {
|
||||
val target = File(getDownloadDirectory(this@GalleryDownloader), galleryID.toString())
|
||||
|
||||
if (!target.exists())
|
||||
target.mkdirs()
|
||||
|
||||
it.copyRecursively(target, true)
|
||||
it.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
||||
|
||||
download = false
|
||||
}
|
||||
|
||||
onCompleteHandler?.invoke()
|
||||
}
|
||||
|
||||
remove(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
downloadJob?.cancel()
|
||||
|
||||
remove(galleryID)
|
||||
}
|
||||
|
||||
suspend fun cancelAndJoin() {
|
||||
downloadJob?.cancelAndJoin()
|
||||
|
||||
remove(galleryID)
|
||||
}
|
||||
|
||||
fun invokeOnReaderLoaded() {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
onReaderLoadedHandler?.invoke(reader?.await() ?: return@launch)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearNotification() {
|
||||
notificationManager.cancel(galleryID)
|
||||
}
|
||||
|
||||
fun invokeOnNotifyChanged() {
|
||||
onNotifyChangedHandler?.invoke(download)
|
||||
}
|
||||
|
||||
private fun initNotification() {
|
||||
val intent = Intent(this, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryID", galleryID)
|
||||
}
|
||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||
addNextIntentWithParentStack(intent)
|
||||
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
|
||||
notificationBuilder = NotificationCompat.Builder(this, "download").apply {
|
||||
setContentTitle(getString(R.string.reader_loading))
|
||||
setContentText(getString(R.string.reader_notification_text))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
setContentIntent(pendingIntent)
|
||||
setProgress(0, 0, true)
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
218
app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt
Normal file
218
app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util.download
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.util.Base64
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.pupil.util.getCachedGallery
|
||||
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||
import xyz.quaver.pupil.util.json
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
|
||||
class Cache(context: Context) : ContextWrapper(context) {
|
||||
|
||||
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
// Search in this order
|
||||
// Download -> Cache
|
||||
fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also {
|
||||
if (!it.exists())
|
||||
it.mkdirs()
|
||||
}
|
||||
|
||||
fun getCachedMetadata(galleryID: Int) : Metadata? {
|
||||
val file = File(getCachedGallery(galleryID), ".metadata")
|
||||
|
||||
if (!file.exists())
|
||||
return null
|
||||
|
||||
return try {
|
||||
json.parse(Metadata.serializer(), file.readText())
|
||||
} catch (e: Exception) {
|
||||
//File corrupted
|
||||
file.delete()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
|
||||
val file = File(getCachedGallery(galleryID), ".metadata").also {
|
||||
if (!it.exists())
|
||||
it.createNewFile()
|
||||
}
|
||||
|
||||
file.writeText(json.stringify(Metadata.serializer(), metadata))
|
||||
}
|
||||
|
||||
suspend fun getThumbnail(galleryID: Int): String? {
|
||||
val metadata = Cache(this).getCachedMetadata(galleryID)
|
||||
|
||||
val thumbnail = if (metadata?.thumbnail == null)
|
||||
withContext(Dispatchers.IO) {
|
||||
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
|
||||
try {
|
||||
Base64.encodeToString(URL(thumbnails?.firstOrNull()).readBytes(), Base64.DEFAULT)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
else
|
||||
metadata.thumbnail
|
||||
|
||||
setCachedMetadata(
|
||||
galleryID,
|
||||
Metadata(Cache(this).getCachedMetadata(galleryID), thumbnail = thumbnail)
|
||||
)
|
||||
|
||||
return thumbnail
|
||||
}
|
||||
|
||||
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
|
||||
val metadata = Cache(this).getCachedMetadata(galleryID)
|
||||
|
||||
val sources = listOf(
|
||||
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||
)
|
||||
|
||||
val galleryBlock = if (metadata?.galleryBlock == null) {
|
||||
CoroutineScope(Dispatchers.IO).async {
|
||||
var galleryBlock: GalleryBlock? = null
|
||||
|
||||
for (source in sources) {
|
||||
galleryBlock = kotlin.runCatching {
|
||||
source.invoke()
|
||||
}.getOrNull()
|
||||
|
||||
if (galleryBlock != null)
|
||||
break
|
||||
}
|
||||
|
||||
galleryBlock
|
||||
}.await() ?: return null
|
||||
}
|
||||
else
|
||||
metadata.galleryBlock
|
||||
|
||||
setCachedMetadata(
|
||||
galleryID,
|
||||
Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock)
|
||||
)
|
||||
|
||||
return galleryBlock
|
||||
}
|
||||
|
||||
fun getReaderOrNull(galleryID: Int): Reader? {
|
||||
return getCachedMetadata(galleryID)?.reader
|
||||
}
|
||||
|
||||
suspend fun getReader(galleryID: Int): Reader? {
|
||||
val metadata = getCachedMetadata(galleryID)
|
||||
val mirrors = preference.getString("mirrors", null)?.split('>') ?: listOf()
|
||||
|
||||
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(
|
||||
Comparator { o1, o2 ->
|
||||
mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
|
||||
}
|
||||
)
|
||||
else
|
||||
it
|
||||
}
|
||||
|
||||
val reader = if (metadata?.reader == null) {
|
||||
CoroutineScope(Dispatchers.IO).async {
|
||||
var retval: Reader? = null
|
||||
|
||||
for (source in sources) {
|
||||
retval = kotlin.runCatching {
|
||||
source.value.invoke()
|
||||
}.getOrNull()
|
||||
|
||||
if (retval != null)
|
||||
break
|
||||
}
|
||||
|
||||
retval
|
||||
}.await() ?: return null
|
||||
} else
|
||||
metadata.reader
|
||||
|
||||
setCachedMetadata(
|
||||
galleryID,
|
||||
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
|
||||
)
|
||||
|
||||
return reader
|
||||
}
|
||||
|
||||
fun getImages(galleryID: Int): List<File?>? {
|
||||
val gallery = getCachedGallery(galleryID)
|
||||
val reader = getReaderOrNull(galleryID) ?: return null
|
||||
val images = gallery.listFiles() ?: return null
|
||||
|
||||
return reader.galleryInfo.indices.map { index ->
|
||||
images.firstOrNull { file -> file.name.startsWith("%05d".format(index)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun putImage(galleryID: Int, name: String, data: ByteArray) {
|
||||
val cache = File(getCachedGallery(galleryID), name).also {
|
||||
if (!it.exists())
|
||||
it.createNewFile()
|
||||
}
|
||||
|
||||
if (!Regex("""^[0-9]+.+$""").matches(name))
|
||||
throw IllegalArgumentException("File name is not a number")
|
||||
|
||||
cache.writeBytes(data)
|
||||
}
|
||||
|
||||
fun moveToDownload(galleryID: Int) {
|
||||
val cache = getCachedGallery(galleryID).also {
|
||||
if (!it.exists())
|
||||
return
|
||||
}
|
||||
val download = File(getDownloadDirectory(this), galleryID.toString())
|
||||
|
||||
cache.copyRecursively(download, true)
|
||||
cache.deleteRecursively()
|
||||
}
|
||||
|
||||
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
|
||||
|
||||
fun setDownloading(galleryID: Int, isDownloading: Boolean) {
|
||||
setCachedMetadata(galleryID, Metadata(getCachedMetadata(galleryID), isDownloading = isDownloading))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util.download
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.util.SparseArray
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import io.fabric.sdk.android.Fabric
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.*
|
||||
import okio.*
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.getReferer
|
||||
import xyz.quaver.hitomi.urlFromUrlFromHash
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hiyobi.createImgList
|
||||
import xyz.quaver.hiyobi.user_agent
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
|
||||
|
||||
private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
//region ProgressListener
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private val progressListener = object: ProgressListener {
|
||||
override fun update(tag: Any?, bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
val (galleryID, index) = (tag as? Pair<Int, Int>) ?: return
|
||||
|
||||
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
|
||||
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
|
||||
}
|
||||
}
|
||||
|
||||
interface ProgressListener {
|
||||
fun update(tag: Any?, bytesRead : Long, contentLength: Long, done: Boolean)
|
||||
}
|
||||
|
||||
class ProgressResponseBody(
|
||||
val tag: Any?,
|
||||
val responseBody: ResponseBody,
|
||||
val progressListener : ProgressListener
|
||||
) : ResponseBody() {
|
||||
private var bufferedSource : BufferedSource? = null
|
||||
|
||||
override fun contentLength() = responseBody.contentLength()
|
||||
override fun contentType() = responseBody.contentType() ?: null
|
||||
|
||||
override fun source(): BufferedSource {
|
||||
if (bufferedSource == null)
|
||||
bufferedSource = Okio.buffer(source(responseBody.source()))
|
||||
|
||||
return bufferedSource!!
|
||||
}
|
||||
|
||||
private fun source(source: Source) = object: ForwardingSource(source) {
|
||||
|
||||
var totalBytesRead = 0L
|
||||
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val bytesRead = super.read(sink, byteCount)
|
||||
|
||||
totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
|
||||
progressListener.update(tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||
|
||||
return bytesRead
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Singleton
|
||||
companion object {
|
||||
|
||||
@Volatile private var instance: DownloadWorker? = null
|
||||
|
||||
fun getInstance(context: Context) =
|
||||
instance ?: synchronized(this) {
|
||||
instance ?: DownloadWorker(context).also { instance = it }
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
val queue = LinkedBlockingQueue<Int>()
|
||||
|
||||
/*
|
||||
* KEY
|
||||
* primary galleryID
|
||||
* secondary index
|
||||
* PRIMARY VALUE
|
||||
* MutableList -> Download in progress
|
||||
* null -> Loading / Gallery doesn't exist
|
||||
* SECONDARY VALUE
|
||||
* 0 <= value < 100 -> Download in progress
|
||||
* Float.POSITIVE_INFINITY -> Download completed
|
||||
* Float.NaN -> Exception
|
||||
*/
|
||||
val progress = SparseArray<MutableList<Float>?>()
|
||||
/*
|
||||
* KEY
|
||||
* primary galleryID
|
||||
* secondary index
|
||||
* PRIMARY VALUE
|
||||
* MutableList -> Download in progress / Loading
|
||||
* null -> Gallery doesn't exist
|
||||
* SECONDARY VALUE
|
||||
* Throwable -> Exception
|
||||
* null -> Download in progress / Loading
|
||||
*/
|
||||
val exception = SparseArray<MutableList<Throwable?>?>()
|
||||
val notification = SparseArray<NotificationCompat.Builder>()
|
||||
|
||||
private val loop = loop()
|
||||
private val worker = SparseArray<Job?>()
|
||||
val clients = SparseArray<OkHttpClient>()
|
||||
|
||||
val interceptor = Interceptor { chain ->
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
|
||||
response.newBuilder()
|
||||
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
||||
.build()
|
||||
}
|
||||
fun buildClient() =
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(interceptor)
|
||||
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
|
||||
.build()
|
||||
|
||||
fun stop() {
|
||||
queue.clear()
|
||||
|
||||
loop.cancel()
|
||||
for (i in 0..worker.size()) {
|
||||
val galleryID = worker.keyAt(i)
|
||||
|
||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||
worker[galleryID]?.cancel()
|
||||
}
|
||||
|
||||
for (i in 0 until clients.size()) {
|
||||
clients.valueAt(i).dispatcher().cancelAll()
|
||||
}
|
||||
clients.clear()
|
||||
|
||||
progress.clear()
|
||||
exception.clear()
|
||||
notification.clear()
|
||||
notificationManager.cancelAll()
|
||||
}
|
||||
|
||||
fun cancel(galleryID: Int) {
|
||||
queue.remove(galleryID)
|
||||
worker[galleryID]?.cancel()
|
||||
|
||||
clients[galleryID]?.dispatcher()?.queuedCalls()
|
||||
?.filter {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID
|
||||
}
|
||||
?.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
clients.remove(galleryID)
|
||||
|
||||
progress.remove(galleryID)
|
||||
exception.remove(galleryID)
|
||||
notification.remove(galleryID)
|
||||
notificationManager.cancel(galleryID)
|
||||
|
||||
if (progress.indexOfKey(galleryID) >= 0) {
|
||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
|
||||
|
||||
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
|
||||
val lowQuality = preferences.getBoolean("low_quality", false)
|
||||
|
||||
val request = Request.Builder().apply {
|
||||
when (reader.code) {
|
||||
Code.HITOMI -> {
|
||||
url(
|
||||
urlFromUrlFromHash(
|
||||
galleryID,
|
||||
reader.galleryInfo[index],
|
||||
if (lowQuality) "webp" else null
|
||||
)
|
||||
)
|
||||
addHeader("Referer", getReferer(galleryID))
|
||||
}
|
||||
Code.HIYOBI -> {
|
||||
url(createImgList(galleryID, reader, lowQuality)[index].path)
|
||||
addHeader("User-Agent", user_agent)
|
||||
addHeader("Cookie", cookie)
|
||||
}
|
||||
else -> {
|
||||
//shouldn't be called anyway
|
||||
}
|
||||
}
|
||||
tag(galleryID to index)
|
||||
}.build()
|
||||
|
||||
clients[galleryID].newCall(request).enqueue(callback)
|
||||
}
|
||||
|
||||
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
||||
val reader = Cache(this@DownloadWorker).getReader(galleryID)
|
||||
|
||||
//gallery doesn't exist
|
||||
if (reader == null) {
|
||||
progress.put(galleryID, null)
|
||||
exception.put(galleryID, null)
|
||||
|
||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val cache = Cache(this@DownloadWorker).getImages(galleryID)
|
||||
|
||||
progress.put(galleryID, reader.galleryInfo.indices.map { index ->
|
||||
if (cache?.get(index) != null)
|
||||
Float.POSITIVE_INFINITY
|
||||
else
|
||||
0F
|
||||
}.toMutableList())
|
||||
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
|
||||
|
||||
if (notification[galleryID] == null)
|
||||
initNotification(galleryID)
|
||||
|
||||
notification[galleryID].setContentTitle(reader.title)
|
||||
notify(galleryID)
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
with(Cache(this@DownloadWorker)) {
|
||||
if (isDownloading(galleryID)) {
|
||||
moveToDownload(galleryID)
|
||||
setDownloading(galleryID, false)
|
||||
}
|
||||
}
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
||||
clients.put(galleryID, buildClient())
|
||||
|
||||
for (i in reader.galleryInfo.indices) {
|
||||
val callback = object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
if (Fabric.isInitialized())
|
||||
Crashlytics.logException(e)
|
||||
|
||||
progress[galleryID]?.set(i, Float.NaN)
|
||||
exception[galleryID]?.set(i, e)
|
||||
|
||||
notify(galleryID)
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
with(Cache(this@DownloadWorker)) {
|
||||
if (isDownloading(galleryID)) {
|
||||
moveToDownload(galleryID)
|
||||
setDownloading(galleryID, false)
|
||||
}
|
||||
}
|
||||
clients.remove(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.body().use {
|
||||
val res = it.bytes()
|
||||
val ext =
|
||||
call.request().url().encodedPath().split('.').last()
|
||||
|
||||
Cache(this@DownloadWorker).putImage(galleryID, "%05d.%s".format(i, ext), res)
|
||||
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
|
||||
}
|
||||
|
||||
notify(galleryID)
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
with(Cache(this@DownloadWorker)) {
|
||||
if (isDownloading(galleryID)) {
|
||||
moveToDownload(galleryID)
|
||||
setDownloading(galleryID, false)
|
||||
}
|
||||
}
|
||||
clients.remove(galleryID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (progress[galleryID]?.get(i)?.isFinite() == true)
|
||||
queueDownload(galleryID, reader, i, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(galleryID: Int) {
|
||||
val max = progress[galleryID]?.size ?: 0
|
||||
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0
|
||||
|
||||
if (isCompleted(galleryID))
|
||||
notification[galleryID]
|
||||
?.setContentText(getString(R.string.reader_notification_complete))
|
||||
?.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
?.setProgress(0, 0, false)
|
||||
?.setOngoing(false)
|
||||
else
|
||||
notification[galleryID]
|
||||
?.setProgress(max, progress, false)
|
||||
?.setContentText("$progress/$max")
|
||||
|
||||
if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null)
|
||||
notificationManager.notify(galleryID, notification[galleryID].build())
|
||||
else
|
||||
notificationManager.cancel(galleryID)
|
||||
}
|
||||
|
||||
private fun initNotification(galleryID: Int) {
|
||||
val intent = Intent(this, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryID", galleryID)
|
||||
}
|
||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||
addNextIntentWithParentStack(intent)
|
||||
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
|
||||
setContentTitle(getString(R.string.reader_loading))
|
||||
setContentText(getString(R.string.reader_notification_text))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P
|
||||
setContentIntent(pendingIntent)
|
||||
setProgress(0, 0, true)
|
||||
setOngoing(true)
|
||||
})
|
||||
}
|
||||
|
||||
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
|
||||
while (true) {
|
||||
if (queue.isEmpty() || clients.size() > preferences.getInt("max_download", 4))
|
||||
continue
|
||||
|
||||
val galleryID = queue.poll() ?: continue
|
||||
|
||||
if (clients.indexOfKey(galleryID) >= 0) // Gallery already downloading!
|
||||
continue
|
||||
|
||||
initNotification(galleryID)
|
||||
if (Cache(this@DownloadWorker).isDownloading(galleryID))
|
||||
notificationManager.notify(galleryID, notification[galleryID].build())
|
||||
worker.put(galleryID, download(galleryID))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
44
app/src/main/java/xyz/quaver/pupil/util/download/Metadata.kt
Normal file
44
app/src/main/java/xyz/quaver/pupil/util/download/Metadata.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util.download
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
val thumbnail: String? = null,
|
||||
val galleryBlock: GalleryBlock? = null,
|
||||
val reader: Reader? = null,
|
||||
val isDownloading: Boolean? = null
|
||||
) {
|
||||
constructor(
|
||||
metadata: Metadata?,
|
||||
thumbnail: String? = null,
|
||||
galleryBlock: GalleryBlock? = null,
|
||||
readers: Reader? = null,
|
||||
isDownloading: Boolean? = null
|
||||
) : this(
|
||||
thumbnail ?: metadata?.thumbnail,
|
||||
galleryBlock ?: metadata?.galleryBlock,
|
||||
readers ?: metadata?.reader,
|
||||
isDownloading ?: metadata?.isDownloading
|
||||
)
|
||||
}
|
||||
@@ -18,26 +18,197 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.os.storage.StorageManager
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.lang.reflect.Array
|
||||
import java.net.URL
|
||||
|
||||
fun getCachedGallery(context: Context, galleryID: Int): File {
|
||||
return File(getDownloadDirectory(context), galleryID.toString()).let {
|
||||
when {
|
||||
it.exists() -> it
|
||||
else -> File(context.cacheDir, "imageCache/$galleryID")
|
||||
|
||||
fun getCachedGallery(context: Context, galleryID: Int) =
|
||||
File(getDownloadDirectory(context), galleryID.toString()).let {
|
||||
if (it.exists())
|
||||
it
|
||||
else
|
||||
File(context.cacheDir, "imageCache/$galleryID")
|
||||
}
|
||||
|
||||
fun getDownloadDirectory(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
|
||||
if (it != null && !it.startsWith("content"))
|
||||
File(it)
|
||||
else
|
||||
context.getExternalFilesDir(null)!!
|
||||
}
|
||||
|
||||
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
||||
|
||||
if (to.parentFile?.exists() == false)
|
||||
to.parentFile!!.mkdirs()
|
||||
|
||||
if (!to.exists())
|
||||
to.createNewFile()
|
||||
|
||||
FileOutputStream(to).use { out ->
|
||||
|
||||
with(openConnection()) {
|
||||
val fileSize = contentLength.toLong()
|
||||
|
||||
getInputStream().use {
|
||||
|
||||
var bytesCopied: Long = 0
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
|
||||
var bytes = it.read(buffer)
|
||||
while (bytes >= 0) {
|
||||
out.write(buffer, 0, bytes)
|
||||
bytesCopied += bytes
|
||||
onDownloadProgress?.invoke(bytesCopied, fileSize)
|
||||
bytes = it.read(buffer)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun getDownloadDirectory(context: Context): File? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
context.getExternalFilesDir("Pupil")
|
||||
else
|
||||
File(Environment.getExternalStorageDirectory(), "Pupil")
|
||||
fun getExtSdCardPaths(context: Context) =
|
||||
ContextCompat.getExternalFilesDirs(context, null).drop(1).map {
|
||||
it.absolutePath.substringBeforeLast("/Android/data").let { path ->
|
||||
runCatching {
|
||||
File(path).canonicalPath
|
||||
}.getOrElse {
|
||||
path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const val PRIMARY_VOLUME_NAME = "primary"
|
||||
fun getVolumePath(context: Context, volumeID: String?): String? {
|
||||
return runCatching {
|
||||
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||
val storageVolumeClass = Class.forName("android.os.storage.StorageVolume")
|
||||
|
||||
val getVolumeList = storageVolumeClass.javaClass.getMethod("getVolumeList")
|
||||
val getUUID = storageVolumeClass.getMethod("getUuid")
|
||||
val getPath = storageVolumeClass.getMethod("getPath")
|
||||
val isPrimary = storageVolumeClass.getMethod("isPrimary")
|
||||
|
||||
val result = getVolumeList.invoke(storageManager)!!
|
||||
|
||||
val length = Array.getLength(result)
|
||||
|
||||
for (i in 0 until length) {
|
||||
val storageVolumeElement = Array.get(result, i)
|
||||
val uuid = getUUID.invoke(storageVolumeElement) as? String
|
||||
val primary = isPrimary.invoke(storageVolumeElement) as? Boolean
|
||||
|
||||
// primary volume?
|
||||
if (primary == true && volumeID == PRIMARY_VOLUME_NAME)
|
||||
return@runCatching getPath.invoke(storageVolumeElement) as? String
|
||||
|
||||
// other volumes?
|
||||
if (volumeID == uuid) {
|
||||
return@runCatching getPath.invoke(storageVolumeElement) as? String
|
||||
}
|
||||
}
|
||||
return@runCatching null
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
// Credits go to https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri/36162691#36162691
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
fun getVolumeIdFromTreeUri(uri: Uri) =
|
||||
DocumentsContract.getTreeDocumentId(uri).split(':').let {
|
||||
if (it.isNotEmpty())
|
||||
it[0]
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
fun getDocumentPathFromTreeUri(uri: Uri) =
|
||||
DocumentsContract.getTreeDocumentId(uri).split(':').let {
|
||||
if (it.size >= 2)
|
||||
it[1]
|
||||
else
|
||||
File.separator
|
||||
}
|
||||
|
||||
fun getFullPathFromTreeUri(context: Context, uri: Uri) : String? {
|
||||
val volumePath = getVolumePath(context, getVolumeIdFromTreeUri(uri) ?: return null).let {
|
||||
it ?: return File.separator
|
||||
|
||||
if (it.endsWith(File.separator))
|
||||
it.dropLast(1)
|
||||
else
|
||||
it
|
||||
}
|
||||
|
||||
val documentPath = getDocumentPathFromTreeUri(uri).let {
|
||||
if (it.endsWith(File.separator))
|
||||
it.dropLast(1)
|
||||
else
|
||||
it
|
||||
}
|
||||
|
||||
return if (documentPath.isNotEmpty()) {
|
||||
if (documentPath.startsWith(File.separator))
|
||||
volumePath + documentPath
|
||||
else
|
||||
volumePath + File.separator + documentPath
|
||||
} else
|
||||
volumePath
|
||||
}
|
||||
|
||||
// Huge thanks to avluis(https://github.com/avluis)
|
||||
// This code is originated from Hentoid(https://github.com/avluis/Hentoid) under Apache-2.0 license.
|
||||
fun Uri.toFile(context: Context): File? {
|
||||
val path = this.path ?: return null
|
||||
|
||||
val pathSeparator = path.indexOf(':')
|
||||
val folderName = path.substring(pathSeparator+1)
|
||||
|
||||
// Determine whether the designated file is
|
||||
// - on a removable media (e.g. SD card, OTG)
|
||||
// or
|
||||
// - on the internal phone memory
|
||||
val removableMediaFolderRoots = getExtSdCardPaths(context)
|
||||
|
||||
/* First test is to compare root names with known roots of removable media
|
||||
* In many cases, the SD card root name is shared between pre-SAF (File) and SAF (DocumentFile) frameworks
|
||||
* (e.g. /storage/3437-3934 vs. /tree/3437-3934)
|
||||
* This is what the following block is trying to do
|
||||
*/
|
||||
for (s in removableMediaFolderRoots) {
|
||||
val sRoot = s.substring(s.lastIndexOf(File.separatorChar))
|
||||
val root = path.substring(0, pathSeparator).let {
|
||||
it.substring(it.lastIndexOf(File.separatorChar))
|
||||
}
|
||||
|
||||
if (sRoot.equals(root, true)) {
|
||||
return File(s + File.separatorChar + folderName)
|
||||
}
|
||||
}
|
||||
/* In some other cases, there is no common name (e.g. /storage/sdcard1 vs. /tree/3437-3934)
|
||||
* We can use a slower method to translate the Uri obtained with SAF into a pre-SAF path
|
||||
* and compare it to the known removable media volume names
|
||||
*/
|
||||
val root = getFullPathFromTreeUri(context, this)
|
||||
|
||||
for (s in removableMediaFolderRoots) {
|
||||
if (root?.startsWith(s) == true) {
|
||||
return File(root)
|
||||
}
|
||||
}
|
||||
|
||||
return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
|
||||
}
|
||||
@@ -18,15 +18,14 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.parseList
|
||||
import kotlinx.serialization.stringify
|
||||
import kotlinx.serialization.list
|
||||
import kotlinx.serialization.serializer
|
||||
import java.io.File
|
||||
|
||||
class Histories(private val file: File) : ArrayList<Int>() {
|
||||
|
||||
val serializer = Int.serializer().list
|
||||
|
||||
init {
|
||||
if (!file.exists())
|
||||
file.parentFile?.mkdirs()
|
||||
@@ -38,21 +37,20 @@ class Histories(private val file: File) : ArrayList<Int>() {
|
||||
}
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun load() : Histories {
|
||||
return apply {
|
||||
super.clear()
|
||||
addAll(
|
||||
Json(JsonConfiguration.Stable).parseList(
|
||||
json.parse(
|
||||
serializer,
|
||||
file.bufferedReader().use { it.readText() }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun save() {
|
||||
file.writeText(Json(JsonConfiguration.Stable).stringify(this))
|
||||
file.writeText(json.stringify(serializer, this))
|
||||
}
|
||||
|
||||
override fun add(element: Int): Boolean {
|
||||
|
||||
@@ -21,9 +21,10 @@ package xyz.quaver.pupil.util
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.list
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
@@ -73,7 +74,6 @@ class LockManager(base: Context): ContextWrapper(base) {
|
||||
load()
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
private fun load() {
|
||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||
|
||||
@@ -82,17 +82,16 @@ class LockManager(base: Context): ContextWrapper(base) {
|
||||
lock.writeText("[]")
|
||||
}
|
||||
|
||||
locks = ArrayList(Json(JsonConfiguration.Stable).parseList(lock.readText()))
|
||||
locks = ArrayList(json.parse(Lock.serializer().list, lock.readText()))
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
private fun save() {
|
||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||
|
||||
if (!lock.exists())
|
||||
lock.createNewFile()
|
||||
|
||||
lock.writeText(Json(JsonConfiguration.Stable).stringify(locks?.toList() ?: listOf()))
|
||||
lock.writeText(json.stringify(Lock.serializer().list, locks?.toList() ?: listOf()))
|
||||
}
|
||||
|
||||
fun add(lock: Lock) {
|
||||
|
||||
@@ -18,18 +18,37 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
fun Context.hasPermission(permission: String) =
|
||||
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
|
||||
import android.annotation.SuppressLint
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@UseExperimental(ExperimentalStdlibApi::class)
|
||||
fun String.wordCapitalize() : String {
|
||||
val result = ArrayList<String>()
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
for (word in this.split(" "))
|
||||
result.add(word.capitalize())
|
||||
result.add(word.capitalize(Locale.US))
|
||||
|
||||
return result.joinToString(" ")
|
||||
}
|
||||
|
||||
fun byteToString(byte: Long, precision : Int = 1) : String {
|
||||
|
||||
val suffix = listOf(
|
||||
"B",
|
||||
"kB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB" //really?
|
||||
)
|
||||
var size = byte.toDouble(); var suffixIndex = 0
|
||||
|
||||
while (size >= 1024) {
|
||||
size /= 1024
|
||||
suffixIndex++
|
||||
}
|
||||
|
||||
return "%.${precision}f ${suffix[suffixIndex]}".format(size)
|
||||
|
||||
}
|
||||
@@ -18,32 +18,52 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import kotlinx.serialization.json.*
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.content
|
||||
import ru.noties.markwon.Markwon
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.R
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
|
||||
fun getReleases(url: String) : JsonArray {
|
||||
return try {
|
||||
URL(url).readText().let {
|
||||
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it)
|
||||
json.parse(JsonArray.serializer(), it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
JsonArray(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
fun checkUpdate(url: String) : JsonObject? {
|
||||
fun checkUpdate(context: Context, url: String) : JsonObject? {
|
||||
val releases = getReleases(url)
|
||||
|
||||
if (releases.isEmpty())
|
||||
return null
|
||||
|
||||
return releases.firstOrNull {
|
||||
if (BuildConfig.PRERELEASE) {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("beta", false))
|
||||
true
|
||||
} else {
|
||||
else
|
||||
it.jsonObject["prerelease"]?.boolean == false
|
||||
}
|
||||
}?.let {
|
||||
if (it.jsonObject["tag_name"]?.content == BuildConfig.VERSION_NAME)
|
||||
null
|
||||
@@ -52,13 +72,139 @@ fun checkUpdate(url: String) : JsonObject? {
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(releases: JsonObject) : Pair<String?, String?>? {
|
||||
fun getApkUrl(releases: JsonObject) : String? {
|
||||
return releases["assets"]?.jsonArray?.firstOrNull {
|
||||
Regex("Pupil-v(\\d+\\.)+\\d+\\.apk").matches(it.jsonObject["name"]?.content ?: "")
|
||||
Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.content ?: "")
|
||||
}.let {
|
||||
if (it == null)
|
||||
null
|
||||
else
|
||||
Pair(it.jsonObject["browser_download_url"]?.content, it.jsonObject["name"]?.content)
|
||||
it?.jsonObject?.get("browser_download_url")?.content
|
||||
}
|
||||
}
|
||||
|
||||
const val UPDATE_NOTIFICATION_ID = 384823
|
||||
fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
|
||||
|
||||
if (!force && ignoreUpdateUntil > System.currentTimeMillis())
|
||||
return
|
||||
|
||||
fun extractReleaseNote(update: JsonObject, locale: Locale) : String {
|
||||
val markdown = update["body"]!!.content
|
||||
|
||||
val target = when(locale.language) {
|
||||
"ko" -> "한국어"
|
||||
"ja" -> "日本語"
|
||||
else -> "English"
|
||||
}
|
||||
|
||||
val releaseNote = Regex("^# Release Note.+$")
|
||||
val language = Regex("^## $target$")
|
||||
val end = Regex("^#.+$")
|
||||
|
||||
var releaseNoteFlag = false
|
||||
var languageFlag = false
|
||||
|
||||
val result = StringBuilder()
|
||||
|
||||
for(line in markdown.lines()) {
|
||||
if (releaseNote.matches(line)) {
|
||||
releaseNoteFlag = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (releaseNoteFlag) {
|
||||
if (language.matches(line)) {
|
||||
languageFlag = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (languageFlag) {
|
||||
if (end.matches(line))
|
||||
break
|
||||
|
||||
result.append(line+"\n")
|
||||
}
|
||||
}
|
||||
|
||||
return context.getString(R.string.update_release_note, update["tag_name"]?.content, result.toString())
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val update =
|
||||
checkUpdate(context, context.getString(R.string.release_url)) ?: return@launch
|
||||
|
||||
val url = getApkUrl(update) ?: return@launch
|
||||
|
||||
val dialog = AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.update_title)
|
||||
val msg = extractReleaseNote(update, Locale.getDefault())
|
||||
setMessage(Markwon.create(context).toMarkdown(msg))
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
val builder = NotificationCompat.Builder(context, "download").apply {
|
||||
setContentTitle(context.getString(R.string.update_notification_description))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
setOngoing(true)
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch io@{
|
||||
val target = File(getDownloadDirectory(context), "Pupil.apk")
|
||||
|
||||
try {
|
||||
URL(url).download(target) { progress, fileSize ->
|
||||
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
|
||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
builder.apply {
|
||||
setContentText(context.getString(R.string.update_failed))
|
||||
setMessage(context.getString(R.string.update_failed_message))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setOngoing(false)
|
||||
}
|
||||
|
||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
||||
|
||||
return@io
|
||||
}
|
||||
|
||||
val install = Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
setDataAndType(FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", target), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||
}
|
||||
|
||||
builder.apply {
|
||||
setContentIntent(PendingIntent.getActivity(context, 0, install, 0))
|
||||
setProgress(0, 0, false)
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setContentTitle(context.getString(R.string.update_download_completed))
|
||||
setContentText(context.getString(R.string.update_download_completed_description))
|
||||
setOngoing(false)
|
||||
}
|
||||
|
||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||
|
||||
if (context.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED))
|
||||
context.startActivity(install)
|
||||
else
|
||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
setNegativeButton(if (force) android.R.string.no else R.string.ignore_update) { _, _ ->
|
||||
if (!force)
|
||||
preferences.edit()
|
||||
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable/hitomi.png
Normal file
BIN
app/src/main/res/drawable/hitomi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@@ -1,4 +1,4 @@
|
||||
<vector android:height="24dp" android:tint="#fff"
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#fff" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
||||
|
||||
8
app/src/main/res/drawable/menu.xml
Normal file
8
app/src/main/res/drawable/menu.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- drawable/menu.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#fff" android:pathData="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" />
|
||||
</vector>
|
||||
34
app/src/main/res/drawable/reader_item_boundary.xml
Normal file
34
app/src/main/res/drawable/reader_item_boundary.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Pupil, Hitomi.la viewer for Android
|
||||
~ Copyright (C) 2020 tom5079
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:bottom="1dp"
|
||||
android:left="1dp"
|
||||
android:right="1dp"
|
||||
android:top="1dp">
|
||||
<shape android:shape="rectangle" >
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#555555" />
|
||||
|
||||
<solid android:color="@color/transparent" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -29,7 +29,29 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/lock_button_layout"/>
|
||||
app:layout_constraintBottom_toTopOf="@id/lock_fingerprint_layout"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/lock_fingerprint_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintTop_toBottomOf="@id/lock_content"
|
||||
app:layout_constraintBottom_toTopOf="@id/lock_button_layout">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/lock_fingerprint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/fingerprint"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
app:backgroundTint="@color/dark_gray"
|
||||
app:fabSize="mini"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/lock_button_layout"
|
||||
@@ -37,7 +59,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/lock_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/lock_fingerprint_layout"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:gravity="center">
|
||||
|
||||
@@ -59,16 +81,6 @@
|
||||
app:backgroundTint="@color/dark_gray"
|
||||
app:fabSize="mini"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/lock_fingerprint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/fingerprint"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
app:backgroundTint="@color/dark_gray"
|
||||
app:fabSize="mini"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/lock_password"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -26,18 +26,11 @@
|
||||
android:background="@color/dark_gray"
|
||||
tools:context=".ui.ReaderActivity">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/reader_recyclerview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/reader_recyclerview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
||||
|
||||
</LinearLayout>
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -18,21 +18,12 @@
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/default_query_dialog_title"
|
||||
style="@style/TextAppearance.AppCompat.Large"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/default_query_dialog_title"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<EditText
|
||||
tools:ignore="Autofill"
|
||||
android:inputType="text"
|
||||
@@ -40,7 +31,7 @@
|
||||
android:id="@+id/default_query_dialog_edittext"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_title"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
@@ -116,14 +107,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/default_query_dialog_ok"
|
||||
style="?borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_guro_layout"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:text="@android:string/ok"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
25
app/src/main/res/layout/dialog_dl_location.xml
Normal file
25
app/src/main/res/layout/dialog_dl_location.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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/>.
|
||||
-->
|
||||
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"/>
|
||||
@@ -40,7 +40,7 @@
|
||||
android:padding="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/gallery_thumbnail"
|
||||
android:id="@+id/gallery_cover"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
@@ -55,7 +55,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@id/gallery_thumbnail"
|
||||
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"/>
|
||||
@@ -66,7 +66,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/gallery_title"
|
||||
app:layout_constraintLeft_toRightOf="@id/gallery_thumbnail"
|
||||
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"/>
|
||||
@@ -83,7 +83,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@id/gallery_thumbnail"
|
||||
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"/>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context=".ui.PatternLockFragment">
|
||||
tools:context=".ui.fragment.PatternLockFragment">
|
||||
|
||||
<com.andrognito.patternlockview.PatternLockView
|
||||
android:id="@+id/lock_pattern_view"
|
||||
|
||||
51
app/src/main/res/layout/item_dl_location.xml
Normal file
51
app/src/main/res/layout/item_dl_location.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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/>.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="horizontal" android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/button"
|
||||
android:clickable="false"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/location_type"
|
||||
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/location_available"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -35,6 +35,6 @@
|
||||
android:id="@+id/gallery_details_tags"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:chipSpacingVertical="8dp"/>
|
||||
app:chipSpacingVertical="4dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -23,161 +23,203 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingLeft="0dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingRight="8dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:clickable="true">
|
||||
android:clipChildren="true">
|
||||
|
||||
<LinearLayout
|
||||
<com.daimajia.swipe.SwipeLayout
|
||||
android:id="@+id/galleryblock_swipe_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ProgressBar
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:id="@+id/galleryblock_progressbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<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"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/galleryblock_thumbnail"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/galleryblock_thumbnail_description"
|
||||
android:adjustViewBounds="true"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<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_marginStart="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_marginStart="8dp"
|
||||
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_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
|
||||
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/galleryblock_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
|
||||
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/galleryblock_language"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
|
||||
app:layout_constraintBottom_toTopOf="@id/galleryblock_padding"
|
||||
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
|
||||
|
||||
<View
|
||||
android:id="@+id/galleryblock_padding"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_language"
|
||||
app:layout_constraintBottom_toTopOf="@id/galleryblock_tag_group"/>
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/galleryblock_tag_group"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:chipSpacing="2dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding"
|
||||
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@android:color/darker_gray"/>
|
||||
app:drag_edge="right"
|
||||
app:show_mode="pull_out">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:orientation="horizontal">
|
||||
android:id="@+id/galleryblock_secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/galleryblock_id"
|
||||
android:id="@+id/galleryblock_download"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="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="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:clickable="true"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/galleryblock_favorite"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
app:srcCompat="@drawable/ic_star_empty"/>
|
||||
<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="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:clickable="true"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/galleryblock_primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:clickable="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ProgressBar
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:id="@+id/galleryblock_progressbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<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"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/galleryblock_thumbnail"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/galleryblock_thumbnail_description"
|
||||
android:adjustViewBounds="true"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<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_marginStart="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_marginStart="8dp"
|
||||
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_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
|
||||
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/galleryblock_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
|
||||
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/galleryblock_language"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
|
||||
app:layout_constraintBottom_toTopOf="@id/galleryblock_padding"
|
||||
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
|
||||
|
||||
<View
|
||||
android:id="@+id/galleryblock_padding"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_language"
|
||||
app:layout_constraintBottom_toTopOf="@id/galleryblock_tag_group"/>
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/galleryblock_tag_group"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:chipSpacing="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding"
|
||||
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@android:color/darker_gray"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/galleryblock_id"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/galleryblock_favorite"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
app:srcCompat="@drawable/ic_star_empty"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.daimajia.swipe.SwipeLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
49
app/src/main/res/layout/item_mirrors.xml
Normal file
49
app/src/main/res/layout/item_mirrors.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Pupil, Hitomi.la viewer for Android
|
||||
~ Copyright (C) 2020 tom5079
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingLeft="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:paddingRight="32dp"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mirror_name"
|
||||
style="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mirror_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/menu"
|
||||
app:tint="?attr/colorControlNormal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -36,7 +36,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
app:srcCompat="@drawable/ic_navigate_next_black_24dp"
|
||||
android:tint="@color/colorAccent"
|
||||
app:tint="@color/colorAccent"
|
||||
android:rotation="180"/>
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
app:srcCompat="@drawable/ic_navigate_next_black_24dp"
|
||||
android:tint="@color/colorAccent"
|
||||
app:tint="@color/colorAccent"
|
||||
android:rotation="180"/>
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -17,11 +17,57 @@
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:contentDescription="@string/reader_imageview_description"
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="100dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:adjustViewBounds="true"/>
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:background="@drawable/reader_item_boundary">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/reader_item_progressbar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="?android:progressBarStyleHorizontal"
|
||||
android:indeterminate="false"
|
||||
android:progress="0"
|
||||
android:max="100"
|
||||
android:visibility="visible"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reader_index"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.AppCompat.Caption"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.github.chrisbanes.photoview.PhotoView
|
||||
android:id="@+id/image"
|
||||
android:contentDescription="@string/reader_imageview_description"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="8dp"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
<ImageView
|
||||
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="@dimen/nav_header_height"
|
||||
android:background="@drawable/side_nav_bar"
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
android:title="@string/main_drawer_group_contact_email"
|
||||
android:icon="@drawable/ic_email"/>
|
||||
<item android:id="@+id/main_drawer_kakaotalk"
|
||||
android:title="@string/main_drawer_grouop_contact_kakaotalk"
|
||||
android:title="@string/main_drawer_grouop_contact_discord"
|
||||
android:icon="@drawable/ic_message"/>
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
@@ -25,11 +25,9 @@
|
||||
android:icon="@drawable/avd_star"
|
||||
app:showAsAction="always"/>
|
||||
|
||||
<item android:id="@+id/reader_menu_use_hiyobi"
|
||||
<item android:id="@+id/reader_type"
|
||||
android:title=""
|
||||
android:icon="@drawable/ic_hiyobi"
|
||||
app:showAsAction="ifRoom"
|
||||
android:visible="false"/>
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item android:id="@+id/reader_menu_page_indicator"
|
||||
android:title="@string/page_indicator_placeholder"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<string name="search_hint_with_page">ギャラリー検索</string>
|
||||
<string name="settings_clear_cache">キャッシュクリア</string>
|
||||
<string name="settings_clear_cache_alert_message">キャッシュをクリアするとイメージのロード速度に影響を与えます。実行しますか?</string>
|
||||
<string name="settings_clear_summary">サイズ: %1$d%2$s</string>
|
||||
<string name="settings_clear_summary">サイズ: %s</string>
|
||||
<string name="settings_default_query">デフォルトキーワード</string>
|
||||
<string name="settings_galleries_per_page">一回にロードするギャラリー数</string>
|
||||
<string name="settings_search_title">検索設定</string>
|
||||
@@ -18,7 +18,7 @@
|
||||
<string name="update_title">新しいアップデートがあります</string>
|
||||
<string name="warning">注意</string>
|
||||
<string name="settings_miscellaneous_title">その他</string>
|
||||
<string name="settings_use_hiyobi_title">hiyobi.meからロード</string>
|
||||
<string name="settings_mirror_title">ミラーサーバー</string>
|
||||
<string name="settings_clear_history">履歴を削除</string>
|
||||
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
|
||||
<string name="settings_clear_history_summary">履歴数: %1$d</string>
|
||||
@@ -61,15 +61,15 @@
|
||||
<string name="main_export_error">エクスポートエラーが発生しました</string>
|
||||
<string name="settings_clear_downloads">ダウンロード削除</string>
|
||||
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか?</string>
|
||||
<string name="settings_use_hiyobi_summary">ロード速度を向上させるため可能であればhiyobi.meからイメージロード</string>
|
||||
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</string>
|
||||
<string name="main_drawer_favorite">お気に入り</string>
|
||||
<string name="main_open_gallery_by_id">ギャラリー番号で見る</string>
|
||||
<string name="main_open_gallery_by_id_error">エラーが発生しました</string>
|
||||
<string name="reader_failed_to_find_gallery">エラーが発生しました</string>
|
||||
<string name="settings_storage">ストレージ</string>
|
||||
<string name="main_drawer_grouop_contact_kakaotalk">カカオトーク</string>
|
||||
<string name="main_drawer_grouop_contact_discord">ディスコード</string>
|
||||
<string name="settings_app_lock">アプリロック</string>
|
||||
<string name="settings_app_lock_type">アップロックの種類</string>
|
||||
<string name="settings_app_version_title">バージョン</string>
|
||||
<string name="settings_app_version_title">バージョン(アップデート確認)</string>
|
||||
<string name="settings_lock_biometrics">生体認識</string>
|
||||
<string name="settings_lock_confirm">ロック確認のためもう一回入力してください。</string>
|
||||
<string name="settings_lock_enabled">有効</string>
|
||||
@@ -84,7 +84,7 @@
|
||||
<string name="main_menu_sort_newest">投稿日時順</string>
|
||||
<string name="main_menu_sort_popular">人気順</string>
|
||||
<string name="update_failed">アップデートに失敗しました</string>
|
||||
<string name="update_failed_message">マニュアルインストールが必要です。APKファイルは</string>
|
||||
<string name="update_failed_message">アップデート中エラーが発生しました</string>
|
||||
<string name="ignore_update">無視</string>
|
||||
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string>
|
||||
<string name="update_no_permission">権限がないため自動アップデートを行えません。ホームページで直接ダウンロードしてください。</string>
|
||||
@@ -99,7 +99,27 @@
|
||||
<string name="gallery_tags">タグ</string>
|
||||
<string name="gallery_thumbnails">サムネイル</string>
|
||||
<string name="gallery_related">おすすめ</string>
|
||||
<string name="settings_nomedia_summary">イメージを隠す</string>
|
||||
<string name="settings_nomedia_title">イメージをギャラリーから見えなくする</string>
|
||||
<string name="settings_nomedia_summary">イメージをギャラリーから見えなくする</string>
|
||||
<string name="settings_nomedia_title">イメージを隠す</string>
|
||||
<string name="reader_help">ヘルプ</string>
|
||||
<string name="main_delete">削除</string>
|
||||
<string name="main_download">ダウンロード</string>
|
||||
<string name="settings_backup_title">お気に入りバックアップ</string>
|
||||
<string name="settings_restore_title">お気に入り復元</string>
|
||||
<string name="settings_backup_snackbar">バックアップファイルを作成しました</string>
|
||||
<string name="settings_backup_checkout">確認</string>
|
||||
<string name="settings_restore_failed">復元に失敗しました</string>
|
||||
<string name="settings_restore_successful">%1$d項目を復元しました</string>
|
||||
<string name="settings_dl_location">ダウンロード場所</string>
|
||||
<string name="settings_dl_location_internal">内部ストレージ</string>
|
||||
<string name="settings_dl_location_removable">外部SDカード</string>
|
||||
<string name="settings_dl_location_available">%s 使用可能</string>
|
||||
<string name="update_download_completed">ダウンロードが完了しました</string>
|
||||
<string name="update_download_completed_description">ここをクリックしてアップデートを行えます</string>
|
||||
<string name="settings_beta">ベータチャンネルでアップデートを受信</string>
|
||||
<string name="settings_app_version_description">v%s</string>
|
||||
<string name="settings_low_quality">低解像度イメージ</string>
|
||||
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
|
||||
<string name="settings_dl_location_custom">手動で設定</string>
|
||||
<string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
|
||||
</resources>
|
||||
@@ -8,7 +8,7 @@
|
||||
<string name="settings_default_query">기본 검색어</string>
|
||||
<string name="settings_clear_cache">캐시 정리하기</string>
|
||||
<string name="settings_clear_cache_alert_message">캐시를 정리하면 이미지 로딩속도가 느려질 수 있습니다. 계속하시겠습니까?</string>
|
||||
<string name="settings_clear_summary">사용량: %1$d%2$s</string>
|
||||
<string name="settings_clear_summary">사용량: %s</string>
|
||||
<string name="settings_galleries_per_page">한 번에 로드할 갤러리 수</string>
|
||||
<string name="settings_search_title">검색 설정</string>
|
||||
<string name="settings_title">설정</string>
|
||||
@@ -18,7 +18,6 @@
|
||||
<string name="main_no_result">결과 없음</string>
|
||||
<string name="main_search">검색</string>
|
||||
<string name="settings_miscellaneous_title">기타</string>
|
||||
<string name="settings_use_hiyobi_title">hiyobi.me 사용</string>
|
||||
<string name="settings_clear_history">기록 삭제</string>
|
||||
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
||||
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
|
||||
@@ -61,15 +60,14 @@
|
||||
<string name="main_export_error">내보내기 오류가 발생했습니다</string>
|
||||
<string name="settings_clear_downloads">다운로드 삭제</string>
|
||||
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
|
||||
<string name="settings_use_hiyobi_summary">속도 향상을 위해 가능하면 hiyobi.me에서 이미지 로드</string>
|
||||
<string name="main_drawer_favorite">즐겨찾기</string>
|
||||
<string name="main_open_gallery_by_id">갤러리 번호로 열기</string>
|
||||
<string name="main_open_gallery_by_id_error">갤러리를 찾지 못했습니다</string>
|
||||
<string name="reader_failed_to_find_gallery">갤러리를 찾지 못했습니다</string>
|
||||
<string name="settings_storage">저장 공간</string>
|
||||
<string name="main_drawer_grouop_contact_kakaotalk">카카오톡 오픈채팅방</string>
|
||||
<string name="main_drawer_grouop_contact_discord">디스코드</string>
|
||||
<string name="settings_app_lock">앱 잠금</string>
|
||||
<string name="settings_app_lock_type">앱 잠금 종류</string>
|
||||
<string name="settings_app_version_title">앱 버전</string>
|
||||
<string name="settings_app_version_title">앱 버전(업데이트 확인)</string>
|
||||
<string name="settings_lock_biometrics">생체 인식</string>
|
||||
<string name="settings_lock_confirm">잠금 확인을 위해 한번 더 입력해주세요</string>
|
||||
<string name="settings_lock_enabled">사용 중</string>
|
||||
@@ -83,8 +81,8 @@
|
||||
<string name="main_menu_sort">정렬</string>
|
||||
<string name="main_menu_sort_popular">인기순</string>
|
||||
<string name="main_menu_sort_newest">시간순</string>
|
||||
<string name="update_failed">"업데이트 "</string>
|
||||
<string name="update_failed_message">수동 업데이트가 필요합니다. APK 파일은 다운로드 폴더에 있습니다.</string>
|
||||
<string name="update_failed">"업데이트 에러</string>
|
||||
<string name="update_failed_message">업데이트 중 에러가 발생했습니다</string>
|
||||
<string name="ignore_update">무시</string>
|
||||
<string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string>
|
||||
<string name="update_no_permission">권한이 부여되어 있지 않아 자동 업데이트를 진행할 수 없습니다. 홈페이지에서 직접 다운로드 받으시기 바랍니다.</string>
|
||||
@@ -99,7 +97,29 @@
|
||||
<string name="gallery_tags">태그</string>
|
||||
<string name="gallery_related">관련 갤러리</string>
|
||||
<string name="gallery_thumbnails">미리보기</string>
|
||||
<string name="settings_nomedia_summary">이미지 숨기기</string>
|
||||
<string name="settings_nomedia_title">갤러리에서 이미지 검색이 되지 않도록 합니다</string>
|
||||
<string name="settings_nomedia_summary">갤러리에서 이미지 검색이 되지 않도록 합니다</string>
|
||||
<string name="settings_nomedia_title">이미지 숨기기</string>
|
||||
<string name="reader_help">도움말</string>
|
||||
<string name="main_delete">삭제</string>
|
||||
<string name="main_download">다운로드</string>
|
||||
<string name="settings_backup_title">즐겨찾기 백업</string>
|
||||
<string name="settings_restore_title">즐겨찾기 복원</string>
|
||||
<string name="settings_backup_snackbar">백업 파일을 생성하였습니다</string>
|
||||
<string name="settings_backup_checkout">확인</string>
|
||||
<string name="settings_restore_failed">복원에 실패했습니다</string>
|
||||
<string name="settings_restore_successful">%1$d개 항목을 복원했습니다</string>
|
||||
<string name="settings_dl_location">다운로드 위치</string>
|
||||
<string name="settings_dl_location_internal">내부 저장공간</string>
|
||||
<string name="settings_dl_location_removable">외부 SD카드</string>
|
||||
<string name="settings_dl_location_available">%s 사용 가능</string>
|
||||
<string name="update_download_completed">다운로드가 완료되었습니다</string>
|
||||
<string name="update_download_completed_description">여기를 클릭해서 업데이트를 진행할 수 있습니다</string>
|
||||
<string name="settings_beta">베타 채널에서 업데이트</string>
|
||||
<string name="settings_app_version_description">v%s</string>
|
||||
<string name="settings_low_quality">저해상도 이미지</string>
|
||||
<string name="settings_low_quality_summary">로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드</string>
|
||||
<string name="settings_mirror_summary">미러 서버에서 이미지 로드</string>
|
||||
<string name="settings_mirror_title">미러 설정</string>
|
||||
<string name="settings_dl_location_custom">직접 설정</string>
|
||||
<string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
|
||||
</resources>
|
||||
@@ -57,4 +57,9 @@
|
||||
<item>japanese|日本語</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="mirrors">
|
||||
<item>HITOMI|hitomi.la</item>
|
||||
<item>HIYOBI|hiyobi.me</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
@@ -7,9 +7,9 @@
|
||||
<string name="home_page" translatable="false">http://bit.ly/2EZDClw</string>
|
||||
<string name="update" translatable="false">http://bit.ly/2ZlOjXJ</string>
|
||||
<string name="help" translatable="false">http://bit.ly/2Z7lNZE</string>
|
||||
<string name="github" translatable="false">https://github.com/tom5079/Pupil-issue/issues/new/choose</string>
|
||||
<string name="github" translatable="false">https://github.com/tom5079/Pupil/</string>
|
||||
<string name="email" translatable="false">mailto:pupil.hentai@gmail.com</string>
|
||||
<string name="kakaotalk" translatable="false">https://open.kakao.com/o/gvNrncsb</string>
|
||||
<string name="discord" translatable="false">https://discord.gg/Stj4b5v</string>
|
||||
<string name="error_help" translatable="false">http://bit.ly/2KYYhto</string>
|
||||
|
||||
<string name="main_settings" translatable="false">Settings</string>
|
||||
@@ -28,7 +28,7 @@
|
||||
<string name="https_block_alert">(Korean only)</string>
|
||||
|
||||
<string name="update_failed">Update failed</string>
|
||||
<string name="update_failed_message">Please install manually. APK file is in the Downloads folder.</string>
|
||||
<string name="update_failed_message">Please install manually by visiting github release page :{ (or try again!)</string>
|
||||
<string name="update_no_permission">Cannot auto update because permission is denied. Please download manually from the webpage.</string>
|
||||
<string name="ignore_update">Ignore</string>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<string name="main_drawer_group_contact_homepage">Visit homepage</string>
|
||||
<string name="main_drawer_group_contact_github">Visit github</string>
|
||||
<string name="main_drawer_group_contact_email">Email me!</string>
|
||||
<string name="main_drawer_grouop_contact_kakaotalk">Kakaotalk</string>
|
||||
<string name="main_drawer_grouop_contact_discord">Discord</string>
|
||||
|
||||
<string name="main_menu_sort">Sort</string>
|
||||
<string name="main_menu_sort_newest">Newest</string>
|
||||
@@ -60,7 +60,7 @@
|
||||
<string name="main_jump_title">Jump to page</string>
|
||||
<string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string>
|
||||
<string name="main_open_gallery_by_id">Open Gallery by ID</string>
|
||||
<string name="main_open_gallery_by_id_error">Failed to open gallery</string>
|
||||
<string name="reader_failed_to_find_gallery">Failed to open gallery</string>
|
||||
|
||||
<string name="main_move">Move to page %1$d</string>
|
||||
|
||||
@@ -71,9 +71,14 @@
|
||||
<string name="main_export_open_folder">Open Folder</string>
|
||||
<string name="main_export_error">Error occurred during export</string>
|
||||
|
||||
<string name="main_download">DOWNLOAD</string>
|
||||
<string name="main_delete">DELETE</string>
|
||||
|
||||
<string name="update_title">Update available</string>
|
||||
<string name="update_download_started">Download started</string>
|
||||
<string name="update_notification_description">Downloading apk…</string>
|
||||
<string name="update_download_completed">Download Completed</string>
|
||||
<string name="update_download_completed_description">Click here to update</string>
|
||||
<string name="update_notification_description">Downloading update…</string>
|
||||
<string name="update_release_note"># Release Note(v%1$s)\n%2$s</string>
|
||||
|
||||
<string name="search_hint">Search galleries</string>
|
||||
@@ -93,6 +98,8 @@
|
||||
<string name="galleryblock_type">Type: %1$s</string>
|
||||
<string name="galleryblock_language">Language: %1$s</string>
|
||||
|
||||
<!-- READER -->
|
||||
|
||||
<string name="reader_loading">Loading</string>
|
||||
<string name="reader_go_to_page">Go to page</string>
|
||||
<string name="reader_fab_fullscreen">Fullscreen</string>
|
||||
@@ -104,31 +111,63 @@
|
||||
|
||||
<string name="reader_help">Help</string>
|
||||
|
||||
<!-- SETTINGS -->
|
||||
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="settings_app_version_title">App version</string>
|
||||
|
||||
<string name="settings_app_version_title">App version(Click to check update)</string>
|
||||
<string name="settings_app_version_description">v%s</string>
|
||||
<string name="settings_beta">Update from beta channel</string>
|
||||
|
||||
<!-- SEARCH -->
|
||||
|
||||
<string name="settings_search_title">Search Settings</string>
|
||||
<string name="settings_galleries_per_page">Galleries per page</string>
|
||||
<string name="settings_default_query">Default query</string>
|
||||
|
||||
<!-- SETTINGS/STORAGE -->
|
||||
|
||||
<string name="settings_storage">Storage</string>
|
||||
<string name="settings_clear_cache">Clear cache</string>
|
||||
<string name="settings_clear_cache_alert_message">Deleting cache can affect image loading speed. Do you want to continue?</string>
|
||||
<string name="settings_clear_summary">Currently using %1$d%2$s</string>
|
||||
<string name="settings_clear_summary">Currently using %s</string>
|
||||
<string name="settings_clear_downloads">Clear downloads</string>
|
||||
<string name="settings_clear_downloads_alert_message">Delete all downloaded galleries.\nDo you want to continue?</string>
|
||||
<string name="settings_clear_history">Clear history</string>
|
||||
<string name="settings_clear_history_alert_message">Do you want to clear histories?</string>
|
||||
<string name="settings_clear_history_summary">%1$d histories saved</string>
|
||||
<string name="settings_dl_location">Download directory</string>
|
||||
<string name="settings_dl_location_removable">Removable Storage</string>
|
||||
<string name="settings_dl_location_internal">Internal Storage</string>
|
||||
<string name="settings_dl_location_available">%s available</string>
|
||||
<string name="settings_dl_location_custom">Custom Location</string>
|
||||
<string name="settings_dl_location_not_writable">This folder is not writable. Please select another folder.</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>
|
||||
|
||||
<!-- SETTINGS/APP LOCK -->
|
||||
|
||||
<string name="settings_app_lock">App lock</string>
|
||||
<string name="settings_app_lock_type">App lock type</string>
|
||||
|
||||
<!-- SETTINGS/MISCELLANEOUS -->
|
||||
|
||||
<string name="settings_miscellaneous_title">Miscellaneous</string>
|
||||
<string name="settings_use_hiyobi_title">Use hiyobi.me</string>
|
||||
<string name="settings_use_hiyobi_summary">Load images from hiyobi.me to improve loading speed (if available)</string>
|
||||
<string name="settings_mirror_summary">Load images from mirrors</string>
|
||||
<string name="settings_security_mode_title">Enable security mode</string>
|
||||
<string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string>
|
||||
<string name="settings_dark_mode_title">Dark mode</string>
|
||||
<string name="settings_dark_mode_summary">Protect yourself against light attacks!</string>
|
||||
<string name="settings_nomedia_title">Hide image from gallery</string>
|
||||
<string name="settings_nomedia_summary">Hides image from gallery</string>
|
||||
<string name="settings_backup_title">Backup favorites</string>
|
||||
<string name="settings_backup_snackbar">Backup file created</string>
|
||||
<string name="settings_backup_checkout">Check out</string>
|
||||
<string name="settings_restore_title">Restore favorites</string>
|
||||
<string name="settings_restore_failed">Restore failed</string>
|
||||
<string name="settings_restore_successful">%1$d entries restored</string>
|
||||
|
||||
<!-- SETTINGS/APP LOCK ACTIVITY -->
|
||||
|
||||
<string name="settings_lock_none">None</string>
|
||||
<string name="settings_lock_pattern">Pattern</string>
|
||||
@@ -141,10 +180,13 @@
|
||||
<string name="settings_lock_remove_message">Do you want to remove lock?</string>
|
||||
<string name="settings_lock_wrong_confirm">Lock is different from last one. Please try again.</string>
|
||||
|
||||
<!-- SETTINGS/DEFAULT QUERY DIALOG -->
|
||||
|
||||
<string name="default_query_dialog_title">Set default query</string>
|
||||
<string name="default_query_dialog_language">Language: </string>
|
||||
<string name="default_query_dialog_filter_BL">Filter BL</string>
|
||||
<string name="default_query_dialog_filter_guro">Filter Guro</string>
|
||||
<string name="default_query_dialog_language_selector_none">Any</string>
|
||||
<string name="settings_mirror_title">Mirrors</string>
|
||||
|
||||
</resources>
|
||||
|
||||
23
app/src/main/res/xml/file_paths.xml
Normal file
23
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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/>.
|
||||
-->
|
||||
|
||||
<paths>
|
||||
<external-path name="external" path="/"/>
|
||||
<external-files-path name="files" path="/"/>
|
||||
</paths>
|
||||
@@ -3,8 +3,12 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Preference
|
||||
app:title="@string/settings_app_version_title"
|
||||
app:key="app_version"/>
|
||||
app:key="app_version"
|
||||
app:title="@string/settings_app_version_title"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="beta"
|
||||
app:title="@string/settings_beta"/>
|
||||
|
||||
<PreferenceCategory
|
||||
app:title="@string/settings_search_title">
|
||||
@@ -28,16 +32,25 @@
|
||||
app:title="@string/settings_storage">
|
||||
|
||||
<Preference
|
||||
app:title="@string/settings_clear_cache"
|
||||
app:key="delete_cache"/>
|
||||
app:key="delete_cache"
|
||||
app:title="@string/settings_clear_cache"/>
|
||||
|
||||
<Preference
|
||||
app:title="@string/settings_clear_downloads"
|
||||
app:key="delete_downloads"/>
|
||||
app:key="delete_downloads"
|
||||
app:title="@string/settings_clear_downloads"/>
|
||||
|
||||
<Preference
|
||||
app:title="@string/settings_clear_history"
|
||||
app:key="clear_history"/>
|
||||
app:key="clear_history"
|
||||
app:title="@string/settings_clear_history"/>
|
||||
|
||||
<Preference
|
||||
app:key="dl_location"
|
||||
app:title="@string/settings_dl_location"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="dl_low_quality"
|
||||
app:title="@string/settings_low_quality"
|
||||
app:summary="@string/settings_low_quality_summary"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
@@ -45,34 +58,43 @@
|
||||
app:title="@string/settings_app_lock">
|
||||
|
||||
<Preference
|
||||
app:title="@string/settings_app_lock_type"
|
||||
app:key="app_lock"/>
|
||||
app:key="app_lock"
|
||||
app:title="@string/settings_app_lock_type"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:title="@string/settings_miscellaneous_title">
|
||||
|
||||
<SwitchPreference
|
||||
app:key="use_hiyobi"
|
||||
app:title="@string/settings_use_hiyobi_title"
|
||||
app:summary="@string/settings_use_hiyobi_summary"/>
|
||||
<Preference
|
||||
app:key="mirrors"
|
||||
app:title="@string/settings_mirror_title"
|
||||
app:summary="@string/settings_mirror_summary"/>
|
||||
|
||||
<SwitchPreference
|
||||
<SwitchPreferenceCompat
|
||||
app:key="security_mode"
|
||||
app:title="@string/settings_security_mode_title"
|
||||
app:summary="@string/settings_security_mode_summary"
|
||||
app:defaultValue="true"/>
|
||||
|
||||
<SwitchPreference
|
||||
<SwitchPreferenceCompat
|
||||
app:key="dark_mode"
|
||||
app:title="@string/settings_dark_mode_title"
|
||||
app:summary="@string/settings_dark_mode_summary"/>
|
||||
|
||||
<SwitchPreference
|
||||
<SwitchPreferenceCompat
|
||||
app:key="nomedia"
|
||||
app:title="@string/settings_nomedia_title"
|
||||
app:summary="@string/settings_nomedia_title"/>
|
||||
|
||||
<Preference
|
||||
app:key="backup"
|
||||
app:title="@string/settings_backup_title"/>
|
||||
|
||||
<Preference
|
||||
app:key="restore"
|
||||
app:title="@string/settings_restore_title"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
||||
@@ -16,26 +16,26 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@file:Suppress("UNUSED_VARIABLE")
|
||||
@file:Suppress("UNUSED_VARIABLE", "IncorrectScope")
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
|
||||
import android.util.SparseArray
|
||||
import org.junit.Test
|
||||
|
||||
class ExampleUnitTest {
|
||||
|
||||
@Test
|
||||
fun test() {
|
||||
val current = "0.1"
|
||||
val latest = "0.2"
|
||||
val arr = SparseArray<Float>()
|
||||
|
||||
print(current < latest)
|
||||
print(arr.indexOfKey(34))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
15
build.gradle
15
build.gradle
@@ -1,23 +1,21 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.3.61'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
maven { url 'https://maven.fabric.io/public' }
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
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.1'
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
classpath 'io.fabric.tools:gradle:1.29.0'
|
||||
classpath 'io.fabric.tools:gradle:1.31.0'
|
||||
classpath 'com.google.firebase:perf-plugin:1.3.1'
|
||||
}
|
||||
}
|
||||
@@ -27,9 +25,10 @@ allprojects {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url "https://jitpack.io" }
|
||||
maven { url 'http://guardian.github.com/maven/repo-releases' }
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
## For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
#
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
#
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
#Thu Jan 30 12:29:48 KST 2020
|
||||
kotlin.code.style=official
|
||||
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableR8.fullMode=true
|
||||
@@ -5,10 +5,10 @@ apply plugin: 'kotlinx-serialization'
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0"
|
||||
implementation 'org.jsoup:jsoup:1.11.3'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
|
||||
implementation 'org.jsoup:jsoup:1.12.1'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
}
|
||||
|
||||
sourceCompatibility = "7"
|
||||
|
||||
24
libpupil/src/main/java/xyz/quaver/Code.kt
Normal file
24
libpupil/src/main/java/xyz/quaver/Code.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2020 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
|
||||
|
||||
|
||||
enum class Code {
|
||||
HITOMI,
|
||||
HIYOBI,
|
||||
SORALA
|
||||
}
|
||||
26
libpupil/src/main/java/xyz/quaver/Utils.kt
Normal file
26
libpupil/src/main/java/xyz/quaver/Utils.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
fun availableInHiyobi(galleryID: Int) : Boolean {
|
||||
return try {
|
||||
xyz.quaver.hiyobi.getReader(galleryID)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,24 @@
|
||||
|
||||
package xyz.quaver.hitomi
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.list
|
||||
import java.net.URL
|
||||
|
||||
const val protocol = "https:"
|
||||
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
fun getGalleryInfo(galleryID: Int) =
|
||||
Json.nonstrict.parse(
|
||||
GalleryInfo.serializer().list,
|
||||
Regex("""\[.+]""").find(
|
||||
URL("$protocol//$domain/galleries/$galleryID.js").readText()
|
||||
)?.value ?: "[]"
|
||||
)
|
||||
|
||||
//common.js
|
||||
var adapose = false
|
||||
const val numberOfFrontends = 2
|
||||
const val numberOfFrontends = 3
|
||||
const val domain = "ltn.hitomi.la"
|
||||
const val galleryblockdir = "galleryblock"
|
||||
const val nozomiextension = ".nozomi"
|
||||
@@ -37,20 +50,14 @@ fun subdomainFromGalleryID(g: Int) : String {
|
||||
fun subdomainFromURL(url: String, base: String? = null) : String {
|
||||
var retval = "a"
|
||||
|
||||
if (base != null)
|
||||
if (!base.isNullOrBlank())
|
||||
retval = base
|
||||
|
||||
val r = Regex("""/\d*(\d)/""")
|
||||
val m = r.find(url)
|
||||
val b = 16
|
||||
val r = Regex("""/[0-9a-f]/([0-9a-f]{2})/""")
|
||||
val m = r.find(url) ?: return retval
|
||||
|
||||
m ?: return retval
|
||||
|
||||
var g = m.groups[1]!!.value.toIntOrNull()
|
||||
|
||||
g ?: return retval
|
||||
|
||||
if (g == 1)
|
||||
g = 0
|
||||
val g = m.groupValues[1].toIntOrNull(b) ?: return retval
|
||||
|
||||
retval = subdomainFromGalleryID(g) + retval
|
||||
|
||||
@@ -58,5 +65,22 @@ fun subdomainFromURL(url: String, base: String? = null) : String {
|
||||
}
|
||||
|
||||
fun urlFromURL(url: String, base: String? = null) : String {
|
||||
return url.replace(Regex("//..?\\.hitomi\\.la/"), "//${subdomainFromURL(url, base)}.hitomi.la/")
|
||||
}
|
||||
return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/")
|
||||
}
|
||||
|
||||
fun fullPathFromHash(hash: String?) : String? {
|
||||
return when {
|
||||
(hash?.length ?: 0) < 3 -> hash
|
||||
else -> hash!!.replace(Regex("^.*(..)(.)$"), "$2/$1/$hash")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NAME_SHADOWING", "UNUSED_PARAMETER")
|
||||
fun urlFromHash(galleryID: Int, image: GalleryInfo, dir: String? = null, ext: String? = null) : String {
|
||||
val ext = ext ?: dir ?: image.name.split('.').last()
|
||||
val dir = dir ?: "images"
|
||||
return "$protocol//a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext"
|
||||
}
|
||||
|
||||
fun urlFromUrlFromHash(galleryID: Int, image: GalleryInfo, dir: String? = null, ext: String? = null, base: String? = null) =
|
||||
urlFromURL(urlFromHash(galleryID, image, dir, ext), base)
|
||||
@@ -16,10 +16,11 @@
|
||||
|
||||
package xyz.quaver.hitomi
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
|
||||
@Serializable
|
||||
data class Gallery(
|
||||
val related: List<Int>,
|
||||
val langList: List<Pair<String, String>>,
|
||||
@@ -35,7 +36,8 @@ data class Gallery(
|
||||
val thumbnails: List<String>
|
||||
)
|
||||
fun getGallery(galleryID: Int) : Gallery {
|
||||
val url = "https://hitomi.la/galleries/$galleryID.html"
|
||||
val url = Jsoup.connect("https://hitomi.la/galleries/$galleryID.html").get()
|
||||
.select("a").attr("href")
|
||||
|
||||
val doc = Jsoup.connect(url).get()
|
||||
|
||||
@@ -46,7 +48,7 @@ fun getGallery(galleryID: Int) : Gallery {
|
||||
}.toList()
|
||||
|
||||
val langList = doc.select("#lang-list a").map {
|
||||
Pair(it.text(), it.attr("href").replace(".html", ""))
|
||||
Pair(it.text(), "$protocol//hitomi.la${it.attr("href")}")
|
||||
}
|
||||
|
||||
val cover = protocol + doc.selectFirst(".cover img").attr("src")
|
||||
@@ -68,11 +70,9 @@ fun getGallery(galleryID: Int) : Gallery {
|
||||
href.slice(5 until href.indexOf('-'))
|
||||
}
|
||||
|
||||
val thumbnails = Regex("'(//tn.hitomi.la/smalltn/\\d+/.+)',")
|
||||
.findAll(doc.select("script").last().html())
|
||||
.map {
|
||||
protocol + it.groups[1]!!.value
|
||||
}.toList()
|
||||
val thumbnails = getGalleryInfo(galleryID).map { galleryInfo ->
|
||||
urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn")
|
||||
}
|
||||
|
||||
return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ package xyz.quaver.hitomi
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import sun.rmi.runtime.Log
|
||||
import xyz.quaver.Code
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
import java.nio.ByteBuffer
|
||||
@@ -34,41 +34,39 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
|
||||
else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
|
||||
}
|
||||
|
||||
try {
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
requestMethod = "GET"
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
requestMethod = "GET"
|
||||
|
||||
if (start != -1 && count != -1) {
|
||||
val startByte = start*4
|
||||
val endByte = (start+count)*4-1
|
||||
if (start != -1 && count != -1) {
|
||||
val startByte = start*4
|
||||
val endByte = (start+count)*4-1
|
||||
|
||||
setRequestProperty("Range", "bytes=$startByte-$endByte")
|
||||
}
|
||||
|
||||
connect()
|
||||
|
||||
val totalItems = getHeaderField("Content-Range")
|
||||
.replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
|
||||
|
||||
val nozomi = ArrayList<Int>()
|
||||
|
||||
val arrayBuffer = ByteBuffer
|
||||
.wrap(inputStream.readBytes())
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
while (arrayBuffer.hasRemaining())
|
||||
nozomi.add(arrayBuffer.int)
|
||||
|
||||
return Pair(nozomi, totalItems)
|
||||
setRequestProperty("Range", "bytes=$startByte-$endByte")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return Pair(emptyList(), 0)
|
||||
|
||||
connect()
|
||||
|
||||
val totalItems = getHeaderField("Content-Range")
|
||||
.replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
|
||||
|
||||
val nozomi = ArrayList<Int>()
|
||||
|
||||
val arrayBuffer = ByteBuffer
|
||||
.wrap(inputStream.readBytes())
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
while (arrayBuffer.hasRemaining())
|
||||
nozomi.add(arrayBuffer.int)
|
||||
|
||||
return Pair(nozomi, totalItems)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GalleryBlock(
|
||||
val code: Code,
|
||||
val id: Int,
|
||||
val galleryUrl: String,
|
||||
val thumbnails: List<String>,
|
||||
val title: String,
|
||||
val artists: List<String>,
|
||||
@@ -80,28 +78,26 @@ data class GalleryBlock(
|
||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
||||
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
|
||||
|
||||
try {
|
||||
val doc = Jsoup.connect(url).get()
|
||||
val doc = Jsoup.connect(url).get()
|
||||
|
||||
val thumbnails = doc.select("img").map { protocol + it.attr("data-src") }
|
||||
val galleryUrl = doc.selectFirst(".lillie").attr("href")
|
||||
|
||||
val title = doc.selectFirst("h1.lillie > a").text()
|
||||
val artists = doc.select("div.artist-list a").map{ it.text() }
|
||||
val series = doc.select("a[href~=^/series/]").map { it.text() }
|
||||
val type = doc.selectFirst("a[href~=^/type/]").text()
|
||||
val thumbnails = doc.select("img").map { protocol + it.attr("data-src") }
|
||||
|
||||
val language = {
|
||||
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
|
||||
href.slice(7 until href.indexOf("-1"))
|
||||
}.invoke()
|
||||
val title = doc.selectFirst("h1.lillie > a").text()
|
||||
val artists = doc.select("div.artist-list a").map{ it.text() }
|
||||
val series = doc.select("a[href~=^/series/]").map { it.text() }
|
||||
val type = doc.selectFirst("a[href~=^/type/]").text()
|
||||
|
||||
val relatedTags = doc.select(".relatedtags a").map {
|
||||
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
||||
href.slice(5 until href.indexOf("-all"))
|
||||
}
|
||||
val language = {
|
||||
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
|
||||
href.slice(7 until href.indexOf("-1"))
|
||||
}.invoke()
|
||||
|
||||
return GalleryBlock(galleryID, thumbnails, title, artists, series, type, language, relatedTags)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
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(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||
}
|
||||
@@ -17,72 +17,28 @@
|
||||
package xyz.quaver.hitomi
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.list
|
||||
import org.jsoup.Jsoup
|
||||
import xyz.quaver.hiyobi.HiyobiReader
|
||||
import java.net.URL
|
||||
import xyz.quaver.Code
|
||||
|
||||
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
|
||||
fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
|
||||
|
||||
fun webpReaderFromReader(reader: Reader) : Reader {
|
||||
if (reader is HiyobiReader)
|
||||
return reader
|
||||
|
||||
return Reader(reader.title, reader.readerItems.map {
|
||||
ReaderItem(
|
||||
if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url,
|
||||
it.galleryInfo
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GalleryInfo(
|
||||
val width: Int,
|
||||
val haswebp: Int,
|
||||
val hash: String? = null,
|
||||
val haswebp: Int = 0,
|
||||
val name: String,
|
||||
val height: Int
|
||||
)
|
||||
@Serializable
|
||||
data class ReaderItem(
|
||||
val url: String,
|
||||
val galleryInfo: GalleryInfo?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
open class Reader(val title: String, val readerItems: List<ReaderItem>)
|
||||
data class Reader(val code: Code, val title: String, val galleryInfo: List<GalleryInfo>)
|
||||
|
||||
//Set header `Referer` to reader url to avoid 403 error
|
||||
fun getReader(galleryID: Int) : Reader {
|
||||
val readerUrl = "https://hitomi.la/reader/$galleryID.html"
|
||||
val galleryInfoUrl = "https://ltn.hitomi.la/galleries/$galleryID.js"
|
||||
|
||||
val doc = Jsoup.connect(readerUrl).get()
|
||||
|
||||
val title = doc.title()
|
||||
|
||||
val images = doc.select(".img-url").map {
|
||||
protocol + urlFromURL(it.text())
|
||||
}
|
||||
|
||||
val galleryInfo = ArrayList<GalleryInfo?>()
|
||||
|
||||
galleryInfo.addAll(
|
||||
Json(JsonConfiguration.Stable).parse(
|
||||
GalleryInfo.serializer().list,
|
||||
Regex("""\[.+]""").find(
|
||||
URL(galleryInfoUrl).readText()
|
||||
)?.value ?: "[]"
|
||||
)
|
||||
)
|
||||
|
||||
if (images.size > galleryInfo.size)
|
||||
galleryInfo.addAll(arrayOfNulls(images.size - galleryInfo.size))
|
||||
|
||||
return Reader(title, (images zip galleryInfo).map {
|
||||
ReaderItem(it.first, it.second)
|
||||
})
|
||||
return Reader(Code.HITOMI, doc.title(), getGalleryInfo(galleryID))
|
||||
}
|
||||
@@ -26,7 +26,7 @@ fun doSearch(query: String, sortByPopularity: Boolean = false) : List<Int> {
|
||||
val terms = query
|
||||
.trim()
|
||||
.replace(Regex("""^\?"""), "")
|
||||
.toLowerCase()
|
||||
.toLowerCase(Locale.US)
|
||||
.split(Regex("\\s+"))
|
||||
.map {
|
||||
it.replace('_', ' ')
|
||||
|
||||
@@ -173,22 +173,18 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List
|
||||
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
||||
}
|
||||
|
||||
try {
|
||||
val bytes = URL(nozomiAddress).readBytes()
|
||||
val bytes = URL(nozomiAddress).readBytes()
|
||||
|
||||
val nozomi = ArrayList<Int>()
|
||||
val nozomi = ArrayList<Int>()
|
||||
|
||||
val arrayBuffer = ByteBuffer
|
||||
.wrap(bytes)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
val arrayBuffer = ByteBuffer
|
||||
.wrap(bytes)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
while (arrayBuffer.hasRemaining())
|
||||
nozomi.add(arrayBuffer.int)
|
||||
while (arrayBuffer.hasRemaining())
|
||||
nozomi.add(arrayBuffer.int)
|
||||
|
||||
return nozomi
|
||||
} catch (e: Exception) {
|
||||
return emptyList()
|
||||
}
|
||||
return nozomi
|
||||
}
|
||||
|
||||
fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> {
|
||||
|
||||
45
libpupil/src/main/java/xyz/quaver/hiyobi/galleryblock.kt
Normal file
45
libpupil/src/main/java/xyz/quaver/hiyobi/galleryblock.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2020 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.hiyobi
|
||||
|
||||
import org.jsoup.Jsoup
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.protocol
|
||||
|
||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
||||
val url = "$protocol//$hiyobi/info/$galleryID"
|
||||
|
||||
val doc = Jsoup.connect(url).get()
|
||||
|
||||
val galleryBlock = doc.selectFirst(".gallery-content")
|
||||
|
||||
val galleryUrl = galleryBlock.selectFirst("a").attr("href")
|
||||
|
||||
val thumbnails = listOf(galleryBlock.selectFirst("img").attr("abs:src"))
|
||||
|
||||
val title = galleryBlock.selectFirst("b").text()
|
||||
val artists = galleryBlock.select("tr:matches(작가) a[href~=artist]").map { it.text() }
|
||||
val series = galleryBlock.select("tr:matches(원작) a").map { it.attr("href").substringAfter("series:").replace('_', ' ') }
|
||||
val type = galleryBlock.selectFirst("tr:matches(종류) a").attr("href").substringAfter("type:").replace('_', ' ')
|
||||
|
||||
val language = "korean"
|
||||
|
||||
val relatedTags = galleryBlock.select("tr:matches(태그) a").map { it.attr("href").substringAfterLast('/').replace('_', ' ') }
|
||||
|
||||
return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||
}
|
||||
@@ -17,19 +17,18 @@
|
||||
package xyz.quaver.hiyobi
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.json.content
|
||||
import kotlinx.serialization.list
|
||||
import org.jsoup.Jsoup
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryInfo
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.ReaderItem
|
||||
import xyz.quaver.hitomi.protocol
|
||||
import java.net.URL
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
const val hiyobi = "xn--9w3b15m8vo.asia"
|
||||
const val hiyobi = "hiyobi.me"
|
||||
const val user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36"
|
||||
|
||||
class HiyobiReader(title: String, readerItems: List<ReaderItem>) : Reader(title, readerItems)
|
||||
|
||||
var cookie: String = ""
|
||||
get() {
|
||||
if (field.isEmpty())
|
||||
@@ -38,6 +37,12 @@ var cookie: String = ""
|
||||
return field
|
||||
}
|
||||
|
||||
data class Images(
|
||||
val path: String,
|
||||
val no: Int,
|
||||
val name: String
|
||||
)
|
||||
|
||||
fun renewCookie() : String {
|
||||
val url = "https://$hiyobi/"
|
||||
|
||||
@@ -59,7 +64,9 @@ fun getReader(galleryID: Int) : Reader {
|
||||
|
||||
val title = Jsoup.connect(reader).get().title()
|
||||
|
||||
val json = Json(JsonConfiguration.Stable).parseJson(
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
val galleryInfo = Json.parse(
|
||||
GalleryInfo.serializer().list,
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
setRequestProperty("User-Agent", user_agent)
|
||||
setRequestProperty("Cookie", cookie)
|
||||
@@ -70,8 +77,14 @@ fun getReader(galleryID: Int) : Reader {
|
||||
}
|
||||
)
|
||||
|
||||
return Reader(title, json.jsonArray.map {
|
||||
val name = it.jsonObject["name"]!!.content
|
||||
ReaderItem("https://$hiyobi/data/$galleryID/$name", null)
|
||||
})
|
||||
}
|
||||
return Reader(Code.HIYOBI, title, galleryInfo)
|
||||
}
|
||||
|
||||
fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) =
|
||||
if (lowQuality)
|
||||
reader.galleryInfo.map {
|
||||
val name = it.name.replace(Regex("/.[^/.]+$"), "") + ".jpg"
|
||||
Images("$protocol//$hiyobi/data/$galleryID/$name.jpg", galleryID, it.name)
|
||||
}
|
||||
else
|
||||
reader.galleryInfo.map { Images("$protocol//$hiyobi/data/$galleryID/${it.name}", galleryID, it.name) }
|
||||
@@ -18,12 +18,17 @@
|
||||
|
||||
package xyz.quaver.hitomi
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import xyz.quaver.availableInHiyobi
|
||||
|
||||
class UnitTest {
|
||||
@Test
|
||||
fun test() {
|
||||
|
||||
assertEquals(
|
||||
"6/2d/c26014fc6153ef717932d85f4d26c75195560fb2ce1da60b431ef376501642d6",
|
||||
fullPathFromHash("c26014fc6153ef717932d85f4d26c75195560fb2ce1da60b431ef376501642d6")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -63,20 +68,45 @@ class UnitTest {
|
||||
|
||||
@Test
|
||||
fun test_getGallery() {
|
||||
val gallery = getGallery(1405267)
|
||||
val gallery = getGallery(1552751)
|
||||
|
||||
print(gallery)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getReader() {
|
||||
val reader = getReader(1442740)
|
||||
val reader = getReader(1567569)
|
||||
|
||||
print(reader)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_hiyobi() {
|
||||
val reader = xyz.quaver.hiyobi.getReader(10000062)
|
||||
|
||||
print(reader)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_urlFromUrlFromHash() {
|
||||
val url = urlFromUrlFromHash(1531795, GalleryInfo(
|
||||
212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300
|
||||
), "webp")
|
||||
|
||||
print(url)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_availableInHiyobi() {
|
||||
val result = availableInHiyobi(1272781)
|
||||
|
||||
print(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_hiyobi_galleryBlock() {
|
||||
val galleryBlock = xyz.quaver.hiyobi.getGalleryBlock(10000027)
|
||||
|
||||
print(galleryBlock)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user