Compare commits

..

70 Commits
4.20 ... 5.0.2

Author SHA1 Message Date
tom5079
1521bc1223 Downloader Bug fix
UI Optimized
Scroller autohide, track disable
2020-09-13 16:19:32 +09:00
tom5079
df3a478ef3 Bug fix 2020-09-12 20:22:34 +09:00
tom5079
974ddf69d5 Bug fix 2020-09-12 19:33:13 +09:00
tom5079
56a91268de Fixed fastscroll 2020-09-12 19:10:15 +09:00
tom5079
3dda2f9a1c Dependency update 2020-09-12 12:35:10 +09:00
tom5079
ed20456f9f VD Optimization 2020-09-12 12:17:02 +09:00
tom5079
281d4a0023 Adjusted Fastscroll UI
VD Optimized
2020-09-12 11:23:38 +09:00
tom5079
2170403662 Fixed Image UI 2020-09-12 10:32:23 +09:00
tom5079
b1c1e96135 Smol fix 2020-09-11 20:45:31 +09:00
tom5079
a8de1429c1 Bug fix 2020-09-11 19:53:49 +09:00
tom5079
3ba6cb81ae Bug fix 2020-09-11 19:40:56 +09:00
tom5079
acc85da80f Bug fix 2020-09-10 22:47:57 +09:00
tom5079
b53de8624d Bug fix 2020-09-10 22:45:27 +09:00
tom5079
6e2eeb29cc Bug fix 2020-09-10 21:41:57 +09:00
tom5079
62eb28ac01 Bug fix 2020-09-10 19:44:08 +09:00
tom5079
fd298529bf Memory usage optimization 2020-09-10 19:16:42 +09:00
tom5079
297ce506b1 Bug fix 2020-09-10 17:03:17 +09:00
tom5079
18c6954be3 Improved Suggestions
resolves #100
2020-09-10 16:50:26 +09:00
tom5079
cea3fb1e65 Bug fix 2020-09-09 16:58:01 +09:00
tom5079
7f274fd238 Added OSS Notice 2020-09-09 13:06:32 +09:00
tom5079
439a8e93ec App built 2020-09-09 11:25:53 +09:00
tom5079
83801feee9 Bug fix 2020-09-09 11:23:22 +09:00
tom5079
8a6860c96e Import cleanup 2020-09-09 09:29:33 +09:00
tom5079
5c959f2987 Bug fix 2020-09-09 09:29:33 +09:00
tom5079
4e4397287a Implemented fast scroll 2020-09-09 09:22:17 +09:00
tom5079
fe02abc9e8 Bug fix 2020-09-08 20:04:12 +09:00
tom5079
59347ab317 Bug fix 2020-09-08 19:16:15 +09:00
tom5079
f408a91176 Bug fix 2020-09-07 10:00:10 +09:00
tom5079
6f6956ce27 Fixed DownloadLocationDialogFragment keep showing up when any button is clicked 2020-09-06 17:19:18 +09:00
tom5079
4ecad8eccc Fixed migration 2020-09-05 18:31:53 +09:00
tom5079
486fbe46a0 Fixed migration 2020-09-05 18:11:20 +09:00
tom5079
1ddb636dd0 Fixed migration 2020-09-05 18:00:15 +09:00
tom5079
081c890b4e Bug fix 2020-09-05 12:51:41 +09:00
tom5079
86d528ba13 v5.0-beta1 2020-09-05 09:13:03 +09:00
tom5079
6bda3cb75a Performance improvement 2020-09-05 08:57:10 +09:00
tom5079
12d8949c9e Fixed context not attached error 2020-09-05 08:31:30 +09:00
tom5079
ffc7c2aa67 Bug fix 2020-09-04 11:05:29 +09:00
tom5079
5ec67488eb Bug fix 2020-09-04 07:51:55 +09:00
tom5079
be64703d3c Minor bug fix 2020-09-03 18:28:40 +09:00
tom5079
705925a050 Migration bug fix 2020-09-03 15:49:04 +09:00
tom5079
29665be34d Added Migration 2020-09-03 10:54:07 +09:00
tom5079
1edf986acf Added Download Folder Dialog 2020-09-02 21:59:17 +09:00
tom5079
37be8ccf7f Bug fix 2020-09-02 20:15:26 +09:00
tom5079
ead68b5201 Moved storage managing settings to another fragment 2020-09-02 13:28:05 +09:00
tom5079
4409664698 nomedia 2020-09-02 13:05:36 +09:00
tom5079
fc6bc7965c Added download folder name change 2020-09-02 12:58:31 +09:00
tom5079
f70eccb1da Created TagChip.kt 2020-09-02 12:29:34 +09:00
tom5079
861994e804 Fixed crash when download folder is changed 2020-09-02 12:15:37 +09:00
tom5079
2b8facfb97 End service when completed
Auto redownload
2020-09-02 11:03:46 +09:00
tom5079
9583897ada working 2020-09-02 00:13:25 +09:00
tom5079
7704c96955 what i got so far 2020-09-01 18:07:16 +09:00
tom5079
c96d609803 Created new cache/downloader 2020-08-31 11:41:12 +09:00
tom5079
aa0e5000ab Deleted redundant code 2020-08-30 20:12:06 +09:00
tom5079
7ca4418a50 Added global preferences object 2020-08-30 19:52:51 +09:00
tom5079
fdd9b02388 (Almost) Full OkHTTP implementation 2020-08-28 22:43:47 +09:00
tom5079
ece127e982 Fixed downloader not fully cancelled after DownloadWorker.cancel() is called 2020-08-28 21:51:44 +09:00
tom5079
5488e14f32 Moved history adding code to ReaderActivity.kt 2020-08-26 23:47:07 +09:00
tom5079
3558d826fb Implemented new favorite backup/restore feature 2020-08-26 23:40:36 +09:00
tom5079
68c94d1d8b Removed old features 2020-08-26 20:00:30 +09:00
tom5079
1a4ae5dfc6 Dependency update 2020-08-26 19:19:26 +09:00
tom5079
1a95afe266 Implemented Right-to-Left pageturn 2020-08-26 18:47:27 +09:00
tom5079
6579db3cc8 Added Search all galleries feature 2020-08-26 18:36:59 +09:00
tom5079
ceac01533a Updated History.kt to use kotlin's delegate feature 2020-08-26 18:01:12 +09:00
tom5079
216914882c Separated libpupil to standalone repository
Migrated to Kotlin 1.4
2020-08-23 20:26:23 +09:00
tom5079
735dbab695 Build optimization 2020-08-10 01:53:11 +09:00
tom5079
dbaab152ef Build optimization 2020-08-10 01:32:56 +09:00
tom5079
9da1b30984 i'm dumb 2020-08-08 16:29:04 +09:00
tom5079
9415ab4ef9 Fixed some images crashing
Added auto pageturn timer
2020-08-08 15:55:17 +09:00
tom5079
647294daf2 what were i thinking?! 2020-08-04 17:50:07 +09:00
tom5079
6ebc386474 Fixed app crashing when deleting cache/download 2020-08-04 12:14:14 +09:00
169 changed files with 3920 additions and 4364 deletions

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@
#Private files
**/google-services.json
**/credentials.json

View File

@@ -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">

View File

@@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value=" Copyright &amp;#36;today.year tom5079&#10;&#10; Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10; you may not use this file except in compliance with the License.&#10; You may obtain a copy of the License at&#10;&#10; http://www.apache.org/licenses/LICENSE-2.0&#10;&#10; Unless required by applicable law or agreed to in writing, software&#10; distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10; See the License for the specific language governing permissions and&#10; limitations under the License." />
<option name="myName" value="Apache" />
</copyright>
</component>

View File

@@ -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
View File

@@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="tom50">
<words>
<w>hitomi</w>
</words>
</dictionary>
</component>

1
.idea/gradle.xml generated
View File

@@ -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" />

View File

@@ -51,5 +51,15 @@
<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>
<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>

View File

@@ -1,3 +0,0 @@
<component name="DependencyValidationManager">
<scope name="libpupil" pattern="file[libpupil]:*/" />
</component>

View File

