Compare commits
50 Commits
5.3-hotfix
...
4.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3e60f9988 | ||
|
|
593197cd7e | ||
|
|
ee1592b478 | ||
|
|
69e85f8b90 | ||
|
|
65e9557d9f | ||
|
|
5fd35b492c | ||
|
|
9bddf95013 | ||
|
|
03444f070f | ||
|
|
2f1a63eb64 | ||
|
|
9d0898b26c | ||
|
|
994aa99797 | ||
|
|
8204a15276 | ||
|
|
4a8bff0b98 | ||
|
|
a4336cd954 | ||
|
|
4f0dbead79 | ||
|
|
c0e7c87ca4 | ||
|
|
b967bf9a26 | ||
|
|
764a265053 | ||
|
|
68c2b2dbfa | ||
|
|
061f1263f4 | ||
|
|
2a27355479 | ||
|
|
ae2a8e8ada | ||
|
|
68dcc2333b | ||
|
|
66fb2e9a62 | ||
|
|
1dbfc64f37 | ||
|
|
98d1f88579 | ||
|
|
bb6fadc182 | ||
|
|
ac1ca71299 | ||
|
|
0d93785581 | ||
|
|
69a9d63e1d | ||
|
|
5dea35343b | ||
|
|
5c768d2121 | ||
|
|
4d5834821a | ||
|
|
ca077c4fee | ||
|
|
85d01f60f1 | ||
|
|
066d73b217 | ||
|
|
ba069d8f8e | ||
|
|
275684c9ce | ||
|
|
49d87a08d2 | ||
|
|
04c500f3d8 | ||
|
|
d05c1e4d08 | ||
|
|
bb63959678 | ||
|
|
842148647f | ||
|
|
19308d840a | ||
|
|
46bd1318cd | ||
|
|
9d1998fe52 | ||
|
|
a714a8230b | ||
|
|
b5432cd0b4 | ||
|
|
5634e94f3e | ||
|
|
c1a71b0db3 |
3
.idea/codeStyles/Project.xml
generated
3
.idea/codeStyles/Project.xml
generated
@@ -1,9 +1,6 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<option name="RIGHT_MARGIN" value="120" />
|
<option name="RIGHT_MARGIN" value="120" />
|
||||||
<AndroidXmlCodeStyleSettings>
|
|
||||||
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
|
|
||||||
</AndroidXmlCodeStyleSettings>
|
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
|
|||||||
3
.idea/gradle.xml
generated
3
.idea/gradle.xml
generated
@@ -1,8 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
<option name="delegatedBuild" value="false" />
|
||||||
|
<option name="testRunner" value="PLATFORM" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# Pupil
|
# Pupil
|
||||||
|
|
||||||

|

