Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f408a91176 | ||
|
|
6f6956ce27 | ||
|
|
4ecad8eccc | ||
|
|
486fbe46a0 | ||
|
|
1ddb636dd0 | ||
|
|
081c890b4e | ||
|
|
86d528ba13 | ||
|
|
6bda3cb75a | ||
|
|
12d8949c9e | ||
|
|
ffc7c2aa67 | ||
|
|
5ec67488eb | ||
|
|
be64703d3c | ||
|
|
705925a050 | ||
|
|
29665be34d | ||
|
|
1edf986acf | ||
|
|
37be8ccf7f | ||
|
|
ead68b5201 | ||
|
|
4409664698 | ||
|
|
fc6bc7965c | ||
|
|
f70eccb1da | ||
|
|
861994e804 | ||
|
|
2b8facfb97 | ||
|
|
9583897ada | ||
|
|
7704c96955 | ||
|
|
c96d609803 | ||
|
|
aa0e5000ab | ||
|
|
7ca4418a50 | ||
|
|
fdd9b02388 | ||
|
|
ece127e982 | ||
|
|
5488e14f32 | ||
|
|
3558d826fb | ||
|
|
68c94d1d8b | ||
|
|
1a4ae5dfc6 | ||
|
|
1a95afe266 | ||
|
|
6579db3cc8 | ||
|
|
ceac01533a | ||
|
|
216914882c | ||
|
|
735dbab695 | ||
|
|
dbaab152ef | ||
|
|
9da1b30984 |
3
.gitignore
vendored
@@ -16,4 +16,5 @@
|
||||
/gh-pages
|
||||
|
||||
#Private files
|
||||
**/google-services.json
|
||||
**/google-services.json
|
||||
**/credentials.json
|
||||
16
.idea/codeStyles/Project.xml
generated
@@ -2,6 +2,22 @@
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="RIGHT_MARGIN" value="120" />
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="java.util" alias="false" withSubpackages="false" />
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
<package name="java" alias="false" withSubpackages="true" />
|
||||
<package name="javax" alias="false" withSubpackages="true" />
|
||||
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||
<package name="" alias="true" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
|
||||
6
.idea/copyright/Apache.xml
generated
@@ -1,6 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value=" Copyright &#36;today.year tom5079 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License." />
|
||||
<option name="myName" value="Apache" />
|
||||
</copyright>
|
||||
</component>
|
||||
1
.idea/copyright/profiles_settings.xml
generated
@@ -2,7 +2,6 @@
|
||||
<settings>
|
||||
<module2copyright>
|
||||
<element module="Pupil" copyright="GPL" />
|
||||
<element module="libpupil" copyright="Apache" />
|
||||
</module2copyright>
|
||||
</settings>
|
||||
</component>
|
||||
7
.idea/dictionaries/tom50.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="tom50">
|
||||
<words>
|
||||
<w>hitomi</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
1
.idea/gradle.xml
generated
@@ -11,7 +11,6 @@
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/libpupil" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
|
||||
5
.idea/jarRepositories.xml
generated
@@ -51,5 +51,10 @@
|
||||
<option name="name" value="maven4" />
|
||||
<option name="url" value="https://maven.fabric.io/public" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenLocal" />
|
||||
<option name="name" value="MavenLocal" />
|
||||
<option name="url" value="file:/$USER_HOME$/.m2/repository/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
3
.idea/scopes/libpupil.xml
generated
@@ -1,3 +0,0 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<scope name="libpupil" pattern="file[libpupil]:*/" />
|
||||
</component>
|
||||
@@ -20,26 +20,35 @@ android {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
versionCode 57
|
||||
versionName "4.21"
|
||||
versionName "5.0-beta4"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled true
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-DEBUG"
|
||||
|
||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
|
||||
ext.enableCrashlytics = false
|
||||
ext.alwaysUpdateBuildId = false
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
|
||||
}
|
||||
compileOptions {
|
||||
@@ -50,29 +59,31 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def markwonVersion = '3.1.0'
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC-HOTFIX1"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
||||
implementation 'com.google.firebase:firebase-core:17.4.4'
|
||||
implementation 'com.google.firebase:firebase-analytics:17.4.4'
|
||||
implementation 'com.google.firebase:firebase-crashlytics:17.1.1'
|
||||
implementation 'com.google.firebase:firebase-core:17.5.0'
|
||||
implementation 'com.google.firebase:firebase-analytics:17.5.0'
|
||||
implementation 'com.google.firebase:firebase-crashlytics:17.2.1'
|
||||
implementation 'com.google.firebase:firebase-perf:19.0.8'
|
||||
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
||||
implementation 'com.github.clans:fab:1.6.4'
|
||||
//noinspection GradleDependency
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
implementation "com.github.bumptech.glide:okhttp3-integration:4.11.0"
|
||||
implementation ("com.github.bumptech.glide:okhttp3-integration:4.11.0") {
|
||||
transitive = false
|
||||
}
|
||||
implementation 'com.github.bumptech.glide:annotations:4.11.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
@@ -84,15 +95,18 @@ dependencies {
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
||||
//implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
|
||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||
implementation "ru.noties.markwon:core:3.1.0"
|
||||
implementation ("xyz.quaver:libpupil:1.3") {
|
||||
exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-serialization-core-jvm'
|
||||
}
|
||||
implementation "xyz.quaver:documentfilex:0.2.14-alpha2"
|
||||
testImplementation 'junit:junit:4.13'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
implementation project(path: ':libpupil')
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
}
|
||||
15
app/proguard-rules.pro
vendored
@@ -35,4 +35,17 @@
|
||||
}
|
||||
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
|
||||
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
|
||||
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.SerializationKt
|
||||
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; } # <-- change package name to your app's
|
||||
-keepclassmembers class xyz.quaver.pupil.** { # <-- change package name to your app's
|
||||
*** Companion;
|
||||
}
|
||||
-keepclasseswithmembers class xyz.quaver.pupil.** { # <-- change package name to your app's
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
|
||||
-keep class xyz.quaver.pupil.util.Preferences
|
||||
@@ -12,7 +12,7 @@
|
||||
"filters": [],
|
||||
"properties": [],
|
||||
"versionCode": 57,
|
||||
"versionName": "4.21",
|
||||
"versionName": "5.0-beta4",
|
||||
"enabled": true,
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
||||
import xyz.quaver.hitomi.getSuggestionsForQuery
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hiyobi.createImgList
|
||||
import xyz.quaver.hiyobi.getReader
|
||||
@@ -121,4 +122,9 @@ class ExampleInstrumentedTest {
|
||||
|
||||
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_suggestion() {
|
||||
getSuggestionsForQuery("female:l")
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
|
||||
<application
|
||||
@@ -20,8 +20,8 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:replace="android:theme"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<provider
|
||||
@@ -36,9 +36,14 @@
|
||||
|
||||
</provider>
|
||||
|
||||
<receiver android:name=".BroadcastReciever" android:exported="true">
|
||||
<service android:name=".services.DownloadService"
|
||||
android:exported="false"/>
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.UpdateBroadcastReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
|
||||
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
@@ -204,7 +209,9 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.SettingsActivity"
|
||||
android:label="@string/settings_title" />
|
||||
android:label="@string/settings_title">
|
||||
<tools:validation testUrl="http://ix.io/eer" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
@@ -215,6 +222,17 @@
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:scheme="http"
|
||||
android:host="ix.io"
|
||||
android:pathPattern="/..*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
||||
</application>
|
||||
|
||||
@@ -18,61 +18,114 @@
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.app.Application
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import xyz.quaver.proxy
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import xyz.quaver.pupil.util.getProxy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.util.*
|
||||
import xyz.quaver.setClient
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class Pupil : MultiDexApplication() {
|
||||
typealias PupilInterceptor = (Interceptor.Chain) -> Response
|
||||
|
||||
lateinit var histories: Histories
|
||||
lateinit var favorites: Histories
|
||||
lateinit var histories: GalleryList
|
||||
private set
|
||||
lateinit var favorites: GalleryList
|
||||
private set
|
||||
|
||||
val interceptors = mutableMapOf<KClass<out Any>, PupilInterceptor>()
|
||||
|
||||
lateinit var clientBuilder: OkHttpClient.Builder
|
||||
|
||||
var clientHolder: OkHttpClient? = null
|
||||
val client: OkHttpClient
|
||||
get() = clientHolder ?: clientBuilder.build().also {
|
||||
clientHolder = it
|
||||
setClient(it)
|
||||
}
|
||||
|
||||
class Pupil : Application() {
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
val userID =
|
||||
if (preference.getString("user_id", "").isNullOrEmpty()) {
|
||||
UUID.randomUUID().toString().also {
|
||||
preference.edit().putString("user_id", it).apply()
|
||||
}
|
||||
} else
|
||||
preference.getString("user_id", "") ?: ""
|
||||
val userID = Preferences["user_id", ""].let { userID ->
|
||||
if (userID.isEmpty()) UUID.randomUUID().toString().also { Preferences["user_id"] = it }
|
||||
else userID
|
||||
}
|
||||
|
||||
FirebaseCrashlytics.getInstance().setUserId(userID)
|
||||
|
||||
proxy = getProxy(this)
|
||||
val proxyInfo = getProxyInfo()
|
||||
|
||||
clientBuilder = OkHttpClient.Builder()
|
||||
.connectTimeout(0, TimeUnit.SECONDS)
|
||||
.readTimeout(0, TimeUnit.SECONDS)
|
||||
.proxyInfo(proxyInfo)
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
val tag = request.tag() ?: return@addInterceptor chain.proceed(request)
|
||||
|
||||
interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request)
|
||||
}
|
||||
|
||||
try {
|
||||
preference.getString("dl_location", null).also {
|
||||
if (!File(it!!).canWrite())
|
||||
Preferences.get<String>("download_folder").also {
|
||||
if (Build.VERSION.SDK_INT > 19)
|
||||
contentResolver.takePersistableUriPermission(
|
||||
Uri.parse(it),
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
|
||||
if (!FileX(this, it).canWrite())
|
||||
throw Exception()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
preference.edit().remove("dl_location").apply()
|
||||
Preferences.remove("download_folder")
|
||||
}
|
||||
|
||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||
histories = GalleryList(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||
favorites = GalleryList(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||
|
||||
if (Preferences["new_history"]) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
histories.reversed().let {
|
||||
histories.clear()
|
||||
histories.addAll(it)
|
||||
}
|
||||
favorites.reversed().let {
|
||||
favorites.clear()
|
||||
favorites.addAll(it)
|
||||
}
|
||||
}
|
||||
Preferences["new_history"] = true
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG)
|
||||
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
||||
@@ -95,6 +148,13 @@ class Pupil : MultiDexApplication() {
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
})
|
||||
|
||||
manager.createNotificationChannel(NotificationChannel("downloader", getString(R.string.channel_downloader), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = getString(R.string.channel_downloader_description)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
})
|
||||
|
||||
manager.createNotificationChannel(NotificationChannel("update", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
|
||||
description = getString(R.string.channel_update_description)
|
||||
enableLights(true)
|
||||
@@ -102,7 +162,7 @@ class Pupil : MultiDexApplication() {
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
})
|
||||
|
||||
manager.createNotificationChannel(NotificationChannel("import", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
|
||||
manager.createNotificationChannel(NotificationChannel("import", getString(R.string.channel_update), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = getString(R.string.channel_update_description)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
@@ -110,8 +170,7 @@ class Pupil : MultiDexApplication() {
|
||||
})
|
||||
}
|
||||
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
|
||||
AppCompatDelegate.setDefaultNightMode(when (Preferences.get<Boolean>("dark_mode")) {
|
||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
})
|
||||
|
||||
@@ -25,7 +25,6 @@ import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import java.io.InputStream
|
||||
|
||||
@GlideModule
|
||||
@@ -35,7 +34,7 @@ class PupilGlideModule : AppGlideModule() {
|
||||
registry.append(
|
||||
GlideUrl::class.java,
|
||||
InputStream::class.java,
|
||||
OkHttpUrlLoader.Factory(DownloadWorker.getInstance(context).client)
|
||||
OkHttpUrlLoader.Factory(client)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Base64
|
||||
import android.util.SparseBooleanArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -31,13 +30,16 @@ import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.daimajia.swipe.SwipeLayout
|
||||
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
||||
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
|
||||
@@ -47,20 +49,22 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.getReader
|
||||
import xyz.quaver.io.util.getChild
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.ui.view.TagChip
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
||||
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
||||
|
||||
enum class ViewType {
|
||||
NEXT,
|
||||
@@ -68,8 +72,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
PREV
|
||||
}
|
||||
|
||||
private lateinit var favorites: Histories
|
||||
|
||||
val timer = Timer()
|
||||
|
||||
var isThin = false
|
||||
@@ -78,33 +80,43 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
var timerTask: TimerTask? = null
|
||||
|
||||
private fun updateProgress(context: Context, galleryID: Int) {
|
||||
val reader = Cache(context).getReaderOrNull(galleryID)
|
||||
val cache = Cache.getInstance(context, galleryID)
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (reader == null || PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false)) {
|
||||
if (cache.metadata.reader == null || Preferences["cache_disable"]) {
|
||||
view.galleryblock_progressbar.visibility = View.GONE
|
||||
view.galleryblock_progress_complete.visibility = View.GONE
|
||||
return@launch
|
||||
}
|
||||
|
||||
with(view.galleryblock_progressbar) {
|
||||
val imageList = cache.metadata.imageList!!
|
||||
|
||||
progress = Cache(context).getImages(galleryID)?.size ?: 0
|
||||
progress = imageList.filterNotNull().size
|
||||
max = imageList.size
|
||||
|
||||
if (visibility == View.GONE) {
|
||||
if (visibility == View.GONE)
|
||||
visibility = View.VISIBLE
|
||||
max = reader.galleryInfo.files.size
|
||||
}
|
||||
|
||||
if (progress == max) {
|
||||
val downloadManager = DownloadManager.getInstance(context)
|
||||
|
||||
if (completeFlag.get(galleryID, false)) {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageResource(R.drawable.ic_progressbar)
|
||||
setImageResource(
|
||||
if (downloadManager.getDownloadFolder(galleryID) != null)
|
||||
R.drawable.ic_progressbar
|
||||
else R.drawable.ic_progressbar_cache
|
||||
)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
|
||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context,
|
||||
if (downloadManager.getDownloadFolder(galleryID) != null)
|
||||
R.drawable.ic_progressbar_complete
|
||||
else R.drawable.ic_progressbar_complete_cache
|
||||
).apply {
|
||||
this?.start()
|
||||
})
|
||||
visibility = View.VISIBLE
|
||||
@@ -117,7 +129,11 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(galleryBlock: GalleryBlock) {
|
||||
fun bind(galleryID: Int) {
|
||||
val cache = Cache.getInstance(view.context, galleryID)
|
||||
|
||||
val galleryBlock = cache.metadata.galleryBlock ?: return
|
||||
|
||||
with(view) {
|
||||
val resources = context.resources
|
||||
val languages = resources.getStringArray(R.array.languages).map {
|
||||
@@ -133,53 +149,50 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
galleryblock_thumbnail.layoutParams.width = context.resources.getDimensionPixelSize(
|
||||
R.dimen.galleryblock_thumbnail_thin
|
||||
)
|
||||
|
||||
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
|
||||
it.start()
|
||||
})
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val thumbnail = Cache(context).getThumbnail(galleryBlock.id).let {
|
||||
if (it != null)
|
||||
Base64.decode(it, Base64.DEFAULT)
|
||||
else
|
||||
null
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val thumbnail = cache.getThumbnail()
|
||||
|
||||
galleryblock_thumbnail.post {
|
||||
glide
|
||||
.load(thumbnail)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
glide
|
||||
.load(thumbnail)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.listener(object: RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
Cache.getInstance(context, galleryID).let {
|
||||
it.cacheFolder.getChild(".thumbnail").let { if (it.exists()) it.delete() }
|
||||
it.downloadFolder?.getChild(".thumbnail")?.let { if (it.exists()) it.delete() }
|
||||
}
|
||||
return false
|
||||
}
|
||||
.into(galleryblock_thumbnail)
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean = false
|
||||
})
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
}.let { launch(Dispatchers.Main) { it.into(galleryblock_thumbnail) } }
|
||||
}
|
||||
|
||||
//Check cache
|
||||
val cache = Cache(context).getCachedGallery(galleryBlock.id)
|
||||
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
|
||||
|
||||
if (reader != null) {
|
||||
val count = cache.listFiles()?.count {
|
||||
Regex("^[0-9]+.+\$").matches(it.name)
|
||||
} ?: 0
|
||||
|
||||
with(galleryblock_progressbar) {
|
||||
max = reader.galleryInfo.files.size
|
||||
progress = count
|
||||
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else
|
||||
galleryblock_progressbar.visibility = View.GONE
|
||||
|
||||
if (timerTask == null)
|
||||
timerTask = timer.schedule(0, 1000) {
|
||||
updateProgress(context, galleryBlock.id)
|
||||
updateProgress(context, galleryID)
|
||||
}
|
||||
|
||||
galleryblock_title.text = galleryBlock.title
|
||||
@@ -212,36 +225,10 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
|
||||
galleryblock_tag_group.removeAllViews()
|
||||
galleryBlock.relatedTags.forEach {
|
||||
galleryblock_tag_group.addView(Chip(context).apply {
|
||||
val tag = Tag.parse(it).let { tag ->
|
||||
when {
|
||||
tag.area != null -> tag
|
||||
else -> Tag("tag", it)
|
||||
}
|
||||
}
|
||||
|
||||
chipIcon = when(tag.area) {
|
||||
"male" -> {
|
||||
setChipBackgroundColorResource(R.color.material_blue_700)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.gender_male)?.apply {
|
||||
colorFilter = PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
|
||||
}
|
||||
}
|
||||
"female" -> {
|
||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.gender_female)?.apply {
|
||||
colorFilter = PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
text = tag.tag.wordCapitalize()
|
||||
setEnsureMinTouchTargetSize(false)
|
||||
setOnClickListener {
|
||||
galleryblock_tag_group.addView(TagChip(context, Tag.parse(it)).apply {
|
||||
setOnClickListener { view ->
|
||||
for (callback in onChipClickedHandler)
|
||||
callback.invoke(tag)
|
||||
callback.invoke((view as TagChip).tag)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -257,9 +244,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
}
|
||||
}
|
||||
|
||||
if (!::favorites.isInitialized)
|
||||
favorites = (context.applicationContext as Pupil).favorites
|
||||
|
||||
with(galleryblock_favorite) {
|
||||
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
|
||||
setOnClickListener {
|
||||
@@ -343,9 +327,9 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is GalleryViewHolder) {
|
||||
val gallery = galleries[position-(if (showPrev) 1 else 0)]
|
||||
val galleryID = galleries[position-(if (showPrev) 1 else 0)]
|
||||
|
||||
holder.bind(gallery)
|
||||
holder.bind(galleryID)
|
||||
|
||||
with(holder.view.galleryblock_primary) {
|
||||
setOnClickListener {
|
||||
@@ -371,7 +355,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
mItemManger.closeAllExcept(layout)
|
||||
|
||||
holder.view.galleryblock_download.text =
|
||||
if (Cache(holder.view.context).isDownloading(gallery.id))
|
||||
if (DownloadManager.getInstance(holder.view.context).isDownloading(galleryID))
|
||||
holder.view.context.getString(android.R.string.cancel)
|
||||
else
|
||||
holder.view.context.getString(R.string.main_download)
|
||||
@@ -396,8 +380,8 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
||||
}
|
||||
|
||||
override fun getItemCount() =
|
||||
(if (galleries.isEmpty()) 0 else galleries.size)+
|
||||
(if (showNext) 1 else 0)+
|
||||
galleries.size +
|
||||
(if (showNext) 1 else 0) +
|
||||
(if (showPrev) 1 else 0)
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
|
||||
@@ -24,10 +24,10 @@ import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.item_mirrors.view.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import java.util.*
|
||||
|
||||
class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewHolder>() {
|
||||
@@ -41,8 +41,7 @@ class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewH
|
||||
}.toMap()
|
||||
|
||||
val list = mirrors.keys.toMutableList().apply {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString("mirrors", "")!!
|
||||
Preferences.get<String>("mirrors")
|
||||
.split(">")
|
||||
.reversed()
|
||||
.forEach {
|
||||
|
||||
@@ -18,17 +18,20 @@
|
||||
|
||||
package xyz.quaver.pupil.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import kotlinx.android.synthetic.main.item_reader.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -40,28 +43,30 @@ import xyz.quaver.hitomi.imageUrlFromImage
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hiyobi.createImgList
|
||||
import xyz.quaver.hiyobi.user_agent
|
||||
import xyz.quaver.io.util.readBytes
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ReaderAdapter(private val glide: RequestManager,
|
||||
private val galleryID: Int,
|
||||
private val activity: Activity) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||
class ReaderAdapter(private val activity: ReaderActivity,
|
||||
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||
|
||||
var reader: Reader? = null
|
||||
val timer = Timer()
|
||||
|
||||
private val glide = Glide.with(activity)
|
||||
|
||||
var isFullScreen = false
|
||||
|
||||
var onItemClickListener : ((Int) -> (Unit))? = null
|
||||
|
||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
||||
|
||||
var downloadWorker: DownloadWorker? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.item_reader, parent, false
|
||||
@@ -70,11 +75,12 @@ class ReaderAdapter(private val glide: RequestManager,
|
||||
}
|
||||
}
|
||||
|
||||
private var cache: Cache? = null
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.view as ConstraintLayout
|
||||
|
||||
if (downloadWorker == null)
|
||||
downloadWorker = DownloadWorker.getInstance(holder.view.context)
|
||||
if (cache == null)
|
||||
cache = Cache.getInstance(holder.view.context, galleryID)
|
||||
|
||||
if (isFullScreen) {
|
||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
||||
@@ -97,9 +103,8 @@ class ReaderAdapter(private val glide: RequestManager,
|
||||
|
||||
holder.view.reader_index.text = (position+1).toString()
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(holder.view.context)
|
||||
if (preferences.getBoolean("cache_disable", false)) {
|
||||
val lowQuality = preferences.getBoolean("low_quality", false)
|
||||
if (Preferences["cache_disable"]) {
|
||||
val lowQuality: Boolean = Preferences["low_quality"]
|
||||
|
||||
val url = when (reader!!.code) {
|
||||
Code.HITOMI ->
|
||||
@@ -127,22 +132,37 @@ class ReaderAdapter(private val glide: RequestManager,
|
||||
.into(holder.view.image)
|
||||
}
|
||||
} else {
|
||||
val image = Cache(holder.view.context).getImage(galleryID, position)
|
||||
val progress = downloadWorker!!.progress[galleryID]?.get(position)
|
||||
val image = cache!!.getImage(position)
|
||||
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
|
||||
|
||||
if (progress?.isInfinite() == true && image != null) {
|
||||
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
|
||||
|
||||
holder.view.image.post {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
glide
|
||||
.load(image)
|
||||
.load(image.readBytes())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.fitCenter()
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.into(holder.view.image)
|
||||
}
|
||||
.listener(object: RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
cache!!.metadata.imageList?.set(position, null)
|
||||
image.delete()
|
||||
DownloadService.cancel(holder.view.context, galleryID)
|
||||
DownloadService.download(holder.view.context, galleryID, true)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean) =
|
||||
false
|
||||
}).let { launch(Dispatchers.Main) { it.into(holder.view.image) } }
|
||||
}
|
||||
} else {
|
||||
holder.view.reader_item_progressbar.visibility = View.VISIBLE
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil
|
||||
package xyz.quaver.pupil.receiver
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.app.PendingIntent
|
||||
@@ -28,18 +28,11 @@ import android.webkit.MimeTypeMap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import xyz.quaver.pupil.util.NOTIFICATION_ID_UPDATE
|
||||
import xyz.quaver.pupil.util.cancelImport
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import java.io.File
|
||||
|
||||
class BroadcastReciever : BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
const val ACTION_CANCEL_IMPORT = "ACTION_CANCEL_IMPORT"
|
||||
|
||||
const val EXTRA_IMPORT_NOTIFICATION_ID = "EXTRA_IMPORT_NOTIFICATION_ID"
|
||||
}
|
||||
class UpdateBroadcastReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
context ?: return
|
||||
@@ -48,12 +41,10 @@ class BroadcastReciever : BroadcastReceiver() {
|
||||
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
||||
|
||||
// Validate download
|
||||
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val downloadID = preference.getLong("update_download_id", -1)
|
||||
val downloadID: Long = Preferences["update_download_id"]
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) != downloadID)
|
||||
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -2) != downloadID)
|
||||
return
|
||||
|
||||
// Get target uri
|
||||
@@ -62,19 +53,21 @@ class BroadcastReciever : BroadcastReceiver() {
|
||||
.setFilterById(downloadID)
|
||||
|
||||
val uri = downloadManager.query(query).use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
if (cursor.moveToFirst()) {
|
||||
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).let {
|
||||
val uri = Uri.parse(it)
|
||||
|
||||
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).let {
|
||||
val uri = Uri.parse(it)
|
||||
|
||||
when (uri.scheme) {
|
||||
"file" ->
|
||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!))
|
||||
"content" -> uri
|
||||
else -> return
|
||||
when (uri.scheme) {
|
||||
"file" ->
|
||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!)
|
||||
)
|
||||
"content" -> uri
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else
|
||||
null
|
||||
} ?: return
|
||||
|
||||
// Build Notification
|
||||
|
||||
@@ -92,10 +85,7 @@ class BroadcastReciever : BroadcastReceiver() {
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(NOTIFICATION_ID_UPDATE, notification)
|
||||
}
|
||||
ACTION_CANCEL_IMPORT -> {
|
||||
cancelImport = true
|
||||
notificationManager.notify(R.id.notification_id_update, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
405
app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
Normal file
@@ -0,0 +1,405 @@
|
||||
/*
|
||||
* 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.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.SparseArray
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okio.*
|
||||
import xyz.quaver.pupil.PupilInterceptor
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.interceptors
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.ellipsize
|
||||
import xyz.quaver.pupil.util.normalizeID
|
||||
import xyz.quaver.pupil.util.requestBuilders
|
||||
import xyz.quaver.pupil.util.startForegroundServiceCompat
|
||||
import java.io.IOException
|
||||
|
||||
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
|
||||
class DownloadService : Service() {
|
||||
data class Tag(val galleryID: Int, val index: Int, val startId: Int? = null)
|
||||
|
||||
//region Notification
|
||||
private val notificationManager by lazy {
|
||||
NotificationManagerCompat.from(this)
|
||||
}
|
||||
|
||||
private val serviceNotification by lazy {
|
||||
NotificationCompat.Builder(this, "downloader")
|
||||
.setContentTitle(getString(R.string.downloader_running))
|
||||
.setProgress(0, 0, false)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setOngoing(true)
|
||||
}
|
||||
|
||||
private val notification = SparseArray<NotificationCompat.Builder?>()
|
||||
|
||||
private fun initNotification(galleryID: Int) {
|
||||
val intent = Intent(this, ReaderActivity::class.java)
|
||||
.putExtra("galleryID", galleryID)
|
||||
|
||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||
addNextIntentWithParentStack(intent)
|
||||
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
val action =
|
||||
NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
||||
PendingIntent.getService(
|
||||
this,
|
||||
R.id.notification_download_cancel_action.normalizeID(),
|
||||
Intent(this, DownloadService::class.java)
|
||||
.putExtra(KEY_COMMAND, COMMAND_CANCEL)
|
||||
.putExtra(KEY_ID, galleryID),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT),
|
||||
).build()
|
||||
|
||||
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
|
||||
setContentTitle(getString(R.string.reader_loading))
|
||||
setContentText(getString(R.string.reader_notification_text))
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
setContentIntent(pendingIntent)
|
||||
addAction(action)
|
||||
setProgress(0, 0, true)
|
||||
setOngoing(true)
|
||||
})
|
||||
|
||||
notify(galleryID)
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun notify(galleryID: Int) {
|
||||
val max = progress[galleryID]?.size ?: 0
|
||||
val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0
|
||||
|
||||
val notification = notification[galleryID] ?: return
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
notification
|
||||
.setContentText(getString(R.string.reader_notification_complete))
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.mActions.clear()
|
||||
|
||||
notificationManager.cancel(galleryID)
|
||||
} else
|
||||
notification
|
||||
.setProgress(max, progress, false)
|
||||
.setContentText("$progress/$max")
|
||||
|
||||
if (DownloadManager.getInstance(this).getDownloadFolder(galleryID) != null)
|
||||
notification.let { notificationManager.notify(galleryID, it.build()) }
|
||||
else
|
||||
notificationManager.cancel(galleryID)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region ProgressListener
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private val progressListener: ProgressListener = { (galleryID, index), bytesRead, contentLength, done ->
|
||||
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
|
||||
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
|
||||
}
|
||||
|
||||
private class ProgressResponseBody(
|
||||
val tag: Any?,
|
||||
val responseBody: ResponseBody,
|
||||
val progressListener : ProgressListener
|
||||
) : ResponseBody() {
|
||||
private var bufferedSource : BufferedSource? = null
|
||||
|
||||
override fun contentLength() = responseBody.contentLength()
|
||||
override fun contentType() = responseBody.contentType()
|
||||
|
||||
override fun source(): BufferedSource {
|
||||
if (bufferedSource == null)
|
||||
bufferedSource = Okio.buffer(source(responseBody.source()))
|
||||
|
||||
return bufferedSource!!
|
||||
}
|
||||
|
||||
private fun source(source: Source) = object: ForwardingSource(source) {
|
||||
var totalBytesRead = 0L
|
||||
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val bytesRead = super.read(sink, byteCount)
|
||||
|
||||
totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
|
||||
progressListener.invoke(tag as Tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||
|
||||
return bytesRead
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val interceptor: PupilInterceptor = { chain ->
|
||||
val request = chain.request()
|
||||
var response = chain.proceed(request)
|
||||
|
||||
var retry = 5
|
||||
while (!response.isSuccessful && retry > 0) {
|
||||
response = chain.proceed(request)
|
||||
retry--
|
||||
}
|
||||
|
||||
response.newBuilder()
|
||||
.body(response.body()?.let {
|
||||
ProgressResponseBody(request.tag(), it, progressListener)
|
||||
}).build()
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Downloader
|
||||
/**
|
||||
* KEY
|
||||
* primary galleryID
|
||||
* secondary index
|
||||
* PRIMARY VALUE
|
||||
* MutableList -> Download in progress
|
||||
* null -> Loading / Gallery doesn't exist
|
||||
* SECONDARY VALUE
|
||||
* 0 <= value < 100 -> Download in progress
|
||||
* Float.POSITIVE_INFINITY -> Download completed
|
||||
*/
|
||||
val progress = SparseArray<MutableList<Float>?>()
|
||||
|
||||
fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
|
||||
|
||||
private val callback = object: Callback {
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
if (e.message?.contains("cancel", true) == false) {
|
||||
val galleryID = (call.request().tag() as Tag).galleryID
|
||||
|
||||
// Retry
|
||||
cancel(galleryID)
|
||||
download(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
val (galleryID, index, startId) = call.request().tag() as Tag
|
||||
val ext = call.request().url().encodedPath().split('.').last()
|
||||
|
||||
kotlin.runCatching {
|
||||
val image = response.body()?.use { it.bytes() } ?: throw Exception()
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
kotlin.runCatching {
|
||||
Cache.getInstance(this@DownloadService, galleryID).putImage(index, "$index.$ext", image)
|
||||
}.onSuccess {
|
||||
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
|
||||
notify(galleryID)
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
if (DownloadManager.getInstance(this@DownloadService)
|
||||
.getDownloadFolder(galleryID) != null)
|
||||
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
|
||||
|
||||
startId?.let { stopSelf(it) }
|
||||
}
|
||||
}.onFailure {
|
||||
cancel(galleryID)
|
||||
download(galleryID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel(startId: Int? = null) {
|
||||
client.dispatcher().queuedCalls().filter {
|
||||
it.request().tag() is Tag
|
||||
}.forEach {
|
||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||
it.cancel()
|
||||
}
|
||||
client.dispatcher().runningCalls().filter {
|
||||
it.request().tag() is Tag
|
||||
}.forEach {
|
||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||
it.cancel()
|
||||
}
|
||||
|
||||
progress.clear()
|
||||
notification.clear()
|
||||
notificationManager.cancelAll()
|
||||
|
||||
startId?.let { stopSelf(it) }
|
||||
}
|
||||
|
||||
fun cancel(galleryID: Int, startId: Int? = null) {
|
||||
client.dispatcher().queuedCalls().filter {
|
||||
(it.request().tag() as? Tag)?.galleryID == galleryID
|
||||
}.forEach {
|
||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||
it.cancel()
|
||||
}
|
||||
client.dispatcher().runningCalls().filter {
|
||||
(it.request().tag() as? Tag)?.galleryID == galleryID
|
||||
}.forEach {
|
||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||
it.cancel()
|
||||
}
|
||||
|
||||
progress.remove(galleryID)
|
||||
notification.remove(galleryID)
|
||||
notificationManager.cancel(galleryID)
|
||||
|
||||
startId?.let { stopSelf(it) }
|
||||
}
|
||||
|
||||
fun delete(galleryID: Int, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
|
||||
cancel(galleryID)
|
||||
DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
|
||||
Cache.delete(galleryID)
|
||||
|
||||
startId?.let { stopSelf(it) }
|
||||
}
|
||||
|
||||
fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
|
||||
if (progress.indexOfKey(galleryID) >= 0)
|
||||
cancel(galleryID)
|
||||
|
||||
val cache = Cache.getInstance(this@DownloadService, galleryID)
|
||||
|
||||
initNotification(galleryID)
|
||||
|
||||
val reader = cache.getReader()
|
||||
|
||||
// Gallery doesn't exist
|
||||
if (reader == null) {
|
||||
delete(galleryID)
|
||||
progress.put(galleryID, null)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (progress.indexOfKey(galleryID) < 0)
|
||||
progress.put(galleryID, MutableList(reader.galleryInfo.files.size) { 0F })
|
||||
|
||||
cache.metadata.imageList?.forEachIndexed { index, image ->
|
||||
progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F)
|
||||
}
|
||||
|
||||
notification[galleryID]?.setContentTitle(reader.galleryInfo.title?.ellipsize(30))
|
||||
notify(galleryID)
|
||||
|
||||
val queued = mutableSetOf<Int>()
|
||||
|
||||
if (priority) {
|
||||
client.dispatcher().queuedCalls().forEach {
|
||||
val queuedID = (it.request().tag() as? Tag)?.galleryID ?: return@forEach
|
||||
|
||||
if (queued.add(queuedID))
|
||||
cancel(queuedID)
|
||||
}
|
||||
}
|
||||
|
||||
reader.requestBuilders.forEachIndexed { index, it ->
|
||||
if (progress[galleryID]?.get(index)?.isInfinite() != true) {
|
||||
val request = it.tag(Tag(galleryID, index, startId)).build()
|
||||
client.newCall(request).enqueue(callback)
|
||||
}
|
||||
}
|
||||
|
||||
queued.forEach { download(it) }
|
||||
}
|
||||
//endregion
|
||||
|
||||
companion object {
|
||||
const val KEY_COMMAND = "COMMAND" // String
|
||||
const val KEY_ID = "ID" // Int
|
||||
const val KEY_PRIORITY = "PRIORITY" // Boolean
|
||||
|
||||
const val COMMAND_DOWNLOAD = "DOWNLOAD"
|
||||
const val COMMAND_CANCEL = "CANCEL"
|
||||
const val COMMAND_DELETE = "DELETE"
|
||||
|
||||
private fun command(context: Context, extras: Intent.() -> Unit) {
|
||||
context.startForegroundServiceCompat(Intent(context, DownloadService::class.java).apply(extras))
|
||||
}
|
||||
|
||||
fun download(context: Context, galleryID: Int, priority: Boolean = false) {
|
||||
command(context) {
|
||||
putExtra(KEY_COMMAND, COMMAND_DOWNLOAD)
|
||||
putExtra(KEY_PRIORITY, priority)
|
||||
putExtra(KEY_ID, galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel(context: Context, galleryID: Int? = null) {
|
||||
command(context) {
|
||||
putExtra(KEY_COMMAND, COMMAND_CANCEL)
|
||||
galleryID?.let { putExtra(KEY_ID, it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(context: Context, galleryID: Int) {
|
||||
command(context) {
|
||||
putExtra(KEY_COMMAND, COMMAND_DELETE)
|
||||
putExtra(KEY_ID, galleryID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.getStringExtra(KEY_COMMAND)) {
|
||||
COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0)
|
||||
download(it, intent.getBooleanExtra(KEY_PRIORITY, false), startId)
|
||||
}
|
||||
COMMAND_CANCEL -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) cancel(it, startId) else cancel(startId = startId) }
|
||||
COMMAND_DELETE -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) delete(it, startId) }
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
inner class Binder : android.os.Binder() {
|
||||
val service = this@DownloadService
|
||||
}
|
||||
|
||||
private val binder = Binder()
|
||||
override fun onBind(p0: Intent?) = binder
|
||||
|
||||
override fun onCreate() {
|
||||
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
||||
interceptors[Tag::class] = interceptor
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
interceptors.remove(Tag::class)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
package xyz.quaver.pupil.types
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
|
||||
@Serializable
|
||||
data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) {
|
||||
@@ -67,7 +68,7 @@ data class Tag(val area: String?, val tag: String, val isNegative: Boolean = fal
|
||||
}
|
||||
}
|
||||
|
||||
class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
||||
class Tags(val tags: MutableSet<Tag> = mutableSetOf()) : MutableSet<Tag> by tags {
|
||||
|
||||
companion object {
|
||||
fun parse(tags: String) : Tags {
|
||||
@@ -77,20 +78,13 @@ class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
||||
Tag.parse(it)
|
||||
else
|
||||
null
|
||||
}
|
||||
}.filterNotNull().toMutableSet()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
tag?.forEach {
|
||||
if (it != null)
|
||||
add(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(element: String): Boolean {
|
||||
forEach {
|
||||
tags.forEach {
|
||||
if (it.toString() == element)
|
||||
return true
|
||||
}
|
||||
@@ -99,23 +93,25 @@ class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
||||
}
|
||||
|
||||
fun add(element: String): Boolean {
|
||||
return super.add(Tag.parse(element))
|
||||
return tags.add(Tag.parse(element))
|
||||
}
|
||||
|
||||
fun remove(element: String) {
|
||||
filter { it.toString() == element }.forEach {
|
||||
remove(it)
|
||||
tags.filter { it.toString() == element }.forEach {
|
||||
tags.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeByArea(area: String, isNegative: Boolean? = null) {
|
||||
filter { it.area == area && (if(isNegative == null) true else (it.isNegative == isNegative)) }.forEach {
|
||||
remove(it)
|
||||
tags.filter { it.area == area && (if(isNegative == null) true else (it.isNegative == isNegative)) }.forEach {
|
||||
tags.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return joinToString(" ") { it.toString() }
|
||||
return tags.joinToString(" ") { it.toString() }
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.andrognito.patternlockview.PatternLockView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_lock.*
|
||||
@@ -38,6 +37,7 @@ import xyz.quaver.pupil.ui.fragment.PINLockFragment
|
||||
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
class LockActivity : AppCompatActivity() {
|
||||
|
||||
@@ -195,7 +195,7 @@ class LockActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (
|
||||
PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lock_fingerprint", false)
|
||||
Preferences["lock_fingerprint"]
|
||||
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
||||
) {
|
||||
lock_fingerprint.apply {
|
||||
|
||||
@@ -39,7 +39,6 @@ import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.arlib.floatingsearchview.FloatingSearchView
|
||||
import com.arlib.floatingsearchview.FloatingSearchViewDayNight
|
||||
@@ -47,25 +46,29 @@ import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import com.arlib.floatingsearchview.util.view.SearchInputView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.activity_main_content.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.builtins.list
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.quaver.hitomi.doSearch
|
||||
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
||||
import xyz.quaver.hitomi.getSuggestionsForQuery
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.histories
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.types.TagSuggestion
|
||||
import xyz.quaver.pupil.types.Tags
|
||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
|
||||
import xyz.quaver.pupil.ui.dialog.GalleryDialog
|
||||
import xyz.quaver.pupil.util.*
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
@@ -82,13 +85,13 @@ class MainActivity : AppCompatActivity() {
|
||||
DOWNLOAD,
|
||||
FAVORITE
|
||||
}
|
||||
|
||||
|
||||
enum class SortMode {
|
||||
NEWEST,
|
||||
POPULAR
|
||||
}
|
||||
|
||||
private val galleries = ArrayList<GalleryBlock>()
|
||||
private val galleries = ArrayList<Int>()
|
||||
|
||||
private var query = ""
|
||||
set(value) {
|
||||
@@ -103,16 +106,11 @@ class MainActivity : AppCompatActivity() {
|
||||
private var mode = Mode.SEARCH
|
||||
private var sortMode = SortMode.NEWEST
|
||||
|
||||
private val REQUEST_SETTINGS = 45162
|
||||
private val REQUEST_LOCK = 561
|
||||
|
||||
private var galleryIDs: Deferred<List<Int>>? = null
|
||||
private var totalItems = 0
|
||||
private var loadingJob: Job? = null
|
||||
private var currentPage = 0
|
||||
|
||||
private lateinit var histories: Histories
|
||||
private lateinit var favorites: Histories
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -132,29 +130,25 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (lockManager.isNotEmpty())
|
||||
startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK)
|
||||
startActivityForResult(Intent(this, LockActivity::class.java), R.id.request_lock.normalizeID())
|
||||
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
if (Locale.getDefault().language == "ko") {
|
||||
if (!preference.getBoolean("https_block_alert", false)) {
|
||||
android.app.AlertDialog.Builder(this).apply {
|
||||
setTitle(R.string.https_block_alert_title)
|
||||
setMessage(R.string.https_block_alert)
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
preference.edit().putBoolean("https_block_alert", true).apply()
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
intent.dataString?.let { url ->
|
||||
restore(favorites, url,
|
||||
onFailure = {
|
||||
Snackbar.make(this.main_recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
||||
}, onSuccess = {
|
||||
Snackbar.make(this.main_recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
with(application as Pupil) {
|
||||
this@MainActivity.histories = histories
|
||||
this@MainActivity.favorites = favorites
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
if (Preferences["download_folder", ""].isEmpty())
|
||||
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
|
||||
|
||||
checkUpdate(this)
|
||||
|
||||
initView()
|
||||
@@ -183,9 +177,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
if (preferences.getBoolean("security_mode", false))
|
||||
if (Preferences["security_mode"])
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
@@ -196,8 +188,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val perPage = preference.getString("per_page", "25")!!.toIntOrNull() ?: 25
|
||||
val perPage = Preferences["per_page", "25"].toInt()
|
||||
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
|
||||
|
||||
return when(keyCode) {
|
||||
@@ -234,9 +225,8 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
when(requestCode) {
|
||||
REQUEST_SETTINGS -> {
|
||||
R.id.request_settings.normalizeID() -> {
|
||||
runOnUiThread {
|
||||
cancelFetch()
|
||||
clearGalleries()
|
||||
@@ -244,10 +234,11 @@ class MainActivity : AppCompatActivity() {
|
||||
loadBlocks()
|
||||
}
|
||||
}
|
||||
REQUEST_LOCK -> {
|
||||
R.id.request_lock.normalizeID() -> {
|
||||
if (resultCode != Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,15 +330,14 @@ class MainActivity : AppCompatActivity() {
|
||||
with(main_fab_cancel) {
|
||||
setImageResource(R.drawable.cancel)
|
||||
setOnClickListener {
|
||||
DownloadWorker.getInstance(context).stop()
|
||||
DownloadService.cancel(this@MainActivity)
|
||||
}
|
||||
}
|
||||
|
||||
with(main_fab_jump) {
|
||||
setImageResource(R.drawable.ic_jump)
|
||||
setOnClickListener {
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val perPage = preference.getString("per_page", "25")!!.toIntOrNull() ?: 25
|
||||
val perPage = Preferences["per_page", "25"].toInt()
|
||||
val editText = EditText(context)
|
||||
|
||||
AlertDialog.Builder(context).apply {
|
||||
@@ -424,8 +414,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
|
||||
histories.add(galleryID)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
@@ -453,19 +441,16 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
onDownloadClickedHandler = { position ->
|
||||
val galleryID = galleries[position].id
|
||||
val worker = DownloadWorker.getInstance(context)
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
|
||||
val galleryID = galleries[position]
|
||||
if (Preferences["cache_disable"])
|
||||
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
|
||||
else {
|
||||
if (worker.progress.indexOfKey(galleryID) >= 0 && Cache(context).isDownloading(galleryID)) { //download in progress
|
||||
Cache(context).setDownloading(galleryID, false)
|
||||
worker.cancel(galleryID)
|
||||
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
|
||||
DownloadService.cancel(this@MainActivity, galleryID)
|
||||
}
|
||||
else {
|
||||
Cache(context).setDownloading(galleryID, true)
|
||||
|
||||
worker.queue.add(galleryID)
|
||||
DownloadManager.getInstance(context).addDownloadFolder(galleryID)
|
||||
DownloadService.download(this@MainActivity, galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,48 +458,41 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
onDeleteClickedHandler = { position ->
|
||||
val galleryID = galleries[position].id
|
||||
val galleryID = galleries[position]
|
||||
DownloadService.delete(this@MainActivity, galleryID)
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
DownloadWorker.getInstance(context).cancel(galleryID)
|
||||
histories.remove(galleryID)
|
||||
|
||||
Cache(context).getCachedGallery(galleryID).deleteRecursively()
|
||||
if (this@MainActivity.mode != Mode.SEARCH)
|
||||
runOnUiThread {
|
||||
cancelFetch()
|
||||
clearGalleries()
|
||||
fetchGalleries(query, sortMode)
|
||||
loadBlocks()
|
||||
}
|
||||
|
||||
histories.remove(galleryID)
|
||||
|
||||
if (this@MainActivity.mode != Mode.SEARCH)
|
||||
runOnUiThread {
|
||||
cancelFetch()
|
||||
clearGalleries()
|
||||
fetchGalleries(query, sortMode)
|
||||
loadBlocks()
|
||||
}
|
||||
|
||||
completeFlag.put(galleryID, false)
|
||||
}
|
||||
completeFlag.put(galleryID, false)
|
||||
|
||||
closeAllItems()
|
||||
}
|
||||
}
|
||||
ItemClickSupport.addTo(this)
|
||||
.setOnItemClickListener { _, position, v ->
|
||||
ItemClickSupport.addTo(this).apply {
|
||||
onItemClickListener = listener@{ _, position, v ->
|
||||
if (v !is CardView)
|
||||
return@setOnItemClickListener
|
||||
return@listener
|
||||
|
||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
||||
val gallery = galleries[position]
|
||||
intent.putExtra("galleryID", gallery.id)
|
||||
intent.putExtra("galleryID", galleries[position])
|
||||
|
||||
//TODO: Maybe sprinkling some transitions will be nice :D
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
histories.add(gallery.id)
|
||||
}.setOnItemLongClickListener { _, position, v ->
|
||||
|
||||
onItemLongClickListener = listener@{ _, position, v ->
|
||||
if (v !is CardView)
|
||||
return@setOnItemLongClickListener true
|
||||
return@listener false
|
||||
|
||||
val galleryID = galleries[position].id
|
||||
val galleryID = galleries[position]
|
||||
|
||||
GalleryDialog(
|
||||
this@MainActivity,
|
||||
@@ -537,11 +515,11 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
var origin = 0f
|
||||
var target = -1
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val perPage = preferences.getString("per_page", "25")!!.toInt()
|
||||
val perPage = Preferences["per_page", "25"].toInt()
|
||||
setOnTouchListener { _, event ->
|
||||
when(event.action) {
|
||||
MotionEvent.ACTION_UP -> {
|
||||
@@ -766,11 +744,10 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
with(main_searchview as FloatingSearchViewDayNight) {
|
||||
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
|
||||
val serializer = Tag.serializer().list
|
||||
|
||||
if (!favoritesFile.exists()) {
|
||||
favoritesFile.createNewFile()
|
||||
favoritesFile.writeText(json.stringify(serializer, Tags(listOf())))
|
||||
favoritesFile.writeText("[]")
|
||||
}
|
||||
|
||||
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
|
||||
@@ -785,7 +762,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
setOnMenuItemClickListener {
|
||||
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), R.id.request_settings.normalizeID())
|
||||
R.id.main_menu_thin -> {
|
||||
main_recyclerview.apply {
|
||||
(adapter as GalleryBlockAdapter).apply {
|
||||
@@ -832,7 +809,7 @@ class MainActivity : AppCompatActivity() {
|
||||
clearSuggestions()
|
||||
|
||||
if (query.isEmpty() or query.endsWith(' ')) {
|
||||
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map {
|
||||
swapSuggestions(Tags(Json.decodeFromString(favoritesFile.readText())).map {
|
||||
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
|
||||
})
|
||||
|
||||
@@ -842,11 +819,13 @@ class MainActivity : AppCompatActivity() {
|
||||
val currentQuery = query.split(" ").last().replace('_', ' ')
|
||||
|
||||
suggestionJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
val suggestions = ArrayList(getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) })
|
||||
val suggestions = kotlin.runCatching {
|
||||
getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }.toMutableList()
|
||||
}.getOrElse { mutableListOf() }
|
||||
|
||||
suggestions.filter {
|
||||
val tag = "${it.n}:${it.s.replace(Regex("\\s"), "_")}"
|
||||
Tags(json.parse(serializer, favoritesFile.readText())).contains(tag)
|
||||
Tags(Json.decodeFromString(favoritesFile.readText())).contains(tag)
|
||||
}.reversed().forEach {
|
||||
suggestions.remove(it)
|
||||
suggestions.add(0, it)
|
||||
@@ -884,7 +863,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
with(suggestionView.findViewById<ImageView>(R.id.right_icon)) {
|
||||
|
||||
if (Tags(json.parse(serializer, favoritesFile.readText())).contains(tag))
|
||||
if (Tags(Json.decodeFromString(favoritesFile.readText())).contains(tag))
|
||||
setImageResource(R.drawable.ic_star_filled)
|
||||
else
|
||||
setImageResource(R.drawable.ic_star_empty)
|
||||
@@ -895,7 +874,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
isClickable = true
|
||||
setOnClickListener {
|
||||
val favorites = Tags(json.parse(serializer, favoritesFile.readText()))
|
||||
val favorites = Tags(Json.decodeFromString(favoritesFile.readText()))
|
||||
|
||||
if (favorites.contains(tag)) {
|
||||
setImageResource(R.drawable.ic_star_empty)
|
||||
@@ -910,7 +889,7 @@ class MainActivity : AppCompatActivity() {
|
||||
favorites.add(tag)
|
||||
}
|
||||
|
||||
favoritesFile.writeText(json.stringify(serializer, favorites))
|
||||
favoritesFile.writeText(Json.encodeToString(favorites.tags))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,7 +929,7 @@ class MainActivity : AppCompatActivity() {
|
||||
setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener {
|
||||
override fun onFocus() {
|
||||
if (query.isEmpty() or query.endsWith(' '))
|
||||
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map {
|
||||
swapSuggestions(Tags(Json.decodeFromString(favoritesFile.readText())).map {
|
||||
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
|
||||
})
|
||||
}
|
||||
@@ -993,14 +972,27 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun fetchGalleries(query: String, sortMode: SortMode) {
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val defaultQuery = preference.getString("default_query", "")!!
|
||||
val defaultQuery: String = Preferences["default_query"]
|
||||
|
||||
if (query != queryStack.lastOrNull()) {
|
||||
queryStack.remove(query)
|
||||
queryStack.add(query)
|
||||
}
|
||||
|
||||
if (query.isNotEmpty() && mode != Mode.SEARCH) {
|
||||
Snackbar.make(this@MainActivity.main_recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply {
|
||||
setAction(android.R.string.yes) {
|
||||
cancelFetch()
|
||||
clearGalleries()
|
||||
currentPage = 0
|
||||
mode = Mode.SEARCH
|
||||
queryStack.clear()
|
||||
fetchGalleries(query, sortMode)
|
||||
loadBlocks()
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
galleryIDs = null
|
||||
|
||||
if (galleryIDs?.isActive == true)
|
||||
@@ -1026,29 +1018,23 @@ class MainActivity : AppCompatActivity() {
|
||||
Mode.HISTORY -> {
|
||||
when {
|
||||
query.isEmpty() -> {
|
||||
histories.toList().also {
|
||||
histories.reversed().also {
|
||||
totalItems = it.size
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val result = doSearch(query).sorted()
|
||||
histories.filter { result.binarySearch(it) >= 0 }.also {
|
||||
histories.reversed().filter { result.binarySearch(it) >= 0 }.also {
|
||||
totalItems = it.size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode.DOWNLOAD -> {
|
||||
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
|
||||
file.isDirectory && file.name.toIntOrNull() != null
|
||||
}?.sortedByDescending {
|
||||
it.lastModified()
|
||||
}?.map {
|
||||
it.name.toInt()
|
||||
} ?: emptyList()
|
||||
val downloads = DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList()
|
||||
|
||||
when {
|
||||
query.isEmpty() -> downloads.also {
|
||||
query.isEmpty() -> downloads.reversed().also {
|
||||
totalItems = it.size
|
||||
}
|
||||
else -> {
|
||||
@@ -1061,7 +1047,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
Mode.FAVORITE -> {
|
||||
when {
|
||||
query.isEmpty() -> favorites.toList().also {
|
||||
query.isEmpty() -> favorites.reversed().also {
|
||||
totalItems = it.size
|
||||
}
|
||||
else -> {
|
||||
@@ -1077,8 +1063,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun loadBlocks() {
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
|
||||
val perPage = Preferences["per_page", "25"].toInt()
|
||||
|
||||
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
val galleryIDs = try {
|
||||
@@ -1103,16 +1088,16 @@ class MainActivity : AppCompatActivity() {
|
||||
for (chunk in chunks)
|
||||
chunk.map { galleryID ->
|
||||
async {
|
||||
Cache(this@MainActivity).getGalleryBlock(galleryID)
|
||||
Cache.getInstance(this@MainActivity, galleryID).getGalleryBlock()?.let {
|
||||
galleryID
|
||||
}
|
||||
}
|
||||
}.forEach {
|
||||
val galleryBlock = it.await()
|
||||
it.await()?.also {
|
||||
withContext(Dispatchers.Main) {
|
||||
main_progressbar.hide()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
main_progressbar.hide()
|
||||
|
||||
if (galleryBlock != null) {
|
||||
galleries.add(galleryBlock)
|
||||
galleries.add(it)
|
||||
main_recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
@@ -33,7 +36,6 @@ import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.android.synthetic.main.activity_reader.*
|
||||
@@ -43,12 +45,14 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.histories
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.concurrent.timer
|
||||
@@ -71,6 +75,18 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var cache: Cache
|
||||
var downloader: DownloadService? = null
|
||||
private val conn = object: ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
downloader = (service as DownloadService.Binder).service
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
downloader = null
|
||||
}
|
||||
}
|
||||
|
||||
private val timer = Timer()
|
||||
private var autoTimer: Timer? = null
|
||||
|
||||
@@ -78,36 +94,29 @@ class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
private var menu: Menu? = null
|
||||
|
||||
private lateinit var favorites: Histories
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_reader)
|
||||
|
||||
title = getString(R.string.reader_loading)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||
|
||||
favorites = (application as Pupil).favorites
|
||||
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
|
||||
setContentView(R.layout.activity_reader)
|
||||
|
||||
handleIntent(intent)
|
||||
|
||||
cache = Cache.getInstance(this, galleryID)
|
||||
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
|
||||
|
||||
if (galleryID == 0) {
|
||||
onBackPressed()
|
||||
return
|
||||
}
|
||||
|
||||
initView()
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("cache_disable", false)) {
|
||||
if (Preferences["cache_disable"]) {
|
||||
reader_download_progressbar.visibility = View.GONE
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val reader = Cache(this@ReaderActivity).getReader(galleryID)
|
||||
val reader = cache.getReader()
|
||||
|
||||
launch(Dispatchers.Main) initDownloader@{
|
||||
if (reader == null) {
|
||||
@@ -117,6 +126,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
return@initDownloader
|
||||
}
|
||||
|
||||
histories.add(galleryID)
|
||||
(reader_recyclerview.adapter as ReaderAdapter).apply {
|
||||
this.reader = reader
|
||||
notifyDataSetChanged()
|
||||
@@ -134,6 +144,8 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
} else
|
||||
initDownloader()
|
||||
|
||||
initView()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
@@ -160,9 +172,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
if (preferences.getBoolean("security_mode", false))
|
||||
if (Preferences["security_mode"])
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
@@ -191,9 +201,9 @@ class ReaderActivity : AppCompatActivity() {
|
||||
R.id.reader_menu_page_indicator -> {
|
||||
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
|
||||
with(view.dialog_number_picker) {
|
||||
minValue=1
|
||||
maxValue=Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.size ?: 0
|
||||
value=currentPage
|
||||
minValue = 1
|
||||
maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0
|
||||
value = currentPage
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this).apply {
|
||||
setView(view)
|
||||
@@ -228,8 +238,11 @@ class ReaderActivity : AppCompatActivity() {
|
||||
timer.cancel()
|
||||
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
|
||||
|
||||
if (!Cache(this).isDownloading(galleryID))
|
||||
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)
|
||||
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
|
||||
DownloadService.cancel(this, galleryID)
|
||||
|
||||
if (downloader != null)
|
||||
unbindService(conn)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
@@ -265,32 +278,33 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun initDownloader() {
|
||||
val worker = DownloadWorker.getInstance(this).apply {
|
||||
cancel(galleryID)
|
||||
queue.add(galleryID)
|
||||
}
|
||||
DownloadService.download(this, galleryID, true)
|
||||
bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
|
||||
|
||||
timer.schedule(1000, 1000) {
|
||||
if (worker.progress.indexOfKey(galleryID) < 0) //loading
|
||||
val downloader = downloader ?: return@schedule
|
||||
|
||||
if (downloader.progress.indexOfKey(galleryID) < 0) //loading
|
||||
return@schedule
|
||||
|
||||
if (worker.progress[galleryID] == null) { //Gallery not found
|
||||
if (downloader.progress[galleryID] == null) { //Gallery not found
|
||||
timer.cancel()
|
||||
Snackbar
|
||||
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
||||
.show()
|
||||
}
|
||||
|
||||
histories.add(galleryID)
|
||||
|
||||
runOnUiThread {
|
||||
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||
reader_download_progressbar.progress = worker.progress[galleryID]?.count { it.isInfinite() } ?: 0
|
||||
reader_download_progressbar.progress = downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
|
||||
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||
|
||||
if (title == getString(R.string.reader_loading)) {
|
||||
val reader = Cache(this@ReaderActivity).getReaderOrNull(galleryID)
|
||||
val reader = cache.metadata.reader
|
||||
|
||||
if (reader != null) {
|
||||
|
||||
with (reader_recyclerview.adapter as ReaderAdapter) {
|
||||
this.reader = reader
|
||||
notifyDataSetChanged()
|
||||
@@ -308,7 +322,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
if (worker.progress[galleryID]?.all { it.isInfinite() } == true) { //Download finished
|
||||
if (downloader.isCompleted(galleryID)) { //Download finished
|
||||
reader_download_progressbar.visibility = View.GONE
|
||||
|
||||
animateDownloadFAB(false)
|
||||
@@ -319,7 +333,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
private fun initView() {
|
||||
with(reader_recyclerview) {
|
||||
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, this@ReaderActivity).apply {
|
||||
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
|
||||
onItemClickListener = {
|
||||
if (isScroll) {
|
||||
isScroll = false
|
||||
@@ -354,18 +368,19 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
with(reader_fab_download) {
|
||||
animateDownloadFAB(Cache(context).isDownloading(galleryID)) //If download in progress, animate button
|
||||
animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button
|
||||
|
||||
setOnClickListener {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("cache_disable", false))
|
||||
Toast.makeText(context, R.string.settings_download_when_cache_disable_warning, Toast.LENGTH_SHORT).show()
|
||||
else {
|
||||
if (Cache(context).isDownloading(galleryID)) {
|
||||
Cache(context).setDownloading(galleryID, false)
|
||||
val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
|
||||
|
||||
if (downloadManager.isDownloading(galleryID)) {
|
||||
downloadManager.deleteDownloadFolder(galleryID)
|
||||
animateDownloadFAB(false)
|
||||
} else {
|
||||
Cache(context).setDownloading(galleryID, true)
|
||||
downloadManager.addDownloadFolder(galleryID)
|
||||
animateDownloadFAB(true)
|
||||
}
|
||||
}
|
||||
@@ -375,10 +390,8 @@ class ReaderActivity : AppCompatActivity() {
|
||||
with(reader_fab_retry) {
|
||||
setImageResource(R.drawable.refresh)
|
||||
setOnClickListener {
|
||||
DownloadWorker.getInstance(context).let {
|
||||
it.cancel(galleryID)
|
||||
it.queue.add(galleryID)
|
||||
}
|
||||
downloader?.cancel(galleryID)
|
||||
downloader?.download(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +454,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this)
|
||||
} else {
|
||||
snapHelper.attachToRecyclerView(reader_recyclerview)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, Preferences["rtl", false])
|
||||
}
|
||||
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
||||
@@ -454,8 +467,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
|
||||
override fun onAnimationEnd(drawable: Drawable?) {
|
||||
val worker = DownloadWorker.getInstance(context)
|
||||
if (worker.progress[galleryID]?.all { it.isInfinite() } == true) // If download is finished, stop animating
|
||||
if (downloader?.isCompleted(galleryID) == true) // If download is finished, stop animating
|
||||
post {
|
||||
setImageResource(R.drawable.ic_download)
|
||||
labelText = getString(R.string.reader_fab_download_cancel)
|
||||
|
||||
@@ -27,14 +27,14 @@ import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.settings_activity.*
|
||||
import kotlinx.serialization.builtins.list
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.ui.fragment.LockSettingsFragment
|
||||
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
||||
import xyz.quaver.pupil.util.*
|
||||
@@ -59,9 +59,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
if (preferences.getBoolean("security_mode", false))
|
||||
if (Preferences["security_mode"])
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
@@ -80,7 +78,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when(requestCode) {
|
||||
REQUEST_LOCK -> {
|
||||
R.id.request_lock.normalizeID() -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
@@ -89,7 +87,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
REQUEST_RESTORE -> {
|
||||
R.id.request_restore.normalizeID() -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val uri = data?.data ?: return
|
||||
|
||||
@@ -100,10 +98,10 @@ class SettingsActivity : AppCompatActivity() {
|
||||
inputStream.readBytes().toString(Charset.defaultCharset())
|
||||
}
|
||||
|
||||
(application as Pupil).favorites.addAll(json.parse(Int.serializer().list, str).also {
|
||||
favorites.addAll(Json.decodeFromString<List<Int>>(str).also {
|
||||
Snackbar.make(
|
||||
window.decorView,
|
||||
getString(R.string.settings_restore_successful, it.size),
|
||||
getString(R.string.settings_restore_success, it.size),
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
})
|
||||
@@ -116,83 +114,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
REQUEST_DOWNLOAD_FOLDER -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
data?.data?.also { uri ->
|
||||
val takeFlags: Int =
|
||||
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
|
||||
val file = uri.toFile(this)
|
||||
|
||||
if (file?.canWrite() != true)
|
||||
Snackbar.make(
|
||||
settings,
|
||||
R.string.settings_dl_location_not_writable,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
else
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString("dl_location", file.canonicalPath)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
REQUEST_DOWNLOAD_FOLDER_OLD -> {
|
||||
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
||||
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
||||
|
||||
if (!File(directory).canWrite())
|
||||
Snackbar.make(
|
||||
settings,
|
||||
R.string.settings_dl_location_not_writable,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
else
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString("dl_location", File(directory).canonicalPath)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
REQUEST_IMPORT_OLD_GALLERIES -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
data?.data?.also { uri ->
|
||||
val takeFlags: Int =
|
||||
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
|
||||
val file = uri.toFile(this)
|
||||
|
||||
if (file?.canRead() != true)
|
||||
Snackbar.make(
|
||||
settings,
|
||||
resources.getText(R.string.import_old_galleries_folder_not_readable),
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
else
|
||||
importOldGalleries(this, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
REQUEST_IMPORT_OLD_GALLERIES_OLD -> {
|
||||
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
||||
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
||||
|
||||
if (!File(directory).canRead())
|
||||
Snackbar.make(
|
||||
settings,
|
||||
resources.getText(R.string.import_old_galleries_folder_not_readable),
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
else {
|
||||
importOldGalleries(this, File(directory))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
@@ -200,13 +121,13 @@ class SettingsActivity : AppCompatActivity() {
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
when (requestCode) {
|
||||
REQUEST_WRITE_PERMISSION_AND_SAF -> {
|
||||
R.id.request_write_permission_and_saf.normalizeID() -> {
|
||||
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||
}
|
||||
|
||||
startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
|
||||
startActivityForResult(intent, R.id.request_download_folder.normalizeID())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.android.synthetic.main.dialog_default_query.*
|
||||
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.types.Tags
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
|
||||
@@ -82,9 +82,8 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun build() : View {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val tags = Tags.parse(
|
||||
preferences.getString("default_query", "") ?: ""
|
||||
Preferences["default_query"]
|
||||
)
|
||||
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.formatDownloadFolder
|
||||
import xyz.quaver.pupil.util.formatDownloadFolderTest
|
||||
import xyz.quaver.pupil.util.formatMap
|
||||
|
||||
class DownloadFolderNameDialogFragment : DialogFragment() {
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun build(): View {
|
||||
val galleryID = Cache.instances.let { if (it.size() == 0) 1199708 else it.keyAt((0 until it.size()).random()) }
|
||||
val galleryBlock = runBlocking {
|
||||
Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
|
||||
}
|
||||
|
||||
return layoutInflater.inflate(R.layout.dialog_download_folder_name, null).apply {
|
||||
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
|
||||
edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
|
||||
edittext.addTextChangedListener {
|
||||
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
|
||||
}
|
||||
ok_button.setOnClickListener {
|
||||
val newValue = edittext.text.toString()
|
||||
|
||||
if ((newValue as? String)?.contains("/") != false) {
|
||||
Snackbar.make(this, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
Preferences["download_folder_name"] = edittext.text.toString()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||
Dialog(requireContext()).apply {
|
||||
setContentView(build())
|
||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RadioButton
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.android.synthetic.main.item_dl_location.view.*
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.io.File
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
||||
|
||||
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val buttons = mutableListOf<Pair<RadioButton, File?>>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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 externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
||||
|
||||
externalFilesDirs.forEachIndexed { index, dir ->
|
||||
|
||||
dir ?: return@forEachIndexed
|
||||
|
||||
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
|
||||
location_type.text = context.getString(when (index) {
|
||||
0 -> R.string.settings_dl_location_internal
|
||||
else -> R.string.settings_dl_location_removable
|
||||
})
|
||||
location_available.text = context.getString(
|
||||
R.string.settings_dl_location_available,
|
||||
byteToString(dir.freeSpace)
|
||||
)
|
||||
setOnClickListener {
|
||||
buttons.forEach { pair ->
|
||||
pair.first.isChecked = false
|
||||
}
|
||||
button.performClick()
|
||||
preference.edit().putString("dl_location", dir.canonicalPath).apply()
|
||||
}
|
||||
buttons.add(button to dir)
|
||||
})
|
||||
}
|
||||
|
||||
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
|
||||
location_type.text = context.getString(R.string.settings_dl_location_custom)
|
||||
setOnClickListener {
|
||||
buttons.forEach { pair ->
|
||||
pair.first.isChecked = false
|
||||
}
|
||||
button.performClick()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
|
||||
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_PERMISSION_AND_SAF)
|
||||
else {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||
}
|
||||
|
||||
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
|
||||
}
|
||||
|
||||
dismiss()
|
||||
} else { // Can't use SAF on old Androids!
|
||||
val config = DirectoryChooserConfig.builder()
|
||||
.newDirectoryName("Pupil")
|
||||
.allowNewDirectoryNameModification(true)
|
||||
.build()
|
||||
|
||||
val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
|
||||
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
||||
}
|
||||
|
||||
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER_OLD)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
buttons.add(button to null)
|
||||
})
|
||||
|
||||
externalFilesDirs.indexOfFirst {
|
||||
it.canonicalPath == getDownloadDirectory(context).canonicalPath
|
||||
}.let { index ->
|
||||
if (index < 0)
|
||||
buttons.first().first.isChecked = true
|
||||
else
|
||||
buttons[index].first.isChecked = true
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* 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.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.item_download_folder.view.*
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.byteToString
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.migrate
|
||||
import xyz.quaver.pupil.util.normalizeID
|
||||
import java.io.File
|
||||
|
||||
class DownloadLocationDialogFragment : DialogFragment() {
|
||||
private val entries = mutableMapOf<File?, View>()
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun build() : View? {
|
||||
val context = context ?: return null
|
||||
|
||||
val view = layoutInflater.inflate(R.layout.dialog_download_folder, null) as LinearLayout
|
||||
|
||||
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
||||
|
||||
externalFilesDirs.forEachIndexed { index, dir ->
|
||||
dir ?: return@forEachIndexed
|
||||
|
||||
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
|
||||
location_type.text = context.getString(when (index) {
|
||||
0 -> R.string.settings_download_folder_internal
|
||||
else -> R.string.settings_download_folder_removable
|
||||
})
|
||||
location_available.text = context.getString(
|
||||
R.string.settings_download_folder_available,
|
||||
byteToString(dir.freeSpace)
|
||||
)
|
||||
setOnClickListener {
|
||||
entries.values.forEach {
|
||||
it.button.isChecked = false
|
||||
}
|
||||
button.performClick()
|
||||
Preferences["download_folder"] = dir.toUri().toString()
|
||||
}
|
||||
entries[dir] = this
|
||||
})
|
||||
}
|
||||
|
||||
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
|
||||
location_type.text = context.getString(R.string.settings_download_folder_custom)
|
||||
setOnClickListener {
|
||||
entries.values.forEach {
|
||||
it.button.isChecked = false
|
||||
}
|
||||
button.performClick()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
startActivityForResult(intent, R.id.request_download_folder.normalizeID())
|
||||
} else { // Can't use SAF on old Androids!
|
||||
val config = DirectoryChooserConfig.builder()
|
||||
.newDirectoryName("Pupil")
|
||||
.allowNewDirectoryNameModification(true)
|
||||
.build()
|
||||
|
||||
val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
|
||||
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
||||
}
|
||||
|
||||
startActivityForResult(intent, R.id.request_download_folder_old.normalizeID())
|
||||
}
|
||||
}
|
||||
entries[null] = this
|
||||
})
|
||||
|
||||
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||
entries[key]!!.button.isChecked = true
|
||||
if (key == null) entries[key]!!.location_available.text = downloadFolder
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
|
||||
builder
|
||||
.setTitle(R.string.settings_download_folder)
|
||||
.setView(build())
|
||||
.setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
|
||||
if (Preferences["download_folder", ""].isEmpty())
|
||||
Preferences["download_folder"] = context?.getExternalFilesDir(null)?.canonicalPath ?: ""
|
||||
|
||||
DownloadManager.getInstance(requireContext()).migrate()
|
||||
}
|
||||
|
||||
isCancelable = false
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
R.id.request_download_folder.normalizeID() -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val activity = activity ?: return
|
||||
val context = context ?: return
|
||||
val dialog = dialog ?: return
|
||||
|
||||
data?.data?.also { uri ->
|
||||
val takeFlags: Int =
|
||||
activity.intent.flags and
|
||||
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
|
||||
if (kotlin.runCatching { FileX(context, uri).canWrite() }.getOrDefault(false))
|
||||
Preferences["download_folder"] = uri.toString()
|
||||
else {
|
||||
Snackbar.make(
|
||||
dialog.window!!.decorView.rootView,
|
||||
R.string.settings_download_folder_not_writable,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||
entries[key]!!.button.isChecked = true
|
||||
if (key == null) entries[key]!!.location_available.text = downloadFolder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
R.id.request_download_folder_old.normalizeID() -> {
|
||||
val context = context ?: return
|
||||
val dialog = dialog ?: return
|
||||
|
||||
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
||||
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
||||
|
||||
if (!File(directory).canWrite()) {
|
||||
Snackbar.make(
|
||||
dialog.window!!.decorView.rootView,
|
||||
R.string.settings_download_folder_not_writable,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||
entries[key]!!.button.isChecked = true
|
||||
if (key == null) entries[key]!!.location_available.text = downloadFolder
|
||||
}
|
||||
else
|
||||
Preferences["download_folder"] = File(directory).canonicalPath
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.dialog_gallery.*
|
||||
import kotlinx.android.synthetic.main.dialog_gallery_details.view.*
|
||||
@@ -38,30 +37,24 @@ import kotlinx.android.synthetic.main.dialog_gallery_dotindicator.view.*
|
||||
import kotlinx.android.synthetic.main.item_gallery_details.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import xyz.quaver.hitomi.Gallery
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.getGallery
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
||||
import xyz.quaver.pupil.histories
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.ui.view.TagChip
|
||||
import xyz.quaver.pupil.util.ItemClickSupport
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
|
||||
class GalleryDialog(context: Context, private val glide: RequestManager, private val galleryID: Int) : Dialog(context) {
|
||||
|
||||
private val languages = context.resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -81,7 +74,7 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryID", galleryID)
|
||||
})
|
||||
(context.applicationContext as Pupil).histories.add(galleryID)
|
||||
histories.add(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +123,7 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
||||
|
||||
private fun addDetails(gallery: Gallery) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
|
||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
||||
gallery_details.setText(R.string.gallery_details)
|
||||
|
||||
@@ -165,28 +158,7 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
||||
|
||||
content.forEach { tag ->
|
||||
gallery_details_tags.addView(
|
||||
Chip(context).apply {
|
||||
chipIcon = when(tag.area) {
|
||||
"male" -> {
|
||||
setChipBackgroundColorResource(R.color.material_blue_700)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.gender_male)
|
||||
}
|
||||
"female" -> {
|
||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.gender_female)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
text = when (tag.area) {
|
||||
"language" -> languages[tag.tag]
|
||||
else -> tag.tag.wordCapitalize()
|
||||
}
|
||||
|
||||
setEnsureMinTouchTargetSize(false)
|
||||
|
||||
TagChip(context, tag).apply {
|
||||
setOnClickListener {
|
||||
onChipClickedHandler.forEach { handler ->
|
||||
handler.invoke(tag)
|
||||
@@ -229,7 +201,7 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
||||
|
||||
private fun addRelated(gallery: Gallery) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val galleries = ArrayList<GalleryBlock>()
|
||||
val galleries = ArrayList<Int>()
|
||||
|
||||
val adapter = GalleryBlockAdapter(glide, galleries).apply {
|
||||
onChipClickedHandler.add { tag ->
|
||||
@@ -239,19 +211,6 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
||||
}
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
gallery.related.forEachIndexed { i, galleryID ->
|
||||
async(Dispatchers.IO) {
|
||||
Cache(context).getGalleryBlock(galleryID)
|
||||
}.let {
|
||||
val galleryBlock = it.await() ?: return@let
|
||||
|
||||
galleries.add(galleryBlock)
|
||||
adapter.notifyItemInserted(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
|
||||
gallery_details.setText(R.string.gallery_related)
|
||||
|
||||
@@ -259,18 +218,18 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
this.adapter = adapter
|
||||
|
||||
ItemClickSupport.addTo(this)
|
||||
.setOnItemClickListener { _, position, _ ->
|
||||
ItemClickSupport.addTo(this).apply {
|
||||
onItemClickListener = { _, position, _ ->
|
||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryID", galleries[position].id)
|
||||
putExtra("galleryID", galleries[position])
|
||||
})
|
||||
(context.applicationContext as Pupil).histories.add(galleries[position].id)
|
||||
histories.add(galleries[position])
|
||||
}
|
||||
.setOnItemLongClickListener { _, position, _ ->
|
||||
onItemLongClickListener = { _, position, _ ->
|
||||
GalleryDialog(
|
||||
context,
|
||||
glide,
|
||||
galleries[position].id
|
||||
galleries[position]
|
||||
).apply {
|
||||
onChipClickedHandler.add { tag ->
|
||||
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
||||
@@ -279,12 +238,25 @@ class GalleryDialog(context: Context, private val glide: RequestManager, private
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}.let {
|
||||
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
||||
}
|
||||
}.let {
|
||||
gallery_contents.addView(it)
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
gallery.related.forEach { galleryID ->
|
||||
Cache.getInstance(context, galleryID).getGalleryBlock()?.let {
|
||||
galleries.add(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,13 +24,13 @@ import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.MirrorAdapter
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
class MirrorDialog(context: Context) : AlertDialog(context) {
|
||||
|
||||
@@ -82,10 +82,7 @@ class MirrorDialog(context: Context) : AlertDialog(context) {
|
||||
}
|
||||
|
||||
onItemMoved = {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putString("mirrors", it.joinToString(">"))
|
||||
.apply()
|
||||
Preferences["mirrors"] = it.joinToString(">")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,23 +27,23 @@ 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 kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.clientBuilder
|
||||
import xyz.quaver.pupil.clientHolder
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.ProxyInfo
|
||||
import xyz.quaver.pupil.util.getProxyInfo
|
||||
import xyz.quaver.pupil.util.json
|
||||
import xyz.quaver.pupil.util.proxyInfo
|
||||
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)
|
||||
|
||||
setContentView(build())
|
||||
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -51,7 +51,7 @@ class ProxyDialog(context: Context) : Dialog(context) {
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun build() : View {
|
||||
val proxyInfo = getProxyInfo(context)
|
||||
val proxyInfo = getProxyInfo()
|
||||
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null)
|
||||
|
||||
@@ -116,12 +116,12 @@ class ProxyDialog(context: Context) : Dialog(context) {
|
||||
}
|
||||
|
||||
ProxyInfo(type, addr, port, username, password).let {
|
||||
Preferences["proxy"] = Json.encodeToString(it)
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putString("proxy",
|
||||
json.stringify(ProxyInfo.serializer(), it)
|
||||
).apply()
|
||||
|
||||
proxy = it.proxy()
|
||||
clientBuilder
|
||||
.proxyInfo(it)
|
||||
clientHolder = null
|
||||
client
|
||||
}
|
||||
|
||||
dismiss()
|
||||
|
||||
@@ -24,15 +24,14 @@ import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.LockActivity
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
class LockSettingsFragment :
|
||||
PreferenceFragmentCompat() {
|
||||
class LockSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
@@ -54,7 +53,7 @@ class LockSettingsFragment :
|
||||
if (lockManager.isEmpty()) {
|
||||
(findPreference<Preference>("lock_fingerprint") as SwitchPreferenceCompat).isChecked = false
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("lock_fingerprint", false).apply()
|
||||
Preferences["lock_fingerprint"] = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import okhttp3.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.util.restore
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class ManageFavoritesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey)
|
||||
|
||||
initPreferences()
|
||||
}
|
||||
|
||||
private fun initPreferences() {
|
||||
val context = context ?: return
|
||||
|
||||
findPreference<Preference>("backup")?.setOnPreferenceClickListener {
|
||||
val request = Request.Builder()
|
||||
.url(context.getString(R.string.backup_url))
|
||||
.post(
|
||||
FormBody.Builder()
|
||||
.add("f:1", File(ContextCompat.getDataDir(context), "favorites.json").readText())
|
||||
.build()
|
||||
).build()
|
||||
|
||||
client.newCall(request).enqueue(object: Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
val view = view ?: return
|
||||
Snackbar.make(view, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, response.body()?.use { it.string() }?.replace("\n", ""))
|
||||
}.let {
|
||||
getContext()?.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
true
|
||||
}
|
||||
findPreference<Preference>("restore")?.setOnPreferenceClickListener {
|
||||
val editText = EditText(context).apply {
|
||||
setText(context.getString(R.string.backup_url), TextView.BufferType.EDITABLE)
|
||||
}
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.settings_restore_title)
|
||||
.setView(editText)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
restore(favorites, editText.text.toString(),
|
||||
onFailure = onFailure@{
|
||||
val view = view ?: return@onFailure
|
||||
Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show()
|
||||
}, onSuccess = onSuccess@{
|
||||
val view = view ?: return@onSuccess
|
||||
Snackbar.make(view, context.getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
|
||||
})
|
||||
}.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// Do Nothing
|
||||
}.show()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.deleteRecursively
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.histories
|
||||
import xyz.quaver.pupil.util.byteToString
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import java.io.File
|
||||
|
||||
class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey)
|
||||
|
||||
initPreferences()
|
||||
}
|
||||
|
||||
override fun onPreferenceClick(preference: Preference?): Boolean {
|
||||
val context = context ?: return false
|
||||
|
||||
with(preference) {
|
||||
this ?: return false
|
||||
|
||||
when (key) {
|
||||
"delete_cache" -> {
|
||||
val dir = File(context.cacheDir, "imageCache")
|
||||
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_cache_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
if (dir.exists())
|
||||
dir.deleteRecursively()
|
||||
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
var size = 0L
|
||||
|
||||
dir.walk().forEach {
|
||||
size += it.length()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"delete_downloads" -> {
|
||||
val dir = DownloadManager.getInstance(context).downloadFolder
|
||||
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_downloads_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
job?.cancel()
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage_loading)
|
||||
}
|
||||
|
||||
if (dir.exists())
|
||||
dir.listFiles()?.forEach { (it as? FileX)?.deleteRecursively() }
|
||||
|
||||
job = launch {
|
||||
var size = 0L
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
||||
}
|
||||
dir.walk().forEach {
|
||||
size += it.length()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"clear_history" -> {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_history_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
histories.clear()
|
||||
summary = context.getString(R.string.settings_clear_history_summary, histories.size)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun initPreferences() {
|
||||
val context = context ?: return
|
||||
|
||||
with(findPreference<Preference>("delete_cache")) {
|
||||
this ?: return@with
|
||||
|
||||
val dir = File(context.cacheDir, "imageCache")
|
||||
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
var size = 0L
|
||||
|
||||
dir.walk().forEach {
|
||||
size += it.length()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPreferenceClickListener = this@ManageStorageFragment
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("delete_downloads")) {
|
||||
this ?: return@with
|
||||
|
||||
val dir = DownloadManager.getInstance(context).downloadFolder
|
||||
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
||||
job?.cancel()
|
||||
job = CoroutineScope(Dispatchers.IO).launch {
|
||||
var size = 0L
|
||||
|
||||
dir.walk().forEach {
|
||||
launch(Dispatchers.Main) {
|
||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
||||
}
|
||||
|
||||
size += it.length()
|
||||
}
|
||||
}
|
||||
|
||||
onPreferenceClickListener = this@ManageStorageFragment
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("clear_history")) {
|
||||
this ?: return@with
|
||||
|
||||
summary = context.getString(R.string.settings_clear_history_summary, histories.size)
|
||||
|
||||
onPreferenceClickListener = this@ManageStorageFragment
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
job?.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,39 +18,22 @@
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.getChild
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.LockActivity
|
||||
import xyz.quaver.pupil.ui.SettingsActivity
|
||||
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
|
||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
|
||||
import xyz.quaver.pupil.ui.dialog.MirrorDialog
|
||||
import xyz.quaver.pupil.ui.dialog.ProxyDialog
|
||||
import xyz.quaver.pupil.ui.dialog.*
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
|
||||
class SettingsFragment :
|
||||
PreferenceFragmentCompat(),
|
||||
@@ -58,8 +41,6 @@ class SettingsFragment :
|
||||
Preference.OnPreferenceChangeListener,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
lateinit var sharedPreference: SharedPreferences
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
@@ -78,19 +59,6 @@ class SettingsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDirSize(dir: File) : String {
|
||||
return if (activity != null)
|
||||
getString(R.string.settings_storage_usage,
|
||||
Runtime.getRuntime().exec("du -hs " + dir.absolutePath).let {
|
||||
BufferedReader(InputStreamReader(it.inputStream)).use { reader ->
|
||||
reader.readLine()?.split('\t')?.firstOrNull() ?: "0"
|
||||
}
|
||||
}
|
||||
)
|
||||
else
|
||||
""
|
||||
}
|
||||
|
||||
override fun onPreferenceClick(preference: Preference?): Boolean {
|
||||
with (preference) {
|
||||
this ?: return false
|
||||
@@ -99,79 +67,20 @@ class SettingsFragment :
|
||||
"app_version" -> {
|
||||
checkUpdate(activity as SettingsActivity, true)
|
||||
}
|
||||
"delete_cache" -> {
|
||||
val dir = File(requireContext().cacheDir, "imageCache")
|
||||
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_cache_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
if (dir.exists())
|
||||
dir.deleteRecursively()
|
||||
|
||||
summary = getString(R.string.settings_storage_usage_loading)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
getDirSize(dir).let {
|
||||
launch(Dispatchers.Main) {
|
||||
this@with.summary = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"delete_downloads" -> {
|
||||
val dir = getDownloadDirectory(requireContext())
|
||||
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_downloads_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
if (dir.exists())
|
||||
dir.deleteRecursively()
|
||||
|
||||
summary = getString(R.string.settings_storage_usage_loading)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
getDirSize(dir).let {
|
||||
launch(Dispatchers.Main) {
|
||||
this@with.summary = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"clear_history" -> {
|
||||
val histories = (requireContext().applicationContext as Pupil).histories
|
||||
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_history_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
histories.clear()
|
||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
"dl_location" -> {
|
||||
DownloadLocationDialog(requireActivity()).show()
|
||||
"download_folder" -> {
|
||||
DownloadLocationDialogFragment().show(requireActivity().supportFragmentManager, "Download Location Dialog")
|
||||
}
|
||||
"default_query" -> {
|
||||
DefaultQueryDialog(requireContext()).apply {
|
||||
onPositiveButtonClickListener = { newTags ->
|
||||
sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
|
||||
Preferences["default_query"] = newTags.toString()
|
||||
summary = newTags.toString()
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
"app_lock" -> {
|
||||
val intent = Intent(requireContext(), LockActivity::class.java)
|
||||
activity?.startActivityForResult(intent, REQUEST_LOCK)
|
||||
activity?.startActivityForResult(intent, R.id.request_lock.normalizeID())
|
||||
}
|
||||
"mirrors" -> {
|
||||
MirrorDialog(requireContext())
|
||||
@@ -181,54 +90,9 @@ class SettingsFragment :
|
||||
ProxyDialog(requireContext())
|
||||
.show()
|
||||
}
|
||||
"nomedia" -> {
|
||||
File(getDownloadDirectory(context), ".nomedia").createNewFile()
|
||||
}
|
||||
"backup" -> {
|
||||
File(ContextCompat.getDataDir(requireContext()), "favorites.json").copyTo(
|
||||
File(getDownloadDirectory(requireContext()), "favorites.json"),
|
||||
true
|
||||
)
|
||||
|
||||
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
"restore" -> {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
|
||||
activity?.startActivityForResult(intent, REQUEST_RESTORE)
|
||||
}
|
||||
"old_import_galleries" -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
|
||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
|
||||
ActivityCompat.requestPermissions(requireActivity(), 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_IMPORT_OLD_GALLERIES)
|
||||
}
|
||||
} else { // Can't use SAF on old Androids!
|
||||
val config = DirectoryChooserConfig.builder()
|
||||
.newDirectoryName("Pupil")
|
||||
.allowNewDirectoryNameModification(true)
|
||||
.build()
|
||||
|
||||
val intent = Intent(requireContext(), DirectoryChooserActivity::class.java).apply {
|
||||
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
||||
}
|
||||
|
||||
activity?.startActivityForResult(intent, REQUEST_IMPORT_OLD_GALLERIES_OLD)
|
||||
}
|
||||
}
|
||||
"user_id" -> {
|
||||
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("user_id", sharedPreference.getString("user_id", ""))
|
||||
ClipData.newPlainText("user_id", Preferences.get<String>("user_id"))
|
||||
)
|
||||
Toast.makeText(context, R.string.settings_user_id_toast, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -244,6 +108,18 @@ class SettingsFragment :
|
||||
this ?: return false
|
||||
|
||||
when (key) {
|
||||
"nomedia" -> {
|
||||
val create = (newValue as? Boolean) ?: return false
|
||||
|
||||
return kotlin.runCatching {
|
||||
val nomedia = DownloadManager.getInstance(context).downloadFolder.getChild(".nomedia")
|
||||
|
||||
if (create)
|
||||
nomedia.createNewFile()
|
||||
else
|
||||
nomedia.delete()
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
"dark_mode" -> {
|
||||
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
|
||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
@@ -265,10 +141,13 @@ class SettingsFragment :
|
||||
|
||||
when (key) {
|
||||
"proxy" -> {
|
||||
summary = getProxyInfo(requireContext()).type.name
|
||||
summary = context?.let { getProxyInfo().type.name }
|
||||
}
|
||||
"dl_location" -> {
|
||||
summary = getDownloadDirectory(requireContext()).canonicalPath
|
||||
"download_folder" -> {
|
||||
summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath
|
||||
}
|
||||
"download_folder_name" -> {
|
||||
summary = Preferences["download_folder_name", "[-id-] -title-"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,12 +156,16 @@ class SettingsFragment :
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
||||
|
||||
sharedPreference = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
sharedPreference.registerOnSharedPreferenceChangeListener(this)
|
||||
Preferences.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
initPreferences()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Preferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun initPreferences() {
|
||||
for (i in 0 until preferenceScreen.preferenceCount) {
|
||||
|
||||
@@ -292,7 +175,7 @@ class SettingsFragment :
|
||||
else
|
||||
listOf(this)
|
||||
}.forEach { preference ->
|
||||
with (preference) {
|
||||
with (preference) with@{
|
||||
|
||||
when (key) {
|
||||
"app_version" -> {
|
||||
@@ -302,47 +185,29 @@ class SettingsFragment :
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"delete_cache" -> {
|
||||
val dir = File(requireContext().cacheDir, "imageCache")
|
||||
"download_folder_name" -> {
|
||||
summary = Preferences["download_folder_name", "[-id-] -title-"]
|
||||
|
||||
summary = getString(R.string.settings_storage_usage_loading)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
getDirSize(dir).let {
|
||||
launch(Dispatchers.Main) {
|
||||
this@with.summary = it
|
||||
}
|
||||
}
|
||||
setOnPreferenceClickListener {
|
||||
DownloadFolderNameDialogFragment().show(requireActivity().supportFragmentManager, "Download Location Dialog")
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
"download_folder" -> {
|
||||
summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"delete_downloads" -> {
|
||||
val dir = getDownloadDirectory(requireContext())
|
||||
"nomedia" -> {
|
||||
(this as SwitchPreferenceCompat).isChecked = kotlin.runCatching {
|
||||
DownloadManager.getInstance(context).downloadFolder.getChild(".nomedia").exists()
|
||||
}.getOrDefault(false)
|
||||
|
||||
summary = getString(R.string.settings_storage_usage_loading)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
getDirSize(dir).let {
|
||||
launch(Dispatchers.Main) {
|
||||
this@with.summary = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"clear_history" -> {
|
||||
val histories = (requireActivity().application as Pupil).histories
|
||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"dl_location" -> {
|
||||
summary = getDownloadDirectory(requireContext()).canonicalPath
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
onPreferenceChangeListener = this@SettingsFragment
|
||||
}
|
||||
"default_query" -> {
|
||||
summary = sharedPreference.getString("default_query", "") ?: ""
|
||||
summary = Preferences.get<String>("default_query")
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
@@ -367,27 +232,18 @@ class SettingsFragment :
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"proxy" -> {
|
||||
summary = getProxyInfo(requireContext()).type.name
|
||||
summary = getProxyInfo().type.name
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"dark_mode" -> {
|
||||
onPreferenceChangeListener = this@SettingsFragment
|
||||
}
|
||||
"nomedia" -> {
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"backup" -> {
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"restore" -> {
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"old_import_galleries" -> {
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"user_id" -> {
|
||||
summary = sharedPreference.getString("user_id", "")
|
||||
summary = Preferences.get<String>("user_id")
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
}
|
||||
|
||||
66
app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.view
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.chip.Chip
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
|
||||
class TagChip(context: Context, val tag: Tag) : Chip(context) {
|
||||
|
||||
private val languages = context.resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
init {
|
||||
val tag = tag.let {
|
||||
when {
|
||||
it.area != null -> it
|
||||
else -> Tag("tag", tag.tag)
|
||||
}
|
||||
}
|
||||
|
||||
chipIcon = when(tag.area) {
|
||||
"male" -> {
|
||||
setChipBackgroundColorResource(R.color.material_blue_700)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.gender_male_white)
|
||||
}
|
||||
"female" -> {
|
||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.gender_female_white)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
text = when (tag.area) {
|
||||
"language" -> languages[tag.tag]
|
||||
else -> tag.tag.wordCapitalize()
|
||||
}
|
||||
|
||||
setEnsureMinTouchTargetSize(false)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +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
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import okhttp3.Dispatcher
|
||||
import okhttp3.OkHttpClient
|
||||
import xyz.quaver.proxy
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
const val REQUEST_LOCK = 38238
|
||||
const val REQUEST_RESTORE = 16546
|
||||
const val REQUEST_IMPORT_OLD_GALLERIES = 6458
|
||||
const val REQUEST_IMPORT_OLD_GALLERIES_OLD = 5946
|
||||
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
||||
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
||||
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
|
||||
|
||||
const val NOTIFICATION_ID_UPDATE = 2345
|
||||
|
||||
val json = Json(JsonConfiguration.Stable)
|
||||
@@ -18,79 +18,63 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.list
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
class Histories(private val file: File) : ArrayList<Int>() {
|
||||
|
||||
val serializer: KSerializer<List<Int>> = Int.serializer().list
|
||||
class GalleryList(private val file: File, private val list: MutableSet<Int> = mutableSetOf()) : MutableSet<Int> by list {
|
||||
|
||||
init {
|
||||
if (!file.exists())
|
||||
if (!file.exists()) {
|
||||
file.parentFile?.mkdirs()
|
||||
|
||||
try {
|
||||
load()
|
||||
} catch (e: Exception) {
|
||||
save()
|
||||
}
|
||||
load()
|
||||
}
|
||||
|
||||
fun load() : Histories {
|
||||
return apply {
|
||||
super.clear()
|
||||
super.addAll(
|
||||
json.parse(
|
||||
serializer,
|
||||
file.bufferedReader().use { it.readText() }
|
||||
)
|
||||
fun load() {
|
||||
synchronized(this) {
|
||||
list.clear()
|
||||
list.addAll(
|
||||
Json.decodeFromString<List<Int>>(file.bufferedReader().use { it.readText() })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun save() {
|
||||
file.writeText(json.stringify(serializer, this))
|
||||
synchronized(this) {
|
||||
file.writeText(Json.encodeToString(list))
|
||||
}
|
||||
}
|
||||
|
||||
override fun add(element: Int): Boolean {
|
||||
load()
|
||||
|
||||
if (contains(element))
|
||||
super.remove(element)
|
||||
|
||||
super.add(0, element)
|
||||
|
||||
save()
|
||||
|
||||
return true
|
||||
return list.add(element).also {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<Int>): Boolean {
|
||||
load()
|
||||
|
||||
for (e in elements) {
|
||||
if (contains(e))
|
||||
super.remove(e)
|
||||
super.add(0, e)
|
||||
return list.addAll(elements).also {
|
||||
save()
|
||||
}
|
||||
|
||||
save()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun remove(element: Int): Boolean {
|
||||
load()
|
||||
val retval = super.remove(element)
|
||||
save()
|
||||
|
||||
return retval
|
||||
return list.remove(element).also {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
super.clear()
|
||||
list.clear()
|
||||
save()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package xyz.quaver.pupil.util;
|
||||
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import xyz.quaver.pupil.R;
|
||||
|
||||
/*
|
||||
Source: http://www.littlerobots.nl/blog/Handle-Android-RecyclerView-Clicks/
|
||||
USAGE:
|
||||
|
||||
ItemClickSupport.addTo(mRecyclerView).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
|
||||
// do it
|
||||
}
|
||||
});
|
||||
|
||||
*/
|
||||
public class ItemClickSupport {
|
||||
private final RecyclerView mRecyclerView;
|
||||
private OnItemClickListener mOnItemClickListener;
|
||||
private OnItemLongClickListener mOnItemLongClickListener;
|
||||
private View.OnClickListener mOnClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mOnItemClickListener != null) {
|
||||
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
|
||||
mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
|
||||
}
|
||||
}
|
||||
};
|
||||
private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (mOnItemLongClickListener != null) {
|
||||
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
|
||||
return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
private RecyclerView.OnChildAttachStateChangeListener mAttachListener
|
||||
= new RecyclerView.OnChildAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onChildViewAttachedToWindow(@NonNull View view) {
|
||||
if (mOnItemClickListener != null) {
|
||||
view.setOnClickListener(mOnClickListener);
|
||||
}
|
||||
if (mOnItemLongClickListener != null) {
|
||||
view.setOnLongClickListener(mOnLongClickListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewDetachedFromWindow(@NonNull View view) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private ItemClickSupport(RecyclerView recyclerView) {
|
||||
mRecyclerView = recyclerView;
|
||||
mRecyclerView.setTag(R.id.item_click_support, this);
|
||||
mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
|
||||
}
|
||||
|
||||
public static ItemClickSupport addTo(RecyclerView view) {
|
||||
ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
|
||||
if (support == null) {
|
||||
support = new ItemClickSupport(view);
|
||||
}
|
||||
return support;
|
||||
}
|
||||
|
||||
public static ItemClickSupport removeFrom(RecyclerView view) {
|
||||
ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
|
||||
if (support != null) {
|
||||
support.detach(view);
|
||||
}
|
||||
return support;
|
||||
}
|
||||
|
||||
public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
|
||||
mOnItemClickListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
|
||||
mOnItemLongClickListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void detach(RecyclerView view) {
|
||||
view.removeOnChildAttachStateChangeListener(mAttachListener);
|
||||
view.setTag(R.id.item_click_support, null);
|
||||
}
|
||||
|
||||
public interface OnItemClickListener {
|
||||
|
||||
void onItemClicked(RecyclerView recyclerView, int position, View v);
|
||||
}
|
||||
|
||||
public interface OnItemLongClickListener {
|
||||
|
||||
boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
|
||||
}
|
||||
}
|
||||
69
app/src/main/java/xyz/quaver/pupil/util/ItemClickSupport.kt
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import xyz.quaver.pupil.R
|
||||
|
||||
class ItemClickSupport(private val recyclerView: RecyclerView) {
|
||||
|
||||
var onItemClickListener: ((RecyclerView, Int, View) -> Unit)? = null
|
||||
var onItemLongClickListener: ((RecyclerView, Int, View) -> Boolean)? = null
|
||||
|
||||
init {
|
||||
recyclerView.apply {
|
||||
setTag(R.id.item_click_support, this)
|
||||
addOnChildAttachStateChangeListener(object: RecyclerView.OnChildAttachStateChangeListener {
|
||||
override fun onChildViewAttachedToWindow(view: View) {
|
||||
onItemClickListener?.let { listener ->
|
||||
view.setOnClickListener {
|
||||
recyclerView.getChildViewHolder(view).let { holder ->
|
||||
listener.invoke(recyclerView, holder.adapterPosition, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
onItemLongClickListener?.let { listener ->
|
||||
view.setOnLongClickListener {
|
||||
recyclerView.getChildViewHolder(view).let { holder ->
|
||||
listener.invoke(recyclerView, holder.adapterPosition, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChildViewDetachedFromWindow(view: View) {
|
||||
// Do Nothing
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun detach() {
|
||||
recyclerView.apply {
|
||||
clearOnChildAttachStateChangeListeners()
|
||||
setTag(R.id.item_click_support, null)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun addTo(view: RecyclerView) = view.let { removeFrom(it); ItemClickSupport(it) }
|
||||
fun removeFrom(view: RecyclerView) = (view.tag as? ItemClickSupport)?.detach()
|
||||
}
|
||||
}
|
||||
48
app/src/main/java/xyz/quaver/pupil/util/Preferences.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.SharedPreferences
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
object Preferences: SharedPreferences by preferences {
|
||||
|
||||
val defMap = mapOf(
|
||||
String::class to "",
|
||||
Int::class to -1,
|
||||
Long::class to -1L,
|
||||
Boolean::class to false,
|
||||
Set::class to emptySet<Any>()
|
||||
)
|
||||
|
||||
operator fun set(key: String, value: String) = edit().putString(key, value).apply()
|
||||
operator fun set(key: String, value: Int) = edit().putInt(key, value).apply()
|
||||
operator fun set(key: String, value: Long) = edit().putLong(key, value).apply()
|
||||
operator fun set(key: String, value: Boolean) = edit().putBoolean(key, value).apply()
|
||||
operator fun set(key: String, value: Set<String>) = edit().putStringSet(key, value).apply()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline operator fun <reified T: Any> get(key: String, defaultVal: T = defMap[T::class] as T): T = (all[key] as? T) ?: defaultVal
|
||||
|
||||
fun remove(key: String) {
|
||||
edit().remove(key).apply()
|
||||
}
|
||||
}
|
||||
@@ -25,23 +25,23 @@ import android.util.SparseArray
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
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 xyz.quaver.readBytes
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.Lock
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
@Deprecated("Use downloader.Cache instead")
|
||||
class Cache(context: Context) : ContextWrapper(context) {
|
||||
|
||||
companion object {
|
||||
@@ -49,20 +49,6 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
private val readers = SparseArray<Reader?>()
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Search in this order
|
||||
@@ -79,7 +65,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
return null
|
||||
|
||||
return try {
|
||||
json.parse(Metadata.serializer(), file.readText())
|
||||
Json.decodeFromString(file.readText())
|
||||
} catch (e: Exception) {
|
||||
//File corrupted
|
||||
file.delete()
|
||||
@@ -96,7 +82,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
it.createNewFile()
|
||||
}
|
||||
|
||||
file.writeText(json.stringify(Metadata.serializer(), metadata))
|
||||
file.writeText(Json.encodeToString(metadata))
|
||||
}
|
||||
|
||||
suspend fun getThumbnail(galleryID: Int): String? {
|
||||
@@ -105,11 +91,12 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
val thumbnail = if (metadata?.thumbnail == null)
|
||||
withContext(Dispatchers.IO) {
|
||||
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
|
||||
val thumbnail = getGalleryBlock(galleryID)?.thumbnails?.firstOrNull() ?: return@withContext null
|
||||
try {
|
||||
Base64.encodeToString(URL(thumbnails?.firstOrNull()).openConnection(proxy).getInputStream().use {
|
||||
it.readBytes()
|
||||
}, Base64.DEFAULT)
|
||||
val data = URL(thumbnail).readBytes().apply {
|
||||
if (isEmpty()) return@withContext null
|
||||
}
|
||||
Base64.encodeToString(data, Base64.DEFAULT)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
@@ -134,7 +121,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
)
|
||||
|
||||
val galleryBlock = if (metadata?.galleryBlock == null) {
|
||||
CoroutineScope(Dispatchers.IO).async {
|
||||
withContext(Dispatchers.IO) {
|
||||
var galleryBlock: GalleryBlock? = null
|
||||
|
||||
for (source in sources) {
|
||||
@@ -149,7 +136,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
}
|
||||
|
||||
galleryBlock
|
||||
}.await() ?: return null
|
||||
} ?: return null
|
||||
}
|
||||
else
|
||||
metadata.galleryBlock
|
||||
@@ -175,11 +162,9 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
|
||||
).let {
|
||||
if (mirrors.isNotEmpty())
|
||||
it.toSortedMap(
|
||||
Comparator { o1, o2 ->
|
||||
mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
|
||||
}
|
||||
)
|
||||
it.toSortedMap{ o1, o2 ->
|
||||
mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
|
||||
}
|
||||
else
|
||||
it
|
||||
}
|
||||
@@ -194,7 +179,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
retval = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
withTimeoutOrNull(1000) {
|
||||
source.value.invoke()
|
||||
source.value.invoke()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -40,14 +40,15 @@ import xyz.quaver.hitomi.imageUrlFromImage
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hiyobi.createImgList
|
||||
import xyz.quaver.hiyobi.user_agent
|
||||
import xyz.quaver.proxy
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.interceptors
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Deprecated("Use DownloadService instead")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
|
||||
|
||||
@@ -86,7 +87,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
}
|
||||
|
||||
private fun source(source: Source) = object: ForwardingSource(source) {
|
||||
|
||||
var totalBytesRead = 0L
|
||||
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
@@ -100,6 +100,24 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
interceptors[Pair::class] = { chain ->
|
||||
val request = chain.request()
|
||||
var response = chain.proceed(request)
|
||||
|
||||
var retry = 5
|
||||
while (!response.isSuccessful && retry > 0) {
|
||||
response = chain.proceed(request)
|
||||
retry--
|
||||
}
|
||||
|
||||
response.newBuilder()
|
||||
.body(response.body()?.let {
|
||||
ProgressResponseBody(request.tag(), it, progressListener)
|
||||
}).build()
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Singleton
|
||||
@@ -135,34 +153,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
private val loop = loop()
|
||||
private val worker = SparseArray<Job?>()
|
||||
|
||||
val interceptor = Interceptor { chain ->
|
||||
val request = chain.request()
|
||||
var response = chain.proceed(request)
|
||||
|
||||
var retry = 5
|
||||
while (!response.isSuccessful && retry > 0) {
|
||||
response = chain.proceed(request)
|
||||
retry--
|
||||
}
|
||||
|
||||
response.newBuilder()
|
||||
.body(response.body()?.let {
|
||||
ProgressResponseBody(request.tag(), it, progressListener)
|
||||
}).build()
|
||||
}
|
||||
|
||||
val client =
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(0, TimeUnit.SECONDS)
|
||||
.addInterceptor(interceptor)
|
||||
.readTimeout(0, TimeUnit.SECONDS)
|
||||
.dispatcher(Dispatcher().apply {
|
||||
maxRequests = 4
|
||||
maxRequestsPerHost = 4
|
||||
})
|
||||
.proxy(proxy)
|
||||
.build()
|
||||
|
||||
fun stop() {
|
||||
queue.clear()
|
||||
|
||||
@@ -179,6 +169,11 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
}.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
client.dispatcher().runningCalls().filter {
|
||||
it.request().tag() is Pair<*, *>
|
||||
}.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
|
||||
progress.clear()
|
||||
notification.clear()
|
||||
@@ -194,6 +189,11 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
}.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
client.dispatcher().runningCalls().filter {
|
||||
((it.request().tag() as Pair<*, *>).first as Int) == galleryID
|
||||
}.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
|
||||
progress.remove(galleryID)
|
||||
notification.remove(galleryID)
|
||||
@@ -275,13 +275,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
if (e.message?.contains("cancel", true) != false)
|
||||
return
|
||||
|
||||
Log.i("PUPILD", "FAIL ${call.request().tag()} (${e.message})")
|
||||
FirebaseCrashlytics.getInstance().apply {
|
||||
log("FAIL ${call.request().tag()} (${e.message})")
|
||||
setCustomKey("POS", "FAIL")
|
||||
recordException(e)
|
||||
}
|
||||
|
||||
cancel(galleryID)
|
||||
queue.add(galleryID)
|
||||
}
|
||||
@@ -290,8 +283,8 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
val ext = call.request().url().encodedPath().split('.').last()
|
||||
|
||||
try {
|
||||
response.body().use {
|
||||
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it!!.byteStream())
|
||||
response.body()!!.use {
|
||||
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream())
|
||||
}
|
||||
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
|
||||
|
||||
|
||||
@@ -22,12 +22,13 @@ import kotlinx.serialization.Serializable
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
|
||||
@Deprecated("Use downloader.Cache.Metadata instead")
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
val thumbnail: String? = null,
|
||||
val galleryBlock: GalleryBlock? = null,
|
||||
val reader: Reader? = null,
|
||||
val isDownloading: Boolean? = null
|
||||
var thumbnail: String? = null,
|
||||
var galleryBlock: GalleryBlock? = null,
|
||||
var reader: Reader? = null,
|
||||
var isDownloading: Boolean? = null
|
||||
) {
|
||||
constructor(
|
||||
metadata: Metadata?,
|
||||
|
||||
232
app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt
Normal file
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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.downloader
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.util.SparseArray
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Request
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.getChild
|
||||
import xyz.quaver.io.util.readBytes
|
||||
import xyz.quaver.io.util.readText
|
||||
import xyz.quaver.io.util.writeBytes
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
var galleryBlock: GalleryBlock? = null,
|
||||
var reader: Reader? = null,
|
||||
var imageList: MutableList<String?>? = null
|
||||
) {
|
||||
fun copy(): Metadata = Metadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
||||
}
|
||||
|
||||
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
|
||||
|
||||
companion object {
|
||||
val instances = SparseArray<Cache>()
|
||||
|
||||
fun getInstance(context: Context, galleryID: Int) =
|
||||
instances[galleryID] ?: synchronized(this) {
|
||||
instances[galleryID] ?: Cache(context, galleryID).also { instances.put(galleryID, it) }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun delete(galleryID: Int) {
|
||||
instances[galleryID]?.cacheFolder?.deleteRecursively()
|
||||
instances.delete(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
cacheFolder.mkdirs()
|
||||
}
|
||||
|
||||
var metadata = kotlin.runCatching {
|
||||
findFile(".metadata")?.readText()?.let {
|
||||
Json.decodeFromString<Metadata>(it)
|
||||
}
|
||||
}.getOrNull() ?: Metadata()
|
||||
|
||||
val downloadFolder: FileX?
|
||||
get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID)
|
||||
|
||||
val cacheFolder: FileX
|
||||
get() = FileX(this, cacheDir, "imageCache/$galleryID").also {
|
||||
if (!it.exists())
|
||||
it.mkdirs()
|
||||
}
|
||||
|
||||
fun findFile(fileName: String): FileX? =
|
||||
downloadFolder?.let { downloadFolder -> downloadFolder.getChild(fileName).let {
|
||||
if (it.exists()) it else null
|
||||
} } ?: cacheFolder.getChild(fileName).let {
|
||||
if (it.exists()) it else null
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
fun setMetadata(change: (Metadata) -> Unit) {
|
||||
change.invoke(metadata)
|
||||
|
||||
val file = cacheFolder.getChild(".metadata")
|
||||
|
||||
kotlin.runCatching {
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
}
|
||||
file.writeText(Json.encodeToString(metadata))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGalleryBlock(): GalleryBlock? {
|
||||
val sources = listOf(
|
||||
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||
)
|
||||
|
||||
return metadata.galleryBlock
|
||||
?: withContext(Dispatchers.IO) {
|
||||
var galleryBlock: GalleryBlock? = null
|
||||
|
||||
for (source in sources) {
|
||||
galleryBlock = try {
|
||||
source.invoke()
|
||||
} catch (e: Exception) { null }
|
||||
|
||||
if (galleryBlock != null)
|
||||
break
|
||||
}
|
||||
|
||||
galleryBlock?.also {
|
||||
setMetadata { metadata -> metadata.galleryBlock = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun getThumbnail(): ByteArray? =
|
||||
findFile(".thumbnail")?.readBytes()
|
||||
?: getGalleryBlock()?.thumbnails?.firstOrNull()?.let { withContext(Dispatchers.IO) {
|
||||
kotlin.runCatching {
|
||||
val request = Request.Builder()
|
||||
.url(it)
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().body()?.use { it.bytes() }
|
||||
}.getOrNull()?.also { kotlin.run {
|
||||
cacheFolder.getChild(".thumbnail").writeBytes(it)
|
||||
} }
|
||||
} }
|
||||
|
||||
suspend fun getReader(): Reader? {
|
||||
val mirrors = Preferences.get<String>("mirrors").let { if (it.isEmpty()) emptyList() else it.split('>') }
|
||||
|
||||
val sources = mapOf(
|
||||
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
|
||||
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
|
||||
).let {
|
||||
if (mirrors.isNotEmpty())
|
||||
it.toSortedMap{ o1, o2 -> mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name) }
|
||||
else
|
||||
it
|
||||
}
|
||||
|
||||
return metadata.reader
|
||||
?: withContext(Dispatchers.IO) {
|
||||
var reader: Reader? = null
|
||||
|
||||
for (source in sources) {
|
||||
reader = try {
|
||||
source.value.invoke()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
if (reader != null)
|
||||
break
|
||||
}
|
||||
|
||||
reader?.also {
|
||||
setMetadata { metadata ->
|
||||
metadata.reader = it
|
||||
|
||||
if (metadata.imageList == null)
|
||||
metadata.imageList = MutableList(reader.galleryInfo.files.size) { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getImage(index: Int): FileX? =
|
||||
metadata.imageList?.get(index)?.let { findFile(it) }
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
fun putImage(index: Int, fileName: String, data: ByteArray) {
|
||||
val file = cacheFolder.getChild(fileName)
|
||||
|
||||
file.createNewFile()
|
||||
file.writeBytes(data)
|
||||
setMetadata { metadata -> metadata.imageList!![index] = fileName }
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
|
||||
val downloadFolder = downloadFolder ?: return@launch
|
||||
|
||||
metadata.imageList?.forEach { imageName ->
|
||||
imageName ?: return@forEach
|
||||
val target = downloadFolder.getChild(imageName)
|
||||
val source = cacheFolder.getChild(imageName)
|
||||
|
||||
if (!source.exists())
|
||||
return@forEach
|
||||
|
||||
kotlin.runCatching {
|
||||
target.createNewFile()
|
||||
source.readBytes()?.let { target.writeBytes(it) }
|
||||
}
|
||||
}
|
||||
|
||||
val cacheMetadata = cacheFolder.getChild(".metadata")
|
||||
val downloadMetadata = downloadFolder.getChild(".metadata")
|
||||
|
||||
if (cacheMetadata.exists()) {
|
||||
kotlin.runCatching {
|
||||
downloadMetadata.createNewFile()
|
||||
downloadMetadata.writeText(Json.encodeToString(metadata))
|
||||
cacheMetadata.delete()
|
||||
}
|
||||
}
|
||||
|
||||
cacheFolder.delete()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.downloader
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Call
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.*
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.formatDownloadFolder
|
||||
|
||||
class DownloadManager private constructor(context: Context) : ContextWrapper(context) {
|
||||
|
||||
companion object {
|
||||
@Volatile private var instance: DownloadManager? = null
|
||||
|
||||
fun getInstance(context: Context) =
|
||||
instance ?: synchronized(this) {
|
||||
instance ?: DownloadManager(context).also { instance = it }
|
||||
}
|
||||
}
|
||||
|
||||
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
|
||||
|
||||
val downloadFolder: FileX
|
||||
get() = {
|
||||
kotlin.runCatching {
|
||||
FileX(this, Preferences.get<String>("download_folder"))
|
||||
}.getOrElse {
|
||||
Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
|
||||
defaultDownloadFolder
|
||||
}
|
||||
}.invoke()
|
||||
|
||||
private var prevDownloadFolder: FileX? = null
|
||||
private var downloadFolderMapInstance: MutableMap<Int, String>? = null
|
||||
val downloadFolderMap: MutableMap<Int, String>
|
||||
@Synchronized
|
||||
get() {
|
||||
if (prevDownloadFolder != downloadFolder) {
|
||||
prevDownloadFolder = downloadFolder
|
||||
downloadFolderMapInstance = {
|
||||
val file = downloadFolder.getChild(".download")
|
||||
|
||||
val data = if (file.exists())
|
||||
kotlin.runCatching {
|
||||
file.readText()?.let { Json.decodeFromString<MutableMap<Int, String>>(it) }
|
||||
}.onFailure { file.delete() }.getOrNull()
|
||||
else
|
||||
null
|
||||
|
||||
data ?: {
|
||||
file.createNewFile()
|
||||
file.writeText("{}")
|
||||
mutableMapOf<Int, String>()
|
||||
}.invoke()
|
||||
}.invoke()
|
||||
}
|
||||
|
||||
return downloadFolderMapInstance!!
|
||||
}
|
||||
|
||||
|
||||
@Synchronized
|
||||
fun isDownloading(galleryID: Int): Boolean {
|
||||
val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
|
||||
|
||||
return downloadFolderMap.containsKey(galleryID)
|
||||
&& client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getDownloadFolder(galleryID: Int): FileX? =
|
||||
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
|
||||
|
||||
@Synchronized
|
||||
fun addDownloadFolder(galleryID: Int) {
|
||||
if (downloadFolderMap.containsKey(galleryID))
|
||||
return
|
||||
|
||||
val name = runBlocking {
|
||||
Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
|
||||
}?.formatDownloadFolder() ?: return
|
||||
|
||||
val folder = downloadFolder.getChild(name)
|
||||
|
||||
if (!folder.exists())
|
||||
folder.mkdir()
|
||||
|
||||
downloadFolderMap[galleryID] = folder.name
|
||||
|
||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
|
||||
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteDownloadFolder(galleryID: Int) {
|
||||
if (!downloadFolderMap.containsKey(galleryID))
|
||||
return
|
||||
|
||||
downloadFolderMap[galleryID]?.let {
|
||||
kotlin.runCatching {
|
||||
downloadFolder.getChild(it).delete()
|
||||
downloadFolderMap.remove(galleryID)
|
||||
|
||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
|
||||
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,20 +18,16 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.storage.StorageManager
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.core.net.toUri
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.lang.reflect.Array
|
||||
import java.net.URL
|
||||
|
||||
|
||||
@Deprecated("Use downloader.Cache instead")
|
||||
fun getCachedGallery(context: Context, galleryID: Int) =
|
||||
File(getDownloadDirectory(context), galleryID.toString()).let {
|
||||
if (it.exists())
|
||||
@@ -40,178 +36,15 @@ fun getCachedGallery(context: Context, galleryID: Int) =
|
||||
File(context.cacheDir, "imageCache/$galleryID")
|
||||
}
|
||||
|
||||
@Deprecated("Use downloader.Cache instead")
|
||||
fun getDownloadDirectory(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
|
||||
if (it != null && !it.startsWith("content"))
|
||||
Preferences.get<String>("dl_location").let {
|
||||
if (it.isNotEmpty() && !it.startsWith("content"))
|
||||
File(it)
|
||||
else
|
||||
context.getExternalFilesDir(null)!!
|
||||
}
|
||||
|
||||
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
||||
|
||||
if (to.parentFile?.exists() == false)
|
||||
to.parentFile!!.mkdirs()
|
||||
|
||||
if (!to.exists())
|
||||
to.createNewFile()
|
||||
|
||||
FileOutputStream(to).use { out ->
|
||||
|
||||
with(openConnection()) {
|
||||
val fileSize = contentLength.toLong()
|
||||
|
||||
getInputStream().use {
|
||||
|
||||
var bytesCopied: Long = 0
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
|
||||
var bytes = it.read(buffer)
|
||||
while (bytes >= 0) {
|
||||
out.write(buffer, 0, bytes)
|
||||
bytesCopied += bytes
|
||||
onDownloadProgress?.invoke(bytesCopied, fileSize)
|
||||
bytes = it.read(buffer)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun getExtSdCardPaths(context: Context) =
|
||||
ContextCompat.getExternalFilesDirs(context, null).drop(1).map {
|
||||
it.absolutePath.substringBeforeLast("/Android/data").let { path ->
|
||||
runCatching {
|
||||
File(path).canonicalPath
|
||||
}.getOrElse {
|
||||
path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const val PRIMARY_VOLUME_NAME = "primary"
|
||||
fun getVolumePath(context: Context, volumeID: String?): String? {
|
||||
return runCatching {
|
||||
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||
val storageVolumeClass = Class.forName("android.os.storage.StorageVolume")
|
||||
|
||||
val getVolumeList = storageVolumeClass.javaClass.getMethod("getVolumeList")
|
||||
val getUUID = storageVolumeClass.getMethod("getUuid")
|
||||
val getPath = storageVolumeClass.getMethod("getPath")
|
||||
val isPrimary = storageVolumeClass.getMethod("isPrimary")
|
||||
|
||||
val result = getVolumeList.invoke(storageManager)!!
|
||||
|
||||
val length = Array.getLength(result)
|
||||
|
||||
for (i in 0 until length) {
|
||||
val storageVolumeElement = Array.get(result, i)
|
||||
val uuid = getUUID.invoke(storageVolumeElement) as? String
|
||||
val primary = isPrimary.invoke(storageVolumeElement) as? Boolean
|
||||
|
||||
// primary volume?
|
||||
if (primary == true && volumeID == PRIMARY_VOLUME_NAME)
|
||||
return@runCatching getPath.invoke(storageVolumeElement) as? String
|
||||
|
||||
// other volumes?
|
||||
if (volumeID == uuid) {
|
||||
return@runCatching getPath.invoke(storageVolumeElement) as? String
|
||||
}
|
||||
}
|
||||
return@runCatching null
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
// Credits go to https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri/36162691#36162691
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
fun getVolumeIdFromTreeUri(uri: Uri) =
|
||||
DocumentsContract.getTreeDocumentId(uri).split(':').let {
|
||||
if (it.isNotEmpty())
|
||||
it[0]
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
fun getDocumentPathFromTreeUri(uri: Uri) =
|
||||
DocumentsContract.getTreeDocumentId(uri).split(':').let {
|
||||
if (it.size >= 2)
|
||||
it[1]
|
||||
else
|
||||
File.separator
|
||||
}
|
||||
|
||||
fun getFullPathFromTreeUri(context: Context, uri: Uri) : String? {
|
||||
val volumePath = getVolumePath(context, getVolumeIdFromTreeUri(uri) ?: return null).let {
|
||||
it ?: return File.separator
|
||||
|
||||
if (it.endsWith(File.separator))
|
||||
it.dropLast(1)
|
||||
else
|
||||
it
|
||||
}
|
||||
|
||||
val documentPath = getDocumentPathFromTreeUri(uri).let {
|
||||
if (it.endsWith(File.separator))
|
||||
it.dropLast(1)
|
||||
else
|
||||
it
|
||||
}
|
||||
|
||||
return if (documentPath.isNotEmpty()) {
|
||||
if (documentPath.startsWith(File.separator))
|
||||
volumePath + documentPath
|
||||
else
|
||||
volumePath + File.separator + documentPath
|
||||
} else
|
||||
volumePath
|
||||
}
|
||||
|
||||
// Huge thanks to avluis(https://github.com/avluis)
|
||||
// This code is originated from Hentoid(https://github.com/avluis/Hentoid) under Apache-2.0 license.
|
||||
fun Uri.toFile(context: Context): File? {
|
||||
val path = this.path ?: return null
|
||||
|
||||
val pathSeparator = path.indexOf(':')
|
||||
val folderName = path.substring(pathSeparator+1)
|
||||
|
||||
// Determine whether the designated file is
|
||||
// - on a removable media (e.g. SD card, OTG)
|
||||
// or
|
||||
// - on the internal phone memory
|
||||
val removableMediaFolderRoots = getExtSdCardPaths(context)
|
||||
|
||||
/* First test is to compare root names with known roots of removable media
|
||||
* In many cases, the SD card root name is shared between pre-SAF (File) and SAF (DocumentFile) frameworks
|
||||
* (e.g. /storage/3437-3934 vs. /tree/3437-3934)
|
||||
* This is what the following block is trying to do
|
||||
*/
|
||||
for (s in removableMediaFolderRoots) {
|
||||
val sRoot = s.substring(s.lastIndexOf(File.separatorChar))
|
||||
val root = path.substring(0, pathSeparator).let {
|
||||
it.substring(it.lastIndexOf(File.separatorChar))
|
||||
}
|
||||
|
||||
if (sRoot.equals(root, true)) {
|
||||
return File(s + File.separatorChar + folderName)
|
||||
}
|
||||
}
|
||||
/* In some other cases, there is no common name (e.g. /storage/sdcard1 vs. /tree/3437-3934)
|
||||
* We can use a slower method to translate the Uri obtained with SAF into a pre-SAF path
|
||||
* and compare it to the known removable media volume names
|
||||
*/
|
||||
val root = getFullPathFromTreeUri(context, this)
|
||||
|
||||
for (s in removableMediaFolderRoots) {
|
||||
if (root?.startsWith(s) == true) {
|
||||
return File(root)
|
||||
}
|
||||
}
|
||||
|
||||
return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
|
||||
}
|
||||
|
||||
@Deprecated("Use FileX instead")
|
||||
fun File.isParentOf(another: File) =
|
||||
another.absolutePath.startsWith(this.absolutePath)
|
||||
@@ -22,7 +22,9 @@ import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.list
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
@@ -41,7 +43,7 @@ fun hashWithSalt(password: String): Pair<String, String> {
|
||||
return Pair(hash(password+salt), salt)
|
||||
}
|
||||
|
||||
val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
const val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
@Serializable
|
||||
data class Lock(val type: Type, val hash: String, val salt: String) {
|
||||
@@ -80,7 +82,7 @@ class LockManager(base: Context): ContextWrapper(base) {
|
||||
lock.writeText("[]")
|
||||
}
|
||||
|
||||
locks = ArrayList(json.parse(Lock.serializer().list, lock.readText()))
|
||||
locks = Json.decodeFromString(lock.readText())
|
||||
}
|
||||
|
||||
private fun save() {
|
||||
@@ -89,7 +91,7 @@ class LockManager(base: Context): ContextWrapper(base) {
|
||||
if (!lock.exists())
|
||||
lock.createNewFile()
|
||||
|
||||
lock.writeText(json.stringify(Lock.serializer().list, locks?.toList() ?: listOf()))
|
||||
lock.writeText(Json.encodeToString(locks?.toList() ?: listOf()))
|
||||
}
|
||||
|
||||
fun add(lock: Lock) {
|
||||
|
||||
@@ -19,6 +19,26 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.getReferer
|
||||
import xyz.quaver.hitomi.imageUrlFromImage
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hiyobi.createImgList
|
||||
import xyz.quaver.hiyobi.user_agent
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.Metadata
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@@ -33,15 +53,15 @@ fun String.wordCapitalize() : String {
|
||||
return result.joinToString(" ")
|
||||
}
|
||||
|
||||
fun byteToString(byte: Long, precision : Int = 1) : String {
|
||||
private val suffix = listOf(
|
||||
"B",
|
||||
"kB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB" //really?
|
||||
)
|
||||
|
||||
val suffix = listOf(
|
||||
"B",
|
||||
"kB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB" //really?
|
||||
)
|
||||
fun byteToString(byte: Long, precision : Int = 1) : String {
|
||||
var size = byte.toDouble(); var suffixIndex = 0
|
||||
|
||||
while (size >= 1024) {
|
||||
@@ -50,5 +70,79 @@ fun byteToString(byte: Long, precision : Int = 1) : String {
|
||||
}
|
||||
|
||||
return "%.${precision}f ${suffix[suffixIndex]}".format(size)
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Convert android generated ID to requestCode
|
||||
* to prevent java.lang.IllegalArgumentException: Can only use lower 16 bits for requestCode
|
||||
*
|
||||
* https://stackoverflow.com/questions/38072322/generate-16-bit-unique-ids-in-android-for-startactivityforresult
|
||||
*/
|
||||
fun Int.normalizeID() = this.and(0xFFFF)
|
||||
|
||||
fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
|
||||
proxy(proxyInfo.proxy())
|
||||
proxyInfo.authenticator()?.let {
|
||||
proxyAuthenticator(it)
|
||||
}
|
||||
}
|
||||
|
||||
val formatMap = mapOf<String, GalleryBlock.() -> (String)>(
|
||||
"-id-" to { id.toString() },
|
||||
"-title-" to { title },
|
||||
"-artist-" to { artists.joinToString() }
|
||||
// TODO
|
||||
)
|
||||
/**
|
||||
* Formats download folder name with given Metadata
|
||||
*/
|
||||
fun GalleryBlock.formatDownloadFolder(): String =
|
||||
Preferences["download_folder_name", "[-id-] -title-"].let {
|
||||
formatMap.entries.fold(it) { str, (k, v) ->
|
||||
str.replace(k, v.invoke(this), true)
|
||||
}
|
||||
}
|
||||
|
||||
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
|
||||
format.let {
|
||||
formatMap.entries.fold(it) { str, (k, v) ->
|
||||
str.replace(k, v.invoke(this), true)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.startForegroundServiceCompat(service: Intent) {
|
||||
if (Build.VERSION.SDK_INT >= 26)
|
||||
startForegroundService(service)
|
||||
else
|
||||
startService(service)
|
||||
}
|
||||
|
||||
val Reader.requestBuilders: List<Request.Builder>
|
||||
get() {
|
||||
val galleryID = this.galleryInfo.id ?: 0
|
||||
val lowQuality = Preferences["low_quality", true]
|
||||
|
||||
return when(code) {
|
||||
Code.HITOMI -> {
|
||||
this.galleryInfo.files.map {
|
||||
Request.Builder()
|
||||
.url(imageUrlFromImage(galleryID, it, !lowQuality))
|
||||
.header("Referer", getReferer(galleryID))
|
||||
}
|
||||
}
|
||||
Code.HIYOBI -> {
|
||||
createImgList(galleryID, this, lowQuality).map {
|
||||
Request.Builder()
|
||||
.url(it.path)
|
||||
.header("User-Agent", user_agent)
|
||||
.header("Cookie", cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.ellipsize(n: Int): String =
|
||||
if (this.length > n)
|
||||
this.slice(0 until n) + "…"
|
||||
else
|
||||
this
|
||||
@@ -19,8 +19,10 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Credentials
|
||||
import java.net.InetSocketAddress
|
||||
@@ -35,29 +37,22 @@ data class ProxyInfo(
|
||||
val password: String? = null
|
||||
) {
|
||||
fun proxy() : Proxy {
|
||||
return if (host == null || port == null)
|
||||
return if (host.isNullOrBlank() || port == null)
|
||||
return Proxy.NO_PROXY
|
||||
else
|
||||
Proxy(type, InetSocketAddress.createUnresolved(host, port))
|
||||
}
|
||||
|
||||
fun authenticator() = Authenticator { _, response ->
|
||||
val credential = Credentials.basic(username ?: "", password ?: "")
|
||||
fun authenticator(): Authenticator? = if (username.isNullOrBlank() || password.isNullOrBlank()) null else
|
||||
Authenticator { _, response ->
|
||||
val credential = Credentials.basic(username, password)
|
||||
|
||||
response.request().newBuilder()
|
||||
.header("Proxy-Authorization", credential)
|
||||
.build()
|
||||
}
|
||||
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)
|
||||
}
|
||||
fun getProxyInfo(): ProxyInfo =
|
||||
Json.decodeFromString(Preferences["proxy", Json.encodeToString(ProxyInfo(Proxy.Type.DIRECT))])
|
||||
@@ -21,60 +21,69 @@ package xyz.quaver.pupil.util
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.DownloadManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.webkit.URLUtil
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.content
|
||||
import okhttp3.*
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import ru.noties.markwon.Markwon
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.getGalleryBlock
|
||||
import xyz.quaver.hitomi.getReader
|
||||
import xyz.quaver.proxy
|
||||
import xyz.quaver.pupil.BroadcastReciever
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.getChild
|
||||
import xyz.quaver.io.util.*
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.download.Cache
|
||||
import xyz.quaver.pupil.util.download.Metadata
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.Metadata
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
fun getReleases(url: String) : JsonArray {
|
||||
return try {
|
||||
URL(url).readText().let {
|
||||
json.parse(JsonArray.serializer(), it)
|
||||
Json.parseToJsonElement(it).jsonArray
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
JsonArray(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
fun checkUpdate(context: Context, url: String) : JsonObject? {
|
||||
fun checkUpdate(url: String) : JsonObject? {
|
||||
val releases = getReleases(url)
|
||||
|
||||
if (releases.isEmpty())
|
||||
return null
|
||||
|
||||
return releases.firstOrNull {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("beta", false))
|
||||
true
|
||||
else
|
||||
it.jsonObject["prerelease"]?.boolean == false
|
||||
Preferences["beta"] || it.jsonObject["prerelease"]?.jsonPrimitive?.booleanOrNull == false
|
||||
}?.let {
|
||||
if (it.jsonObject["tag_name"]?.content == BuildConfig.VERSION_NAME)
|
||||
if (it.jsonObject["tag_name"]?.jsonPrimitive?.contentOrNull == BuildConfig.VERSION_NAME)
|
||||
null
|
||||
else
|
||||
it.jsonObject
|
||||
@@ -83,13 +92,12 @@ fun checkUpdate(context: Context, url: String) : JsonObject? {
|
||||
|
||||
fun getApkUrl(releases: JsonObject) : String? {
|
||||
return releases["assets"]?.jsonArray?.firstOrNull {
|
||||
Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.content ?: "")
|
||||
Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.jsonPrimitive?.contentOrNull ?: "")
|
||||
}.let {
|
||||
it?.jsonObject?.get("browser_download_url")?.content
|
||||
it?.jsonObject?.get("browser_download_url")?.jsonPrimitive?.contentOrNull
|
||||
}
|
||||
}
|
||||
|
||||
const val UPDATE_NOTIFICATION_ID = 384823
|
||||
fun checkUpdate(context: Context, force: Boolean = false) {
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
@@ -99,7 +107,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
||||
return
|
||||
|
||||
fun extractReleaseNote(update: JsonObject, locale: Locale) : String {
|
||||
val markdown = update["body"]!!.content
|
||||
val markdown = update["body"]!!.jsonPrimitive.content
|
||||
|
||||
val target = when(locale.language) {
|
||||
"ko" -> "한국어"
|
||||
@@ -137,12 +145,12 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
return context.getString(R.string.update_release_note, update["tag_name"]?.content, result.toString())
|
||||
return context.getString(R.string.update_release_note, update["tag_name"]?.jsonPrimitive?.contentOrNull, result.toString())
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val update =
|
||||
checkUpdate(context, context.getString(R.string.release_url)) ?: return@launch
|
||||
checkUpdate(context.getString(R.string.release_url)) ?: return@launch
|
||||
|
||||
val url = getApkUrl(update) ?: return@launch
|
||||
|
||||
@@ -152,13 +160,11 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
||||
setMessage(Markwon.create(context).toMarkdown(msg))
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
//Cancel any download queued before
|
||||
|
||||
val id = preference.getLong("update_download_id", -1)
|
||||
val id: Long = Preferences["update_download_id"]
|
||||
|
||||
if (id != -1L)
|
||||
downloadManager.remove(id)
|
||||
@@ -172,7 +178,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
||||
.setDestinationUri(Uri.fromFile(target))
|
||||
|
||||
downloadManager.enqueue(request).also {
|
||||
preference.edit().putLong("update_download_id", it).apply()
|
||||
Preferences["update_download_id"] = it
|
||||
}
|
||||
}
|
||||
setNegativeButton(if (force) android.R.string.no else R.string.ignore_update) { _, _ ->
|
||||
@@ -189,147 +195,136 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
var cancelImport = false
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun importOldGalleries(context: Context, folder: File) = CoroutineScope(Dispatchers.IO).launch {
|
||||
val client = OkHttpClient.Builder()
|
||||
.connectTimeout(0, TimeUnit.SECONDS)
|
||||
.readTimeout(0, TimeUnit.SECONDS)
|
||||
.proxy(proxy)
|
||||
fun restore(favorites: GalleryList, url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<Int>) -> Unit)? = null) {
|
||||
if (!URLUtil.isValidUrl(url)) {
|
||||
onFailure?.invoke(IllegalArgumentException())
|
||||
return
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val cancelIntent = Intent(context, BroadcastReciever::class.java).apply {
|
||||
action = BroadcastReciever.ACTION_CANCEL_IMPORT
|
||||
putExtra(BroadcastReciever.EXTRA_IMPORT_NOTIFICATION_ID, 0)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, 0)
|
||||
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
val notificationBuilder = NotificationCompat.Builder(context, "import").apply {
|
||||
setContentTitle(context.getText(R.string.import_old_galleries_notification))
|
||||
setProgress(0, 0, true)
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
addAction(0, context.getText(android.R.string.cancel), pendingIntent)
|
||||
setOngoing(true)
|
||||
}
|
||||
|
||||
notificationManager.notify(0, notificationBuilder.build())
|
||||
|
||||
if (!folder.isDirectory)
|
||||
return@launch
|
||||
|
||||
val galleryRegex = Regex("""[0-9]+$""")
|
||||
val imageRegex = Regex("""^[0-9]+\..+$""")
|
||||
var size = 0
|
||||
fun setProgress(progress: Int) {
|
||||
notificationBuilder.apply {
|
||||
setContentText(
|
||||
context.getString(
|
||||
R.string.import_old_galleries_notification_text,
|
||||
progress,
|
||||
size
|
||||
)
|
||||
)
|
||||
setProgress(size, progress, false)
|
||||
client.newCall(request).enqueue(object: Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
onFailure?.invoke(e)
|
||||
}
|
||||
|
||||
notificationManager.notify(0, notificationBuilder.build())
|
||||
}
|
||||
|
||||
folder.listFiles { _, name ->
|
||||
galleryRegex.matches(name)
|
||||
}?.also {
|
||||
size = it.size
|
||||
setProgress(0)
|
||||
}?.forEachIndexed { index, gallery ->
|
||||
if (cancelImport)
|
||||
return@forEachIndexed
|
||||
|
||||
setProgress(index)
|
||||
|
||||
val galleryID = gallery.name.toIntOrNull() ?: return@forEachIndexed
|
||||
|
||||
File(getDownloadDirectory(context), galleryID.toString()).mkdirs()
|
||||
|
||||
val reader = async {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
kotlin.runCatching {
|
||||
json.parse(Reader.serializer(), File(gallery, "reader.json").readText())
|
||||
}.getOrElse {
|
||||
getReader(galleryID)
|
||||
}
|
||||
}
|
||||
val galleryBlock = async {
|
||||
kotlin.runCatching {
|
||||
json.parse(GalleryBlock.serializer(), File(gallery, "galleryBlock.json").readText())
|
||||
}.getOrElse {
|
||||
getGalleryBlock(galleryID)
|
||||
}
|
||||
}
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val thumbnail = async thumbnail@{
|
||||
val galleryBlock = galleryBlock.await()
|
||||
|
||||
Base64.encodeToString(try {
|
||||
File(gallery, "thumbnail.jpg").readBytes()
|
||||
} catch (e: Exception) {
|
||||
val url = galleryBlock?.thumbnails?.firstOrNull()
|
||||
|
||||
if (url == null)
|
||||
null
|
||||
else {
|
||||
val request = Request.Builder().url(url).build()
|
||||
|
||||
var done = false
|
||||
var result: ByteArray? = null
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call?, e: IOException?) {
|
||||
done = true
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call?, response: Response?) {
|
||||
result = response?.body()?.use {
|
||||
it.bytes()
|
||||
}
|
||||
done = true
|
||||
}
|
||||
})
|
||||
|
||||
if (!done)
|
||||
yield()
|
||||
|
||||
result
|
||||
Json.decodeFromString<List<Int>>(response.body().use { it?.string() } ?: "[]").let {
|
||||
favorites.addAll(it)
|
||||
onSuccess?.invoke(it)
|
||||
}
|
||||
} ?: return@thumbnail null, Base64.DEFAULT)
|
||||
}.onFailure { onFailure?.invoke(it) }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Cache(context).setCachedMetadata(galleryID,
|
||||
Metadata(
|
||||
thumbnail.await(),
|
||||
galleryBlock.await(),
|
||||
reader.await()
|
||||
)
|
||||
)
|
||||
private var job: Job? = null
|
||||
private val receiver = object: BroadcastReceiver() {
|
||||
val ACTION_CANCEL = "ACTION_IMPORT_CANCEL"
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
context ?: return
|
||||
|
||||
File(gallery, "images").listFiles { _, name ->
|
||||
imageRegex.matches(name)
|
||||
}?.forEach {
|
||||
if (cancelImport)
|
||||
return@forEach
|
||||
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val index = it.nameWithoutExtension.toIntOrNull() ?: return@forEach
|
||||
|
||||
Cache(context).putImage(galleryID, index, it.extension, it.inputStream())
|
||||
when (intent?.action) {
|
||||
ACTION_CANCEL -> {
|
||||
job?.cancel()
|
||||
NotificationManagerCompat.from(context).cancel(R.id.notification_id_import)
|
||||
context.unregisterReceiver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
|
||||
registerReceiver(receiver, IntentFilter().apply { addAction(receiver.ACTION_CANCEL) })
|
||||
|
||||
notificationBuilder.apply {
|
||||
setContentText(context.getText(R.string.import_old_galleries_notification_done))
|
||||
setProgress(0, 0, false)
|
||||
setOngoing(false)
|
||||
mActions.clear()
|
||||
val notificationManager = NotificationManagerCompat.from(this)
|
||||
val action = NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
||||
PendingIntent.getBroadcast(this, R.id.notification_import_cancel_action.normalizeID(), Intent(receiver.ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
).build()
|
||||
val notification = NotificationCompat.Builder(this, "import")
|
||||
.setContentTitle(getText(R.string.import_old_galleries_notification))
|
||||
.setProgress(0, 0, true)
|
||||
.addAction(action)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setOngoing(true)
|
||||
|
||||
DownloadService.cancel(this)
|
||||
|
||||
job?.cancel()
|
||||
job = CoroutineScope(Dispatchers.IO).launch {
|
||||
val downloadFolders = downloadFolder.listFiles { folder ->
|
||||
(folder as? FileX)?.isDirectory == true && !downloadFolderMap.values.contains(folder.name)
|
||||
}
|
||||
|
||||
if (downloadFolders.isNullOrEmpty()) return@launch
|
||||
|
||||
downloadFolders.forEachIndexed { index, folder ->
|
||||
notification
|
||||
.setContentText(getString(R.string.import_old_galleries_notification_text, index, downloadFolders.size))
|
||||
.setProgress(index, downloadFolders.size, false)
|
||||
notificationManager.notify(R.id.notification_id_import, notification.build())
|
||||
|
||||
kotlin.runCatching {
|
||||
if (folder !is FileX) return@runCatching
|
||||
|
||||
val metadata = kotlin.runCatching {
|
||||
folder.getChild(".metadata").readText()?.let { Json.parseToJsonElement(it).jsonObject }
|
||||
}.getOrNull()
|
||||
|
||||
val galleryID = folder.name.toIntOrNull() ?: return@runCatching
|
||||
|
||||
val galleryBlock: GalleryBlock? = kotlin.runCatching {
|
||||
metadata?.get("galleryBlock")?.let { Json.decodeFromJsonElement<GalleryBlock>(it) }
|
||||
}.getOrNull() ?: getGalleryBlock(galleryID)
|
||||
val reader: Reader? = kotlin.runCatching {
|
||||
metadata?.get("reader")?.let { Json.decodeFromJsonElement<Reader>(it) }
|
||||
}.getOrNull() ?: getReader(galleryID)
|
||||
|
||||
metadata?.get("thumbnail")?.jsonPrimitive?.contentOrNull?.also { thumbnail ->
|
||||
val file = folder.getChild(".thumbnail").also {
|
||||
if (it.exists())
|
||||
it.delete()
|
||||
it.createNewFile()
|
||||
}
|
||||
|
||||
file.writeBytes(Base64.decode(thumbnail, Base64.DEFAULT))
|
||||
}
|
||||
|
||||
val list: MutableList<String?> =
|
||||
MutableList(reader!!.galleryInfo.files.size) { null }
|
||||
|
||||
folder.listFiles { file ->
|
||||
file?.nameWithoutExtension?.let {
|
||||
Regex("""\d{5}""").matches(it) && it.toIntOrNull() != null
|
||||
} == true
|
||||
}?.forEach {
|
||||
list[it.nameWithoutExtension.toInt()] = it.name
|
||||
}
|
||||
|
||||
folder.getChild(".metadata").also { if (it.exists()) it.delete(); it.createNewFile() }.writeText(
|
||||
Json.encodeToString(Metadata(galleryBlock, reader, list))
|
||||
)
|
||||
|
||||
synchronized(Cache) {
|
||||
Cache.delete(galleryID)
|
||||
}
|
||||
downloadFolderMap[galleryID] = folder.name
|
||||
|
||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile(); it.writeText(Json.encodeToString(downloadFolderMap)) }
|
||||
}
|
||||
}
|
||||
|
||||
notification
|
||||
.setContentText(getText(R.string.import_old_galleries_notification_done))
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.mActions.clear()
|
||||
notificationManager.notify(R.id.notification_id_import, notification.build())
|
||||
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
notificationManager.notify(0, notificationBuilder.build())
|
||||
|
||||
cancelImport = false
|
||||
}
|
||||
|
Before Width: | Height: | Size: 197 B |
|
Before Width: | Height: | Size: 361 B |
|
Before Width: | Height: | Size: 470 B |
|
Before Width: | Height: | Size: 145 B |
|
Before Width: | Height: | Size: 224 B |
|
Before Width: | Height: | Size: 236 B |
|
Before Width: | Height: | Size: 226 B |
|
Before Width: | Height: | Size: 370 B |
|
Before Width: | Height: | Size: 416 B |
|
Before Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 602 B |
|
Before Width: | Height: | Size: 687 B |
8
app/src/main/res/drawable/gender_female_white.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- drawable/gender_female.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,4A6,6 0 0,1 18,10C18,12.97 15.84,15.44 13,15.92V18H15V20H13V22H11V20H9V18H11V15.92C8.16,15.44 6,12.97 6,10A6,6 0 0,1 12,4M12,6A4,4 0 0,0 8,10A4,4 0 0,0 12,14A4,4 0 0,0 16,10A4,4 0 0,0 12,6Z" />
|
||||
</vector>
|
||||
8
app/src/main/res/drawable/gender_male_white.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- drawable/gender_male.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="M9,9C10.29,9 11.5,9.41 12.47,10.11L17.58,5H13V3H21V11H19V6.41L13.89,11.5C14.59,12.5 15,13.7 15,15A6,6 0 0,1 9,21A6,6 0 0,1 3,15A6,6 0 0,1 9,9M9,11A4,4 0 0,0 5,15A4,4 0 0,0 9,19A4,4 0 0,0 13,15A4,4 0 0,0 9,11Z" />
|
||||
</vector>
|
||||
@@ -1,8 +0,0 @@
|
||||
<!-- drawable/export.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="M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z" />
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
|
||||
</vector>
|
||||
22
app/src/main/res/drawable/ic_progressbar_cache.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tintMode="multiply">
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:pathData="M 0 12 L 24 12"
|
||||
android:fillColor="#000"
|
||||
android:strokeColor="#80d8ff"
|
||||
android:strokeWidth="24"/>
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 0 12 L 24 12"
|
||||
android:fillColor="#000"
|
||||
android:strokeColor="#0091ea"
|
||||
android:strokeWidth="24"/>
|
||||
</vector>
|
||||
38
app/src/main/res/drawable/ic_progressbar_complete_cache.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
tools:ignore="NewApi">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tintMode="multiply">
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:pathData="M 0 12 L 24 12"
|
||||
android:fillColor="#000"
|
||||
android:strokeColor="#80d8ff"
|
||||
android:strokeWidth="24"/>
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 0 12 L 24 12"
|
||||
android:fillColor="#000"
|
||||
android:strokeColor="#0091ea"
|
||||
android:strokeWidth="24"/>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="trimPathEnd"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -1,8 +0,0 @@
|
||||
<!-- drawable/sort_variant.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#fff" android:pathData="M3,13H15V11H3M3,6V8H21V6M3,18H9V16H3V18Z" />
|
||||
</vector>
|
||||
@@ -17,9 +17,7 @@
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/main_layout"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/main_layout"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/default_query_dialog_loli_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
|
||||
59
app/src/main/res/layout/dialog_download_folder_name.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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
|
||||
style="?android:textAppearanceLarge"
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_download_folder_name"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edittext"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/message"
|
||||
android:layout_margin="8dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ok_button"
|
||||
style="?borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@android:string/ok"
|
||||
app:layout_constraintTop_toBottomOf="@id/edittext"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -18,6 +18,7 @@
|
||||
-->
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/gallery_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -47,7 +48,8 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/gallery_title"/>
|
||||
app:layout_constraintRight_toLeftOf="@id/gallery_title"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gallery_title"
|
||||
@@ -72,7 +74,6 @@
|
||||
android:layout_marginStart="8dp"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/gallery_padding"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/gallery_artist"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<androidx.cardview.widget.CardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
@@ -34,8 +35,7 @@
|
||||
app:show_mode="pull_out">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/galleryblock_secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
@@ -50,7 +50,8 @@
|
||||
android:text="@string/main_download"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:clickable="true"/>
|
||||
android:clickable="true"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/galleryblock_delete"
|
||||
@@ -64,7 +65,8 @@
|
||||
android:text="@string/main_delete"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:clickable="true"/>
|
||||
android:clickable="true"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -75,7 +77,8 @@
|
||||
android:orientation="vertical"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:clickable="true">
|
||||
android:clickable="true"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
@@ -44,6 +45,7 @@
|
||||
app:tint="?attr/colorControlNormal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -23,9 +23,7 @@
|
||||
<item android:id="@+id/main_menu_thin"
|
||||
android:title="@string/main_menu_thin"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/main_menu_sort"
|
||||
android:title="@string/main_menu_sort">
|
||||
<item android:title="@string/main_menu_sort">
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item android:id="@+id/main_menu_sort_newest"
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
<string name="galleryblock_series">シリーズ: %1$s</string>
|
||||
<string name="galleryblock_type">タイプ: %1$s</string>
|
||||
<string name="main_no_result">結果なし</string>
|
||||
<string name="main_search">検索</string>
|
||||
<string name="search_hint">ギャラリー検索</string>
|
||||
<string name="search_hint_with_page">ギャラリー検索</string>
|
||||
<string name="settings_clear_cache">キャッシュクリア</string>
|
||||
<string name="settings_clear_cache_alert_message">キャッシュをクリアするとイメージのロード速度に影響を与えます。実行しますか?</string>
|
||||
<string name="settings_storage_usage">%s使用中</string>
|
||||
@@ -25,7 +23,6 @@
|
||||
<string name="settings_clear_history_summary">履歴数: %1$d</string>
|
||||
<string name="main_drawer_history">履歴</string>
|
||||
<string name="main_drawer_home">トップ</string>
|
||||
<string name="update_download_started">ダウンロード中</string>
|
||||
<string name="update_release_note"># リリースノート(v%1$s)\n%2$s</string>
|
||||
<string name="settings_security_mode_title">セキュリティーモード</string>
|
||||
<string name="settings_security_mode_summary">アプリ履歴でアプリの画面を表示しない</string>
|
||||
@@ -46,24 +43,16 @@
|
||||
<string name="reader_fab_download">バックグラウンドダウンロード</string>
|
||||
<string name="reader_notification_text">ダウンロード中…</string>
|
||||
<string name="reader_notification_complete">ダウンロード完了</string>
|
||||
<string name="reader_notification_error">ダウンロードエラー</string>
|
||||
<string name="reader_fab_download_cancel">バックグラウンドダウンロード中止</string>
|
||||
<string name="main_dialog_delete">このギャラリーを削除</string>
|
||||
<string name="main_drawer_downloads">ダウンロード</string>
|
||||
<string name="main_jump_title">ページ移動</string>
|
||||
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string>
|
||||
<string name="unable_to_connect">hitomi.laに接続できません</string>
|
||||
<string name="main_move">%1$dページへ移動</string>
|
||||
<string name="https_block_alert_title">(Korean only)</string>
|
||||
<string name="https_block_alert">(Korean only)</string>
|
||||
<string name="main_dialog_export">ギャラリーエクスポート</string>
|
||||
<string name="main_export_complete">エクスポート完了</string>
|
||||
<string name="main_export_open_folder">フォルダを開く</string>
|
||||
<string name="main_export_error">エクスポートエラーが発生しました</string>
|
||||
<string name="settings_clear_downloads">ダウンロード削除</string>
|
||||
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか?</string>
|
||||
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</string>
|
||||
<string name="main_drawer_favorite">お気に入り</string>
|
||||
<string name="main_drawer_favorite">ブックマーク</string>
|
||||
<string name="main_open_gallery_by_id">ギャラリー番号で見る</string>
|
||||
<string name="reader_failed_to_find_gallery">エラーが発生しました</string>
|
||||
<string name="settings_storage">ストレージ</string>
|
||||
@@ -84,11 +73,8 @@
|
||||
<string name="main_menu_sort">ソート</string>
|
||||
<string name="main_menu_sort_newest">投稿日時順</string>
|
||||
<string name="main_menu_sort_popular">人気順</string>
|
||||
<string name="update_failed">アップデートに失敗しました</string>
|
||||
<string name="update_failed_message">アップデート中エラーが発生しました</string>
|
||||
<string name="ignore_update">無視</string>
|
||||
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string>
|
||||
<string name="update_no_permission">権限がないため自動アップデートを行えません。ホームページで直接ダウンロードしてください。</string>
|
||||
<string name="settings_dark_mode_title">ダークモード</string>
|
||||
<string name="settings_dark_mode_summary">夜にシコりたい方々へ</string>
|
||||
<string name="gallery_details">ギャラリー情報</string>
|
||||
@@ -101,27 +87,25 @@
|
||||
<string name="gallery_thumbnails">サムネイル</string>
|
||||
<string name="gallery_related">おすすめ</string>
|
||||
<string name="settings_nomedia_title">イメージを隠す</string>
|
||||
<string name="reader_help">ヘルプ</string>
|
||||
<string name="main_delete">削除</string>
|
||||
<string name="main_download">ダウンロード</string>
|
||||
<string name="settings_backup_title">お気に入りバックアップ</string>
|
||||
<string name="settings_restore_title">お気に入り復元</string>
|
||||
<string name="settings_backup_snackbar">バックアップファイルを作成しました</string>
|
||||
<string name="settings_backup_checkout">確認</string>
|
||||
<string name="settings_backup_title">ブックマークバックアップ</string>
|
||||
<string name="settings_restore_title">ブックマーク復元</string>
|
||||
<string name="settings_backup_file_created">バックアップファイルを作成しました</string>
|
||||
<string name="settings_restore_failed">復元に失敗しました</string>
|
||||
<string name="settings_restore_successful">%1$d項目を復元しました</string>
|
||||
<string name="settings_dl_location">ダウンロード場所</string>
|
||||
<string name="settings_dl_location_internal">内部ストレージ</string>
|
||||
<string name="settings_dl_location_removable">外部SDカード</string>
|
||||
<string name="settings_dl_location_available">%s 使用可能</string>
|
||||
<string name="settings_restore_success">%1$d項目を復元しました</string>
|
||||
<string name="settings_download_folder">ダウンロード場所</string>
|
||||
<string name="settings_download_folder_internal">内部ストレージ</string>
|
||||
<string name="settings_download_folder_removable">外部SDカード</string>
|
||||
<string name="settings_download_folder_available">%s 使用可能</string>
|
||||
<string name="update_download_completed">ダウンロードが完了しました</string>
|
||||
<string name="update_download_completed_description">ここをクリックしてアップデートを行えます</string>
|
||||
<string name="settings_beta">ベータチャンネルでアップデートを受信</string>
|
||||
<string name="settings_app_version_description">v%s</string>
|
||||
<string name="settings_low_quality">低解像度イメージ</string>
|
||||
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
|
||||
<string name="settings_dl_location_custom">手動で設定</string>
|
||||
<string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
|
||||
<string name="settings_download_folder_custom">手動で設定</string>
|
||||
<string name="settings_download_folder_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
|
||||
<string name="settings_proxy_title">プロクシ</string>
|
||||
<string name="proxy_dialog_username_hint">ID</string>
|
||||
<string name="proxy_dialog_type">プロクシタイプ</string>
|
||||
@@ -134,8 +118,6 @@
|
||||
<string name="main_fab_cancel">すべてのダウンロードキャンセル</string>
|
||||
<string name="channel_update">アップデート</string>
|
||||
<string name="channel_update_description">アップデートの進行状態を表示</string>
|
||||
<string name="channel_import">インポート</string>
|
||||
<string name="channel_import_description">インポート状態を表示</string>
|
||||
<string name="settings_import_old_galleries">旧ギャラリーインポート</string>
|
||||
<string name="import_old_galleries_folder_not_readable">フォルダを読めません</string>
|
||||
<string name="import_old_galleries_notification">旧ギャラリーインポート中…</string>
|
||||
@@ -149,7 +131,18 @@
|
||||
<string name="settings_download_when_cache_disable_warning">キャッシュを使用しないため、ダウンロードできません</string>
|
||||
<string name="settings_user_id">ユーザーID</string>
|
||||
<string name="settings_user_id_toast">ユーザーIDをクリップボードにコピーしました</string>
|
||||
<string name="reader_error_retry">ダウンロードエラーが発生しました。リトライしますか?</string>
|
||||
<string name="reader_fab_retry">リトライ</string>
|
||||
<string name="reader_fab_auto">自動スクロール</string>
|
||||
<string name="search_all">全てのギャラリーを対象に検索</string>
|
||||
<string name="settings_rtl">綴じ方向を左にする</string>
|
||||
<string name="settings_manage_favorites">ブックマーク管理</string>
|
||||
<string name="settings_backup_failed">エラーが発生しました</string>
|
||||
<string name="settings_backup_share">バックアップ共有</string>
|
||||
<string name="channel_downloader">ダウンローダ</string>
|
||||
<string name="channel_downloader_description">ダウンローダの状態を表示</string>
|
||||
<string name="downloader_running">ダウンローダー起動中</string>
|
||||
<string name="settings_download_folder_name">フォルダ名パターン</string>
|
||||
<string name="settings_invalid_download_folder_name">フォルダ名に使用できない文字が含まれています</string>
|
||||
<string name="settings_download_folder_name_message">%sに含まれている文字列を対応する変数に置換します\n\n%s</string>
|
||||
<string name="settings_manage_storage">ストレージ管理</string>
|
||||
</resources>
|
||||
@@ -4,7 +4,6 @@
|
||||
<string name="galleryblock_series">시리즈: %1$s</string>
|
||||
<string name="galleryblock_type">종류: %1$s</string>
|
||||
<string name="search_hint">갤러리 검색</string>
|
||||
<string name="search_hint_with_page">갤러리 검색</string>
|
||||
<string name="settings_default_query">기본 검색어</string>
|
||||
<string name="settings_clear_cache">캐시 정리하기</string>
|
||||
<string name="settings_clear_cache_alert_message">캐시를 정리하면 이미지 로딩속도가 느려질 수 있습니다. 계속하시겠습니까?</string>
|
||||
@@ -17,14 +16,12 @@
|
||||
<string name="update_title">업데이트가 있습니다!</string>
|
||||
<string name="warning">경고</string>
|
||||
<string name="main_no_result">결과 없음</string>
|
||||
<string name="main_search">검색</string>
|
||||
<string name="settings_miscellaneous_title">기타</string>
|
||||
<string name="settings_clear_history">기록 삭제</string>
|
||||
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
||||
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
|
||||
<string name="main_drawer_history">기록</string>
|
||||
<string name="main_drawer_home">홈</string>
|
||||
<string name="update_download_started">다운로드 중</string>
|
||||
<string name="update_release_note"># 릴리즈 노트(v%1$s)\n%2$s</string>
|
||||
<string name="settings_security_mode_summary">최근 앱 목록 창에서 앱 화면을 보이지 않게 합니다</string>
|
||||
<string name="settings_security_mode_title">보안 모드 활성화</string>
|
||||
@@ -45,20 +42,12 @@
|
||||
<string name="reader_fab_download">백그라운드 다운로드</string>
|
||||
<string name="reader_notification_text">다운로드 중…</string>
|
||||
<string name="reader_notification_complete">다운로드 완료</string>
|
||||
<string name="reader_notification_error">다운로드 오류</string>
|
||||
<string name="reader_fab_download_cancel">백그라운드 다운로드 취소</string>
|
||||
<string name="main_dialog_delete">갤러리 삭제</string>
|
||||
<string name="main_drawer_downloads">다운로드</string>
|
||||
<string name="main_jump_title">페이지 이동</string>
|
||||
<string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string>
|
||||
<string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string>
|
||||
<string name="main_move">%1$d 페이지로 이동</string>
|
||||
<string name="https_block_alert_title">접속 불가 현상 안내</string>
|
||||
<string name="https_block_alert">최근 https 차단으로 접속이 안 되는 경우가 발생하고 있습니다 이 경우 플레이스토어에서 Intra앱을 이용하시면 정상이용이 가능합니다.</string>
|
||||
<string name="main_dialog_export">갤러리 내보내기</string>
|
||||
<string name="main_export_complete">내보내기 완료</string>
|
||||
<string name="main_export_open_folder">폴더 열기</string>
|
||||
<string name="main_export_error">내보내기 오류가 발생했습니다</string>
|
||||
<string name="settings_clear_downloads">다운로드 삭제</string>
|
||||
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
|
||||
<string name="main_drawer_favorite">즐겨찾기</string>
|
||||
@@ -82,11 +71,8 @@
|
||||
<string name="main_menu_sort">정렬</string>
|
||||
<string name="main_menu_sort_popular">인기순</string>
|
||||
<string name="main_menu_sort_newest">시간순</string>
|
||||
<string name="update_failed">"업데이트 에러</string>
|
||||
<string name="update_failed_message">업데이트 중 에러가 발생했습니다</string>
|
||||
<string name="ignore_update">무시</string>
|
||||
<string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string>
|
||||
<string name="update_no_permission">권한이 부여되어 있지 않아 자동 업데이트를 진행할 수 없습니다. 홈페이지에서 직접 다운로드 받으시기 바랍니다.</string>
|
||||
<string name="settings_dark_mode_title">다크 모드</string>
|
||||
<string name="settings_dark_mode_summary">딥 다크한 모오드</string>
|
||||
<string name="gallery_details">갤러리 정보</string>
|
||||
@@ -99,19 +85,17 @@
|
||||
<string name="gallery_related">관련 갤러리</string>
|
||||
<string name="gallery_thumbnails">미리보기</string>
|
||||
<string name="settings_nomedia_title">이미지 숨기기</string>
|
||||
<string name="reader_help">도움말</string>
|
||||
<string name="main_delete">삭제</string>
|
||||
<string name="main_download">다운로드</string>
|
||||
<string name="settings_backup_title">즐겨찾기 백업</string>
|
||||
<string name="settings_restore_title">즐겨찾기 복원</string>
|
||||
<string name="settings_backup_snackbar">백업 파일을 생성하였습니다</string>
|
||||
<string name="settings_backup_checkout">확인</string>
|
||||
<string name="settings_backup_file_created">백업 파일을 생성하였습니다</string>
|
||||
<string name="settings_restore_failed">복원에 실패했습니다</string>
|
||||
<string name="settings_restore_successful">%1$d개 항목을 복원했습니다</string>
|
||||
<string name="settings_dl_location">다운로드 위치</string>
|
||||
<string name="settings_dl_location_internal">내부 저장공간</string>
|
||||
<string name="settings_dl_location_removable">외부 SD카드</string>
|
||||
<string name="settings_dl_location_available">%s 사용 가능</string>
|
||||
<string name="settings_restore_success">%1$d개 항목을 복원했습니다</string>
|
||||
<string name="settings_download_folder">다운로드 위치</string>
|
||||
<string name="settings_download_folder_internal">내부 저장공간</string>
|
||||
<string name="settings_download_folder_removable">외부 SD카드</string>
|
||||
<string name="settings_download_folder_available">%s 사용 가능</string>
|
||||
<string name="update_download_completed">다운로드가 완료되었습니다</string>
|
||||
<string name="update_download_completed_description">여기를 클릭해서 업데이트를 진행할 수 있습니다</string>
|
||||
<string name="settings_beta">베타 채널에서 업데이트</string>
|
||||
@@ -120,8 +104,8 @@
|
||||
<string name="settings_low_quality_summary">로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드</string>
|
||||
<string name="settings_mirror_summary">미러 서버에서 이미지 로드</string>
|
||||
<string name="settings_mirror_title">미러 설정</string>
|
||||
<string name="settings_dl_location_custom">직접 설정</string>
|
||||
<string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
|
||||
<string name="settings_download_folder_custom">직접 설정</string>
|
||||
<string name="settings_download_folder_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
|
||||
<string name="settings_proxy_title">프록시</string>
|
||||
<string name="proxy_dialog_username_hint">ID</string>
|
||||
<string name="proxy_dialog_type">프록시 타입</string>
|
||||
@@ -134,8 +118,6 @@
|
||||
<string name="main_fab_cancel">다운로드 모두 취소</string>
|
||||
<string name="channel_update">업데이트</string>
|
||||
<string name="channel_update_description">업데이트 진행상황 표시</string>
|
||||
<string name="channel_import">가져오기</string>
|
||||
<string name="channel_import_description">가져오기 상태 표시</string>
|
||||
<string name="settings_import_old_galleries">이전 버전 갤러리 가져오기</string>
|
||||
<string name="import_old_galleries_folder_not_readable">폴더를 읽을 수 없습니다</string>
|
||||
<string name="import_old_galleries_notification">이전 버전 갤러리 가져오는 중…</string>
|
||||
@@ -149,7 +131,18 @@
|
||||
<string name="settings_download_when_cache_disable_warning">캐시를 활성화 해야 다운로드를 진행할 수 있습니다</string>
|
||||
<string name="settings_user_id">유저 ID</string>
|
||||
<string name="settings_user_id_toast">유저 ID를 클립보드에 복사했습니다</string>
|
||||
<string name="reader_error_retry">다운로드 에러가 발생했습니. 재시도 하시겠습니까?</string>
|
||||
<string name="reader_fab_retry">재시도</string>
|
||||
<string name="reader_fab_auto">자동 스크롤</string>
|
||||
<string name="search_all">모든 갤러리 검색</string>
|
||||
<string name="settings_rtl">좌측으로 페이지 넘기기</string>
|
||||
<string name="settings_manage_favorites">즐겨찾기 관리</string>
|
||||
<string name="settings_backup_failed">업로드 실패</string>
|
||||
<string name="settings_backup_share">백업 공유</string>
|
||||
<string name="channel_downloader">다운로더</string>
|
||||
<string name="channel_downloader_description">다운로더 작동 여부 표시</string>
|
||||
<string name="downloader_running">다운로더 작동중…</string>
|
||||
<string name="settings_download_folder_name">폴더명 패턴</string>
|
||||
<string name="settings_invalid_download_folder_name">폴더 패턴에 사용할 수 없는 문자가 포함되어 있습니다</string>
|
||||
<string name="settings_download_folder_name_message">지원되는 변수는 %s 입니다\n\n%s</string>
|
||||
<string name="settings_manage_storage">저장소 관리</string>
|
||||
</resources>
|
||||
@@ -6,16 +6,6 @@
|
||||
<item>25</item>
|
||||
<item>50</item>
|
||||
</string-array>
|
||||
<!-- Reply Preference -->
|
||||
<string-array name="reply_entries">
|
||||
<item>Reply</item>
|
||||
<item>Reply to all</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="reply_values">
|
||||
<item>reply</item>
|
||||
<item>reply_all</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="languages">
|
||||
<item>indonesian|Bahasa Indonesia</item>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<color name="colorPrimary">#4fc3f7</color>
|
||||
<color name="colorPrimaryDark">#0093c4</color>
|
||||
<color name="colorAccent">#D81B60</color>
|
||||
<color name="appbar">#FFFFFF</color>
|
||||
|
||||
<color name="material_pink_600">#d81b60</color>
|
||||
<color name="material_blue_700">#1976d2</color>
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="appbar_padding">64dp</dimen>
|
||||
<dimen name="progress_view_start">32dp</dimen>
|
||||
<dimen name="progress_view_offset">96dp</dimen>
|
||||
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
<dimen name="nav_header_vertical_spacing">8dp</dimen>
|
||||
<dimen name="nav_header_height">176dp</dimen>
|
||||
|
||||
<dimen name="thumbnail_margin">8dp</dimen>
|
||||
|
||||
<dimen name="galleryblock_thumbnail_thin">100dp</dimen>
|
||||
<dimen name="galleryblock_thumbnail_normal">150dp</dimen>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,4 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="item_click_support" type="id" />
|
||||
<item name="request_settings" type="id" />
|
||||
<item name="request_lock" type="id" />
|
||||
<item name="request_restore" type="id" />
|
||||
|
||||
<item name="request_download_folder" type="id" />
|
||||
<item name="request_download_folder_old" type="id" />
|
||||
<item name="request_write_permission_and_saf" type="id" />
|
||||
|
||||
<item name="notification_id_update" type="id" />
|
||||
<item name="notification_id_import" type="id" />
|
||||
|
||||
<item name="downloader_notification_id" type="id" />
|
||||
<item name="downloader_notification_request" type="id" />
|
||||
|
||||
<item name="notification_download_cancel_action" type="id" />
|
||||
<item name="notification_import_cancel_action" type="id" />
|
||||
</resources>
|
||||
@@ -2,15 +2,14 @@
|
||||
<string name="app_name" translatable="false" tools:override="true">Pupil</string>
|
||||
|
||||
<string name="release_url" translatable="false">https://api.github.com/repos/tom5079/Pupil/releases</string>
|
||||
<string name="release_name" translatable="false">Pupil-v(\\d+\\.)+\\d+\\.apk</string>
|
||||
|
||||
<string name="home_page" translatable="false">http://bit.ly/2EZDClw</string>
|
||||
<string name="update" translatable="false">http://bit.ly/2ZlOjXJ</string>
|
||||
<string name="help" translatable="false">http://bit.ly/2Z7lNZE</string>
|
||||
<string name="github" translatable="false">https://github.com/tom5079/Pupil/</string>
|
||||
<string name="email" translatable="false">mailto:pupil.hentai@gmail.com</string>
|
||||
<string name="discord" translatable="false">https://discord.gg/Stj4b5v</string>
|
||||
<string name="error_help" translatable="false">http://bit.ly/2KYYhto</string>
|
||||
|
||||
<string name="backup_url" translatable="false">http://ix.io/</string>
|
||||
|
||||
<string name="main_settings" translatable="false">Settings</string>
|
||||
<string name="galleryblock_thumbnail_description" translatable="false">Thumbnail</string>
|
||||
@@ -18,34 +17,25 @@
|
||||
<string name="reader_imageview_description" translatable="false">Content ImageView</string>
|
||||
<string name="page_indicator_placeholder" translatable="false">-/-</string>
|
||||
|
||||
<string name="plus_to_close" translatable="false">Fab</string>
|
||||
|
||||
<!-- Translate needed down here -->
|
||||
|
||||
<string name="warning">Warning</string>
|
||||
|
||||
<string name="https_block_alert_title">(Korean only)</string>
|
||||
<string name="https_block_alert">(Korean only)</string>
|
||||
|
||||
<string name="update_failed">Update failed</string>
|
||||
<string name="update_failed_message">Please install manually by visiting github release page :{ (or try again!)</string>
|
||||
<string name="update_no_permission">Cannot auto update because permission is denied. Please download manually from the webpage.</string>
|
||||
<string name="ignore_update">Ignore</string>
|
||||
|
||||
<string name="channel_download">Download</string>
|
||||
<string name="channel_download_description">Shows download status</string>
|
||||
|
||||
<string name="channel_downloader">Downloader</string>
|
||||
<string name="channel_downloader_description">Shows downloader status</string>
|
||||
|
||||
<string name="channel_update">Update</string>
|
||||
<string name="channel_update_description">Shows update progress</string>
|
||||
|
||||
<string name="channel_import">Import</string>
|
||||
<string name="channel_import_description">Shows progress of import</string>
|
||||
|
||||
<string name="unable_to_connect">Unable to connect to hitomi.la</string>
|
||||
|
||||
|
||||
<string name="lock_corrupted">Lock file corrupted! Please re-install Pupil</string>
|
||||
|
||||
<string name="main_search">Search</string>
|
||||
<string name="main_no_result">No result</string>
|
||||
|
||||
<string name="main_drawer_home">Home</string>
|
||||
@@ -74,25 +64,17 @@
|
||||
|
||||
<string name="main_move">Move to page %1$d</string>
|
||||
|
||||
<string name="main_dialog_delete">Delete this gallery</string>
|
||||
<string name="main_dialog_export">Export this gallery</string>
|
||||
|
||||
<string name="main_export_complete">Export completed</string>
|
||||
<string name="main_export_open_folder">Open Folder</string>
|
||||
<string name="main_export_error">Error occurred during export</string>
|
||||
|
||||
<string name="main_download">DOWNLOAD</string>
|
||||
<string name="main_delete">DELETE</string>
|
||||
|
||||
<string name="update_title">Update available</string>
|
||||
<string name="update_download_started">Download started</string>
|
||||
<string name="update_download_completed">Download Completed</string>
|
||||
<string name="update_download_completed_description">Click here to update</string>
|
||||
<string name="update_notification_description">Downloading update…</string>
|
||||
<string name="update_release_note"># Release Note(v%1$s)\n%2$s</string>
|
||||
|
||||
<string name="search_hint">Search galleries</string>
|
||||
<string name="search_hint_with_page">Search galleries</string>
|
||||
<string name="search_all">Search all galleries</string>
|
||||
|
||||
<string name="gallery_details">Details</string>
|
||||
<string name="gallery_thumbnails">Thumbnails</string>
|
||||
@@ -120,11 +102,9 @@
|
||||
<string name="reader_fab_download_cancel">Cancel background download</string>
|
||||
<string name="reader_notification_text">Downloading…</string>
|
||||
<string name="reader_notification_complete">Download complete</string>
|
||||
<string name="reader_notification_error">Download error</string>
|
||||
|
||||
<string name="reader_error_retry">Download Error. Retry?</string>
|
||||
|
||||
<string name="reader_help">Help</string>
|
||||
<!-- DOWNLOADER -->
|
||||
<string name="downloader_running">Downloader running…</string>
|
||||
|
||||
<!-- SETTINGS -->
|
||||
|
||||
@@ -143,6 +123,10 @@
|
||||
<!-- SETTINGS/STORAGE -->
|
||||
|
||||
<string name="settings_storage">Storage</string>
|
||||
|
||||
<!-- SETTINGS/STORAGE / MANAGE STORAGE -->
|
||||
|
||||
<string name="settings_manage_storage">Manage Storage</string>
|
||||
<string name="settings_storage_usage">Currently using %s</string>
|
||||
<string name="settings_storage_usage_loading">Calculating storage usage…</string>
|
||||
<string name="settings_clear_cache">Clear cache</string>
|
||||
@@ -152,12 +136,18 @@
|
||||
<string name="settings_clear_history">Clear history</string>
|
||||
<string name="settings_clear_history_alert_message">Do you want to clear histories?</string>
|
||||
<string name="settings_clear_history_summary">%1$d histories saved</string>
|
||||
<string name="settings_dl_location">Download directory</string>
|
||||
<string name="settings_dl_location_removable">Removable Storage</string>
|
||||
<string name="settings_dl_location_internal">Internal Storage</string>
|
||||
<string name="settings_dl_location_available">%s available</string>
|
||||
<string name="settings_dl_location_custom">Custom Location</string>
|
||||
<string name="settings_dl_location_not_writable">This folder is not writable. Please select another folder.</string>
|
||||
|
||||
<!-- SETTINGS/STORAGE / MISCELLANEOUS -->
|
||||
|
||||
<string name="settings_download_folder_name">Folder naming pattern</string>
|
||||
<string name="settings_invalid_download_folder_name">Folder naming pattern is containing invalid characters</string>
|
||||
<string name="settings_download_folder_name_message">%s will be replaced to its corresponding value\n\n%s</string>
|
||||
<string name="settings_download_folder">Download folder</string>
|
||||
<string name="settings_download_folder_removable">Removable Storage</string>
|
||||
<string name="settings_download_folder_internal">Internal Storage</string>
|
||||
<string name="settings_download_folder_available">%s available</string>
|
||||
<string name="settings_download_folder_custom">Custom Location</string>
|
||||
<string name="settings_download_folder_not_writable">This folder is not writable. Please select another folder.</string>
|
||||
<string name="settings_cache_disable">Disable Cache</string>
|
||||
<string name="settings_download_when_cache_disable_warning">Download is disabled when the cache is disabled</string>
|
||||
<string name="settings_low_quality">Low quality images</string>
|
||||
@@ -173,21 +163,27 @@
|
||||
<string name="settings_miscellaneous_title">Miscellaneous</string>
|
||||
<string name="settings_mirror_summary">Load images from mirrors</string>
|
||||
<string name="settings_proxy_title">Proxy</string>
|
||||
<string name="settings_rtl">Turn pages Right-to-Left</string>
|
||||
<string name="settings_security_mode_title">Enable security mode</string>
|
||||
<string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string>
|
||||
<string name="settings_dark_mode_title">Dark mode</string>
|
||||
<string name="settings_dark_mode_summary">Protect yourself against light attacks!</string>
|
||||
<string name="settings_nomedia_title">Hide image from gallery</string>
|
||||
<string name="settings_backup_title">Backup favorites</string>
|
||||
<string name="settings_backup_snackbar">Backup file created</string>
|
||||
<string name="settings_backup_checkout">Check out</string>
|
||||
<string name="settings_restore_title">Restore favorites</string>
|
||||
<string name="settings_restore_failed">Restore failed</string>
|
||||
<string name="settings_restore_successful">%1$d entries restored</string>
|
||||
<string name="settings_import_old_galleries">Import old galleries</string>
|
||||
<string name="settings_user_id">User ID</string>
|
||||
<string name="settings_user_id_toast">User ID is copied to clipboard</string>
|
||||
|
||||
<!-- MANAGE FAVORITES -->
|
||||
|
||||
<string name="settings_manage_favorites">Manage favorites</string>
|
||||
<string name="settings_backup_title">Backup favorites</string>
|
||||
<string name="settings_backup_failed">Upload Failed</string>
|
||||
<string name="settings_backup_share">Share Backup</string>
|
||||
<string name="settings_backup_file_created">Backup file created</string>
|
||||
<string name="settings_restore_title">Restore favorites</string>
|
||||
<string name="settings_restore_failed">Restore failed</string>
|
||||
<string name="settings_restore_success">%1$d entries restored</string>
|
||||
|
||||
<!-- SETTINGS/APP LOCK ACTIVITY -->
|
||||
|
||||
<string name="settings_lock_none">None</string>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen
|
||||
<PreferenceScreen
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Preference
|
||||
@@ -23,4 +23,4 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Pupil, Hitomi.la viewer for Android
|
||||
~ Copyright (C) 2019 tom5079
|
||||
~ Copyright (C) 2020 tom5079
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
@@ -17,12 +17,14 @@
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"
|
||||
android:layout_width="match_parent" android:layout_height="wrap_content">
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"/>
|
||||
<Preference
|
||||
app:key="backup"
|
||||
app:title="@string/settings_backup_title"/>
|
||||
|
||||
</LinearLayout>
|
||||
<Preference
|
||||
app:key="restore"
|
||||
app:title="@string/settings_restore_title"/>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Pupil, Hitomi.la viewer for Android
|
||||
~ Copyright (C) 2019 tom5079
|
||||
~ Copyright (C) 2020 tom5079
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
@@ -17,19 +17,18 @@
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/gallery_favorite"
|
||||
android:icon="@drawable/ic_star_empty"
|
||||
android:title=""
|
||||
app:showAsAction="ifRoom"/>
|
||||
<Preference
|
||||
app:key="delete_cache"
|
||||
app:title="@string/settings_clear_cache"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/gallery_download"
|
||||
android:icon="@drawable/ic_download"
|
||||
android:title=""
|
||||
app:showAsAction="always"/>
|
||||
<Preference
|
||||
app:key="delete_downloads"
|
||||
app:title="@string/settings_clear_downloads"/>
|
||||
|
||||
</menu>
|
||||
<Preference
|
||||
app:key="clear_history"
|
||||
app:title="@string/settings_clear_history"/>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Pupil, Hitomi.la viewer for Android
|
||||
~ Copyright (C) 2019 tom5079
|
||||
~ Copyright (C) 2020 tom5079
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
@@ -17,7 +17,9 @@
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="3"/>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">ix.io</domain>
|
||||
</domain-config>
|
||||
|
||||
</network-security-config>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen
|
||||
<PreferenceScreen
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Preference
|
||||
@@ -32,25 +32,26 @@
|
||||
app:title="@string/settings_storage">
|
||||
|
||||
<Preference
|
||||
app:key="delete_cache"
|
||||
app:title="@string/settings_clear_cache"/>
|
||||
app:fragment="xyz.quaver.pupil.ui.fragment.ManageStorageFragment"
|
||||
app:title="@string/settings_manage_storage" />
|
||||
|
||||
<Preference
|
||||
app:key="delete_downloads"
|
||||
app:title="@string/settings_clear_downloads"/>
|
||||
app:key="download_folder_name"
|
||||
app:title="@string/settings_download_folder_name"
|
||||
app:defaultValue="[-id-] -title-"/>
|
||||
|
||||
<Preference
|
||||
app:key="clear_history"
|
||||
app:title="@string/settings_clear_history"/>
|
||||
|
||||
<Preference
|
||||
app:key="dl_location"
|
||||
app:title="@string/settings_dl_location"/>
|
||||
app:key="download_folder"
|
||||
app:title="@string/settings_download_folder"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="cache_disable"
|
||||
app:title="@string/settings_cache_disable"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="nomedia"
|
||||
app:title="@string/settings_nomedia_title"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="low_quality"
|
||||
app:title="@string/settings_low_quality"
|
||||
@@ -80,11 +81,15 @@
|
||||
app:key="proxy"
|
||||
app:title="@string/settings_proxy_title"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="rtl"
|
||||
app:title="@string/settings_rtl"
|
||||
app:defaultValue="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="security_mode"
|
||||
app:title="@string/settings_security_mode_title"
|
||||
app:summary="@string/settings_security_mode_summary"
|
||||
app:defaultValue="true"/>
|
||||
app:summary="@string/settings_security_mode_summary"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="dark_mode"
|
||||
@@ -92,20 +97,8 @@
|
||||
app:summary="@string/settings_dark_mode_summary"/>
|
||||
|
||||
<Preference
|
||||
app:key="nomedia"
|
||||
app:title="@string/settings_nomedia_title"/>
|
||||
|
||||
<Preference
|
||||
app:key="backup"
|
||||
app:title="@string/settings_backup_title"/>
|
||||
|
||||
<Preference
|
||||
app:key="restore"
|
||||
app:title="@string/settings_restore_title"/>
|
||||
|
||||
<Preference
|
||||
app:key="old_import_galleries"
|
||||
app:title="@string/settings_import_old_galleries"/>
|
||||
app:fragment="xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment"
|
||||
app:title="@string/settings_manage_favorites"/>
|
||||
|
||||
<Preference
|
||||
app:key="user_id"
|
||||
@@ -113,4 +106,4 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
@@ -26,14 +26,14 @@ package xyz.quaver.pupil
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
|
||||
import android.util.SparseArray
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.Test
|
||||
|
||||
class ExampleUnitTest {
|
||||
|
||||
@Test
|
||||
fun test() {
|
||||
val arr = SparseArray<Float>()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.72'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
@@ -14,7 +13,7 @@ buildscript {
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.1'
|
||||
classpath 'com.google.firebase:perf-plugin:1.3.1'
|
||||
}
|
||||
}
|
||||
@@ -23,6 +22,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
maven { url "https://jitpack.io" }
|
||||
maven { url 'https://guardian.github.com/maven/repo-releases' }
|
||||
}
|
||||
|
||||
@@ -11,7 +11,12 @@
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
#Thu Jan 30 12:29:48 KST 2020
|
||||
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||
org.gradle.parallel=true
|
||||
org.gradle.daemon=true
|
||||
org.gradle.configureondemand=true
|
||||
kotlin.code.style=official
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
|
||||
android.useAndroidX=true
|
||||
android.useAndroidX=true
|
||||
|
||||
kotlin_version=1.4.0
|
||||
1
libpupil/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
202
libpupil/LICENSE
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
@@ -1,36 +0,0 @@
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0"
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
compileKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
|
||||
}
|
||||
}
|
||||
compileTestKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||