@@ -3,6 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization'
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) {
logger.lifecycle("Firebase Enabled")
@@ -14,32 +15,41 @@ if (file("google-services.json").exists() && file("src/debug/google-services.jso
}
android {
compileSdkVersion 29
compileSdkVersion 30
defaultConfig {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
targetSdkVersion 29
versionCode 57
versionName "4.20"
targetSdkVersion 30
versionCode 59
versionName "5.0.2"
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 +60,33 @@ 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.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
//implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC"
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.google.android.gms:play-services-oss-licenses:17.0.0'
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4'
//implementation 'com.quiph.ui:recyclerviewfastscroller:0.2.1'
//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,13 +98,16 @@ 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.6") {
exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-serialization-core-jvm'
}
implementation "xyz.quaver:documentfilex:0.2.15"
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 {

Binary file not shown.

Binary file not shown.

View File

@@ -36,3 +36,16 @@
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.SerializationKt
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; } # <-- change package name to your app's
-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

View File

@@ -11,8 +11,8 @@
"type": "SINGLE",
"filters": [],
"properties": [],
"versionCode": 57,
"versionName": "4.20",
"versionCode": 59,
"versionName": "5.0.1-hotfix2",
"enabled": true,
"outputFile": "app-release.apk"
}

View File

@@ -20,25 +20,10 @@
package xyz.quaver.pupil
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.getDownloadDirectory
import java.io.InputStreamReader
import java.net.URL
import javax.net.ssl.HttpsURLConnection
/**
* Instrumented test, which will execute on an Android device.
@@ -53,72 +38,4 @@ class ExampleInstrumentedTest {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
}
@Test
fun checkCacheDir() {
val activityTestRule = ActivityTestRule(LockActivity::class.java)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Runtime.getRuntime().exec("du -hs " + getDownloadDirectory(appContext)).let {
InputStreamReader(it.inputStream).readLines().forEach { res ->
Log.i("PUPILD", res)
}
}
}
@Test
fun test_nozomi() {
val nozomi = getGalleryIDsFromNozomi(null, "index", "all")
Log.i("PUPILD", nozomi.size.toString())
}
@Test
fun test_doSearch() {
val reader = getReader( 1426382)
val data: ByteArray
with(URL(createImgList(1426382, reader)[0].path).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie)
data = inputStream.readBytes()
}
Log.d("Pupil", data.size.toString())
}
@Test
fun test_downloadWorker() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val galleryID = 515515
val worker = DownloadWorker.getInstance(context)
worker.queue.add(galleryID)
while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) {
Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null")
if (worker.progress[galleryID]?.all { it.isInfinite() } == true)
break
}
Log.i("PUPILD", "DONE!!")
}
@Test
fun test_getReaderOrNull() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val galleryID = 1561552
runBlocking {
Log.i("PUPILD", Cache(context).getReader(galleryID)?.galleryInfo?.title ?: "null")
}
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.title ?: "null")
}
}

View File

@@ -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>

View File

@@ -19,13 +19,199 @@
package com.arlib.floatingsearchview
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Animatable
import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.suggestions.SearchSuggestionsAdapter
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView
import xyz.quaver.pupil.R
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.types.*
import java.util.*
class FloatingSearchViewDayNight @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null)
: FloatingSearchView(context, attrs) {
class FloatingSearchViewDayNight @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FloatingSearchView(context, attrs),
FloatingSearchView.OnSearchListener,
SearchSuggestionsAdapter.OnBindSuggestionCallback,
TextWatcher
{
private val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
var onHistoryDeleteClickedListener: ((String) -> Unit)? = null
var onFavoriteHistorySwitchClickListener: (() -> Unit)? = null
init {
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
searchInputView.addTextChangedListener(this)
setOnSearchListener(this)
setOnBindSuggestionCallback(this)
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
s ?: return
if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
}
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
when (searchSuggestion) {
is TagSuggestion -> {
with(searchInputView.text) {
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length)
append("${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")} ")
}
}
is Suggestion -> {
with(searchInputView.text) {
clear()
append(searchSuggestion.str)
}
}
is FavoriteHistorySwitch -> onFavoriteHistorySwitchClickListener?.invoke()
}
}
override fun onSearchAction(currentQuery: String?) {}
override fun onBindSuggestion(
suggestionView: View?,
leftIcon: ImageView?,
textView: TextView?,
item: SearchSuggestion?,
itemPosition: Int
) {
when(item) {
is TagSuggestion -> {
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
leftIcon?.setImageDrawable(
ResourcesCompat.getDrawable(
resources,
when(item.n) {
"female" -> R.drawable.gender_female
"male" -> R.drawable.gender_male
"language" -> R.drawable.translate
"group" -> R.drawable.account_group
"character" -> R.drawable.account_star
"series" -> R.drawable.book_open
"artist" -> R.drawable.brush
else -> R.drawable.tag
},
context.theme)
)
with(suggestionView?.findViewById<ImageView>(R.id.right_icon)) {
this ?: return@with
if (favoriteTags.contains(Tag.parse(tag)))
setImageResource(R.drawable.ic_star_filled)
else
setImageResource(R.drawable.ic_star_empty)
visibility = View.VISIBLE
rotation = 0f
isEnabled = true
isClickable = true
setOnClickListener {
val tag = Tag.parse(tag)
if (favoriteTags.contains(tag)) {
setImageResource(R.drawable.ic_star_empty)
favoriteTags.remove(tag)
}
else {
setImageDrawable(
AnimatedVectorDrawableCompat.create(context,
R.drawable.avd_star
))
(drawable as Animatable).start()
favoriteTags.add(tag)
}
}
}
if (item.t == -1) {
textView?.text = item.s
} else {
(suggestionView as? LinearLayout)?.let {
val count = it.findViewById<TextView>(R.id.count)
if (count == null)
it.addView(
LayoutInflater.from(context).inflate(R.layout.suggestion_count, suggestionView, false)
.apply {
this as TextView
text = item.t.toString()
}, 2
)
else
count.text = item.t.toString()
}
}
}
is FavoriteHistorySwitch -> {
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.swap_horizontal, context.theme))
}
is Suggestion -> {
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.history, context.theme))
with(suggestionView?.findViewById<ImageView>(R.id.right_icon)) {
this ?: return@with
setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.delete, context.theme))
visibility = View.VISIBLE
rotation = 0f
isEnabled = true
isClickable = true
setOnClickListener {
onHistoryDeleteClickedListener?.invoke(item.str)
}
}
}
is LoadingSuggestion -> {
leftIcon?.setImageDrawable(CircularProgressDrawable(context).also {
it.setStyle(CircularProgressDrawable.DEFAULT)
it.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN)
it.start()
})
}
is NoResultSuggestion -> {
leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.close, context.theme))
}
}
}
// hack to remove color attributes which should not be reused
override fun onSaveInstanceState(): Parcelable? {

View File

@@ -18,61 +18,119 @@
package xyz.quaver.pupil
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.*
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.types.Tag
import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.downloader.DownloadManager
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: SavedSet<Int>
private set
lateinit var favorites: SavedSet<Int>
private set
lateinit var favoriteTags: SavedSet<Tag>
private set
lateinit var searchHistory: SavedSet<String>
private set
init {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
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)
}
override fun onCreate() {
val preference = PreferenceManager.getDefaultSharedPreferences(this)
class Pupil : Application() {
val userID =
if (preference.getString("user_id", "").isNullOrEmpty()) {
UUID.randomUUID().toString().also {
preference.edit().putString("user_id", it).apply()
}
} else
preference.getString("user_id", "") ?: ""
override fun onCreate() {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
preferences = PreferenceManager.getDefaultSharedPreferences(this)
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 (it.startsWith("content") && Build.VERSION.SDK_INT > 19)
contentResolver.takePersistableUriPermission(
Uri.parse(it),
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
if (!FileX(this, it).canWrite())
throw Exception()
DownloadManager.getInstance(this).migrate()
}
} 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 = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0)
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0)
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
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 +153,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 +167,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 +175,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
})

View File

@@ -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)
)
}

View File

@@ -20,44 +20,47 @@ package xyz.quaver.pupil.adapters
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.Base64
import android.util.Log
import android.util.SparseBooleanArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.cardview.widget.CardView
import 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
import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.item_galleryblock.view.*
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,
@@ -65,8 +68,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
PREV
}
private lateinit var favorites: Histories
val timer = Timer()
var isThin = false
@@ -75,33 +76,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
@@ -114,7 +125,13 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
}
}
fun bind(galleryBlock: GalleryBlock) {
fun bind(galleryID: Int) {
val time = System.currentTimeMillis()
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 {
@@ -130,53 +147,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
@@ -208,35 +222,15 @@ 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)
CoroutineScope(Dispatchers.Default).launch {
galleryBlock.relatedTags.forEach {
TagChip(context, Tag.parse(it)).apply {
setOnClickListener { view ->
for (callback in onChipClickedHandler)
callback.invoke((view as TagChip).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)
}
"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 = tag.tag.wordCapitalize()
setEnsureMinTouchTargetSize(false)
setOnClickListener {
for (callback in onChipClickedHandler)
callback.invoke(tag)
}
})
}.let { launch(Dispatchers.Main) { galleryblock_tag_group.addView(it) } }
}
}
galleryblock_id.text = galleryBlock.id.toString()
@@ -250,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 {
@@ -288,6 +279,7 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
galleryblock_tag_group.visibility = View.GONE
}
}
Log.i("PUPILD", "${System.currentTimeMillis() - time}")
}
}
class NextViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
@@ -336,9 +328,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 {
@@ -364,7 +356,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)
@@ -389,8 +381,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 {

View File

@@ -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 {

View File

@@ -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
@@ -37,31 +40,31 @@ import xyz.quaver.Code
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.BuildConfig
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,36 +73,34 @@ 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
holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
holder.view.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
} else {
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
holder.view.container.layoutParams.height = 0
holder.view.layoutParams.height = ConstraintLayout.LayoutParams.WRAP_CONTENT
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "W,${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
(holder.view.progress_layout.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
}
holder.view.image.setOnPhotoTapListener { _, _, _ ->
onItemClickListener?.invoke(position)
}
holder.view.container.setOnClickListener {
holder.view.setOnClickListener {
onItemClickListener?.invoke(position)
}
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 ->
@@ -111,10 +112,7 @@ class ReaderAdapter(private val glide: RequestManager,
)
, LazyHeaders.Builder().addHeader("Referer", getReferer(galleryID)).build())
Code.HIYOBI ->
GlideUrl(createImgList(galleryID, reader!!, lowQuality)[position].path, LazyHeaders.Builder()
.addHeader("User-Agent", user_agent)
.addHeader("Cookie", cookie)
.build())
GlideUrl(createImgList(galleryID, reader!!, lowQuality)[position].path)
else -> null
}
holder.view.image.post {
@@ -122,27 +120,64 @@ class ReaderAdapter(private val glide: RequestManager,
.load(url!!)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.fitCenter()
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
else
override(
holder.view.context.resources.displayMetrics.widthPixels,
holder.view.context.resources.getDimensionPixelSize(R.dimen.reader_max_height)
)
}
.error(R.drawable.image_broken_variant)
.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.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.fitCenter()
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
else
override(
holder.view.context.resources.displayMetrics.widthPixels,
holder.view.context.resources.getDimensionPixelSize(R.dimen.reader_max_height)
)
}
.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

View File

@@ -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)
}
}
}

View File

@@ -0,0 +1,426 @@
/*
* 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 androidx.core.content.ContextCompat
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 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.also { if (it.code() != 200) throw IOException() }.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
}
progress.put(galleryID, MutableList(reader.galleryInfo.files.size) { 0F })
cache.metadata.imageList?.let {
if (progress[galleryID]?.size != it.size) {
cache.metadata.imageList?.filterNotNull()?.forEach { file ->
cache.findFile(file)?.delete()
}
cache.metadata.imageList = MutableList(reader.galleryInfo.files.size) { null }
return@let
}
it.forEachIndexed { index, image ->
progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F)
}
}
if (isCompleted(galleryID)) {
if (DownloadManager.getInstance(this@DownloadService)
.getDownloadFolder(galleryID) != null)
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
notificationManager.cancel(galleryID)
startId?.let { stopSelf(it) }
return@launch
}
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) {
ContextCompat.startForegroundService(context, 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 {
startForeground(R.id.downloader_notification_id, serviceNotification.build())
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)
}
}

View File

@@ -30,3 +30,24 @@ data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String
return s
}
}
@Parcelize
class Suggestion(val str: String) : SearchSuggestion {
override fun getBody() = str
}
@Parcelize
class NoResultSuggestion(val str: String) : SearchSuggestion {
override fun getBody() = str
}
@Parcelize
class LoadingSuggestion(val str: String) : SearchSuggestion {
override fun getBody() = str
}
@Parcelize
@Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY")
class FavoriteHistorySwitch(private val body: String) : SearchSuggestion {
override fun getBody() = body
}

View File

@@ -24,7 +24,7 @@ import kotlinx.serialization.Serializable
data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) {
companion object {
fun parse(tag: String) : Tag {
if (tag.first() == '-') {
if (tag.firstOrNull() == '-') {
tag.substring(1).split(Regex(":"), 2).let {
return when(it.size) {
2 -> Tag(it[0], it[1], true)
@@ -62,12 +62,10 @@ data class Tag(val area: String?, val tag: String, val isNegative: Boolean = fal
return false
}
override fun hashCode(): Int {
return super.hashCode()
}
override fun hashCode() = toString().hashCode()
}
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 +75,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 +90,22 @@ 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() }
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.PersistableBundle
import android.view.WindowManager
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.LockManager
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.normalizeID
open class BaseActivity : AppCompatActivity() {
private var locked: Boolean = true
@CallSuper
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
locked = !LockManager(this).locks.isNullOrEmpty()
}
@CallSuper
override fun onResume() {
super.onResume()
if (Preferences["security_mode"])
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
if (locked)
startActivityForResult(Intent(this, LockActivity::class.java), R.id.request_lock.normalizeID())
}
@CallSuper
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) {
R.id.request_lock.normalizeID() -> {
if (resultCode == Activity.RESULT_OK)
locked = false
else
finish()
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
}

View File

@@ -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,7 +37,9 @@ 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
private var lastUnlocked = 0L
class LockActivity : AppCompatActivity() {
private lateinit var lockManager: LockManager
@@ -52,6 +53,7 @@ class LockActivity : AppCompatActivity() {
val result = lockManager.check(it)
if (result == true) {
lastUnlocked = System.currentTimeMillis()
setResult(Activity.RESULT_OK)
finish()
} else
@@ -86,6 +88,7 @@ class LockActivity : AppCompatActivity() {
val result = lockManager.check(it)
if (result == true) {
lastUnlocked = System.currentTimeMillis()
setResult(Activity.RESULT_OK)
finish()
} else {
@@ -157,6 +160,7 @@ class LockActivity : AppCompatActivity() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
lastUnlocked = System.currentTimeMillis()
setResult(RESULT_OK)
finish()
return
@@ -185,6 +189,7 @@ class LockActivity : AppCompatActivity() {
}
mode = intent.getStringExtra("mode")
val force = intent.getBooleanExtra("force", false)
when(mode) {
null -> {
@@ -194,8 +199,15 @@ class LockActivity : AppCompatActivity() {
return
}
if (System.currentTimeMillis() - lastUnlocked < 5*60*1000 && !force) {
lastUnlocked = System.currentTimeMillis()
setResult(RESULT_OK)
finish()
return
}
if (
PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lock_fingerprint", false)
Preferences["lock_fingerprint"]
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
) {
lock_fingerprint.apply {

View File

@@ -19,62 +19,50 @@
package xyz.quaver.pupil.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.graphics.drawable.Animatable
import android.net.Uri
import android.os.Bundle
import android.text.*
import android.text.style.AlignmentSpan
import android.util.TypedValue
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.text.InputType
import android.view.*
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
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
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.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main_content.*
import kotlinx.coroutines.*
import kotlinx.serialization.builtins.list
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.*
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.types.TagSuggestion
import xyz.quaver.pupil.types.Tags
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.min
import kotlin.math.roundToInt
class MainActivity : AppCompatActivity() {
class MainActivity :
BaseActivity(),
FloatingSearchView.OnMenuItemClickListener,
NavigationView.OnNavigationItemSelectedListener
{
enum class Mode {
SEARCH,
@@ -88,7 +76,7 @@ class MainActivity : AppCompatActivity() {
POPULAR
}
private val galleries = ArrayList<GalleryBlock>()
private val galleries = ArrayList<Int>()
private var query = ""
set(value) {
@@ -103,57 +91,31 @@ 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?) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val lockManager = try {
LockManager(this)
} catch (e: Exception) {
android.app.AlertDialog.Builder(this).apply {
setTitle(R.string.warning)
setMessage(R.string.lock_corrupted)
setPositiveButton(android.R.string.ok) { _, _ ->
finish()
}
}.show()
return
}
if (lockManager.isNotEmpty())
startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK)
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(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)
@@ -182,22 +144,8 @@ class MainActivity : AppCompatActivity() {
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
}
override fun onResume() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
if (preferences.getBoolean("security_mode", false))
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
super.onResume()
}
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 +182,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 +191,7 @@ class MainActivity : AppCompatActivity() {
loadBlocks()
}
}
REQUEST_LOCK -> {
if (resultCode != Activity.RESULT_OK)
finish()
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
@@ -270,84 +214,19 @@ class MainActivity : AppCompatActivity() {
)
//NavigationView
main_nav_view.setNavigationItemSelectedListener {
runOnUiThread {
main_drawer_layout.closeDrawers()
when(it.itemId) {
R.id.main_drawer_home -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.SEARCH
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_history -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.HISTORY
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_downloads -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.DOWNLOAD
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_favorite -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.FAVORITE
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_help -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
}
R.id.main_drawer_github -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github))))
}
R.id.main_drawer_homepage -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page))))
}
R.id.main_drawer_email -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
}
R.id.main_drawer_kakaotalk -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
}
}
}
true
}
main_nav_view.setNavigationItemSelectedListener(this)
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 +303,6 @@ class MainActivity : AppCompatActivity() {
}
startActivity(intent)
histories.add(galleryID)
}
}.show()
}
@@ -453,19 +330,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 +347,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 +404,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 -> {
@@ -742,37 +609,24 @@ class MainActivity : AppCompatActivity() {
}
}
private var isFavorite = false
private val defaultSuggestions: List<SearchSuggestion>
get() = when {
isFavorite -> {
favoriteTags.map {
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
} + FavoriteHistorySwitch(getString(R.string.search_show_histories))
}
else -> {
searchHistory.map {
Suggestion(it)
}.takeLast(20) + FavoriteHistorySwitch(getString(R.string.search_show_tags))
}
}.reversed()
private var suggestionJob : Job? = null
private fun setupSearchBar() {
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
//Change upper case letters to lower case
searchInputView.addTextChangedListener(object: TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
s ?: return
if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
}
})
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
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())))
}
setOnLeftMenuClickListener(object: FloatingSearchView.OnLeftMenuClickListener {
override fun onMenuOpened() {
(this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
@@ -783,176 +637,55 @@ 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_thin -> {
main_recyclerview.apply {
(adapter as GalleryBlockAdapter).apply {
isThin = !isThin
}
adapter = adapter // Force to redraw
}
}
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
it.isChecked = true
runOnUiThread {
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
R.id.main_menu_sort_popular -> {
sortMode = SortMode.POPULAR
it.isChecked = true
runOnUiThread {
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
}
onHistoryDeleteClickedListener = {
searchHistory.remove(it)
swapSuggestions(defaultSuggestions)
}
onFavoriteHistorySwitchClickListener = {
isFavorite = !isFavorite
swapSuggestions(defaultSuggestions)
}
setOnMenuItemClickListener(this@MainActivity)
setOnQueryChangeListener { _, query ->
this@MainActivity.query = query
suggestionJob?.cancel()
clearSuggestions()
if (query.isEmpty() or query.endsWith(' ')) {
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map {
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
})
swapSuggestions(defaultSuggestions)
return@setOnQueryChangeListener
}
swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString())))
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)
favoriteTags.contains(Tag.parse(tag))
}.reversed().forEach {
suggestions.remove(it)
suggestions.add(0, it)
}
withContext(Dispatchers.Main) {
swapSuggestions(suggestions)
swapSuggestions(if (suggestions.isNotEmpty()) suggestions else listOf(NoResultSuggestion(getText(R.string.main_no_result).toString())))
}
}
}
setOnBindSuggestionCallback { suggestionView, leftIcon, textView, item, _ ->
item as TagSuggestion
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
val color = TypedValue()
theme.resolveAttribute(R.attr.colorControlNormal, color, true)
leftIcon.setImageDrawable(
ResourcesCompat.getDrawable(
resources,
when(item.n) {
"female" -> R.drawable.gender_female
"male" -> R.drawable.gender_male
"language" -> R.drawable.translate
"group" -> R.drawable.account_group
"character" -> R.drawable.account_star
"series" -> R.drawable.book_open
"artist" -> R.drawable.brush
else -> R.drawable.tag
},
context.theme)
)
with(suggestionView.findViewById<ImageView>(R.id.right_icon)) {
if (Tags(json.parse(serializer, favoritesFile.readText())).contains(tag))
setImageResource(R.drawable.ic_star_filled)
else
setImageResource(R.drawable.ic_star_empty)
visibility = View.VISIBLE
rotation = 0f
isEnabled = true
isClickable = true
setOnClickListener {
val favorites = Tags(json.parse(serializer, favoritesFile.readText()))
if (favorites.contains(tag)) {
setImageResource(R.drawable.ic_star_empty)
favorites.remove(tag)
}
else {
setImageDrawable(AnimatedVectorDrawableCompat.create(context,
R.drawable.avd_star
))
(drawable as Animatable).start()
favorites.add(tag)
}
favoritesFile.writeText(json.stringify(serializer, favorites))
}
}
if (item.t == -1) {
textView.text = item.s
} else {
val text = "${item.s}\n ${item.t}"
val len = text.length
val left = item.s.length
textView.text = SpannableString(text).apply {
val s = AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE)
setSpan(s, left, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
setSpan(SetLineOverlap(true), 1, len-2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
setSpan(SetLineOverlap(false), len-1, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
setOnSearchListener(object : FloatingSearchView.OnSearchListener {
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
if (searchSuggestion !is TagSuggestion)
return
with(searchInputView.text) {
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length)
append("${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")} ")
}
}
override fun onSearchAction(currentQuery: String?) {
//Do search on onFocusCleared()
}
})
setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener {
override fun onFocus() {
if (query.isEmpty() or query.endsWith(' '))
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map {
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
})
swapSuggestions(defaultSuggestions)
}
override fun onFocusCleared() {
@@ -972,6 +705,113 @@ class MainActivity : AppCompatActivity() {
}
}
override fun onActionMenuItemSelected(item: MenuItem?) {
when(item?.itemId) {
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 {
isThin = !isThin
}
adapter = adapter // Force to redraw
}
}
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
item.isChecked = true
runOnUiThread {
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
R.id.main_menu_sort_popular -> {
sortMode = SortMode.POPULAR
item.isChecked = true
runOnUiThread {
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
}
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
runOnUiThread {
main_drawer_layout.closeDrawers()
when(item.itemId) {
R.id.main_drawer_home -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.SEARCH
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_history -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.HISTORY
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_downloads -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.DOWNLOAD
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_favorite -> {
cancelFetch()
clearGalleries()
currentPage = 0
query = ""
queryStack.clear()
mode = Mode.FAVORITE
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_help -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
}
R.id.main_drawer_github -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github))))
}
R.id.main_drawer_homepage -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page))))
}
R.id.main_drawer_email -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
}
R.id.main_drawer_kakaotalk -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
}
}
}
return true
}
private fun cancelFetch() {
galleryIDs?.cancel()
loadingJob?.cancel()
@@ -993,14 +833,30 @@ 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.isNotBlank())
searchHistory.add(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.ok) {
cancelFetch()
clearGalleries()
currentPage = 0
mode = Mode.SEARCH
queryStack.clear()
fetchGalleries(query, sortMode)
loadBlocks()
}
}.show()
}
galleryIDs = null
if (galleryIDs?.isActive == true)
@@ -1026,34 +882,28 @@ 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 -> {
val result = doSearch(query).sorted()
downloads.filter { result.binarySearch(it) >= 0 }.also {
downloads.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
@@ -1061,12 +911,12 @@ class MainActivity : AppCompatActivity() {
}
Mode.FAVORITE -> {
when {
query.isEmpty() -> favorites.toList().also {
query.isEmpty() -> favorites.reversed().also {
totalItems = it.size
}
else -> {
val result = doSearch(query).sorted()
favorites.filter { result.binarySearch(it) >= 0 }.also {
favorites.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size
}
}
@@ -1077,8 +927,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 +952,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)
}
}
@@ -1120,4 +969,14 @@ class MainActivity : AppCompatActivity() {
}
}
}
override fun onLowMemory() {
super.onLowMemory()
Glide.get(this).onLowMemory()
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Glide.get(this).onTrimMemory(level)
}
}

View File

@@ -18,14 +18,16 @@
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
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
@@ -36,6 +38,7 @@ 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 com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
@@ -43,16 +46,19 @@ 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
class ReaderActivity : AppCompatActivity() {
class ReaderActivity : BaseActivity() {
private var galleryID = 0
private var currentPage = 0
@@ -70,30 +76,34 @@ 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
private val snapHelper = PagerSnapHelper()
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) {
@@ -101,11 +111,10 @@ class ReaderActivity : AppCompatActivity() {
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) {
@@ -115,6 +124,7 @@ class ReaderActivity : AppCompatActivity() {
return@initDownloader
}
histories.add(galleryID)
(reader_recyclerview.adapter as ReaderAdapter).apply {
this.reader = reader
notifyDataSetChanged()
@@ -132,6 +142,8 @@ class ReaderActivity : AppCompatActivity() {
}
} else
initDownloader()
initView()
}
override fun onNewIntent(intent: Intent) {
@@ -157,19 +169,6 @@ class ReaderActivity : AppCompatActivity() {
}
}
override fun onResume() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
if (preferences.getBoolean("security_mode", false))
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
super.onResume()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.reader, menu)
@@ -184,14 +183,14 @@ class ReaderActivity : AppCompatActivity() {
return true
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when(item?.itemId) {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
R.id.reader_menu_page_indicator -> {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
with(view.dialog_number_picker) {
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)
@@ -226,8 +225,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() {
@@ -263,32 +265,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()
@@ -306,7 +309,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)
@@ -317,7 +320,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
@@ -352,18 +355,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)
}
}
@@ -373,9 +377,31 @@ 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)
}
}
with(reader_fab_auto) {
setImageResource(R.drawable.clock_start)
setOnClickListener {
if (autoTimer == null) {
autoTimer = timer(initialDelay = 10000L, period = 10000L) {
CoroutineScope(Dispatchers.Main).launch {
with(this@ReaderActivity.reader_recyclerview) {
val lastItem =
(layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition()
if (lastItem < adapter!!.itemCount - 1)
(layoutManager as LinearLayoutManager).scrollToPosition(lastItem + 1)
}
}
}
setImageResource(R.drawable.clock_end)
} else {
autoTimer?.cancel()
autoTimer = null
setImageResource(R.drawable.clock_start)
}
}
}
@@ -397,10 +423,22 @@ class ReaderActivity : AppCompatActivity() {
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
supportActionBar?.hide()
this@ReaderActivity.reader_fab.visibility = View.INVISIBLE
this@ReaderActivity.scroller.let {
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_height)
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_width)
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb_horizontal)
it.fastScrollDirection = RecyclerViewFastScroller.FastScrollDirection.HORIZONTAL
}
} else {
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
supportActionBar?.show()
this@ReaderActivity.reader_fab.visibility = View.VISIBLE
this@ReaderActivity.scroller.let {
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_width)
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_height)
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb)
it.fastScrollDirection = RecyclerViewFastScroller.FastScrollDirection.VERTICAL
}
}
window.attributes = this
@@ -415,7 +453,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)
@@ -428,8 +466,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)
@@ -450,4 +487,14 @@ class ReaderActivity : AppCompatActivity() {
}
}
}
override fun onLowMemory() {
super.onLowMemory()
Glide.get(this).onLowMemory()
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Glide.get(this).onTrimMemory(level)
}
}

View File

@@ -22,34 +22,25 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.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 net.rdrei.android.dirchooser.DirectoryChooserActivity
import xyz.quaver.pupil.Pupil
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
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.*
import java.io.File
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.normalizeID
import java.nio.charset.Charset
class SettingsActivity : AppCompatActivity() {
class SettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
setContentView(R.layout.settings_activity)
supportFragmentManager
.beginTransaction()
@@ -58,155 +49,24 @@ class SettingsActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onResume() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
if (preferences.getBoolean("security_mode", false))
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
super.onResume()
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()
}
return true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) {
REQUEST_LOCK -> {
if (resultCode == Activity.RESULT_OK) {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, LockSettingsFragment())
.addToBackStack("Lock")
.commitAllowingStateLoss()
}
}
REQUEST_RESTORE -> {
if (resultCode == Activity.RESULT_OK) {
val uri = data?.data ?: return
try {
val str = contentResolver.openInputStream(uri).use { inputStream ->
inputStream!!
inputStream.readBytes().toString(Charset.defaultCharset())
}
(application as Pupil).favorites.addAll(json.parse(Int.serializer().list, str).also {
Snackbar.make(
window.decorView,
getString(R.string.settings_restore_successful, it.size),
Snackbar.LENGTH_LONG
).show()
})
} catch (e: Exception) {
Snackbar.make(
window.decorView,
R.string.settings_restore_failed,
Snackbar.LENGTH_LONG
).show()
}
}
}
REQUEST_DOWNLOAD_FOLDER -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
val takeFlags: Int =
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
contentResolver.takePersistableUriPermission(uri, takeFlags)
val file = uri.toFile(this)
if (file?.canWrite() != true)
Snackbar.make(
settings,
R.string.settings_dl_location_not_writable,
Snackbar.LENGTH_LONG
).show()
else
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", file.canonicalPath)
.apply()
}
}
}
REQUEST_DOWNLOAD_FOLDER_OLD -> {
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canWrite())
Snackbar.make(
settings,
R.string.settings_dl_location_not_writable,
Snackbar.LENGTH_LONG
).show()
else
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", File(directory).canonicalPath)
.apply()
}
}
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)
}
}
@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())
}
}
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)?.toUri()?.toString() ?: ""
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)
}
}
}

View File

@@ -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)
}
}
@@ -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()
}
}
}
}

View File

@@ -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(">")
}
}
}

View File

@@ -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()

View File

@@ -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
}
}
@@ -75,11 +74,11 @@ class LockSettingsFragment :
setTitle(R.string.warning)
setMessage(R.string.settings_lock_remove_message)
setPositiveButton(android.R.string.yes) { _, _ ->
setPositiveButton(android.R.string.ok) { _, _ ->
lockManager.remove(Lock.Type.PATTERN)
onResume()
}
setNegativeButton(android.R.string.no) { _, _ -> }
setNegativeButton(android.R.string.cancel) { _, _ -> }
}.show()
} else {
val intent = Intent(requireContext(), LockActivity::class.java).apply {
@@ -108,11 +107,11 @@ class LockSettingsFragment :
setTitle(R.string.warning)
setMessage(R.string.settings_lock_remove_message)
setPositiveButton(android.R.string.yes) { _, _ ->
setPositiveButton(android.R.string.ok) { _, _ ->
lockManager.remove(Lock.Type.PIN)
onResume()
}
setNegativeButton(android.R.string.no) { _, _ -> }
setNegativeButton(android.R.string.cancel) { _, _ -> }
}.show()
} else {
val intent = Intent(requireContext(), LockActivity::class.java).apply {

View File

@@ -0,0 +1,106 @@
/*
* 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) {
if (response.code() != 200) {
response.close()
return
}
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(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
}
}
}

View File

@@ -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.ok) { _, _ ->
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.cancel) { _, _ -> }
}.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.ok) { _, _ ->
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.cancel) { _, _ -> }
}.show()
}
"clear_history" -> {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_history_alert_message)
setPositiveButton(android.R.string.ok) { _, _ ->
histories.clear()
summary = context.getString(R.string.settings_clear_history_summary, histories.size)
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
}.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()
}
}

View File

@@ -18,39 +18,29 @@
package xyz.quaver.pupil.ui.fragment
import android.Manifest
import android.app.Activity
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 androidx.preference.SwitchPreferenceCompat
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild
import xyz.quaver.pupil.R
import xyz.quaver.pupil.favorites
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
import java.nio.charset.Charset
class SettingsFragment :
PreferenceFragmentCompat(),
@@ -58,8 +48,6 @@ class SettingsFragment :
Preference.OnPreferenceChangeListener,
SharedPreferences.OnSharedPreferenceChangeListener {
lateinit var sharedPreference: SharedPreferences
override fun onResume() {
super.onResume()
@@ -78,16 +66,6 @@ class SettingsFragment :
}
}
private fun getDirSize(dir: File) : String {
return 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"
}
}
)
}
override fun onPreferenceClick(preference: Preference?): Boolean {
with (preference) {
this ?: return false
@@ -96,75 +74,22 @@ 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()
CoroutineScope(Dispatchers.IO).launch {
summary = getString(R.string.settings_storage_usage_loading)
launch(Dispatchers.Main) {
this@with.summary = getDirSize(dir)
}
}
}
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()
CoroutineScope(Dispatchers.IO).launch {
summary = getString(R.string.settings_storage_usage_loading)
launch(Dispatchers.Main) {
this@with.summary = getDirSize(dir)
}
}
}
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(parentFragmentManager, "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)
val intent = Intent(requireContext(), LockActivity::class.java).apply {
putExtra("force", true)
}
startActivityForResult(intent, R.id.request_lock.normalizeID())
}
"mirrors" -> {
MirrorDialog(requireContext())
@@ -174,54 +99,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()
}
@@ -237,6 +117,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
@@ -258,10 +150,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-"]
}
}
}
@@ -270,12 +165,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) {
@@ -285,7 +184,7 @@ class SettingsFragment :
else
listOf(this)
}.forEach { preference ->
with (preference) {
with (preference) with@{
when (key) {
"app_version" -> {
@@ -295,45 +194,29 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment
}
"delete_cache" -> {
val dir = File(requireContext().cacheDir, "imageCache")
"download_folder_name" -> {
summary = Preferences["download_folder_name", "[-id-] -title-"]
CoroutineScope(Dispatchers.IO).launch {
summary = getString(R.string.settings_storage_usage_loading)
setOnPreferenceClickListener {
DownloadFolderNameDialogFragment().show(requireActivity().supportFragmentManager, "Download Location Dialog")
launch(Dispatchers.Main) {
this@with.summary = getDirSize(dir)
}
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)
CoroutineScope(Dispatchers.IO).launch {
summary = getString(R.string.settings_storage_usage_loading)
launch(Dispatchers.Main) {
this@with.summary = getDirSize(dir)
}
}
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
}
@@ -358,33 +241,45 @@ 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
}
"oss" -> {
setOnPreferenceClickListener {
context?.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
true
}
}
}
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) {
R.id.request_lock.normalizeID() -> {
if (resultCode == Activity.RESULT_OK) {
parentFragmentManager
.beginTransaction()
.replace(R.id.settings, LockSettingsFragment())
.addToBackStack("Lock")
.commitAllowingStateLoss()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
}

View 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.ui.view
import android.annotation.SuppressLint
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
@SuppressLint("ViewConstructor")
class TagChip(context: Context, tag: Tag) : Chip(context) {
val tag: Tag =
tag.let {
when {
it.area != null -> it
else -> Tag("tag", tag.tag)
}
}
private val languages = context.resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
Pair(split[0], split[1])
}
}.toMap()
init {
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)
}
}

View File

@@ -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)

View File

@@ -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);
}
}

View 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()
}
}

View 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()
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import java.io.File
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalSerializationApi::class)
val serializer: KSerializer<List<T>>
get() = ListSerializer(serializer(any::class.java) as KSerializer<T>)
init {
if (!file.exists()) {
file.parentFile?.mkdirs()
save()
}
load()
}
fun load() {
synchronized(this) {
set.clear()
kotlin.runCatching {
Json.decodeFromString(serializer, file.readText())
}.onSuccess {
set.addAll(it)
}
}
}
@OptIn(ExperimentalSerializationApi::class)
fun save() {
synchronized(this) {
file.writeText(Json.encodeToString(serializer, set.toList()))
}
}
override fun add(element: T): Boolean {
load()
return set.add(element).also {
save()
}
}
override fun addAll(elements: Collection<T>): Boolean {
load()
return set.addAll(elements).also {
save()
}
}
override fun remove(element: T): Boolean {
load()
return set.remove(element).also {
save()
}
}
override fun clear() {
set.clear()
save()
}
}

View File

@@ -25,23 +25,24 @@ 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
@Suppress("DEPRECATION")
@Deprecated("Use downloader.Cache instead")
class Cache(context: Context) : ContextWrapper(context) {
companion object {
@@ -49,20 +50,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 +66,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 +83,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 +92,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 +122,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 +137,7 @@ class Cache(context: Context) : ContextWrapper(context) {
}
galleryBlock
}.await() ?: return null
} ?: return null
}
else
metadata.galleryBlock
@@ -175,11 +163,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 +180,7 @@ class Cache(context: Context) : ContextWrapper(context) {
retval = try {
withContext(Dispatchers.IO) {
withTimeoutOrNull(1000) {
source.value.invoke()
source.value.invoke()
}
}
} catch (e: Exception) {

View File

@@ -37,17 +37,17 @@ import xyz.quaver.Code
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.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
@Suppress("DEPRECATION")
@Deprecated("Use DownloadService instead")
@OptIn(ExperimentalCoroutinesApi::class)
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
@@ -86,7 +86,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 +99,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 +152,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 +168,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 +188,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)
@@ -219,8 +218,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
}
Code.HIYOBI -> {
url(createImgList(galleryID, reader, lowQuality)[index].path)
addHeader("User-Agent", user_agent)
addHeader("Cookie", cookie)
}
else -> {
//shouldn't be called anyway
@@ -275,23 +272,22 @@ 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)
}
override fun onResponse(call: Call, response: Response) {
if (response.code() != 200) {
response.close()
onFailure(call, IOException())
return
}
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)

View File

@@ -22,12 +22,14 @@ import kotlinx.serialization.Serializable
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
@Suppress("DEPRECATION")
@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?,

View File

@@ -0,0 +1,248 @@
/*
* 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.*
import xyz.quaver.pupil.client
import xyz.quaver.pupil.util.Preferences
import java.io.IOException
@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().also { if (it.code() != 200) throw IOException() }.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
if (downloadFolder.getChild(".metadata").exists())
return@launch
metadata.imageList?.forEach { imageName ->
imageName ?: return@forEach
val target = downloadFolder.getChild(imageName)
val source = cacheFolder.getChild(imageName)
if (!source.exists() || target.exists())
return@forEach
kotlin.runCatching {
target.createNewFile()
target.outputStream()?.use { target -> source.inputStream()?.use { source ->
source.copyTo(target)
} }
}
}
val cacheThumbnail = cacheFolder.getChild(".thumbnail")
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
if (cacheThumbnail.exists() && !downloadThumbnail.exists()) {
kotlin.runCatching {
downloadThumbnail.createNewFile()
downloadThumbnail.outputStream()?.use { target -> cacheThumbnail.inputStream()?.use { source ->
source.copyTo(target)
} }
cacheThumbnail.delete()
}
}
val cacheMetadata = cacheFolder.getChild(".metadata")
val downloadMetadata = downloadFolder.getChild(".metadata")
if (cacheMetadata.exists() && !downloadMetadata.exists()) {
kotlin.runCatching {
downloadMetadata.createNewFile()
downloadMetadata.writeText(Json.encodeToString(metadata))
cacheMetadata.delete()
}
}
cacheFolder.delete()
}
}

View File

@@ -0,0 +1,128 @@
/*
* 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()
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) {
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) {
downloadFolderMap[galleryID]?.let {
kotlin.runCatching {
downloadFolder.getChild(it).deleteRecursively()
downloadFolderMap.remove(galleryID)
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
}
}
}
}

View File

@@ -18,20 +18,17 @@
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
@Suppress("DEPRECATION")
@Deprecated("Use downloader.Cache instead")
fun getCachedGallery(context: Context, galleryID: Int) =
File(getDownloadDirectory(context), galleryID.toString()).let {
if (it.exists())
@@ -40,178 +37,17 @@ fun getCachedGallery(context: Context, galleryID: Int) =
File(context.cacheDir, "imageCache/$galleryID")
}
@Suppress("DEPRECATION")
@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)
}
@Suppress("DEPRECATION")
@Deprecated("Use FileX instead")
fun File.isParentOf(another: File) =
another.absolutePath.startsWith(this.absolutePath)

View File

@@ -1,96 +0,0 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.list
import kotlinx.serialization.builtins.serializer
import java.io.File
class Histories(private val file: File) : ArrayList<Int>() {
val serializer: KSerializer<List<Int>> = Int.serializer().list
init {
if (!file.exists())
file.parentFile?.mkdirs()
try {
load()
} catch (e: Exception) {
save()
}
}
fun load() : Histories {
return apply {
super.clear()
super.addAll(
json.parse(
serializer,
file.bufferedReader().use { it.readText() }
)
)
}
}
fun save() {
file.writeText(json.stringify(serializer, this))
}
override fun add(element: Int): Boolean {
load()
if (contains(element))
super.remove(element)
super.add(0, element)
save()
return true
}
override fun addAll(elements: Collection<Int>): Boolean {
load()
for (e in elements) {
if (contains(e))
super.remove(e)
super.add(0, e)
}
save()
return true
}
override fun remove(element: Int): Boolean {
load()
val retval = super.remove(element)
save()
return retval
}
override fun clear() {
super.clear()
save()
}
}

View File

@@ -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) {

View File

@@ -19,6 +19,18 @@
package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.content.ContextCompat
import okhttp3.OkHttpClient
import okhttp3.Request
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.createImgList
import java.util.*
import kotlin.collections.ArrayList
@@ -33,15 +45,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 +62,70 @@ 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)
}
}.replace("/", "")
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
format.let {
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
}
}.replace("/", "")
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)
}
}
}
}
fun String.ellipsize(n: Int): String =
if (this.length > n)
this.slice(0 until n) + ""
else
this

View File

@@ -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))])

View File

@@ -21,60 +21,70 @@ 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.favorites
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.Metadata
import java.io.File
import java.io.IOException
import java.net.URL
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 +93,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 +108,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 +146,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
@@ -150,15 +159,13 @@ fun checkUpdate(context: Context, force: Boolean = false) {
setTitle(R.string.update_title)
val msg = extractReleaseNote(update, Locale.getDefault())
setMessage(Markwon.create(context).toMarkdown(msg))
setPositiveButton(android.R.string.yes) { _, _ ->
val preference = PreferenceManager.getDefaultSharedPreferences(context)
setPositiveButton(android.R.string.ok) { _, _ ->
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,10 +179,10 @@ 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) { _, _ ->
setNegativeButton(if (force) android.R.string.cancel else R.string.ignore_update) { _, _ ->
if (!force)
preferences.edit()
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
@@ -189,147 +196,141 @@ 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(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.also { if (it.code() != 200) throw IOException() }.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()
)
)
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())
}
}
notificationBuilder.apply {
setContentText(context.getText(R.string.import_old_galleries_notification_done))
setProgress(0, 0, false)
setOngoing(false)
mActions.clear()
}
notificationManager.notify(0, notificationBuilder.build())
cancelImport = false
})
}
private var job: Job? = null
private val receiver = object: BroadcastReceiver() {
val ACTION_CANCEL = "ACTION_IMPORT_CANCEL"
override fun onReceive(context: Context?, intent: Intent?) {
context ?: return
when (intent?.action) {
ACTION_CANCEL -> {
job?.cancel()
NotificationManagerCompat.from(context).cancel(R.id.notification_id_import)
context.unregisterReceiver(this)
}
}
}
}
@SuppressLint("RestrictedApi")
fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
registerReceiver(receiver, IntentFilter().apply { addAction(receiver.ACTION_CANCEL) })
val notificationManager = NotificationManagerCompat.from(this)
val action = NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
PendingIntent.getBroadcast(this, R.id.notification_import_cancel_action.normalizeID(), Intent(receiver.ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)
).build()
val notification = NotificationCompat.Builder(this, "import")
.setContentTitle(getText(R.string.import_old_galleries_notification))
.setProgress(0, 0, true)
.addAction(action)
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
DownloadService.cancel(this)
job?.cancel()
job = CoroutineScope(Dispatchers.IO).launch {
val downloadFolders = downloadFolder.listFiles { folder ->
folder.isDirectory && !downloadFolderMap.values.contains(folder.name)
}?.map {
if (it !is FileX)
FileX(this@migrate, it)
else
it
}
if (downloadFolders.isNullOrEmpty()) return@launch
downloadFolders.forEachIndexed { index, folder ->
notification
.setContentText(getString(R.string.import_old_galleries_notification_text, index, downloadFolders.size))
.setProgress(index, downloadFolders.size, false)
notificationManager.notify(R.id.notification_id_import, notification.build())
kotlin.runCatching {
val metadata = kotlin.runCatching {
folder.getChild(".metadata").readText()?.let { Json.parseToJsonElement(it).jsonObject }
}.getOrNull()
val galleryID = folder.name.toIntOrNull() ?: return@runCatching
val galleryBlock: GalleryBlock? = kotlin.runCatching {
metadata?.get("galleryBlock")?.let { Json.decodeFromJsonElement<GalleryBlock>(it) }
}.getOrNull() ?: getGalleryBlock(galleryID)
val reader: Reader? = kotlin.runCatching {
metadata?.get("reader")?.let { Json.decodeFromJsonElement<Reader>(it) }
}.getOrNull() ?: getReader(galleryID)
metadata?.get("thumbnail")?.jsonPrimitive?.contentOrNull?.also { thumbnail ->
val file = folder.getChild(".thumbnail").also {
if (it.exists())
it.delete()
it.createNewFile()
}
file.writeBytes(Base64.decode(thumbnail, Base64.DEFAULT))
}
val list: MutableList<String?> =
MutableList(reader!!.galleryInfo.files.size) { null }
folder.listFiles { file ->
file?.nameWithoutExtension?.let {
Regex("""\d{5}""").matches(it) && it.toIntOrNull() != null
} == true
}?.forEach {
list[it.nameWithoutExtension.toInt()] = it.name
}
folder.getChild(".metadata").also { if (it.exists()) it.delete(); it.createNewFile() }.writeText(
Json.encodeToString(Metadata(galleryBlock, reader, list))
)
synchronized(Cache) {
Cache.delete(galleryID)
}
downloadFolderMap[galleryID] = folder.name
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile(); it.writeText(Json.encodeToString(downloadFolderMap)) }
}
}
notification
.setContentText(getText(R.string.import_old_galleries_notification_done))
.setProgress(0, 0, false)
.setOngoing(false)
.mActions.clear()
notificationManager.notify(R.id.notification_id_import, notification.build())
kotlin.runCatching {
unregisterReceiver(receiver)
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 B

View File

@@ -1,8 +1,4 @@
<!-- drawable/account_group.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="?attr/colorControlNormal" android:pathData="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z" />
<!--drawable/account_group.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="?attr/colorControlNormal" android:pathData="M12 5.5A3.5 3.5 0 0 1 15.5 9a3.5 3.5 0 0 1-3.5 3.5A3.5 3.5 0 0 1 8.5 9 3.5 3.5 0 0 1 12 5.5M5 8c0.56 0 1.08 0.15 1.53 0.42-0.15 1.43 0.27 2.85 1.13 3.96C7.16 13.34 6.16 14 5 14a3 3 0 0 1-3-3 3 3 0 0 1 3-3m14 0a3 3 0 0 1 3 3 3 3 0 0 1-3 3c-1.16 0-2.16-0.66-2.66-1.62 0.86-1.11 1.28-2.53 1.13-3.96C17.92 8.15 18.44 8 19 8M5.5 18.25c0-2.07 2.91-3.75 6.5-3.75s6.5 1.68 6.5 3.75V20h-13v-1.75M0 20v-1.5c0-1.39 1.89-2.56 4.45-2.9-0.59 0.68-0.95 1.62-0.95 2.65V20H0m24 0h-3.5v-1.75c0-1.03-0.36-1.97-0.95-2.65 2.56 0.34 4.45 1.51 4.45 2.9V20z"/>
</vector>

View File

@@ -1,8 +1,4 @@
<!-- drawable/account_star.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="?attr/colorControlNormal" android:pathData="M15,14C12.33,14 7,15.33 7,18V20H23V18C23,15.33 17.67,14 15,14M15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12M5,13.28L7.45,14.77L6.8,11.96L9,10.08L6.11,9.83L5,7.19L3.87,9.83L1,10.08L3.18,11.96L2.5,14.77L5,13.28Z" />
<!--drawable/account_star.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="?attr/colorControlNormal" android:pathData="M15 14c-2.67 0-8 1.33-8 4v2h16v-2c0-2.67-5.33-4-8-4m0-2a4 4 0 0 0 4-4 4 4 0 0 0-4-4 4 4 0 0 0-4 4 4 4 0 0 0 4 4M5 13.28l2.45 1.49-0.65-2.81L9 10.08 6.11 9.83 5 7.19 3.87 9.83 1 10.08l2.18 1.88-0.68 2.81L5 13.28z"/>
</vector>

View File

@@ -1,8 +1,4 @@
<!-- drawable/arrow_right.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="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" />
<!--drawable/arrow_right.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="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11H4z"/>
</vector>

View File

@@ -1,37 +1,14 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="newApi">
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" xmlns:tools="http://schemas.android.com/tools" tools:ignore="newApi">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z"
android:fillColor="@color/material_orange_500"/>
<clip-path
android:name="clip"
android:pathData="M 2 21 L 2 21 L 22 21 L 22 21 Z"/>
<path
android:name="path_1"
android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z"
android:fillColor="@color/material_orange_500"/>
<vector android:name="vector" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:name="path" android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z" android:fillColor="@color/material_orange_500"/>
<clip-path android:name="clip" android:pathData="M 2 21 L 2 21 L 22 21 L 22 21 Z"/>
<path android:name="path_1" android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z" android:fillColor="@color/material_orange_500"/>
</vector>
</aapt:attr>
<target android:name="clip">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="pathData"
android:duration="500"
android:valueFrom="M 2 21 L 2 21 L 22 21 L 22 21 Z"
android:valueTo="M 2 2 L 2 21 L 22 21 L 22 2 Z"
android:valueType="pathType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator android:propertyName="pathData" android:duration="500" android:valueFrom="M 2 21 L 2 21 L 22 21 L 22 21 Z" android:valueTo="M 2 2 L 2 21 L 22 21 L 22 2 Z" android:valueType="pathType" android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -1,8 +1,4 @@
<!-- drawable/backspace_outline.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="#000" android:pathData="M19,15.59L17.59,17L14,13.41L10.41,17L9,15.59L12.59,12L9,8.41L10.41,7L14,10.59L17.59,7L19,8.41L15.41,12L19,15.59M22,3A2,2 0 0,1 24,5V19A2,2 0 0,1 22,21H7C6.31,21 5.77,20.64 5.41,20.11L0,12L5.41,3.88C5.77,3.35 6.31,3 7,3H22M22,5H7L2.28,12L7,19H22V5Z" />
<!--drawable/backspace_outline.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="#000" android:pathData="M19 15.59L17.59 17 14 13.41 10.41 17 9 15.59 12.59 12 9 8.41 10.41 7 14 10.59 17.59 7 19 8.41 15.41 12 19 15.59M22 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H7c-0.69 0-1.23-0.36-1.59-0.89L0 12l5.41-8.12C5.77 3.35 6.31 3 7 3h15m0 2H7l-4.72 7L7 19h15V5z"/>
</vector>

View File

@@ -1,8 +1,4 @@
<!-- drawable/book_open.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="?attr/colorControlNormal" android:pathData="M13,12H20V13.5H13M13,9.5H20V11H13M13,14.5H20V16H13M21,4H3A2,2 0 0,0 1,6V19A2,2 0 0,0 3,21H21A2,2 0 0,0 23,19V6A2,2 0 0,0 21,4M21,19H12V6H21" />
<!--drawable/book_open.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="?attr/colorControlNormal" android:pathData="M13 12h7v1.5h-7m0-4h7V11h-7m0 3.5h7V16h-7m8-12H3a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2m0 15h-9V6h9"/>
</vector>

View File

@@ -1,8 +1,4 @@
<!-- drawable/brush.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="?attr/colorControlNormal" android:pathData="M20.71,4.63L19.37,3.29C19,2.9 18.35,2.9 17.96,3.29L9,12.25L11.75,15L20.71,6.04C21.1,5.65 21.1,5 20.71,4.63M7,14A3,3 0 0,0 4,17C4,18.31 2.84,19 2,19C2.92,20.22 4.5,21 6,21A4,4 0 0,0 10,17A3,3 0 0,0 7,14Z" />
<!--drawable/brush.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="?attr/colorControlNormal" android:pathData="M20.71 4.63l-1.34-1.34c-0.37-0.39-1.02-0.39-1.41 0L9 12.25 11.75 15l8.96-8.96c0.39-0.39 0.39-1.04 0-1.41M7 14a3 3 0 0 0-3 3c0 1.31-1.16 2-2 2 0.92 1.22 2.5 2 4 2a4 4 0 0 0 4-4 3 3 0 0 0-3-3z"/>
</vector>

View File

@@ -1,8 +1,4 @@
<!-- drawable/cancel.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,13.85 4.63,15.55 5.68,16.91L16.91,5.68C15.55,4.63 13.85,4 12,4M12,20A8,8 0 0,0 20,12C20,10.15 19.37,8.45 18.32,7.09L7.09,18.32C8.45,19.37 10.15,20 12,20Z" />
<!--drawable/cancel.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2m0 2a8 8 0 0 0-8 8c0 1.85 0.63 3.55 1.68 4.91L16.91 5.68C15.55 4.63 13.85 4 12 4m0 16a8 8 0 0 0 8-8c0-1.85-0.63-3.55-1.68-4.91L7.09 18.32C8.45 19.37 10.15 20 12 20z"/>
</vector>

View File

@@ -0,0 +1,4 @@
<!--drawable/clock_end.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 1C8.14 1 5 4.14 5 8a7 7 0 0 0 7 7c3.86 0 7-3.13 7-7 0-3.86-3.14-7-7-7m0 2.15c2.67 0 4.85 2.17 4.85 4.85 0 2.68-2.18 4.85-4.85 4.85A4.85 4.85 0 0 1 7.15 8 4.85 4.85 0 0 1 12 3.15M11 5v3.69l3.19 1.84 0.75-1.3-2.44-1.41V5M15 16v3H3v2h12v3l4-4m0 0v4h2v-8h-2"/>
</vector>

View File

@@ -0,0 +1,4 @@
<!--drawable/clock_start.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 1C8.14 1 5 4.14 5 8a7 7 0 0 0 7 7c3.86 0 7-3.13 7-7 0-3.86-3.14-7-7-7m0 2.15c2.67 0 4.85 2.17 4.85 4.85 0 2.68-2.18 4.85-4.85 4.85A4.85 4.85 0 0 1 7.15 8 4.85 4.85 0 0 1 12 3.15M11 5v3.69l3.19 1.84 0.75-1.3-2.44-1.41V5M4 16v8h2v-3h12v3l4-4-4-4v3H6v-3"/>
</vector>

View File

@@ -0,0 +1,4 @@
<!--drawable/close.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="?attr/colorControlNormal" android:pathData="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
</vector>

View File

@@ -0,0 +1,4 @@
<!--drawable/delete.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="?attr/colorControlNormal" android:pathData="M19 4h-3.5l-1-1h-5l-1 1H5v2h14M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6v12z"/>
</vector>

View File

@@ -1,10 +1,4 @@
<!-- drawable/fingerprint.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M17.81,4.47C17.73,4.47 17.65,4.45 17.58,4.41C15.66,3.42 14,3 12,3C10.03,3 8.15,3.47 6.44,4.41C6.2,4.54 5.9,4.45 5.76,4.21C5.63,3.97 5.72,3.66 5.96,3.53C7.82,2.5 9.86,2 12,2C14.14,2 16,2.47 18.04,3.5C18.29,3.65 18.38,3.95 18.25,4.19C18.16,4.37 18,4.47 17.81,4.47M3.5,9.72C3.4,9.72 3.3,9.69 3.21,9.63C3,9.47 2.93,9.16 3.09,8.93C4.08,7.53 5.34,6.43 6.84,5.66C10,4.04 14,4.03 17.15,5.65C18.65,6.42 19.91,7.5 20.9,8.9C21.06,9.12 21,9.44 20.78,9.6C20.55,9.76 20.24,9.71 20.08,9.5C19.18,8.22 18.04,7.23 16.69,6.54C13.82,5.07 10.15,5.07 7.29,6.55C5.93,7.25 4.79,8.25 3.89,9.5C3.81,9.65 3.66,9.72 3.5,9.72M9.75,21.79C9.62,21.79 9.5,21.74 9.4,21.64C8.53,20.77 8.06,20.21 7.39,19C6.7,17.77 6.34,16.27 6.34,14.66C6.34,11.69 8.88,9.27 12,9.27C15.12,9.27 17.66,11.69 17.66,14.66A0.5,0.5 0 0,1 17.16,15.16A0.5,0.5 0 0,1 16.66,14.66C16.66,12.24 14.57,10.27 12,10.27C9.43,10.27 7.34,12.24 7.34,14.66C7.34,16.1 7.66,17.43 8.27,18.5C8.91,19.66 9.35,20.15 10.12,20.93C10.31,21.13 10.31,21.44 10.12,21.64C10,21.74 9.88,21.79 9.75,21.79M16.92,19.94C15.73,19.94 14.68,19.64 13.82,19.05C12.33,18.04 11.44,16.4 11.44,14.66A0.5,0.5 0 0,1 11.94,14.16A0.5,0.5 0 0,1 12.44,14.66C12.44,16.07 13.16,17.4 14.38,18.22C15.09,18.7 15.92,18.93 16.92,18.93C17.16,18.93 17.56,18.9 17.96,18.83C18.23,18.78 18.5,18.96 18.54,19.24C18.59,19.5 18.41,19.77 18.13,19.82C17.56,19.93 17.06,19.94 16.92,19.94M14.91,22C14.87,22 14.82,22 14.78,22C13.19,21.54 12.15,20.95 11.06,19.88C9.66,18.5 8.89,16.64 8.89,14.66C8.89,13.04 10.27,11.72 11.97,11.72C13.67,11.72 15.05,13.04 15.05,14.66C15.05,15.73 16,16.6 17.13,16.6C18.28,16.6 19.21,15.73 19.21,14.66C19.21,10.89 15.96,7.83 11.96,7.83C9.12,7.83 6.5,9.41 5.35,11.86C4.96,12.67 4.76,13.62 4.76,14.66C4.76,15.44 4.83,16.67 5.43,18.27C5.53,18.53 5.4,18.82 5.14,18.91C4.88,19 4.59,18.87 4.5,18.62C4,17.31 3.77,16 3.77,14.66C3.77,13.46 4,12.37 4.45,11.42C5.78,8.63 8.73,6.82 11.96,6.82C16.5,6.82 20.21,10.33 20.21,14.65C20.21,16.27 18.83,17.59 17.13,17.59C15.43,17.59 14.05,16.27 14.05,14.65C14.05,13.58 13.12,12.71 11.97,12.71C10.82,12.71 9.89,13.58 9.89,14.65C9.89,16.36 10.55,17.96 11.76,19.16C12.71,20.1 13.62,20.62 15.03,21C15.3,21.08 15.45,21.36 15.38,21.62C15.33,21.85 15.12,22 14.91,22Z"
tools:ignore="VectorPath" />
<!--drawable/fingerprint.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M17.81 4.47c-0.08 0-0.16-0.02-0.23-0.06C15.66 3.42 14 3 12 3c-1.97 0-3.85 0.47-5.56 1.41C6.2 4.54 5.9 4.45 5.76 4.21c-0.13-0.24-0.04-0.55 0.2-0.68C7.82 2.5 9.86 2 12 2s4 0.47 6.04 1.5c0.25 0.15 0.34 0.45 0.21 0.69-0.09 0.18-0.25 0.28-0.44 0.28M3.5 9.72c-0.1 0-0.2-0.03-0.29-0.09C3 9.47 2.93 9.16 3.09 8.93c0.99-1.4 2.25-2.5 3.75-3.27C10 4.04 14 4.03 17.15 5.65c1.5 0.77 2.76 1.85 3.75 3.25 0.16 0.22 0.1 0.54-0.12 0.7-0.23 0.16-0.54 0.11-0.7-0.1-0.9-1.28-2.04-2.27-3.39-2.96-2.87-1.47-6.54-1.47-9.4 0.01-1.36 0.7-2.5 1.7-3.4 2.95C3.81 9.65 3.66 9.72 3.5 9.72m6.25 12.07c-0.13 0-0.25-0.05-0.35-0.15-0.87-0.87-1.34-1.43-2.01-2.64-0.69-1.23-1.05-2.73-1.05-4.34 0-2.97 2.54-5.39 5.66-5.39s5.66 2.42 5.66 5.39a0.5 0.5 0 0 1-0.5 0.5 0.5 0.5 0 0 1-0.5-0.5c0-2.42-2.09-4.39-4.66-4.39-2.57 0-4.66 1.97-4.66 4.39 0 1.44 0.32 2.77 0.93 3.84 0.64 1.16 1.08 1.65 1.85 2.43 0.19 0.2 0.19 0.51 0 0.71-0.12 0.1-0.24 0.15-0.37 0.15m7.17-1.85c-1.19 0-2.24-0.3-3.1-0.89-1.49-1.01-2.38-2.65-2.38-4.39a0.5 0.5 0 0 1 0.5-0.5 0.5 0.5 0 0 1 0.5 0.5c0 1.41 0.72 2.74 1.94 3.56 0.71 0.48 1.54 0.71 2.54 0.71 0.24 0 0.64-0.03 1.04-0.1 0.27-0.05 0.54 0.13 0.58 0.41 0.05 0.26-0.13 0.53-0.41 0.58-0.57 0.11-1.07 0.12-1.21 0.12M14.91 22h-0.13c-1.59-0.46-2.63-1.05-3.72-2.12-1.4-1.38-2.17-3.24-2.17-5.22 0-1.62 1.38-2.94 3.08-2.94 1.7 0 3.08 1.32 3.08 2.94 0 1.07 0.95 1.94 2.08 1.94 1.15 0 2.08-0.87 2.08-1.94 0-3.77-3.25-6.83-7.25-6.83-2.84 0-5.46 1.58-6.61 4.03-0.39 0.81-0.59 1.76-0.59 2.8 0 0.78 0.07 2.01 0.67 3.61 0.1 0.26-0.03 0.55-0.29 0.64C4.88 19 4.59 18.87 4.5 18.62 4 17.31 3.77 16 3.77 14.66c0-1.2 0.23-2.29 0.68-3.24 1.33-2.79 4.28-4.6 7.51-4.6 4.54 0 8.25 3.51 8.25 7.83 0 1.62-1.38 2.94-3.08 2.94-1.7 0-3.08-1.32-3.08-2.94 0-1.07-0.93-1.94-2.08-1.94s-2.08 0.87-2.08 1.94c0 1.71 0.66 3.31 1.87 4.51 0.95 0.94 1.86 1.46 3.27 1.84 0.27 0.08 0.42 0.36 0.35 0.62-0.05 0.23-0.26 0.38-0.47 0.38z" tools:ignore="VectorPath"/>
</vector>

View File

@@ -1,8 +1,4 @@
<!-- 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="?attr/colorControlNormal" 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" />
<!--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="?attr/colorControlNormal" android:pathData="M12 4a6 6 0 0 1 6 6c0 2.97-2.16 5.44-5 5.92V18h2v2h-2v2h-2v-2H9v-2h2v-2.08C8.16 15.44 6 12.97 6 10a6 6 0 0 1 6-6m0 2a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4z"/>
</vector>

View File

@@ -0,0 +1,4 @@
<!--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 6 6c0 2.97-2.16 5.44-5 5.92V18h2v2h-2v2h-2v-2H9v-2h2v-2.08C8.16 15.44 6 12.97 6 10a6 6 0 0 1 6-6m0 2a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4z"/>
</vector>

View File

@@ -1,8 +1,4 @@
<!-- 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="?attr/colorControlNormal" 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" />
<!--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="?attr/colorControlNormal" android:pathData="M9 9c1.29 0 2.5 0.41 3.47 1.11L17.58 5H13V3h8v8h-2V6.41l-5.11 5.09c0.7 1 1.11 2.2 1.11 3.5a6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6m0 2a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4z"/>
</vector>

View File

@@ -0,0 +1,4 @@
<!--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 9c1.29 0 2.5 0.41 3.47 1.11L17.58 5H13V3h8v8h-2V6.41l-5.11 5.09c0.7 1 1.11 2.2 1.11 3.5a6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6m0 2a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4z"/>
</vector>

View File

@@ -1,8 +1,4 @@
<!-- drawable/github-circle.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="#000" android:pathData="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
<!--drawable/github-circle.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="#000" android:pathData="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5 0.5 0.08 0.66-0.23 0.66-0.5v-1.69c-2.77 0.6-3.36-1.34-3.36-1.34-0.46-1.16-1.11-1.47-1.11-1.47-0.91-0.62 0.07-0.6 0.07-0.6 1 0.07 1.53 1.03 1.53 1.03 0.87 1.52 2.34 1.07 2.91 0.83 0.09-0.65 0.35-1.09 0.63-1.34-2.22-0.25-4.55-1.11-4.55-4.92 0-1.11 0.38-2 1.03-2.71-0.1-0.25-0.45-1.29 0.1-2.64 0 0 0.84-0.27 2.75 1.02 0.79-0.22 1.65-0.33 2.5-0.33 0.85 0 1.71 0.11 2.5 0.33 1.91-1.29 2.75-1.02 2.75-1.02 0.55 1.35 0.2 2.39 0.1 2.64 0.65 0.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91 0.36 0.31 0.69 0.92 0.69 1.85V21c0 0.27 0.16 0.59 0.67 0.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2z"/>
</vector>

View File

@@ -0,0 +1,4 @@
<!--drawable/history.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="?attr/colorControlNormal" android:pathData="M13.5 8H12v5l4.28 2.54 0.72-1.21-3.5-2.08V8M13 3a9 9 0 0 0-9 9H1l3.96 4.03L9 12H6a7 7 0 0 1 7-7 7 7 0 0 1 7 7 7 7 0 0 1-7 7c-1.93 0-3.68-0.79-4.94-2.06l-1.42 1.42C8.27 20 10.5 21 13 21a9 9 0 0 0 9-9 9 9 0 0 0-9-9"/>
</vector>

View File

@@ -1,5 +1,3 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#fff" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
<vector android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#fff" android:pathData="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
</vector>

View File

@@ -1,73 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<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">
<!--~ Pupil, Hitomi.la viewer for Android ~ Copyright (C) 2019 tom5079 ~ ~ This program is free software: you can redistribute it and/or modify ~ it under the terms of the GNU General Public License as published by ~ the Free Software Foundation, either version 3 of the License, or ~ (at your option) any later version. ~ ~ This program is distributed in the hope that it will be useful, ~ but WITHOUT ANY WARRANTY; without even the implied warranty of ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ~ GNU General Public License for more details. ~ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see <http://www.gnu.org/licenses/>.-->
<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">
<path
android:name="path"
android:pathData="M 19 9 L 15 9 L 15 3 L 9 3 L 9 9 L 5 9 L 12 16 L 19 9 Z"
android:fillColor="#fff"
android:strokeWidth="1"/>
<path
android:name="path_2"
android:pathData="M 5 19 L 19 19"
android:fillColor="#fff"
android:strokeColor="#fff"
android:strokeWidth="2"
android:strokeLineCap="butt"/>
<vector android:name="vector" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:name="path" android:pathData="M 19 9 L 15 9 L 15 3 L 9 3 L 9 9 L 5 9 L 12 16 L 19 9 Z" android:fillColor="#fff" android:strokeWidth="1"/>
<path android:name="path_2" android:pathData="M 5 19 L 19 19" android:fillColor="#fff" android:strokeColor="#fff" android:strokeWidth="2"/>
</vector>
</aapt:attr>
<target android:name="path_2">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:propertyName="trimPathEnd"
android:duration="500"
android:valueFrom="0"
android:valueTo="0.8"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator
android:propertyName="trimPathStart"
android:startOffset="500"
android:duration="500"
android:valueFrom="0"
android:valueTo="0.8"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator
android:propertyName="trimPathOffset"
android:duration="1000"
android:valueFrom="0"
android:valueTo="0.2"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator android:propertyName="trimPathEnd" android:duration="500" android:valueFrom="0" android:valueTo="0.8" android:valueType="floatType" android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator android:propertyName="trimPathStart" android:startOffset="500" android:duration="500" android:valueFrom="0" android:valueTo="0.8" android:valueType="floatType" android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator android:propertyName="trimPathOffset" android:duration="1000" android:valueFrom="0" android:valueTo="0.2" android:valueType="floatType" android:interpolator="@android:interpolator/fast_out_slow_in"/>
</set>
</aapt:attr>
</target>

View File

@@ -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>

View File

@@ -1,14 +1,4 @@
<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="#fff"
android:pathData="M18.4,10.6C16.55,8.99 14.15,8 11.5,8c-4.65,0 -8.58,3.03 -9.96,7.22L3.9,16c1.05,-3.19 4.05,-5.5 7.6,-5.5 1.95,0 3.73,0.72 5.12,1.88L13,16h9V7l-3.6,3.6z"/>
<path
android:fillColor="#fff"
android:pathData="M8.5,15
a1.5,1.5 0 1,1 1.5,1.5
a1.5,1.5 0 0,1 -1.5,-1.5 Z"/>
<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="#fff" android:pathData="M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73 0.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/>
<path android:fillColor="#fff" android:pathData="M8.5 15a1.5 1.5 0 1 1 1.5 1.5A1.5 1.5 0 0 1 8.5 15z"/>
</vector>

View File

@@ -1,8 +1,4 @@
<!-- drawable/message.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="#000" android:pathData="M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4C22,2.89 21.1,2 20,2Z" />
<!--drawable/message.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="#000" android:pathData="M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4c0-1.11-0.9-2-2-2z"/>
</vector>

View File

@@ -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>

View File

@@ -1,9 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
<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="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More