|
||||||
*Pupil, Hitomi.la viewer for Android*
|
*Pupil, Hitomi.la viewer for Android*
|
||||||
|
|
||||||
|
[](https://discord.gg/Stj4b5v)
|
||||||
|
I can speak English, Japanese and Korean. If you have any questions, head over to my discord server or DM me!
|
||||||
|
|
||||||
# Screenshot
|
# Screenshot
|
||||||

|

|
||||||
|
|||||||
@@ -4,7 +4,7 @@ apply plugin: 'kotlin-kapt'
|
|||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
if (file("google-services.json").exists()) {
|
if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
|
||||||
logger.lifecycle("Firebase Enabled")
|
logger.lifecycle("Firebase Enabled")
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
apply plugin: 'io.fabric'
|
apply plugin: 'io.fabric'
|
||||||
@@ -19,19 +19,27 @@ android {
|
|||||||
applicationId "xyz.quaver.pupil"
|
applicationId "xyz.quaver.pupil"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 37
|
versionCode 46
|
||||||
versionName "5.3-beta4"
|
versionName "4.10"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
debug {
|
||||||
minifyEnabled false
|
debuggable true
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
versionNameSuffix "-DEBUG"
|
||||||
|
|
||||||
|
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
buildTypes.each {
|
release {
|
||||||
it.buildConfigField('boolean', 'CENSOR', 'false')
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
|
||||||
|
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@@ -41,9 +49,6 @@ android {
|
|||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
}
|
|
||||||
buildToolsVersion = '29.0.2'
|
buildToolsVersion = '29.0.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,28 +65,25 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.preference:preference:1.1.0'
|
implementation 'androidx.preference:preference:1.1.0'
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
|
||||||
implementation "androidx.biometric:biometric:1.0.1"
|
implementation "androidx.biometric:biometric:1.0.1"
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
implementation 'com.android.support:multidex:1.0.3'
|
||||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||||
implementation 'com.google.android.material:material:1.2.0-alpha04'
|
implementation 'com.google.android.material:material:1.2.0-alpha05'
|
||||||
implementation 'com.google.firebase:firebase-core:17.2.2'
|
implementation 'com.google.firebase:firebase-core:17.2.2'
|
||||||
implementation 'com.google.firebase:firebase-perf:19.0.5'
|
implementation 'com.google.firebase:firebase-perf:19.0.5'
|
||||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
||||||
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
||||||
implementation 'com.github.clans:fab:1.6.4'
|
implementation 'com.github.clans:fab:1.6.4'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.10.0'
|
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||||
implementation('com.github.bumptech.glide:recyclerview-integration: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
|
transitive = false
|
||||||
}
|
}
|
||||||
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
|
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
|
||||||
implementation 'com.gu:option:1.3'
|
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
||||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||||
kapt 'com.github.bumptech.glide:compiler:4.10.0'
|
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
androidTestImplementation 'androidx.test:rules: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
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# 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":37,"versionName":"5.3-beta4","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":46,"versionName":"4.10","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release","dirName":""},"path":"app-release.apk","properties":{}}]
|
||||||
@@ -26,11 +26,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.rule.ActivityTestRule
|
import androidx.test.rule.ActivityTestRule
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import xyz.quaver.hiyobi.cookie
|
import xyz.quaver.hiyobi.cookie
|
||||||
@@ -40,9 +35,6 @@ import xyz.quaver.hiyobi.user_agent
|
|||||||
import xyz.quaver.pupil.ui.LockActivity
|
import xyz.quaver.pupil.ui.LockActivity
|
||||||
import xyz.quaver.pupil.util.download.Cache
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
import xyz.quaver.pupil.util.getDownloadDirectory
|
|
||||||
import xyz.quaver.pupil.util.updateOldReaderGalleries
|
|
||||||
import java.io.File
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
|
||||||
@@ -58,8 +50,6 @@ class ExampleInstrumentedTest {
|
|||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
Log.i("PUPILD", getDownloadDirectory(appContext).absolutePath ?: "")
|
|
||||||
assertEquals("xyz.quaver.pupil", appContext.packageName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -88,40 +78,6 @@ class ExampleInstrumentedTest {
|
|||||||
Log.d("Pupil", data.size.toString())
|
Log.d("Pupil", data.size.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
@Test
|
|
||||||
fun test_deleteCodeFromReader() {
|
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
|
|
||||||
val json = Json(JsonConfiguration.Stable)
|
|
||||||
|
|
||||||
listOf(
|
|
||||||
getDownloadDirectory(context),
|
|
||||||
File(context.cacheDir, "imageCache")
|
|
||||||
).forEach { root ->
|
|
||||||
root.listFiles()?.forEach gallery@{ gallery ->
|
|
||||||
val reader = json.parseJson(File(gallery, "reader.json").apply {
|
|
||||||
if (!exists())
|
|
||||||
return@gallery
|
|
||||||
}.readText())
|
|
||||||
.jsonObject.toMutableMap()
|
|
||||||
|
|
||||||
Log.d("PUPILD", gallery.name)
|
|
||||||
|
|
||||||
reader.remove("code")
|
|
||||||
|
|
||||||
File(gallery, "reader.json").writeText(JsonObject(reader).toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_updateOldReader() {
|
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
|
|
||||||
updateOldReaderGalleries(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test_downloadWorker() {
|
fun test_downloadWorker() {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
@@ -149,9 +105,9 @@ class ExampleInstrumentedTest {
|
|||||||
val galleryID = 1561552
|
val galleryID = 1561552
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
Log.i("PUPILD", Cache(context).getReader(galleryID)?.title ?: "null")
|
Log.i("PUPILD", Cache(context).getReader(galleryID)?.galleryInfo?.title ?: "null")
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.title ?: "null")
|
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.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>
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
android:maxSdkVersion="21" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Pupil"
|
android:name=".Pupil"
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
tools:replace="android:theme">
|
tools:replace="android:theme"
|
||||||
|
android:requestLegacyExternalStorage="true">
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import android.app.Notification
|
|||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -31,7 +30,10 @@ import androidx.preference.PreferenceManager
|
|||||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
import com.google.android.gms.security.ProviderInstaller
|
||||||
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
|
import xyz.quaver.proxy
|
||||||
import xyz.quaver.pupil.util.Histories
|
import xyz.quaver.pupil.util.Histories
|
||||||
|
import xyz.quaver.pupil.util.getProxy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class Pupil : MultiDexApplication() {
|
class Pupil : MultiDexApplication() {
|
||||||
@@ -46,19 +48,19 @@ class Pupil : MultiDexApplication() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
proxy = getProxy(this)
|
||||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
|
||||||
|
|
||||||
val download = try {
|
try {
|
||||||
preference.getString("dl_location", null)
|
preference.getString("dl_location", null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
preference.edit().remove("dl_location").apply()
|
preference.edit().remove("dl_location").apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (download == null) {
|
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||||
val default = ContextCompat.getExternalFilesDirs(this, null)[0]
|
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||||
preference.edit().putString("dl_location", Uri.fromFile(default).toString()).apply()
|
|
||||||
}
|
if (BuildConfig.DEBUG)
|
||||||
|
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProviderInstaller.installIfNeeded(this)
|
ProviderInstaller.installIfNeeded(this)
|
||||||
|
|||||||
@@ -49,13 +49,12 @@ import xyz.quaver.pupil.R
|
|||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
import xyz.quaver.pupil.util.Histories
|
import xyz.quaver.pupil.util.Histories
|
||||||
import xyz.quaver.pupil.util.download.Cache
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
class GalleryBlockAdapter(private val context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
||||||
|
|
||||||
enum class ViewType {
|
enum class ViewType {
|
||||||
NEXT,
|
NEXT,
|
||||||
@@ -68,47 +67,48 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
|||||||
|
|
||||||
val timer = Timer()
|
val timer = Timer()
|
||||||
|
|
||||||
|
var isThin = false
|
||||||
|
|
||||||
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||||
var timerTask: TimerTask? = null
|
var timerTask: TimerTask? = null
|
||||||
|
|
||||||
fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.Main).launch {
|
private fun updateProgress(context: Context, galleryID: Int) {
|
||||||
val cache = Cache(context).getCachedGallery(galleryID)
|
|
||||||
val reader = Cache(context).getReaderOrNull(galleryID)
|
val reader = Cache(context).getReaderOrNull(galleryID)
|
||||||
|
|
||||||
if (reader == null) {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
view.galleryblock_progressbar.visibility = View.GONE
|
if (reader == null) {
|
||||||
view.galleryblock_progress_complete.visibility = View.GONE
|
view.galleryblock_progressbar.visibility = View.GONE
|
||||||
return@launch
|
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) {
|
with(view.galleryblock_progressbar) {
|
||||||
if (completeFlag.get(galleryID, false)) {
|
|
||||||
with(view.galleryblock_progress_complete) {
|
progress = Cache(context).getImages(galleryID)?.size ?: 0
|
||||||
setImageResource(R.drawable.ic_progressbar)
|
|
||||||
visibility = View.VISIBLE
|
if (visibility == View.GONE) {
|
||||||
}
|
visibility = View.VISIBLE
|
||||||
} else {
|
max = reader.galleryInfo.files.size
|
||||||
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
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +124,10 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
|||||||
val artists = galleryBlock.artists
|
val artists = galleryBlock.artists
|
||||||
val series = galleryBlock.series
|
val series = galleryBlock.series
|
||||||
|
|
||||||
|
if (isThin)
|
||||||
|
galleryblock_thumbnail.layoutParams.width = context.resources.getDimensionPixelSize(
|
||||||
|
R.dimen.galleryblock_thumbnail_thin
|
||||||
|
)
|
||||||
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
|
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
|
||||||
it.start()
|
it.start()
|
||||||
})
|
})
|
||||||
@@ -136,29 +140,31 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
glide
|
galleryblock_thumbnail.post {
|
||||||
.load(thumbnail)
|
glide
|
||||||
.skipMemoryCache(true)
|
.load(thumbnail)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.skipMemoryCache(true)
|
||||||
.error(R.drawable.image_broken_variant)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.apply {
|
.error(R.drawable.image_broken_variant)
|
||||||
if (BuildConfig.CENSOR)
|
.apply {
|
||||||
override(5, 8)
|
if (BuildConfig.CENSOR)
|
||||||
}
|
override(5, 8)
|
||||||
.into(galleryblock_thumbnail)
|
}
|
||||||
|
.into(galleryblock_thumbnail)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check cache
|
//Check cache
|
||||||
val cache = Cache(context).getCachedGallery(galleryBlock.id)
|
val cache = Cache(context).getCachedGallery(galleryBlock.id)
|
||||||
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
|
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
|
||||||
|
|
||||||
if (cache != null && reader != null) {
|
if (reader != null) {
|
||||||
val count = cache.listFiles().count {
|
val count = cache.listFiles()?.count {
|
||||||
Regex("^[0-9]+.+\$").matches(it.name!!)
|
Regex("^[0-9]+.+\$").matches(it.name)
|
||||||
}
|
} ?: 0
|
||||||
|
|
||||||
with(galleryblock_progressbar) {
|
with(galleryblock_progressbar) {
|
||||||
max = reader.galleryInfo.size
|
max = reader.galleryInfo.files.size
|
||||||
progress = count
|
progress = count
|
||||||
|
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@@ -262,6 +268,14 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Make some views invisible to make it thinner
|
||||||
|
if (isThin) {
|
||||||
|
galleryblock_language.visibility = View.GONE
|
||||||
|
galleryblock_type.visibility = View.GONE
|
||||||
|
galleryblock_tag_group.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,10 +353,10 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
|||||||
mItemManger.closeAllExcept(layout)
|
mItemManger.closeAllExcept(layout)
|
||||||
|
|
||||||
holder.view.galleryblock_download.text =
|
holder.view.galleryblock_download.text =
|
||||||
if (DownloadWorker.getInstance(holder.view.context).progress.indexOfKey(gallery.id) < 0)
|
if (Cache(context).isDownloading(gallery.id))
|
||||||
holder.view.context.getString(R.string.main_download)
|
|
||||||
else
|
|
||||||
holder.view.context.getString(android.R.string.cancel)
|
holder.view.context.getString(android.R.string.cancel)
|
||||||
|
else
|
||||||
|
holder.view.context.getString(R.string.main_download)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClose(layout: SwipeLayout?) {}
|
override fun onClose(layout: SwipeLayout?) {}
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import android.view.ViewGroup
|
|||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.ListPreloader
|
||||||
|
import com.bumptech.glide.RequestBuilder
|
||||||
|
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||||
import com.crashlytics.android.Crashlytics
|
import com.crashlytics.android.Crashlytics
|
||||||
import io.fabric.sdk.android.Fabric
|
import io.fabric.sdk.android.Fabric
|
||||||
import kotlinx.android.synthetic.main.item_reader.view.*
|
import kotlinx.android.synthetic.main.item_reader.view.*
|
||||||
@@ -37,6 +39,7 @@ import xyz.quaver.pupil.BuildConfig
|
|||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.util.download.Cache
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -44,22 +47,39 @@ import kotlin.math.roundToInt
|
|||||||
class ReaderAdapter(private val context: Context,
|
class ReaderAdapter(private val context: Context,
|
||||||
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||||
|
|
||||||
var isFullScreen = false
|
val glide = Glide.with(context)
|
||||||
|
|
||||||
var reader: Reader? = null
|
//region Glide.RecyclerView
|
||||||
private val glide = Glide.with(context)
|
val sizeProvider = ListPreloader.PreloadSizeProvider<File> { _, _, position ->
|
||||||
val timer = Timer()
|
Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.getOrNull(position)?.let {
|
||||||
|
arrayOf(it.width, it.height).toIntArray()
|
||||||
var onItemClickListener : ((Int) -> (Unit))? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
reader = Cache(context).getReader(galleryID)
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val modelProvider = object: ListPreloader.PreloadModelProvider<File> {
|
||||||
|
override fun getPreloadItems(position: Int): MutableList<File> {
|
||||||
|
return listOf(Cache(context).getImages(galleryID)?.getOrNull(position)).filterNotNullTo(mutableListOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? {
|
||||||
|
return glide
|
||||||
|
.load(item)
|
||||||
|
.fitCenter()
|
||||||
|
.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
|
||||||
|
|
||||||
|
var onItemClickListener : ((Int) -> (Unit))? = null
|
||||||
|
|
||||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
@@ -74,10 +94,16 @@ class ReaderAdapter(private val context: Context,
|
|||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
holder.view as ConstraintLayout
|
holder.view as ConstraintLayout
|
||||||
|
|
||||||
if (isFullScreen)
|
if (isFullScreen) {
|
||||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
||||||
else
|
holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
||||||
|
} else {
|
||||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
|
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
|
||||||
|
holder.view.container.layoutParams.height = 0
|
||||||
|
|
||||||
|
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
||||||
|
.dimensionRatio = "W,${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
|
||||||
|
}
|
||||||
|
|
||||||
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
||||||
onItemClickListener?.invoke(position)
|
onItemClickListener?.invoke(position)
|
||||||
@@ -87,29 +113,28 @@ class ReaderAdapter(private val context: Context,
|
|||||||
onItemClickListener?.invoke(position)
|
onItemClickListener?.invoke(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
|
||||||
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
|
|
||||||
|
|
||||||
holder.view.reader_index.text = (position+1).toString()
|
holder.view.reader_index.text = (position+1).toString()
|
||||||
|
|
||||||
val images = Cache(context).getImages(galleryID)
|
val images = Cache(context).getImage(galleryID, position)
|
||||||
|
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
||||||
|
|
||||||
|
if (progress?.isInfinite() == true && images != null) {
|
||||||
|
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
|
||||||
|
|
||||||
|
holder.view.image.post {
|
||||||
|
glide
|
||||||
|
.load(images)
|
||||||
|
.fitCenter()
|
||||||
|
.error(R.drawable.image_broken_variant)
|
||||||
|
.into(holder.view.image)
|
||||||
|
}
|
||||||
|
|
||||||
if (images?.get(position) != null) {
|
|
||||||
glide
|
|
||||||
.load(images[position]?.uri)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.skipMemoryCache(true)
|
|
||||||
.error(R.drawable.image_broken_variant)
|
|
||||||
.apply {
|
|
||||||
if (BuildConfig.CENSOR)
|
|
||||||
override(5, 8)
|
|
||||||
}
|
|
||||||
.into(holder.view.image)
|
|
||||||
} else {
|
} else {
|
||||||
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
holder.view.reader_item_progressbar.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
glide.clear(holder.view.image)
|
||||||
|
|
||||||
if (progress?.isNaN() == true) {
|
if (progress?.isNaN() == true) {
|
||||||
|
|
||||||
if (Fabric.isInitialized())
|
if (Fabric.isInitialized())
|
||||||
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
|
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
|
||||||
|
|
||||||
@@ -118,17 +143,16 @@ class ReaderAdapter(private val context: Context,
|
|||||||
.into(holder.view.image)
|
.into(holder.view.image)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
holder.view.reader_item_progressbar.progress =
|
||||||
|
if (progress?.isInfinite() == true)
|
||||||
|
100
|
||||||
|
else
|
||||||
|
progress?.roundToInt() ?: 0
|
||||||
|
|
||||||
|
holder.view.image.setImageDrawable(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.view.reader_item_progressbar.progress =
|
|
||||||
if (progress?.isInfinite() == true)
|
|
||||||
100
|
|
||||||
else
|
|
||||||
progress?.roundToInt() ?: 0
|
|
||||||
|
|
||||||
holder.view.image.setImageDrawable(null)
|
|
||||||
|
|
||||||
|
|
||||||
timer.schedule(1000) {
|
timer.schedule(1000) {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
@@ -137,6 +161,6 @@ class ReaderAdapter(private val context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = reader?.galleryInfo?.size ?: 0
|
override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -45,15 +45,13 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|||||||
import com.arlib.floatingsearchview.FloatingSearchView
|
import com.arlib.floatingsearchview.FloatingSearchView
|
||||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import com.arlib.floatingsearchview.util.view.SearchInputView
|
import com.arlib.floatingsearchview.util.view.SearchInputView
|
||||||
|
import com.crashlytics.android.Crashlytics
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import io.fabric.sdk.android.Fabric
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import kotlinx.android.synthetic.main.activity_main_content.*
|
import kotlinx.android.synthetic.main.activity_main_content.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
|
||||||
import kotlinx.serialization.list
|
import kotlinx.serialization.list
|
||||||
import kotlinx.serialization.stringify
|
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.doSearch
|
import xyz.quaver.hitomi.doSearch
|
||||||
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
||||||
@@ -179,7 +177,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
(main_recyclerview.adapter as GalleryBlockAdapter).timer.cancel()
|
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -332,6 +330,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
with(main_fab_cancel) {
|
||||||
|
setImageResource(R.drawable.cancel)
|
||||||
|
setOnClickListener {
|
||||||
|
DownloadWorker.getInstance(context).stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
with(main_fab_jump) {
|
with(main_fab_jump) {
|
||||||
setImageResource(R.drawable.ic_jump)
|
setImageResource(R.drawable.ic_jump)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
@@ -410,7 +415,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
if (!completeFlag.get(galleryID, false)) {
|
if (!completeFlag.get(galleryID, false)) {
|
||||||
val worker = DownloadWorker.getInstance(context)
|
val worker = DownloadWorker.getInstance(context)
|
||||||
|
|
||||||
if (worker.progress.indexOfKey(galleryID) >= 0) //download in progress
|
if (Cache(context).isDownloading(galleryID)) //download in progress
|
||||||
worker.cancel(galleryID)
|
worker.cancel(galleryID)
|
||||||
else {
|
else {
|
||||||
Cache(context).setDownloading(galleryID, true)
|
Cache(context).setDownloading(galleryID, true)
|
||||||
@@ -429,12 +434,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
DownloadWorker.getInstance(context).cancel(galleryID)
|
DownloadWorker.getInstance(context).cancel(galleryID)
|
||||||
|
|
||||||
var cache = Cache(context).getCachedGallery(galleryID)
|
Cache(context).getCachedGallery(galleryID).deleteRecursively()
|
||||||
|
|
||||||
while (cache != null) {
|
|
||||||
cache.deleteRecursively()
|
|
||||||
cache = Cache(context).getCachedGallery(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
histories.remove(galleryID)
|
histories.remove(galleryID)
|
||||||
|
|
||||||
@@ -698,7 +698,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var suggestionJob : Job? = null
|
private var suggestionJob : Job? = null
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
private fun setupSearchBar() {
|
private fun setupSearchBar() {
|
||||||
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
||||||
//Change upper case letters to lower case
|
//Change upper case letters to lower case
|
||||||
@@ -722,17 +721,25 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
with(main_searchview as FloatingSearchView) {
|
with(main_searchview as FloatingSearchView) {
|
||||||
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
|
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
|
||||||
val json = Json(JsonConfiguration.Stable)
|
|
||||||
val serializer = Tag.serializer().list
|
val serializer = Tag.serializer().list
|
||||||
|
|
||||||
if (!favoritesFile.exists()) {
|
if (!favoritesFile.exists()) {
|
||||||
favoritesFile.createNewFile()
|
favoritesFile.createNewFile()
|
||||||
favoritesFile.writeText(json.stringify(Tags(listOf())))
|
favoritesFile.writeText(json.stringify(serializer, Tags(listOf())))
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
when(it.itemId) {
|
when(it.itemId) {
|
||||||
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
|
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
|
||||||
|
R.id.main_menu_thin -> {
|
||||||
|
main_recyclerview.apply {
|
||||||
|
(adapter as GalleryBlockAdapter).apply {
|
||||||
|
isThin = !isThin
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = adapter // Force to redraw
|
||||||
|
}
|
||||||
|
}
|
||||||
R.id.main_menu_sort_newest -> {
|
R.id.main_menu_sort_newest -> {
|
||||||
sortMode = SortMode.NEWEST
|
sortMode = SortMode.NEWEST
|
||||||
it.isChecked = true
|
it.isChecked = true
|
||||||
@@ -845,7 +852,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
favorites.add(tag)
|
favorites.add(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
favoritesFile.writeText(json.stringify(favorites))
|
favoritesFile.writeText(json.stringify(serializer, favorites))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -944,58 +951,60 @@ class MainActivity : AppCompatActivity() {
|
|||||||
when(sortMode) {
|
when(sortMode) {
|
||||||
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
|
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
|
||||||
else -> getGalleryIDsFromNozomi(null, "index", "all")
|
else -> getGalleryIDsFromNozomi(null, "index", "all")
|
||||||
}.apply {
|
}.also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).apply {
|
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode.HISTORY -> {
|
Mode.HISTORY -> {
|
||||||
when {
|
when {
|
||||||
query.isEmpty() -> {
|
query.isEmpty() -> {
|
||||||
histories.toList().apply {
|
histories.toList().also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val result = doSearch(query).sorted()
|
val result = doSearch(query).sorted()
|
||||||
histories.filter { result.binarySearch(it) >= 0 }.apply {
|
histories.filter { result.binarySearch(it) >= 0 }.also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode.DOWNLOAD -> {
|
Mode.DOWNLOAD -> {
|
||||||
val downloads = getDownloadDirectory(this@MainActivity).listFiles().filter { file ->
|
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
|
||||||
file.isDirectory && (file.name!!.toIntOrNull() != null) && file.findFile(".metadata") != null
|
file.isDirectory && file.name.toIntOrNull() != null
|
||||||
}.map {
|
}?.sortedByDescending {
|
||||||
it.name!!.toInt()
|
it.lastModified()
|
||||||
}
|
}?.map {
|
||||||
|
it.name.toInt()
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
when {
|
when {
|
||||||
query.isEmpty() -> downloads.apply {
|
query.isEmpty() -> downloads.also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val result = doSearch(query).sorted()
|
val result = doSearch(query).sorted()
|
||||||
downloads.filter { result.binarySearch(it) >= 0 }.apply {
|
downloads.filter { result.binarySearch(it) >= 0 }.also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode.FAVORITE -> {
|
Mode.FAVORITE -> {
|
||||||
when {
|
when {
|
||||||
query.isEmpty() -> favorites.toList().apply {
|
query.isEmpty() -> favorites.toList().also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val result = doSearch(query).sorted()
|
val result = doSearch(query).sorted()
|
||||||
favorites.filter { result.binarySearch(it) >= 0 }.apply {
|
favorites.filter { result.binarySearch(it) >= 0 }.also {
|
||||||
totalItems = size
|
totalItems = it.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1009,9 +1018,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
|
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
|
||||||
|
|
||||||
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val galleryIDs = galleryIDs?.await()
|
val galleryIDs = try {
|
||||||
|
galleryIDs!!.await().also {
|
||||||
|
if (it.isEmpty())
|
||||||
|
throw Exception("No result")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
if (Fabric.isInitialized() && e.message != "No result")
|
||||||
|
Crashlytics.logException(e)
|
||||||
|
|
||||||
if (galleryIDs.isNullOrEmpty()) { //No result
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
main_noresult.visibility = View.VISIBLE
|
main_noresult.visibility = View.VISIBLE
|
||||||
main_progressbar.hide()
|
main_progressbar.hide()
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import io.fabric.sdk.android.Fabric
|
|||||||
import kotlinx.android.synthetic.main.activity_reader.*
|
import kotlinx.android.synthetic.main.activity_reader.*
|
||||||
import kotlinx.android.synthetic.main.activity_reader.view.*
|
import kotlinx.android.synthetic.main.activity_reader.view.*
|
||||||
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
|
||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
@@ -141,7 +140,6 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
menuInflater.inflate(R.menu.reader, menu)
|
menuInflater.inflate(R.menu.reader, menu)
|
||||||
|
|
||||||
@@ -239,7 +237,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
queue.add(galleryID)
|
queue.add(galleryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
timer.schedule(0, 1000) {
|
timer.schedule(1000, 1000) {
|
||||||
if (worker.progress.indexOfKey(galleryID) < 0) //loading
|
if (worker.progress.indexOfKey(galleryID) < 0) //loading
|
||||||
return@schedule
|
return@schedule
|
||||||
|
|
||||||
@@ -256,11 +254,17 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||||
|
|
||||||
if (title == getString(R.string.reader_loading)) {
|
if (title == getString(R.string.reader_loading)) {
|
||||||
val reader = (reader_recyclerview.adapter as ReaderAdapter).reader
|
val reader = Cache(this@ReaderActivity).getReaderOrNull(galleryID)
|
||||||
|
|
||||||
if (reader != null) {
|
if (reader != null) {
|
||||||
title = reader.title
|
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.size}"
|
with (reader_recyclerview.adapter as ReaderAdapter) {
|
||||||
|
this.reader = reader
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
title = reader.galleryInfo.title
|
||||||
|
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.files.size}"
|
||||||
|
|
||||||
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
|
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
|
||||||
when (reader.code) {
|
when (reader.code) {
|
||||||
@@ -296,6 +300,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addOnScrollListener((adapter as ReaderAdapter).preloader)
|
||||||
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
@@ -356,6 +361,8 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
window.attributes = this
|
window.attributes = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scrollMode(isScroll: Boolean) {
|
private fun scrollMode(isScroll: Boolean) {
|
||||||
@@ -381,7 +388,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
|
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
|
||||||
post {
|
post {
|
||||||
setImageResource(R.drawable.ic_download)
|
setImageResource(R.drawable.ic_download)
|
||||||
labelText = getString(R.string.reader_fab_download)
|
labelText = getString(R.string.reader_fab_download_cancel)
|
||||||
}
|
}
|
||||||
else // Or continue animate
|
else // Or continue animate
|
||||||
post {
|
post {
|
||||||
|
|||||||
@@ -18,30 +18,26 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.settings_activity.*
|
import kotlinx.android.synthetic.main.settings_activity.*
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
import kotlinx.serialization.list
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.serializer
|
||||||
import kotlinx.serialization.parseList
|
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.ui.fragment.LockFragment
|
import xyz.quaver.pupil.ui.fragment.LockFragment
|
||||||
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
||||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
|
import xyz.quaver.pupil.util.*
|
||||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
|
|
||||||
import xyz.quaver.pupil.util.REQUEST_LOCK
|
|
||||||
import xyz.quaver.pupil.util.REQUEST_RESTORE
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
@@ -82,7 +78,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
when(requestCode) {
|
when(requestCode) {
|
||||||
REQUEST_LOCK -> {
|
REQUEST_LOCK -> {
|
||||||
@@ -99,13 +94,13 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
val uri = data?.data ?: return
|
val uri = data?.data ?: return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val json = contentResolver.openInputStream(uri).use { inputStream ->
|
val str = contentResolver.openInputStream(uri).use { inputStream ->
|
||||||
inputStream!!
|
inputStream!!
|
||||||
|
|
||||||
inputStream.readBytes().toString(Charset.defaultCharset())
|
inputStream.readBytes().toString(Charset.defaultCharset())
|
||||||
}
|
}
|
||||||
|
|
||||||
(application as Pupil).favorites.addAll(Json.parseList<Int>(json).also {
|
(application as Pupil).favorites.addAll(json.parse(Int.serializer().list, str).also {
|
||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
window.decorView,
|
window.decorView,
|
||||||
getString(R.string.settings_restore_successful, it.size),
|
getString(R.string.settings_restore_successful, it.size),
|
||||||
@@ -124,16 +119,23 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
REQUEST_DOWNLOAD_FOLDER -> {
|
REQUEST_DOWNLOAD_FOLDER -> {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
data?.data?.also { uri ->
|
data?.data?.also { uri ->
|
||||||
val takeFlags: Int = intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
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)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||||
|
|
||||||
if (DocumentFile.fromTreeUri(this, uri)?.canWrite() == false)
|
val file = uri.toFile(this)
|
||||||
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
|
|
||||||
|
if (file?.canWrite() != true)
|
||||||
|
Snackbar.make(
|
||||||
|
settings,
|
||||||
|
R.string.settings_dl_location_not_writable,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
else
|
else
|
||||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||||
.putString("dl_location", uri.toString())
|
.putString("dl_location", file.canonicalPath)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,14 +145,33 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
||||||
|
|
||||||
if (!File(directory).canWrite())
|
if (!File(directory).canWrite())
|
||||||
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(
|
||||||
|
settings,
|
||||||
|
R.string.settings_dl_location_not_writable,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
else
|
else
|
||||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||||
.putString("dl_location", Uri.fromFile(File(directory)).toString())
|
.putString("dl_location", File(directory).canonicalPath)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -46,16 +46,12 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
private val excludeBL = "-male:yaoi"
|
private val excludeBL = "-male:yaoi"
|
||||||
private val excludeGuro = listOf("-female:guro", "-male:guro")
|
private val excludeGuro = listOf("-female:guro", "-male:guro")
|
||||||
|
|
||||||
private lateinit var dialogView : View
|
|
||||||
|
|
||||||
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
|
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
initDialog()
|
|
||||||
|
|
||||||
setTitle(R.string.default_query_dialog_title)
|
setTitle(R.string.default_query_dialog_title)
|
||||||
setView(dialogView)
|
setView(build())
|
||||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
|
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
|
||||||
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
|
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
|
||||||
|
|
||||||
@@ -79,15 +75,15 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
private fun initDialog() {
|
private fun build() : View {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
val tags = Tags.parse(
|
val tags = Tags.parse(
|
||||||
preferences.getString("default_query", "") ?: ""
|
preferences.getString("default_query", "") ?: ""
|
||||||
)
|
)
|
||||||
|
|
||||||
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
||||||
|
|
||||||
with(dialogView.default_query_dialog_language_selector) {
|
with(view.default_query_dialog_language_selector) {
|
||||||
adapter =
|
adapter =
|
||||||
ArrayAdapter(
|
ArrayAdapter(
|
||||||
context,
|
context,
|
||||||
@@ -110,13 +106,13 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(dialogView.default_query_dialog_BL_checkbox) {
|
with(view.default_query_dialog_BL_checkbox) {
|
||||||
isChecked = tags.contains(excludeBL)
|
isChecked = tags.contains(excludeBL)
|
||||||
if (tags.contains(excludeBL))
|
if (tags.contains(excludeBL))
|
||||||
tags.remove(excludeBL)
|
tags.remove(excludeBL)
|
||||||
}
|
}
|
||||||
|
|
||||||
with(dialogView.default_query_dialog_guro_checkbox) {
|
with(view.default_query_dialog_guro_checkbox) {
|
||||||
isChecked = excludeGuro.all { tags.contains(it) }
|
isChecked = excludeGuro.all { tags.contains(it) }
|
||||||
if (excludeGuro.all { tags.contains(it) })
|
if (excludeGuro.all { tags.contains(it) })
|
||||||
excludeGuro.forEach {
|
excludeGuro.forEach {
|
||||||
@@ -124,7 +120,7 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(dialogView.default_query_dialog_edittext) {
|
with(view.default_query_dialog_edittext) {
|
||||||
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
|
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
|
||||||
addTextChangedListener(object : TextWatcher {
|
addTextChangedListener(object : TextWatcher {
|
||||||
override fun beforeTextChanged(
|
override fun beforeTextChanged(
|
||||||
@@ -149,6 +145,8 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,33 +18,45 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
package xyz.quaver.pupil.ui.dialog
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.RadioButton
|
import android.widget.RadioButton
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.android.synthetic.main.item_dl_location.view.*
|
import kotlinx.android.synthetic.main.item_dl_location.view.*
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
|
import xyz.quaver.pupil.util.*
|
||||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
|
import java.io.File
|
||||||
import xyz.quaver.pupil.util.byteToString
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
||||||
|
|
||||||
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
private val buttons = mutableListOf<Pair<RadioButton, Uri?>>()
|
private val buttons = mutableListOf<Pair<RadioButton, File?>>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
setTitle(R.string.settings_dl_location)
|
||||||
|
|
||||||
|
setView(build())
|
||||||
|
|
||||||
|
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ -> }
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun build() : View {
|
||||||
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
|
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
|
||||||
|
|
||||||
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
||||||
@@ -67,9 +79,9 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
|||||||
pair.first.isChecked = false
|
pair.first.isChecked = false
|
||||||
}
|
}
|
||||||
button.performClick()
|
button.performClick()
|
||||||
preference.edit().putString("dl_location", Uri.fromFile(dir).toString()).apply()
|
preference.edit().putString("dl_location", dir.canonicalPath).apply()
|
||||||
}
|
}
|
||||||
buttons.add(button to Uri.fromFile(dir))
|
buttons.add(button to dir)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,11 +94,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
|||||||
button.performClick()
|
button.performClick()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
|
||||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
|
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()
|
dismiss()
|
||||||
} else { // Can't use SAF on old Androids!
|
} else { // Can't use SAF on old Androids!
|
||||||
@@ -106,25 +123,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
|||||||
buttons.add(button to null)
|
buttons.add(button to null)
|
||||||
})
|
})
|
||||||
|
|
||||||
val pref = Uri.parse(preference.getString("dl_location", null))
|
externalFilesDirs.indexOfFirst {
|
||||||
val index = externalFilesDirs.indexOfFirst {
|
it.canonicalPath == getDownloadDirectory(context).canonicalPath
|
||||||
Uri.fromFile(it).toString() == pref.toString()
|
}.let { index ->
|
||||||
|
if (index < 0)
|
||||||
|
buttons.first().first.isChecked = true
|
||||||
|
else
|
||||||
|
buttons[index].first.isChecked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 0)
|
return view
|
||||||
buttons.last().first.isChecked = true
|
|
||||||
else
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
@@ -56,21 +57,17 @@ class MirrorDialog(context: Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var recyclerView: RecyclerView
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
initDialog()
|
|
||||||
|
|
||||||
setTitle(R.string.settings_mirror_title)
|
setTitle(R.string.settings_mirror_title)
|
||||||
setView(recyclerView)
|
setView(build())
|
||||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
|
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initDialog() {
|
private fun build() : View {
|
||||||
recyclerView = RecyclerView(context).apply recyclerview@{
|
return RecyclerView(context).apply recyclerview@{
|
||||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = LinearLayoutManager(context)
|
||||||
adapter = MirrorAdapter(context).apply adapter@{
|
adapter = MirrorAdapter(context).apply adapter@{
|
||||||
|
|||||||
133
app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt
Normal file
133
app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.ui.dialog
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import kotlinx.android.synthetic.main.dialog_proxy.view.*
|
||||||
|
import xyz.quaver.proxy
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.util.ProxyInfo
|
||||||
|
import xyz.quaver.pupil.util.getProxyInfo
|
||||||
|
import xyz.quaver.pupil.util.json
|
||||||
|
import java.net.Proxy
|
||||||
|
|
||||||
|
class ProxyDialog(context: Context) : Dialog(context) {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
val view = build()
|
||||||
|
|
||||||
|
setTitle(R.string.settings_proxy_title)
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
|
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
private fun build() : View {
|
||||||
|
val proxyInfo = getProxyInfo(context)
|
||||||
|
|
||||||
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null)
|
||||||
|
|
||||||
|
val enabler = { enable: Boolean ->
|
||||||
|
view?.proxy_addr?.isEnabled = enable
|
||||||
|
view?.proxy_port?.isEnabled = enable
|
||||||
|
view?.proxy_username?.isEnabled = enable
|
||||||
|
view?.proxy_password?.isEnabled = enable
|
||||||
|
|
||||||
|
if (!enable) {
|
||||||
|
view?.proxy_addr?.text = null
|
||||||
|
view?.proxy_port?.text = null
|
||||||
|
view?.proxy_username?.text = null
|
||||||
|
view?.proxy_password?.text = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with(view.proxy_type_selector) {
|
||||||
|
adapter = ArrayAdapter(
|
||||||
|
context,
|
||||||
|
android.R.layout.simple_spinner_dropdown_item,
|
||||||
|
context.resources.getStringArray(R.array.proxy_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
setSelection(proxyInfo.type.ordinal)
|
||||||
|
|
||||||
|
onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
enabler.invoke(position != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.proxy_addr.setText(proxyInfo.host)
|
||||||
|
view.proxy_port.setText(proxyInfo.port?.toString())
|
||||||
|
view.proxy_username.setText(proxyInfo.username)
|
||||||
|
view.proxy_password.setText(proxyInfo.password)
|
||||||
|
|
||||||
|
enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT)
|
||||||
|
|
||||||
|
view.proxy_cancel.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
view.proxy_ok.setOnClickListener {
|
||||||
|
val type = Proxy.Type.values()[view.proxy_type_selector.selectedItemPosition]
|
||||||
|
val addr = view.proxy_addr.text?.toString()
|
||||||
|
val port = view.proxy_port.text?.toString()?.toIntOrNull()
|
||||||
|
val username = view.proxy_username.text?.toString()
|
||||||
|
val password = view.proxy_password.text?.toString()
|
||||||
|
|
||||||
|
if (type != Proxy.Type.DIRECT) {
|
||||||
|
if (addr == null || addr.isEmpty())
|
||||||
|
view.proxy_addr.error = context.getText(R.string.proxy_dialog_error)
|
||||||
|
if (port == null)
|
||||||
|
view.proxy_port.error = context.getText(R.string.proxy_dialog_error)
|
||||||
|
|
||||||
|
if (addr == null || addr.isEmpty() || port == null)
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyInfo(type, addr, port, username, password).let {
|
||||||
|
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().putString("proxy",
|
||||||
|
json.stringify(ProxyInfo.serializer(), it)
|
||||||
|
).apply()
|
||||||
|
|
||||||
|
proxy = it.proxy()
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@ import android.os.Bundle
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
@@ -37,6 +36,7 @@ import xyz.quaver.pupil.ui.SettingsActivity
|
|||||||
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
|
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
|
||||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
|
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
|
||||||
import xyz.quaver.pupil.ui.dialog.MirrorDialog
|
import xyz.quaver.pupil.ui.dialog.MirrorDialog
|
||||||
|
import xyz.quaver.pupil.ui.dialog.ProxyDialog
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class SettingsFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDirSize(dir: DocumentFile) : String {
|
private fun getDirSize(dir: File) : String {
|
||||||
val size = dir.walk().map { it.length() }.sum()
|
val size = dir.walk().map { it.length() }.sum()
|
||||||
|
|
||||||
return getString(R.string.settings_clear_summary, byteToString(size))
|
return getString(R.string.settings_clear_summary, byteToString(size))
|
||||||
@@ -86,7 +86,7 @@ class SettingsFragment :
|
|||||||
checkUpdate(activity as SettingsActivity, true)
|
checkUpdate(activity as SettingsActivity, true)
|
||||||
}
|
}
|
||||||
"delete_cache" -> {
|
"delete_cache" -> {
|
||||||
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
|
val dir = File(context.cacheDir, "imageCache")
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
setTitle(R.string.warning)
|
setTitle(R.string.warning)
|
||||||
@@ -147,15 +147,14 @@ class SettingsFragment :
|
|||||||
MirrorDialog(context)
|
MirrorDialog(context)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
"proxy" -> {
|
||||||
|
ProxyDialog(context)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
"backup" -> {
|
"backup" -> {
|
||||||
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
|
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
|
||||||
context,
|
File(getDownloadDirectory(context), "favorites.json"),
|
||||||
getDownloadDirectory(context).let {
|
true
|
||||||
if (it.findFile("favorites.json") != null)
|
|
||||||
it
|
|
||||||
else
|
|
||||||
it.createFile("null", "favorites.json")!!
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
|
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
|
||||||
@@ -195,9 +194,18 @@ class SettingsFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
when (key) {
|
key ?: return
|
||||||
"dl_location" -> {
|
|
||||||
findPreference<Preference>(key)?.summary = getDownloadDirectory(context!!).uri.path
|
with(findPreference<Preference>(key)) {
|
||||||
|
this ?: return
|
||||||
|
|
||||||
|
when (key) {
|
||||||
|
"proxy" -> {
|
||||||
|
summary = getProxyInfo(context).type.name
|
||||||
|
}
|
||||||
|
"dl_location" -> {
|
||||||
|
summary = getDownloadDirectory(context!!).canonicalPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,7 +236,7 @@ class SettingsFragment :
|
|||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"delete_cache" -> {
|
"delete_cache" -> {
|
||||||
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
|
val dir = File(context.cacheDir, "imageCache")
|
||||||
summary = getDirSize(dir)
|
summary = getDirSize(dir)
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
@@ -246,13 +254,12 @@ class SettingsFragment :
|
|||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"dl_location" -> {
|
"dl_location" -> {
|
||||||
summary = getDownloadDirectory(context).uri.path
|
summary = getDownloadDirectory(context).canonicalPath
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"default_query" -> {
|
"default_query" -> {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
summary = PreferenceManager.getDefaultSharedPreferences(context).getString("default_query", "") ?: ""
|
||||||
summary = preferences.getString("default_query", "") ?: ""
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
@@ -276,6 +283,11 @@ class SettingsFragment :
|
|||||||
"mirrors" -> {
|
"mirrors" -> {
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
|
"proxy" -> {
|
||||||
|
summary = getProxyInfo(context).type.name
|
||||||
|
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
"dark_mode" -> {
|
"dark_mode" -> {
|
||||||
onPreferenceChangeListener = this@SettingsFragment
|
onPreferenceChangeListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,13 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonConfiguration
|
||||||
|
|
||||||
const val REQUEST_LOCK = 38238
|
const val REQUEST_LOCK = 38238
|
||||||
const val REQUEST_RESTORE = 16546
|
const val REQUEST_RESTORE = 16546
|
||||||
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
||||||
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
||||||
|
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
|
||||||
|
|
||||||
|
val json = Json(JsonConfiguration.Stable)
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) 2007-2008 OpenIntents.org
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import android.content.ContentUris;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.provider.DocumentsContract;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @version 2009-07-03
|
|
||||||
* @author Peli
|
|
||||||
* @version 2013-12-11
|
|
||||||
* @author paulburke (ipaulpro)
|
|
||||||
*/
|
|
||||||
public class FileUtils {
|
|
||||||
/**
|
|
||||||
* Get a file path from a Uri. This will get the the path for Storage Access
|
|
||||||
* Framework Documents, as well as the _data field for the MediaStore and
|
|
||||||
* other file-based ContentProviders.
|
|
||||||
*
|
|
||||||
* @param context The context.
|
|
||||||
* @param uri The Uri to query.
|
|
||||||
* @author paulburke
|
|
||||||
*/
|
|
||||||
public static String getPath(final Context context, final Uri uri) {
|
|
||||||
|
|
||||||
// DocumentProvider
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
|
|
||||||
// ExternalStorageProvider
|
|
||||||
if (isExternalStorageDocument(uri)) {
|
|
||||||
final String docId = DocumentsContract.getDocumentId(uri);
|
|
||||||
final String[] split = docId.split(":");
|
|
||||||
final String type = split[0];
|
|
||||||
|
|
||||||
if ("primary".equalsIgnoreCase(type)) {
|
|
||||||
return context.getExternalFilesDir(null).getParentFile().getParentFile().getParentFile().getParent() + "/" + split[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO handle non-primary volumes
|
|
||||||
}
|
|
||||||
// DownloadsProvider
|
|
||||||
else if (isDownloadsDocument(uri)) {
|
|
||||||
|
|
||||||
final String id = DocumentsContract.getDocumentId(uri);
|
|
||||||
final Uri contentUri = ContentUris.withAppendedId(
|
|
||||||
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
|
||||||
|
|
||||||
return getDataColumn(context, contentUri, null, null);
|
|
||||||
}
|
|
||||||
// MediaProvider
|
|
||||||
else if (isMediaDocument(uri)) {
|
|
||||||
final String docId = DocumentsContract.getDocumentId(uri);
|
|
||||||
final String[] split = docId.split(":");
|
|
||||||
final String type = split[0];
|
|
||||||
|
|
||||||
Uri contentUri = null;
|
|
||||||
if ("image".equals(type)) {
|
|
||||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
|
||||||
} else if ("video".equals(type)) {
|
|
||||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
|
||||||
} else if ("audio".equals(type)) {
|
|
||||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String selection = "_id=?";
|
|
||||||
final String[] selectionArgs = new String[] {
|
|
||||||
split[1]
|
|
||||||
};
|
|
||||||
|
|
||||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// MediaStore (and general)
|
|
||||||
else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
|
||||||
return getDataColumn(context, uri, null, null);
|
|
||||||
}
|
|
||||||
// File
|
|
||||||
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
|
||||||
return uri.getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the value of the data column for this Uri. This is useful for
|
|
||||||
* MediaStore Uris, and other file-based ContentProviders.
|
|
||||||
*
|
|
||||||
* @param context The context.
|
|
||||||
* @param uri The Uri to query.
|
|
||||||
* @param selection (Optional) Filter used in the query.
|
|
||||||
* @param selectionArgs (Optional) Selection arguments used in the query.
|
|
||||||
* @return The value of the _data column, which is typically a file path.
|
|
||||||
*/
|
|
||||||
public static String getDataColumn(Context context, Uri uri, String selection,
|
|
||||||
String[] selectionArgs) {
|
|
||||||
|
|
||||||
Cursor cursor = null;
|
|
||||||
final String column = "_data";
|
|
||||||
final String[] projection = {
|
|
||||||
column
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
|
|
||||||
null);
|
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
|
||||||
final int column_index = cursor.getColumnIndexOrThrow(column);
|
|
||||||
return cursor.getString(column_index);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (cursor != null)
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param uri The Uri to check.
|
|
||||||
* @return Whether the Uri authority is ExternalStorageProvider.
|
|
||||||
*/
|
|
||||||
public static boolean isExternalStorageDocument(Uri uri) {
|
|
||||||
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param uri The Uri to check.
|
|
||||||
* @return Whether the Uri authority is DownloadsProvider.
|
|
||||||
*/
|
|
||||||
public static boolean isDownloadsDocument(Uri uri) {
|
|
||||||
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param uri The Uri to check.
|
|
||||||
* @return Whether the Uri authority is MediaProvider.
|
|
||||||
*/
|
|
||||||
public static boolean isMediaDocument(Uri uri) {
|
|
||||||
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,52 +21,59 @@ package xyz.quaver.pupil.util.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import android.util.Log
|
||||||
|
import android.util.SparseArray
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import com.crashlytics.android.Crashlytics
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.io.InputStream
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.parse
|
|
||||||
import kotlinx.serialization.stringify
|
|
||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.Reader
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.proxy
|
||||||
|
import xyz.quaver.pupil.util.getCachedGallery
|
||||||
|
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||||
|
import xyz.quaver.pupil.util.isParentOf
|
||||||
|
import xyz.quaver.pupil.util.json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.util.concurrent.locks.Lock
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
|
||||||
class Cache(context: Context) : ContextWrapper(context) {
|
class Cache(context: Context) : ContextWrapper(context) {
|
||||||
|
|
||||||
|
private val locks = SparseArray<Lock>()
|
||||||
|
private fun lock(galleryID: Int) {
|
||||||
|
synchronized(locks) {
|
||||||
|
if (locks.indexOfKey(galleryID) < 0)
|
||||||
|
locks.put(galleryID, ReentrantLock())
|
||||||
|
}
|
||||||
|
|
||||||
|
locks[galleryID].lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unlock(galleryID: Int) {
|
||||||
|
locks[galleryID]?.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
// Search in this order
|
// Search in this order
|
||||||
// Download -> Cache
|
// Download -> Cache
|
||||||
fun getCachedGallery(galleryID: Int) : DocumentFile? {
|
fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also {
|
||||||
var file = getDownloadDirectory(this).findFile(galleryID.toString())
|
if (!it.exists())
|
||||||
|
it.mkdirs()
|
||||||
if (file?.exists() == true)
|
|
||||||
return file
|
|
||||||
|
|
||||||
file = DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID"))
|
|
||||||
|
|
||||||
return if (file.exists())
|
|
||||||
file
|
|
||||||
else
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
fun getCachedMetadata(galleryID: Int) : Metadata? {
|
fun getCachedMetadata(galleryID: Int) : Metadata? {
|
||||||
val file = (getCachedGallery(galleryID) ?: return null).findFile(".metadata")
|
val file = File(getCachedGallery(galleryID), ".metadata")
|
||||||
|
|
||||||
if (file?.exists() != true)
|
if (!file.exists())
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
Json.parse(file.readText(this))
|
json.parse(Metadata.serializer(), file.readText())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
//File corrupted
|
//File corrupted
|
||||||
file.delete()
|
file.delete()
|
||||||
@@ -74,15 +81,13 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
|
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
|
||||||
val file = getCachedGallery(galleryID)?.findFile(".metadata") ?:
|
val file = File(getCachedGallery(galleryID), ".metadata").also {
|
||||||
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
|
|
||||||
if (!it.exists())
|
if (!it.exists())
|
||||||
it.mkdirs()
|
it.createNewFile()
|
||||||
}).createFile("null", ".metadata") ?: return
|
}
|
||||||
|
|
||||||
file.writeText(this, Json.stringify(metadata))
|
file.writeText(json.stringify(Metadata.serializer(), metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getThumbnail(galleryID: Int): String? {
|
suspend fun getThumbnail(galleryID: Int): String? {
|
||||||
@@ -92,7 +97,9 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
|
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
|
||||||
try {
|
try {
|
||||||
Base64.encodeToString(URL(thumbnails?.firstOrNull()).readBytes(), Base64.DEFAULT)
|
Base64.encodeToString(URL(thumbnails?.firstOrNull()).openConnection(proxy).getInputStream().use {
|
||||||
|
it.readBytes()
|
||||||
|
}, Base64.DEFAULT)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -111,21 +118,29 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
|
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
|
||||||
val metadata = Cache(this).getCachedMetadata(galleryID)
|
val metadata = Cache(this).getCachedMetadata(galleryID)
|
||||||
|
|
||||||
val source = mapOf(
|
val sources = listOf(
|
||||||
Code.HITOMI to { xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||||
Code.HIYOBI to { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||||
)
|
)
|
||||||
|
|
||||||
val galleryBlock = if (metadata?.galleryBlock == null)
|
val galleryBlock = if (metadata?.galleryBlock == null) {
|
||||||
source.entries.map {
|
CoroutineScope(Dispatchers.IO).async {
|
||||||
CoroutineScope(Dispatchers.IO).async {
|
var galleryBlock: GalleryBlock? = null
|
||||||
kotlin.runCatching {
|
|
||||||
it.value.invoke()
|
for (source in sources) {
|
||||||
}.getOrNull()
|
galleryBlock = try {
|
||||||
}
|
source.invoke()
|
||||||
}.firstOrNull {
|
} catch (e: Exception) {
|
||||||
it.await() != null
|
null
|
||||||
}?.await()
|
}
|
||||||
|
|
||||||
|
if (galleryBlock != null)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryBlock
|
||||||
|
}.await() ?: return null
|
||||||
|
}
|
||||||
else
|
else
|
||||||
metadata.galleryBlock
|
metadata.galleryBlock
|
||||||
|
|
||||||
@@ -164,68 +179,93 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
var retval: Reader? = null
|
var retval: Reader? = null
|
||||||
|
|
||||||
for (source in sources) {
|
for (source in sources) {
|
||||||
retval = kotlin.runCatching {
|
retval = try {
|
||||||
source.value.invoke()
|
source.value.invoke()
|
||||||
}.getOrNull()
|
} catch (e: Exception) {
|
||||||
|
Crashlytics.logException(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
if (retval != null)
|
if (retval != null)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
retval
|
retval
|
||||||
}.await()
|
}.await() ?: return null
|
||||||
} else
|
} else
|
||||||
metadata.reader
|
metadata.reader
|
||||||
|
|
||||||
if (reader != null)
|
setCachedMetadata(
|
||||||
setCachedMetadata(
|
galleryID,
|
||||||
galleryID,
|
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
|
||||||
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return reader
|
return reader
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getImages(galleryID: Int): List<DocumentFile?>? {
|
val imageNameRegex = Regex("""^\d+\..+$""")
|
||||||
val gallery = getCachedGallery(galleryID) ?: return null
|
fun getImages(galleryID: Int): List<File?>? {
|
||||||
val reader = getReaderOrNull(galleryID) ?: return null
|
val gallery = getCachedGallery(galleryID)
|
||||||
val images = gallery.listFiles()
|
|
||||||
|
|
||||||
return reader.galleryInfo.indices.map { index ->
|
return gallery.list { _, name ->
|
||||||
images.firstOrNull { file -> file.name?.startsWith("%05d".format(index)) == true }
|
imageNameRegex.matches(name)
|
||||||
|
}?.map {
|
||||||
|
File(gallery, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putImage(galleryID: Int, name: String, data: ByteArray) {
|
val imageExtensions = listOf(
|
||||||
val cache = getCachedGallery(galleryID) ?:
|
"png",
|
||||||
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
|
"jpg",
|
||||||
if (!it.exists())
|
"webp",
|
||||||
it.mkdirs()
|
"gif"
|
||||||
}) ?: return
|
)
|
||||||
|
fun getImage(galleryID: Int, index: Int): File? {
|
||||||
|
val gallery = getCachedGallery(galleryID)
|
||||||
|
|
||||||
if (!Regex("""^[0-9]+.+$""").matches(name))
|
for (ext in imageExtensions) {
|
||||||
throw IllegalArgumentException("File name is not a number")
|
File(gallery, "%05d.$ext".format(index)).let {
|
||||||
|
if (it.exists())
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cache.let {
|
return null
|
||||||
if (it.findFile(name) != null)
|
|
||||||
it
|
|
||||||
else
|
|
||||||
it.createFile("null", name)
|
|
||||||
}?.writeBytes(this, data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveToDownload(galleryID: Int) {
|
|
||||||
val cache = getCachedGallery(galleryID)
|
|
||||||
|
|
||||||
if (cache != null) {
|
fun putImage(galleryID: Int, index: Int, ext: String, data: InputStream) {
|
||||||
val download = getDownloadDirectory(this)
|
val cache = File(getCachedGallery(galleryID), "%05d.$ext".format(index)).also {
|
||||||
|
if (!it.exists())
|
||||||
|
it.createNewFile()
|
||||||
|
}
|
||||||
|
|
||||||
if (!download.isParentOf(cache)) {
|
data.use {
|
||||||
cache.copyRecursively(this, download)
|
it.copyTo(FileOutputStream(cache))
|
||||||
cache.deleteRecursively()
|
}
|
||||||
}
|
}
|
||||||
} else
|
|
||||||
getDownloadDirectory(this).createDirectory(galleryID.toString())
|
fun moveToDownload(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val cache = getCachedGallery(galleryID).also {
|
||||||
|
if (!it.exists())
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val download = File(getDownloadDirectory(this@Cache), galleryID.toString())
|
||||||
|
|
||||||
|
if (download.isParentOf(cache))
|
||||||
|
return@launch
|
||||||
|
|
||||||
|
Log.i("PUPILD", "MOVING ${cache.canonicalPath} --> ${download.canonicalPath}")
|
||||||
|
|
||||||
|
cache.copyRecursively(download, true) { file, err ->
|
||||||
|
Log.i("PUPILD", "MOVING ERROR ${file.canonicalPath} ${err.message}")
|
||||||
|
OnErrorAction.SKIP
|
||||||
|
}
|
||||||
|
Log.i("PUPILD", "MOVED ${cache.canonicalPath}")
|
||||||
|
|
||||||
|
Log.i("PUPILD", "DELETING ${cache.canonicalPath}")
|
||||||
|
cache.deleteRecursively()
|
||||||
|
Log.i("PUPILD", "DELETED ${cache.canonicalPath}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
|
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.content.Context
|
|||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
import android.util.SparseArray
|
import android.util.SparseArray
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
@@ -36,15 +37,18 @@ import okio.*
|
|||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.Reader
|
||||||
import xyz.quaver.hitomi.getReferer
|
import xyz.quaver.hitomi.getReferer
|
||||||
import xyz.quaver.hitomi.urlFromUrlFromHash
|
import xyz.quaver.hitomi.imageUrlFromImage
|
||||||
import xyz.quaver.hiyobi.cookie
|
import xyz.quaver.hiyobi.cookie
|
||||||
import xyz.quaver.hiyobi.createImgList
|
import xyz.quaver.hiyobi.createImgList
|
||||||
import xyz.quaver.hiyobi.user_agent
|
import xyz.quaver.hiyobi.user_agent
|
||||||
|
import xyz.quaver.proxy
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
import xyz.quaver.pupil.ui.ReaderActivity
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||||
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
|
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
|
||||||
@@ -145,31 +149,29 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
|
|
||||||
private val loop = loop()
|
private val loop = loop()
|
||||||
private val worker = SparseArray<Job?>()
|
private val worker = SparseArray<Job?>()
|
||||||
@Volatile var nRunners = 0
|
|
||||||
|
|
||||||
private val client = OkHttpClient.Builder()
|
val interceptor = Interceptor { chain ->
|
||||||
.addInterceptor { chain ->
|
val request = chain.request()
|
||||||
val request = chain.request()
|
val response = chain.proceed(request)
|
||||||
var response = chain.proceed(request)
|
|
||||||
|
|
||||||
var retry = preferences.getInt("retry", 3)
|
response.newBuilder()
|
||||||
while (!response.isSuccessful && retry > 0) {
|
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
||||||
response = chain.proceed(request)
|
.build()
|
||||||
retry--
|
}
|
||||||
}
|
val client =
|
||||||
|
OkHttpClient.Builder()
|
||||||
response.newBuilder()
|
.addInterceptor(interceptor)
|
||||||
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
.connectTimeout(0, TimeUnit.SECONDS)
|
||||||
.build()
|
.readTimeout(0, TimeUnit.SECONDS)
|
||||||
}
|
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
|
||||||
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
|
.proxy(proxy)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
queue.clear()
|
queue.clear()
|
||||||
|
|
||||||
loop.cancel()
|
loop.cancel()
|
||||||
for (i in 0..worker.size()) {
|
for (i in 0 until worker.size()) {
|
||||||
val galleryID = worker.keyAt(i)
|
val galleryID = worker.keyAt(i)
|
||||||
|
|
||||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||||
@@ -182,68 +184,40 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
exception.clear()
|
exception.clear()
|
||||||
notification.clear()
|
notification.clear()
|
||||||
notificationManager.cancelAll()
|
notificationManager.cancelAll()
|
||||||
|
|
||||||
nRunners = 0
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel(galleryID: Int) {
|
fun cancel(galleryID: Int) {
|
||||||
queue.remove(galleryID)
|
queue.remove(galleryID)
|
||||||
worker[galleryID]?.cancel()
|
worker[galleryID]?.cancel()
|
||||||
|
|
||||||
client.dispatcher().queuedCalls()
|
client.dispatcher().queuedCalls().filter {
|
||||||
.filter {
|
((it.request().tag() as Pair<*, *>).first as Int) == galleryID
|
||||||
@Suppress("UNCHECKED_CAST")
|
}.forEach {
|
||||||
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID
|
it.cancel()
|
||||||
}
|
}
|
||||||
.forEach {
|
|
||||||
it.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.remove(galleryID)
|
progress.remove(galleryID)
|
||||||
exception.remove(galleryID)
|
exception.remove(galleryID)
|
||||||
notification.remove(galleryID)
|
notification.remove(galleryID)
|
||||||
notificationManager.cancel(galleryID)
|
notificationManager.cancel(galleryID)
|
||||||
|
|
||||||
if (progress.indexOfKey(galleryID) >= 0) {
|
if (progress.indexOfKey(galleryID) >= 0)
|
||||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||||
nRunners--
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
|
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
|
||||||
|
|
||||||
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
|
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
|
||||||
val cache = Cache(this@DownloadWorker).getImages(galleryID)
|
|
||||||
val lowQuality = preferences.getBoolean("low_quality", false)
|
val lowQuality = preferences.getBoolean("low_quality", false)
|
||||||
|
|
||||||
//Cache exists :P
|
|
||||||
cache?.get(index)?.let {
|
|
||||||
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
|
|
||||||
|
|
||||||
notify(galleryID)
|
|
||||||
|
|
||||||
if (isCompleted(galleryID)) {
|
|
||||||
with(Cache(this@DownloadWorker)) {
|
|
||||||
if (isDownloading(galleryID)) {
|
|
||||||
moveToDownload(galleryID)
|
|
||||||
setDownloading(galleryID, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nRunners--
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = Request.Builder().apply {
|
val request = Request.Builder().apply {
|
||||||
when (reader.code) {
|
when (reader.code) {
|
||||||
Code.HITOMI -> {
|
Code.HITOMI -> {
|
||||||
url(
|
url(
|
||||||
urlFromUrlFromHash(
|
imageUrlFromImage(
|
||||||
galleryID,
|
galleryID,
|
||||||
reader.galleryInfo[index],
|
reader.galleryInfo.files[index],
|
||||||
if (lowQuality) "webp" else null
|
lowQuality
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
addHeader("Referer", getReferer(galleryID))
|
addHeader("Referer", getReferer(galleryID))
|
||||||
@@ -272,23 +246,41 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
exception.put(galleryID, null)
|
exception.put(galleryID, null)
|
||||||
|
|
||||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||||
nRunners--
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList())
|
val cache = Cache(this@DownloadWorker).getImages(galleryID)
|
||||||
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
|
|
||||||
|
progress.put(galleryID, reader.galleryInfo.files.indices.map { index ->
|
||||||
|
if (cache?.firstOrNull { it?.nameWithoutExtension?.toIntOrNull() == index } != null)
|
||||||
|
Float.POSITIVE_INFINITY
|
||||||
|
else
|
||||||
|
0F
|
||||||
|
}.toMutableList())
|
||||||
|
exception.put(galleryID, reader.galleryInfo.files.map { null }.toMutableList())
|
||||||
|
|
||||||
if (notification[galleryID] == null)
|
if (notification[galleryID] == null)
|
||||||
initNotification(galleryID)
|
initNotification(galleryID)
|
||||||
|
|
||||||
notification[galleryID].setContentTitle(reader.title)
|
notification[galleryID].setContentTitle(reader.galleryInfo.title)
|
||||||
notify(galleryID)
|
notify(galleryID)
|
||||||
|
|
||||||
for (i in reader.galleryInfo.indices) {
|
if (isCompleted(galleryID)) {
|
||||||
|
with(Cache(this@DownloadWorker)) {
|
||||||
|
if (isDownloading(galleryID)) {
|
||||||
|
moveToDownload(galleryID)
|
||||||
|
setDownloading(galleryID, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in reader.galleryInfo.files.indices) {
|
||||||
val callback = object : Callback {
|
val callback = object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
if (Fabric.isInitialized())
|
Log.i("PUPILD", "FAIL ${call.request().tag()} (${e.message})")
|
||||||
|
if (Fabric.isInitialized() && e.message != "Canceled")
|
||||||
Crashlytics.logException(e)
|
Crashlytics.logException(e)
|
||||||
|
|
||||||
progress[galleryID]?.set(i, Float.NaN)
|
progress[galleryID]?.set(i, Float.NaN)
|
||||||
@@ -296,40 +288,74 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
|
|
||||||
notify(galleryID)
|
notify(galleryID)
|
||||||
|
|
||||||
if (isCompleted(galleryID)) {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val cache = Cache(this@DownloadWorker)
|
if (isCompleted(galleryID)) {
|
||||||
if (cache.isDownloading(galleryID)) {
|
with(Cache(this@DownloadWorker)) {
|
||||||
cache.moveToDownload(galleryID)
|
if (isDownloading(galleryID)) {
|
||||||
cache.setDownloading(galleryID, false)
|
moveToDownload(galleryID)
|
||||||
|
setDownloading(galleryID, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
nRunners--
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
response.body().use {
|
Log.i("PUPILD", "OK ${call.request().tag()}")
|
||||||
val res = it.bytes()
|
|
||||||
val ext =
|
|
||||||
call.request().url().encodedPath().split('.').last()
|
|
||||||
|
|
||||||
Cache(this@DownloadWorker).putImage(galleryID, "%05d.%s".format(i, ext), res)
|
val ext = call.request().url().encodedPath().split('.').last()
|
||||||
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
|
|
||||||
}
|
|
||||||
|
|
||||||
notify(galleryID)
|
try {
|
||||||
|
response.body().use {
|
||||||
if (isCompleted(galleryID)) {
|
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream())
|
||||||
val cache = Cache(this@DownloadWorker)
|
|
||||||
if (cache.isDownloading(galleryID)) {
|
|
||||||
cache.moveToDownload(galleryID)
|
|
||||||
cache.setDownloading(galleryID, false)
|
|
||||||
}
|
}
|
||||||
nRunners--
|
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
|
||||||
|
|
||||||
|
notify(galleryID)
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
if (isCompleted(galleryID)) {
|
||||||
|
with(Cache(this@DownloadWorker)) {
|
||||||
|
if (isDownloading(galleryID)) {
|
||||||
|
moveToDownload(galleryID)
|
||||||
|
setDownloading(galleryID, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i("PUPILD", "SUCCESS ${call.request().tag()}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
progress[galleryID]?.set(i, Float.NaN)
|
||||||
|
exception[galleryID]?.set(i, e)
|
||||||
|
|
||||||
|
notify(galleryID)
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
if (isCompleted(galleryID)) {
|
||||||
|
with(Cache(this@DownloadWorker)) {
|
||||||
|
if (isDownloading(galleryID)) {
|
||||||
|
moveToDownload(galleryID)
|
||||||
|
setDownloading(galleryID, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File(Cache(this@DownloadWorker).getCachedGallery(galleryID), "%05d.$ext".format(i)).delete()
|
||||||
|
|
||||||
|
Log.i("PUPILD", "FAIL ON OK ${call.request().tag()} (${e.message})")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queueDownload(galleryID, reader, i, callback)
|
if (progress[galleryID]?.get(i)?.isFinite() == true) {
|
||||||
|
queueDownload(galleryID, reader, i, callback)
|
||||||
|
Log.i("PUPILD", "$galleryID QUEUED $i")
|
||||||
|
} else {
|
||||||
|
Log.i("PUPILD", "$galleryID SKIPPED $i (${progress[galleryID]?.get(i)})")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,11 +363,17 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
val max = progress[galleryID]?.size ?: 0
|
val max = progress[galleryID]?.size ?: 0
|
||||||
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0
|
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0
|
||||||
|
|
||||||
if (isCompleted(galleryID))
|
Log.i("PUPILD", "NOTIFY $galleryID ${isCompleted(galleryID)} $progress/$max")
|
||||||
|
|
||||||
|
if (isCompleted(galleryID)) {
|
||||||
notification[galleryID]
|
notification[galleryID]
|
||||||
?.setContentText(getString(R.string.reader_notification_complete))
|
?.setContentText(getString(R.string.reader_notification_complete))
|
||||||
|
?.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
?.setProgress(0, 0, false)
|
?.setProgress(0, 0, false)
|
||||||
else
|
?.setOngoing(false)
|
||||||
|
|
||||||
|
notificationManager.cancel(galleryID)
|
||||||
|
} else
|
||||||
notification[galleryID]
|
notification[galleryID]
|
||||||
?.setProgress(max, progress, false)
|
?.setProgress(max, progress, false)
|
||||||
?.setContentText("$progress/$max")
|
?.setContentText("$progress/$max")
|
||||||
@@ -358,7 +390,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
}
|
}
|
||||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||||
addNextIntentWithParentStack(intent)
|
addNextIntentWithParentStack(intent)
|
||||||
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
|
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
|
||||||
@@ -367,24 +399,30 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P
|
setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P
|
||||||
setContentIntent(pendingIntent)
|
setContentIntent(pendingIntent)
|
||||||
setProgress(0, 0, true)
|
setProgress(0, 0, true)
|
||||||
|
setOngoing(true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
|
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (queue.isEmpty() || nRunners > preferences.getInt("max_download", 4))
|
if (queue.isEmpty())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
val galleryID = queue.poll() ?: continue
|
val galleryID = queue.peek() ?: continue
|
||||||
|
|
||||||
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
|
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
|
||||||
continue
|
continue
|
||||||
|
|
||||||
initNotification(galleryID)
|
if (notification[galleryID] == null)
|
||||||
|
initNotification(galleryID)
|
||||||
|
|
||||||
if (Cache(this@DownloadWorker).isDownloading(galleryID))
|
if (Cache(this@DownloadWorker).isDownloading(galleryID))
|
||||||
notificationManager.notify(galleryID, notification[galleryID].build())
|
notificationManager.notify(galleryID, notification[galleryID].build())
|
||||||
|
|
||||||
|
Log.i("PUPILD", "QUEUED $galleryID")
|
||||||
|
|
||||||
worker.put(galleryID, download(galleryID))
|
worker.put(galleryID, download(galleryID))
|
||||||
nRunners++
|
queue.poll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,43 +18,45 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.content.FileProvider
|
import android.os.Build
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import android.os.storage.StorageManager
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.lang.reflect.Array
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
fun getCachedGallery(context: Context, galleryID: Int) =
|
fun getCachedGallery(context: Context, galleryID: Int) =
|
||||||
getDownloadDirectory(context).findFile(galleryID.toString()) ?:
|
File(getDownloadDirectory(context), galleryID.toString()).let {
|
||||||
DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID"))
|
if (it.exists())
|
||||||
|
it
|
||||||
fun getDownloadDirectory(context: Context) : DocumentFile {
|
|
||||||
val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
|
|
||||||
if (it != null)
|
|
||||||
Uri.parse(it)
|
|
||||||
else
|
else
|
||||||
Uri.fromFile(context.getExternalFilesDir(null))
|
File(context.cacheDir, "imageCache/$galleryID")
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (uri.toString().startsWith("file"))
|
fun getDownloadDirectory(context: Context) =
|
||||||
DocumentFile.fromFile(File(uri.path!!))
|
PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
|
||||||
else
|
if (it != null && !it.startsWith("content"))
|
||||||
DocumentFile.fromTreeUri(context, uri) ?: DocumentFile.fromFile(context.getExternalFilesDir(null)!!)
|
File(it)
|
||||||
}
|
else
|
||||||
|
context.getExternalFilesDir(null)!!
|
||||||
|
}
|
||||||
|
|
||||||
fun convertUpdateUri(context: Context, uri: Uri) : Uri =
|
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
||||||
if (uri.toString().startsWith("file"))
|
|
||||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!.substringAfter("file:///")))
|
|
||||||
else
|
|
||||||
uri
|
|
||||||
|
|
||||||
fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
if (to.parentFile?.exists() == false)
|
||||||
context.contentResolver.openOutputStream(to.uri).use { out ->
|
to.parentFile!!.mkdirs()
|
||||||
out!!
|
|
||||||
|
if (!to.exists())
|
||||||
|
to.createNewFile()
|
||||||
|
|
||||||
|
FileOutputStream(to).use { out ->
|
||||||
|
|
||||||
with(openConnection()) {
|
with(openConnection()) {
|
||||||
val fileSize = contentLength.toLong()
|
val fileSize = contentLength.toLong()
|
||||||
@@ -78,74 +80,138 @@ fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DocumentFile.isParentOf(file: DocumentFile?) : Boolean {
|
fun getExtSdCardPaths(context: Context) =
|
||||||
var parent = file?.parentFile
|
ContextCompat.getExternalFilesDirs(context, null).drop(1).map {
|
||||||
while (parent != null) {
|
it.absolutePath.substringBeforeLast("/Android/data").let { path ->
|
||||||
if (this.uri.path == parent.uri.path)
|
runCatching {
|
||||||
return true
|
File(path).canonicalPath
|
||||||
|
}.getOrElse {
|
||||||
parent = parent.parentFile
|
path
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun DocumentFile.reader(context: Context, charset: Charset = Charsets.UTF_8) = context.contentResolver.openInputStream(uri)!!.reader(charset)
|
|
||||||
fun DocumentFile.readBytes(context: Context) = context.contentResolver.openInputStream(uri)!!.readBytes()
|
|
||||||
fun DocumentFile.readText(context: Context, charset: Charset = Charsets.UTF_8) = reader(context, charset).use { it.readText() }
|
|
||||||
|
|
||||||
fun DocumentFile.writeBytes(context: Context, array: ByteArray) = context.contentResolver.openOutputStream(uri)!!.write(array)
|
|
||||||
fun DocumentFile.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8) = writeBytes(context, text.toByteArray(charset))
|
|
||||||
|
|
||||||
fun DocumentFile.copyRecursively(
|
|
||||||
context: Context,
|
|
||||||
target: DocumentFile
|
|
||||||
) {
|
|
||||||
if (!exists())
|
|
||||||
throw Exception("The source file doesn't exist.")
|
|
||||||
|
|
||||||
if (this.isFile) {
|
|
||||||
target.let {
|
|
||||||
if (it.findFile(name!!) != null)
|
|
||||||
it
|
|
||||||
else
|
|
||||||
createFile("null", name!!)!!
|
|
||||||
}.writeBytes(
|
|
||||||
context,
|
|
||||||
readBytes(context)
|
|
||||||
)
|
|
||||||
} else if (this.isDirectory) {
|
|
||||||
target.createDirectory(name!!).also { newTarget ->
|
|
||||||
listFiles().forEach { child ->
|
|
||||||
child.copyRecursively(context, newTarget!!)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DocumentFile.deleteRecursively() {
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isDirectory)
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
listFiles().forEach {
|
fun getDocumentPathFromTreeUri(uri: Uri) =
|
||||||
it.deleteRecursively()
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
this.delete()
|
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)
|
||||||
|
|
||||||
fun DocumentFile.walk(state: LinkedList<DocumentFile> = LinkedList()) : Queue<DocumentFile> {
|
for (s in removableMediaFolderRoots) {
|
||||||
if (state.isEmpty())
|
if (root?.startsWith(s) == true) {
|
||||||
state.push(this)
|
return File(root)
|
||||||
|
|
||||||
listFiles().forEach {
|
|
||||||
state.push(it)
|
|
||||||
|
|
||||||
if (it.isDirectory) {
|
|
||||||
it.walk(state)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return state
|
return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun File.copyTo(context: Context, target: DocumentFile) = target.writeBytes(context, this.readBytes())
|
fun File.isParentOf(another: File) =
|
||||||
|
another.absolutePath.startsWith(this.absolutePath)
|
||||||
@@ -18,15 +18,14 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
import kotlinx.serialization.list
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.serializer
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
|
||||||
import kotlinx.serialization.parseList
|
|
||||||
import kotlinx.serialization.stringify
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class Histories(private val file: File) : ArrayList<Int>() {
|
class Histories(private val file: File) : ArrayList<Int>() {
|
||||||
|
|
||||||
|
val serializer = Int.serializer().list
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (!file.exists())
|
if (!file.exists())
|
||||||
file.parentFile?.mkdirs()
|
file.parentFile?.mkdirs()
|
||||||
@@ -38,21 +37,20 @@ class Histories(private val file: File) : ArrayList<Int>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
fun load() : Histories {
|
fun load() : Histories {
|
||||||
return apply {
|
return apply {
|
||||||
super.clear()
|
super.clear()
|
||||||
addAll(
|
addAll(
|
||||||
Json(JsonConfiguration.Stable).parseList(
|
json.parse(
|
||||||
|
serializer,
|
||||||
file.bufferedReader().use { it.readText() }
|
file.bufferedReader().use { it.readText() }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
fun save() {
|
fun save() {
|
||||||
file.writeText(Json(JsonConfiguration.Stable).stringify(this))
|
file.writeText(json.stringify(serializer, this))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(element: Int): Boolean {
|
override fun add(element: Int): Boolean {
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ package xyz.quaver.pupil.util
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
import kotlinx.serialization.json.JsonConfiguration
|
||||||
|
import kotlinx.serialization.list
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
@@ -73,7 +74,6 @@ class LockManager(base: Context): ContextWrapper(base) {
|
|||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
private fun load() {
|
private fun load() {
|
||||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||||
|
|
||||||
@@ -82,17 +82,16 @@ class LockManager(base: Context): ContextWrapper(base) {
|
|||||||
lock.writeText("[]")
|
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() {
|
private fun save() {
|
||||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||||
|
|
||||||
if (!lock.exists())
|
if (!lock.exists())
|
||||||
lock.createNewFile()
|
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) {
|
fun add(lock: Lock) {
|
||||||
|
|||||||
63
app/src/main/java/xyz/quaver/pupil/util/proxy.kt
Normal file
63
app/src/main/java/xyz/quaver/pupil/util/proxy.kt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import okhttp3.Authenticator
|
||||||
|
import okhttp3.Credentials
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ProxyInfo(
|
||||||
|
val type: Proxy.Type,
|
||||||
|
val host: String? = null,
|
||||||
|
val port: Int? = null,
|
||||||
|
val username: String? = null,
|
||||||
|
val password: String? = null
|
||||||
|
) {
|
||||||
|
fun proxy() : Proxy {
|
||||||
|
return if (host == null || port == null)
|
||||||
|
return Proxy.NO_PROXY
|
||||||
|
else
|
||||||
|
Proxy(type, InetSocketAddress.createUnresolved(host, port))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authenticator() = Authenticator { _, response ->
|
||||||
|
val credential = Credentials.basic(username, password)
|
||||||
|
|
||||||
|
response.request().newBuilder()
|
||||||
|
.header("Proxy-Authorization", credential)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProxy(context: Context) =
|
||||||
|
getProxyInfo(context).proxy()
|
||||||
|
|
||||||
|
fun getProxyInfo(context: Context) =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).getString("proxy", null).let {
|
||||||
|
if (it == null)
|
||||||
|
ProxyInfo(Proxy.Type.DIRECT)
|
||||||
|
else
|
||||||
|
json.parse(ProxyInfo.serializer(), it)
|
||||||
|
}
|
||||||
@@ -26,22 +26,27 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.*
|
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 ru.noties.markwon.Markwon
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
fun getReleases(url: String) : JsonArray {
|
fun getReleases(url: String) : JsonArray {
|
||||||
return try {
|
return try {
|
||||||
URL(url).readText().let {
|
URL(url).readText().let {
|
||||||
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it)
|
json.parse(JsonArray.serializer(), it)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
JsonArray(emptyList())
|
JsonArray(emptyList())
|
||||||
@@ -143,18 +148,14 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
setContentTitle(context.getString(R.string.update_notification_description))
|
setContentTitle(context.getString(R.string.update_notification_description))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
priority = NotificationCompat.PRIORITY_LOW
|
priority = NotificationCompat.PRIORITY_LOW
|
||||||
|
setOngoing(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch io@{
|
CoroutineScope(Dispatchers.IO).launch io@{
|
||||||
val target = getDownloadDirectory(context).let {
|
val target = File(getDownloadDirectory(context), "Pupil.apk")
|
||||||
if (it.findFile("Pupil.apk") != null)
|
|
||||||
it
|
|
||||||
else
|
|
||||||
it.createFile("null", "Pupil.apk")!!
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
URL(url).download(context, target) { progress, fileSize ->
|
URL(url).download(target) { progress, fileSize ->
|
||||||
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
|
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
|
||||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
||||||
}
|
}
|
||||||
@@ -163,6 +164,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
setContentText(context.getString(R.string.update_failed))
|
setContentText(context.getString(R.string.update_failed))
|
||||||
setMessage(context.getString(R.string.update_failed_message))
|
setMessage(context.getString(R.string.update_failed_message))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
setOngoing(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||||
@@ -173,7 +175,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
|
|
||||||
val install = Intent(Intent.ACTION_VIEW).apply {
|
val install = Intent(Intent.ACTION_VIEW).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
setDataAndType(convertUpdateUri(context, target.uri), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
setDataAndType(FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", target), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.apply {
|
builder.apply {
|
||||||
@@ -182,6 +184,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
setContentTitle(context.getString(R.string.update_download_completed))
|
setContentTitle(context.getString(R.string.update_download_completed))
|
||||||
setContentText(context.getString(R.string.update_download_completed_description))
|
setContentText(context.getString(R.string.update_download_completed_description))
|
||||||
|
setOngoing(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||||
|
|||||||
8
app/src/main/res/drawable/cancel.xml
Normal file
8
app/src/main/res/drawable/cancel.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!-- drawable/cancel.xml -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#fff" android:pathData="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,13.85 4.63,15.55 5.68,16.91L16.91,5.68C15.55,4.63 13.85,4 12,4M12,20A8,8 0 0,0 20,12C20,10.15 19.37,8.45 18.32,7.09L7.09,18.32C8.45,19.37 10.15,20 12,20Z" />
|
||||||
|
</vector>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB |
@@ -82,6 +82,13 @@
|
|||||||
android:layout_margin="16dp"
|
android:layout_margin="16dp"
|
||||||
app:menu_colorNormal="@color/colorAccent">
|
app:menu_colorNormal="@color/colorAccent">
|
||||||
|
|
||||||
|
<com.github.clans.fab.FloatingActionButton
|
||||||
|
android:id="@+id/main_fab_cancel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:fab_label="@string/main_fab_cancel"
|
||||||
|
app:fab_size="mini"/>
|
||||||
|
|
||||||
<com.github.clans.fab.FloatingActionButton
|
<com.github.clans.fab.FloatingActionButton
|
||||||
android:id="@+id/main_fab_jump"
|
android:id="@+id/main_fab_jump"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
123
app/src/main/res/layout/dialog_proxy.xml
Normal file
123
app/src/main/res/layout/dialog_proxy.xml
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Pupil, Hitomi.la viewer for Android
|
||||||
|
~ Copyright (C) 2020 tom5079
|
||||||
|
~
|
||||||
|
~ This program is free software: you can redistribute it and/or modify
|
||||||
|
~ it under the terms of the GNU General Public License as published by
|
||||||
|
~ the Free Software Foundation, either version 3 of the License, or
|
||||||
|
~ (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ This program is distributed in the hope that it will be useful,
|
||||||
|
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
~ GNU General Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License
|
||||||
|
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/proxy_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
style="@style/TextAppearance.AppCompat.Large"
|
||||||
|
android:text="@string/settings_proxy_title"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/proxy_type_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/proxy_title"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:text="@string/proxy_dialog_type"
|
||||||
|
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/proxy_type_selector"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/proxy_type_text"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/proxy_server_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/proxy_type_selector"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:text="@string/proxy_dialog_server"
|
||||||
|
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/proxy_address_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/proxy_server_text">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
|
android:id="@+id/proxy_addr"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="2"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/proxy_dialog_addr_hint"/>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
|
android:id="@+id/proxy_port"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/proxy_dialog_port_hint"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
|
android:id="@+id/proxy_username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/proxy_address_layout"
|
||||||
|
android:hint="@string/proxy_dialog_username_hint"
|
||||||
|
android:enabled="false"/>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
|
android:id="@+id/proxy_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/proxy_username"
|
||||||
|
android:hint="@string/proxy_dialog_password_hint"
|
||||||
|
android:enabled="false"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/proxy_cancel"
|
||||||
|
style="?borderlessButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@android:string/cancel"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/proxy_password"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/proxy_ok"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/proxy_ok"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/proxy_ok"
|
||||||
|
style="?borderlessButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@android:string/ok"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/proxy_password"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -25,10 +25,13 @@
|
|||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/container"
|
android:id="@+id/container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintHeight_max="2000dp"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:background="@drawable/reader_item_boundary">
|
android:background="@drawable/reader_item_boundary">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -61,9 +64,12 @@
|
|||||||
|
|
||||||
<com.github.chrisbanes.photoview.PhotoView
|
<com.github.chrisbanes.photoview.PhotoView
|
||||||
android:id="@+id/image"
|
android:id="@+id/image"
|
||||||
android:contentDescription="@string/reader_imageview_description"
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
android:paddingBottom="8dp"/>
|
android:paddingBottom="8dp"/>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item android:id="@+id/main_menu_thin"
|
||||||
|
android:title="@string/main_menu_thin"/>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/main_menu_sort"
|
android:id="@+id/main_menu_sort"
|
||||||
android:title="@string/main_menu_sort">
|
android:title="@string/main_menu_sort">
|
||||||
|
|||||||
26
app/src/main/res/values-ja/arrays.xml
Normal file
26
app/src/main/res/values-ja/arrays.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?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>
|
||||||
|
<string-array name="proxy_type">
|
||||||
|
<item>ダイレクト</item>
|
||||||
|
<item>HTTP</item>
|
||||||
|
<item>SOCKS</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
@@ -122,4 +122,14 @@
|
|||||||
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
|
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
|
||||||
<string name="settings_dl_location_custom">手動で設定</string>
|
<string name="settings_dl_location_custom">手動で設定</string>
|
||||||
<string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
|
<string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
|
||||||
|
<string name="settings_proxy_title">プロクシ</string>
|
||||||
|
<string name="proxy_dialog_username_hint">ID</string>
|
||||||
|
<string name="proxy_dialog_type">プロクシタイプ</string>
|
||||||
|
<string name="proxy_dialog_port_hint">ポート</string>
|
||||||
|
<string name="proxy_dialog_password_hint">パスワード</string>
|
||||||
|
<string name="proxy_dialog_error">エラー</string>
|
||||||
|
<string name="proxy_dialog_addr_hint">サーバーアドレス</string>
|
||||||
|
<string name="proxy_dialog_server">サーバー</string>
|
||||||
|
<string name="main_menu_thin">簡単モード</string>
|
||||||
|
<string name="main_fab_cancel">すべてのダウンロードキャンセル</string>
|
||||||
</resources>
|
</resources>
|
||||||
26
app/src/main/res/values-ko/arrays.xml
Normal file
26
app/src/main/res/values-ko/arrays.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?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>
|
||||||
|
<string-array name="proxy_type">
|
||||||
|
<item>다이렉트</item>
|
||||||
|
<item>HTTP</item>
|
||||||
|
<item>SOCKS</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
@@ -122,4 +122,14 @@
|
|||||||
<string name="settings_mirror_title">미러 설정</string>
|
<string name="settings_mirror_title">미러 설정</string>
|
||||||
<string name="settings_dl_location_custom">직접 설정</string>
|
<string name="settings_dl_location_custom">직접 설정</string>
|
||||||
<string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
|
<string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
|
||||||
|
<string name="settings_proxy_title">프록시</string>
|
||||||
|
<string name="proxy_dialog_username_hint">ID</string>
|
||||||
|
<string name="proxy_dialog_type">프록시 타입</string>
|
||||||
|
<string name="proxy_dialog_port_hint">포트</string>
|
||||||
|
<string name="proxy_dialog_password_hint">비밀번호</string>
|
||||||
|
<string name="proxy_dialog_error">잘못된 값</string>
|
||||||
|
<string name="proxy_dialog_addr_hint">서버 주소</string>
|
||||||
|
<string name="proxy_dialog_server">서버</string>
|
||||||
|
<string name="main_menu_thin">간단히 보기 모드</string>
|
||||||
|
<string name="main_fab_cancel">다운로드 모두 취소</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -62,4 +62,10 @@
|
|||||||
<item>HIYOBI|hiyobi.me</item>
|
<item>HIYOBI|hiyobi.me</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="proxy_type">
|
||||||
|
<item>Direct</item>
|
||||||
|
<item>HTTP</item>
|
||||||
|
<item>SOCKS</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -10,4 +10,7 @@
|
|||||||
<dimen name="nav_header_height">176dp</dimen>
|
<dimen name="nav_header_height">176dp</dimen>
|
||||||
|
|
||||||
<dimen name="thumbnail_margin">8dp</dimen>
|
<dimen name="thumbnail_margin">8dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="galleryblock_thumbnail_thin">100dp</dimen>
|
||||||
|
<dimen name="galleryblock_thumbnail_normal">150dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -53,6 +53,8 @@
|
|||||||
<string name="main_drawer_group_contact_email">Email me!</string>
|
<string name="main_drawer_group_contact_email">Email me!</string>
|
||||||
<string name="main_drawer_grouop_contact_discord">Discord</string>
|
<string name="main_drawer_grouop_contact_discord">Discord</string>
|
||||||
|
|
||||||
|
<string name="main_menu_thin">Toggle Thin Mode</string>
|
||||||
|
|
||||||
<string name="main_menu_sort">Sort</string>
|
<string name="main_menu_sort">Sort</string>
|
||||||
<string name="main_menu_sort_newest">Newest</string>
|
<string name="main_menu_sort_newest">Newest</string>
|
||||||
<string name="main_menu_sort_popular">Popular</string>
|
<string name="main_menu_sort_popular">Popular</string>
|
||||||
@@ -61,6 +63,7 @@
|
|||||||
<string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</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">Open Gallery by ID</string>
|
||||||
<string name="reader_failed_to_find_gallery">Failed to open gallery</string>
|
<string name="reader_failed_to_find_gallery">Failed to open gallery</string>
|
||||||
|
<string name="main_fab_cancel">Cancel all downloads</string>
|
||||||
|
|
||||||
<string name="main_move">Move to page %1$d</string>
|
<string name="main_move">Move to page %1$d</string>
|
||||||
|
|
||||||
@@ -154,6 +157,7 @@
|
|||||||
|
|
||||||
<string name="settings_miscellaneous_title">Miscellaneous</string>
|
<string name="settings_miscellaneous_title">Miscellaneous</string>
|
||||||
<string name="settings_mirror_summary">Load images from mirrors</string>
|
<string name="settings_mirror_summary">Load images from mirrors</string>
|
||||||
|
<string name="settings_proxy_title">Proxy</string>
|
||||||
<string name="settings_security_mode_title">Enable security mode</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_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_title">Dark mode</string>
|
||||||
@@ -189,4 +193,13 @@
|
|||||||
<string name="default_query_dialog_language_selector_none">Any</string>
|
<string name="default_query_dialog_language_selector_none">Any</string>
|
||||||
<string name="settings_mirror_title">Mirrors</string>
|
<string name="settings_mirror_title">Mirrors</string>
|
||||||
|
|
||||||
|
<!-- PROXY DIALOG -->
|
||||||
|
<string name="proxy_dialog_type">type</string>
|
||||||
|
<string name="proxy_dialog_addr_hint">address</string>
|
||||||
|
<string name="proxy_dialog_port_hint">port</string>
|
||||||
|
<string name="proxy_dialog_username_hint">username</string>
|
||||||
|
<string name="proxy_dialog_password_hint">password</string>
|
||||||
|
<string name="proxy_dialog_error">Wrong value</string>
|
||||||
|
<string name="proxy_dialog_server">server</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
app:title="@string/settings_dl_location"/>
|
app:title="@string/settings_dl_location"/>
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
app:key="dl_low_quality"
|
app:key="low_quality"
|
||||||
app:title="@string/settings_low_quality"
|
app:title="@string/settings_low_quality"
|
||||||
app:summary="@string/settings_low_quality_summary"/>
|
app:summary="@string/settings_low_quality_summary"/>
|
||||||
|
|
||||||
@@ -71,6 +71,10 @@
|
|||||||
app:title="@string/settings_mirror_title"
|
app:title="@string/settings_mirror_title"
|
||||||
app:summary="@string/settings_mirror_summary"/>
|
app:summary="@string/settings_mirror_summary"/>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:key="proxy"
|
||||||
|
app:title="@string/settings_proxy_title"/>
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
app:key="security_mode"
|
app:key="security_mode"
|
||||||
app:title="@string/settings_security_mode_title"
|
app:title="@string/settings_security_mode_title"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ buildscript {
|
|||||||
maven { url 'https://maven.fabric.io/public' }
|
maven { url 'https://maven.fabric.io/public' }
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
classpath 'com.android.tools.build:gradle:3.6.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
@@ -31,4 +31,4 @@ allprojects {
|
|||||||
|
|
||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
5555
dependencies.txt
Normal file
5555
dependencies.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,4 +14,4 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
|
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
#Fri Aug 23 08:21:15 KST 2019
|
#Tue Feb 25 09:55:23 KST 2020
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
|
|
||||||
package xyz.quaver
|
package xyz.quaver
|
||||||
|
|
||||||
|
import java.net.Proxy
|
||||||
|
|
||||||
|
var proxy = Proxy.NO_PROXY
|
||||||
|
|
||||||
fun availableInHiyobi(galleryID: Int) : Boolean {
|
fun availableInHiyobi(galleryID: Int) : Boolean {
|
||||||
return try {
|
return try {
|
||||||
xyz.quaver.hiyobi.getReader(galleryID)
|
xyz.quaver.hiyobi.getReader(galleryID)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
package xyz.quaver.hitomi
|
package xyz.quaver.hitomi
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.list
|
import xyz.quaver.proxy
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
const val protocol = "https:"
|
const val protocol = "https:"
|
||||||
@@ -25,10 +25,10 @@ const val protocol = "https:"
|
|||||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||||
fun getGalleryInfo(galleryID: Int) =
|
fun getGalleryInfo(galleryID: Int) =
|
||||||
Json.nonstrict.parse(
|
Json.nonstrict.parse(
|
||||||
GalleryInfo.serializer().list,
|
GalleryInfo.serializer(),
|
||||||
Regex("""\[.+]""").find(
|
URL("$protocol//$domain/galleries/$galleryID.js").openConnection(proxy).getInputStream().use {
|
||||||
URL("$protocol//$domain/galleries/$galleryID.js").readText()
|
it.reader().readText()
|
||||||
)?.value ?: "[]"
|
}.replace("var galleryinfo = ", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
//common.js
|
//common.js
|
||||||
@@ -68,6 +68,7 @@ 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? {
|
fun fullPathFromHash(hash: String?) : String? {
|
||||||
return when {
|
return when {
|
||||||
(hash?.length ?: 0) < 3 -> hash
|
(hash?.length ?: 0) < 3 -> hash
|
||||||
@@ -76,11 +77,20 @@ fun fullPathFromHash(hash: String?) : String? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("NAME_SHADOWING", "UNUSED_PARAMETER")
|
@Suppress("NAME_SHADOWING", "UNUSED_PARAMETER")
|
||||||
fun urlFromHash(galleryID: Int, image: GalleryInfo, dir: String? = null, ext: String? = null) : String {
|
fun urlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null) : String {
|
||||||
val ext = ext ?: dir ?: image.name.split('.').last()
|
val ext = ext ?: dir ?: image.name.split('.').last()
|
||||||
val dir = dir ?: "images"
|
val dir = dir ?: "images"
|
||||||
return "$protocol//a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext"
|
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) =
|
fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null) =
|
||||||
urlFromURL(urlFromHash(galleryID, image, dir, ext), base)
|
urlFromURL(urlFromHash(galleryID, image, dir, ext), base)
|
||||||
|
|
||||||
|
fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
|
||||||
|
val webp = if (image.hash != null && image.haswebp != 0 && !noWebp)
|
||||||
|
"webp"
|
||||||
|
else
|
||||||
|
null
|
||||||
|
|
||||||
|
return urlFromUrlFromHash(galleryID, image, webp)
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ package xyz.quaver.hitomi
|
|||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
import xyz.quaver.proxy
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -36,7 +37,7 @@ data class Gallery(
|
|||||||
val thumbnails: List<String>
|
val thumbnails: List<String>
|
||||||
)
|
)
|
||||||
fun getGallery(galleryID: Int) : Gallery {
|
fun getGallery(galleryID: Int) : Gallery {
|
||||||
val url = Jsoup.connect("https://hitomi.la/galleries/$galleryID.html").get()
|
val url = Jsoup.connect("https://hitomi.la/galleries/$galleryID.html").proxy(proxy).get()
|
||||||
.select("a").attr("href")
|
.select("a").attr("href")
|
||||||
|
|
||||||
val doc = Jsoup.connect(url).get()
|
val doc = Jsoup.connect(url).get()
|
||||||
@@ -70,7 +71,7 @@ fun getGallery(galleryID: Int) : Gallery {
|
|||||||
href.slice(5 until href.indexOf('-'))
|
href.slice(5 until href.indexOf('-'))
|
||||||
}
|
}
|
||||||
|
|
||||||
val thumbnails = getGalleryInfo(galleryID).map { galleryInfo ->
|
val thumbnails = getGalleryInfo(galleryID).files.map { galleryInfo ->
|
||||||
urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn")
|
urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package xyz.quaver.hitomi
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
|
import xyz.quaver.proxy
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
@@ -34,35 +35,31 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
|
|||||||
else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
|
else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
requestMethod = "GET"
|
||||||
requestMethod = "GET"
|
|
||||||
|
|
||||||
if (start != -1 && count != -1) {
|
if (start != -1 && count != -1) {
|
||||||
val startByte = start*4
|
val startByte = start*4
|
||||||
val endByte = (start+count)*4-1
|
val endByte = (start+count)*4-1
|
||||||
|
|
||||||
setRequestProperty("Range", "bytes=$startByte-$endByte")
|
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)
|
|
||||||
}
|
}
|
||||||
} 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,30 +79,26 @@ data class GalleryBlock(
|
|||||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
||||||
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
|
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
|
||||||
|
|
||||||
try {
|
val doc = Jsoup.connect(url).proxy(proxy).get()
|
||||||
val doc = Jsoup.connect(url).get()
|
|
||||||
|
|
||||||
val galleryUrl = doc.selectFirst(".lillie").attr("href")
|
val galleryUrl = doc.selectFirst(".lillie").attr("href")
|
||||||
|
|
||||||
val thumbnails = doc.select("img").map { protocol + it.attr("data-src") }
|
val thumbnails = doc.select("img").map { protocol + it.attr("data-src") }
|
||||||
|
|
||||||
val title = doc.selectFirst("h1.lillie > a").text()
|
val title = doc.selectFirst("h1.lillie > a").text()
|
||||||
val artists = doc.select("div.artist-list a").map{ it.text() }
|
val artists = doc.select("div.artist-list a").map{ it.text() }
|
||||||
val series = doc.select("a[href~=^/series/]").map { it.text() }
|
val series = doc.select("a[href~=^/series/]").map { it.text() }
|
||||||
val type = doc.selectFirst("a[href~=^/type/]").text()
|
val type = doc.selectFirst("a[href~=^/type/]").text()
|
||||||
|
|
||||||
val language = {
|
val language = {
|
||||||
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
|
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
|
||||||
href.slice(7 until href.indexOf("-1"))
|
href.slice(7 until href.indexOf("-1"))
|
||||||
}.invoke()
|
}.invoke()
|
||||||
|
|
||||||
val relatedTags = doc.select(".relatedtags a").map {
|
val relatedTags = doc.select(".relatedtags a").map {
|
||||||
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
||||||
href.slice(5 until href.indexOf("-all"))
|
href.slice(5 until href.indexOf("-all"))
|
||||||
}
|
|
||||||
|
|
||||||
return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||||
}
|
}
|
||||||
@@ -17,28 +17,35 @@
|
|||||||
package xyz.quaver.hitomi
|
package xyz.quaver.hitomi
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
|
|
||||||
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
|
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GalleryInfo(
|
data class GalleryInfo(
|
||||||
|
val language_localname: String? = null,
|
||||||
|
val language: String? = null,
|
||||||
|
val date: String? = null,
|
||||||
|
val files: List<GalleryFiles>,
|
||||||
|
val id: Int? = null,
|
||||||
|
val type: String? = null,
|
||||||
|
val title: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GalleryFiles(
|
||||||
val width: Int,
|
val width: Int,
|
||||||
val hash: String? = null,
|
val hash: String? = null,
|
||||||
val haswebp: Int = 0,
|
val haswebp: Int = 0,
|
||||||
val name: String,
|
val name: String,
|
||||||
val height: Int
|
val height: Int,
|
||||||
|
val hasavif: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Reader(val code: Code, val title: String, val galleryInfo: List<GalleryInfo>)
|
data class Reader(val code: Code, val galleryInfo: GalleryInfo)
|
||||||
|
|
||||||
//Set header `Referer` to reader url to avoid 403 error
|
//Set header `Referer` to reader url to avoid 403 error
|
||||||
fun getReader(galleryID: Int) : Reader {
|
fun getReader(galleryID: Int) : Reader {
|
||||||
val readerUrl = "https://hitomi.la/reader/$galleryID.html"
|
return Reader(Code.HITOMI, getGalleryInfo(galleryID))
|
||||||
|
|
||||||
val doc = Jsoup.connect(readerUrl).get()
|
|
||||||
|
|
||||||
return Reader(Code.HITOMI, doc.title(), getGalleryInfo(galleryID))
|
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package xyz.quaver.hitomi
|
package xyz.quaver.hitomi
|
||||||
|
|
||||||
|
import xyz.quaver.proxy
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
@@ -49,8 +50,9 @@ fun sanitize(input: String) : String {
|
|||||||
|
|
||||||
fun getIndexVersion(name: String) : String {
|
fun getIndexVersion(name: String) : String {
|
||||||
return try {
|
return try {
|
||||||
URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}")
|
URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").openConnection(proxy).getInputStream().use {
|
||||||
.readText()
|
it.reader().readText()
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
@@ -173,22 +175,20 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List
|
|||||||
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
val bytes = URL(nozomiAddress).openConnection(proxy).getInputStream().use {
|
||||||
val bytes = URL(nozomiAddress).readBytes()
|
it.readBytes()
|
||||||
|
|
||||||
val nozomi = ArrayList<Int>()
|
|
||||||
|
|
||||||
val arrayBuffer = ByteBuffer
|
|
||||||
.wrap(bytes)
|
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
|
||||||
|
|
||||||
while (arrayBuffer.hasRemaining())
|
|
||||||
nozomi.add(arrayBuffer.int)
|
|
||||||
|
|
||||||
return nozomi
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val nozomi = ArrayList<Int>()
|
||||||
|
|
||||||
|
val arrayBuffer = ByteBuffer
|
||||||
|
.wrap(bytes)
|
||||||
|
.order(ByteOrder.BIG_ENDIAN)
|
||||||
|
|
||||||
|
while (arrayBuffer.hasRemaining())
|
||||||
|
nozomi.add(arrayBuffer.int)
|
||||||
|
|
||||||
|
return nozomi
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> {
|
fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> {
|
||||||
@@ -242,7 +242,7 @@ fun getNodeAtAddress(field: String, address: Long) : Node? {
|
|||||||
|
|
||||||
fun getURLAtRange(url: String, range: LongRange) : ByteArray? {
|
fun getURLAtRange(url: String, range: LongRange) : ByteArray? {
|
||||||
try {
|
try {
|
||||||
with (URL(url).openConnection() as HttpsURLConnection) {
|
with (URL(url).openConnection(proxy) as HttpsURLConnection) {
|
||||||
requestMethod = "GET"
|
requestMethod = "GET"
|
||||||
|
|
||||||
setRequestProperty("Range", "bytes=${range.first}-${range.last}")
|
setRequestProperty("Range", "bytes=${range.first}-${range.last}")
|
||||||
|
|||||||
@@ -20,30 +20,27 @@ import org.jsoup.Jsoup
|
|||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.protocol
|
import xyz.quaver.hitomi.protocol
|
||||||
|
import xyz.quaver.proxy
|
||||||
|
|
||||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
||||||
val url = "$protocol//$hiyobi/search/$galleryID"
|
val url = "$protocol//$hiyobi/info/$galleryID"
|
||||||
|
|
||||||
try {
|
val doc = Jsoup.connect(url).proxy(proxy).get()
|
||||||
val doc = Jsoup.connect(url).get()
|
|
||||||
|
|
||||||
val galleryBlock = doc.selectFirst(".gallery-content")
|
val galleryBlock = doc.selectFirst(".gallery-content")
|
||||||
|
|
||||||
val galleryUrl = galleryBlock.selectFirst("a").attr("href")
|
val galleryUrl = galleryBlock.selectFirst("a").attr("href")
|
||||||
|
|
||||||
val thumbnails = listOf(galleryBlock.selectFirst("img").attr("abs:src"))
|
val thumbnails = listOf(galleryBlock.selectFirst("img").attr("abs:src"))
|
||||||
|
|
||||||
val title = galleryBlock.selectFirst("b").text()
|
val title = galleryBlock.selectFirst("b").text()
|
||||||
val artists = galleryBlock.select("tr:matches(작가) a[href~=artist]").map { it.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 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 type = galleryBlock.selectFirst("tr:matches(종류) a").attr("href").substringAfter("type:").replace('_', ' ')
|
||||||
|
|
||||||
val language = "korean"
|
val language = "korean"
|
||||||
|
|
||||||
val relatedTags = galleryBlock.select("tr:matches(태그) a").map { it.attr("href").substringAfterLast('/').replace('_', ' ') }
|
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)
|
return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||||
} catch (e: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -16,13 +16,16 @@
|
|||||||
|
|
||||||
package xyz.quaver.hiyobi
|
package xyz.quaver.hiyobi
|
||||||
|
|
||||||
|
import kotlinx.serialization.UnstableDefault
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.list
|
import kotlinx.serialization.list
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
|
import xyz.quaver.hitomi.GalleryFiles
|
||||||
import xyz.quaver.hitomi.GalleryInfo
|
import xyz.quaver.hitomi.GalleryInfo
|
||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.Reader
|
||||||
import xyz.quaver.hitomi.protocol
|
import xyz.quaver.hitomi.protocol
|
||||||
|
import xyz.quaver.proxy
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ fun renewCookie() : String {
|
|||||||
val url = "https://$hiyobi/"
|
val url = "https://$hiyobi/"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
with(URL(url).openConnection(proxy) as HttpsURLConnection) {
|
||||||
setRequestProperty("User-Agent", user_agent)
|
setRequestProperty("User-Agent", user_agent)
|
||||||
connectTimeout = 2000
|
connectTimeout = 2000
|
||||||
connect()
|
connect()
|
||||||
@@ -58,16 +61,16 @@ fun renewCookie() : String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseExperimental(UnstableDefault::class)
|
||||||
fun getReader(galleryID: Int) : Reader {
|
fun getReader(galleryID: Int) : Reader {
|
||||||
val reader = "https://$hiyobi/reader/$galleryID"
|
val reader = "https://$hiyobi/reader/$galleryID"
|
||||||
val url = "https://$hiyobi/data/json/${galleryID}_list.json"
|
val url = "https://cdn.hiyobi.me/data/json/${galleryID}_list.json"
|
||||||
|
|
||||||
val title = Jsoup.connect(reader).get().title()
|
val title = Jsoup.connect(reader).proxy(proxy).get().title()
|
||||||
|
|
||||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
val galleryFiles = Json.nonstrict.parse(
|
||||||
val galleryInfo = Json.parse(
|
GalleryFiles.serializer().list,
|
||||||
GalleryInfo.serializer().list,
|
with(URL(url).openConnection(proxy) as HttpsURLConnection) {
|
||||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
|
||||||
setRequestProperty("User-Agent", user_agent)
|
setRequestProperty("User-Agent", user_agent)
|
||||||
setRequestProperty("Cookie", cookie)
|
setRequestProperty("Cookie", cookie)
|
||||||
connectTimeout = 2000
|
connectTimeout = 2000
|
||||||
@@ -77,14 +80,14 @@ fun getReader(galleryID: Int) : Reader {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return Reader(Code.HIYOBI, title, galleryInfo)
|
return Reader(Code.HIYOBI, GalleryInfo(title = title, files = galleryFiles))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) =
|
fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) =
|
||||||
if (lowQuality)
|
if (lowQuality)
|
||||||
reader.galleryInfo.map {
|
reader.galleryInfo.files.map {
|
||||||
val name = it.name.replace(Regex("/.[^/.]+$"), "") + ".jpg"
|
val name = it.name.replace(Regex("""\.[^/.]+$"""), "")
|
||||||
Images("$protocol//$hiyobi/data/$galleryID/$name.jpg", galleryID, it.name)
|
Images("$protocol//$hiyobi/data_r/$galleryID/$name.jpg", galleryID, it.name)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
reader.galleryInfo.map { Images("$protocol//$hiyobi/data/$galleryID/${it.name}", galleryID, it.name) }
|
reader.galleryInfo.files.map { Images("$protocol//$hiyobi/data/$galleryID/${it.name}", galleryID, it.name) }
|
||||||
@@ -82,14 +82,14 @@ class UnitTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test_hiyobi() {
|
fun test_hiyobi() {
|
||||||
val reader = xyz.quaver.hiyobi.getReader(10000062)
|
val reader = xyz.quaver.hiyobi.getReader(1574736)
|
||||||
|
|
||||||
print(reader)
|
print(reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test_urlFromUrlFromHash() {
|
fun test_urlFromUrlFromHash() {
|
||||||
val url = urlFromUrlFromHash(1531795, GalleryInfo(
|
val url = urlFromUrlFromHash(1531795, GalleryFiles(
|
||||||
212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300
|
212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300
|
||||||
), "webp")
|
), "webp")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user